1.5 Enumerating graphs with Pólya’s theorem and GMP

by Marko Riedel

1.5.1 Introduction

The object of this recipe is to enumerate non-isomorphic graphs on n vertices using Pólya’s theorem and GMP (the GNU multiple precision arithmetic library). We will be concerned with the algorithmics rather than the mathematics. You are encouraged to learn more about the latter, e.g. from Frank Harary’s books on the subject.

Fig. 1 shows all possible graphs on three vertices. There are eight of these; for a graph on n vertices we have 2n(n-1)2 such graphs. We want a different statistic, namely one that takes isomorphisms into account. Fig. 2 shows that there are really only four different graphs on three vertices. This is because e.g. there exists a vertex permutation that maps the three graphs with one edge to one another. We say that these three graphs constitute an equivalence class.


PIC

Figure 1: All eight graphs on three vertices.



PIC

Figure 2: All non-isomorphic graphs on three vertices.


Let us now reformulate the problem so that Pólya’s theorem applies. Given n, we want to know how many non-isomorphic graphs, i.e. equivalence classes there are with k edges. We can think of the edges as slots that are either filled (edge present) or empty (edge absent). An empty slot has size zero and a filled one, size one. The number of edges is the sum of the sizes of the slots. The slots (edge locations) are being permuted by any of the n! vertex permutations. There is one edge permutation for each vertex permutation. We will obtain the values for all k at once, namely in a generating function, i.e. in a function where the term qzk denotes the presence of q non-isomorphic graphs with k edges.

The generic definition of our problem is this: given a set of slots that are being permuted by a permutation group (more on this later) and a set of objects of different sizes that go into the slots (one object per slot), count the number of equivalence classes of filled slot configurations with respect to the permutation group whose total size (i.e. sum of the sizes of the objects placed in the slots) is k. In our case the group is the pair group obtained from the symmetric group acting on n vertices and the objects that go into the slots are present/absent edges.

Example. Recall Fig. 1 and label the vertices 1, 2, 3. The table shows the vertex and the corresponding edge permutations (the three edges are 12, 13 and 23; the permutations are in table notation, e.g. 1 2, 2 3 and 3 1 is written as 231):



1 2 312 13 23


1 3 213 12 23


2 1 312 23 13


2 3 123 12 13


3 1 213 23 12


3 2 123 13 12


The eight graphs correspond to the three slots (edges) being filled or empty.




12 13 23






emptyemptyempty



emptyempty filled



empty filled empty



empty filled filled



filled emptyempty



filled empty filled



filled filled empty



filled filled filled



The edge permutations permute the columns of the table, e.g. 12 23 13 takes line two to line three, so these are in the same equivalence class. Permuting edges does not change the total size of the configuration (number of edges), so there is no permutation that takes line two to line four.

Another common application of Pólya’s theorem is to count, say, the number of different necklaces, where a necklace is made up of n gemstones and there are m different types of gemstones (colors) available. We can choose these to have the same size or different sizes. In terms of the above discussion, the slots are the positions on the necklace, the objects that go into the slots are the gemstones and the permutations rotate or flip the necklace or both.

1.5.2 Permutations

We will be using only a few basic facts about permutations and permutation groups. The elements of the latter are permutations and the multiplication operation is permutation composition. There must not be any permutations whose product is not in the group. E.g. {123,231} is not a group because 231 231 = 312, which is not in the set. We are concerned with two groups: the symmetric group Sn on n vertices, which includes all n! permutations and the corresponding pair group Sn(2) on n(n - 1)2 edges, whose elements are obtained from the action of the vertex permutation on the edges. E.g. consider the vertex permutation 23451 and the edges 12 13 14 15 23 24 25 34 35 45. The corresponding edge permutation ε is 23 24 25 12 34 35 13 45 14 15.

We now switch to a more useful way of writing permutations, i.e. the so-called disjoint cycle decomposition. This consists in writing the permutation as a product of its cycles. E.g. the permutation 164325 (table notation) would be written as (1) (265) (34) since e.g. it takes 2 to 6, 6 to 5 and 5 back to 2, and the edge permutation ε as (12 23 34 45 15) (13 24 35 14 25).

The disjoint cycle decomposition tells us about the shape of the permutation. This shape information is captured by a product notation, where the variable ak stands for a cycle of length k and jk for the number of cycles with this length, giving the product k=1pakjk. E.g. the product for (1) (265) (34) would be a 1a2a3, whith j1 = j2 = j3 = 1 and j4 = j5 = j6 = 0 and for ε, a52, with j1 = j2 = j3 = j4 = 0, j5 = 2 and j6 = j7 = j8 = j9 = j10 = 0.

1.5.3 The Cycle Index

The cycle index Z(S) of a permutation group S is the average of k=1pakjk over all permutations in the group. E.g. the cycle indices of the symmetric group S3 and the pair group S3(2) are obtained from the following table.





1 2 3 a13 12 13 23 a13




1 3 2a1a213 12 23a1a2




2 1 3a1a212 23 13a1a2




2 3 1 a3 23 12 13 a3




3 1 2 a3 13 23 12 a3




3 2 1a1a223 13 12a1a2




This special case yields

                1     1      1
Z(S3) = Z(S(23)) =-a31 +-a1a2 +- a3.
                6     2      3

1.5.4 Pólya’s theorem

This is what the theorem says: suppose you have some set of objects of different sizes and distribute these objects into n slots, where a permutation group acts on the slots to create equivalence classes of filled slot configurations. The generating function of these equivalence classes is obtained by replacing ak by f(zk) in the cycle index, where f is the generating function of the objects.

The permutation group is Sn(2) (pair group acting on the vertices) when we enumerate graphs and the generating function of the objects is 1 + z, indicating whether an edge is present (size one, z) or not (1). Substitute ak = 1 + zk into Z(S3(2)) to obtain

1(1+z)3+ 1(1+z)(1+z2)+  1(1+z3) = 1z3+ 1z2+ 1z+ 1+ 1z3+ 1z2+ 1z +1 + 1z3+ 1= z3+z2 +z +1,
6        2              3         6    2    2   6  2    2    2   2   3    3

which says that there is one graph with three edges, one with two, one with one edge and one with no edges, as is also evident from Fig.2.

1.5.5 Cycle index of the pair group

We need the cyle index of Sn(2) in order to enumerate graphs. There is one permutation for every permutation from Sn in Sn(2). Fortunately we needn’t iterate over all n! permutations in Sn, which would make the computation infeasible for all but small n. Recall that the shape of the permutation determines the product that it contributes to the cycle index. This means that e.g. (1 2 3) (4) (5 6), (1) (2 3) (4 5 6) and (1 2) (3 4 5) (6) all contribute the same term to S6, namely a1a2a3 (three copies of it).

Hence we can enumerate the permutations in Sn by iterating over all possible shapes and computing how often each shape occurs. These shapes are precisely the partitions of n into some number of summands. E.g. the cycle index for S3 contains one product term for each of the possible partitions 1 + 1 + 1, 1 + 2 and 3. We need the coefficient associated to each of these three partitions, i.e. the number of times this shape occurs.

The disjoint cycle decomposition k=1nakjk occurs

          ∏n (   )jk∏n
∏---n!----     k!      -1-=  ∏n--n!----
  nk=1(k!)jkk=1  k    k=1jk!    k=1 kjkjk!

times. (Partition the n elements into subsets, one for each cycle. A subset of size k generates k!∕k cycles. The same size jk set of k-cycles may be chosen in jk! different ways.)

For S3 this yields the following results:

This computation confirms the value for S3 that we computed earlier.

How to obtain the possible partitions? This is simple, since there is a folklore algorithm for the problem. Instead of computing the set of partitions Pn of n, we start with Pn,k, the set of partitions of n into k summands. Now every partition in Pn,k either contains one, and can be obtained by including one in a partition from P(n- 1,k - 1), or it does not contain one and n-k >= k, and it can be obtained from a partition from P(n-k,k) by incrementing its elements. We can compute Pn,k recursively, the base case being P1,1 = {[1]}. Pn is the union of the Pn,k for 1 k n.


PIC

Figure 3: Vertex permutation with the shape a43 a8 and the three possible types of edges.


At this point we have all possible shapes and we know how often they occur. The last step is to compute the shape of the edge permutation from the shape of the vertex permutation. An edge connects two vertices. There are three possiblities: these vertices lie on the same cycle of the vertex permutation, they lie on different cycles of the same length or they lie on different cycles of different lengths. These three possibilities are shown in Fig. 3. We must compute the length of the edge cycle (i.e. how long it takes for the vertex pair to return to the start edge when it moves in parallel along the vertex cycle) in each case and how often it occurs. There is an additional distinction to be made when the vertices lie on the same cycle: a cycle of even length contains vertex pairs that return to the start edge after having covered exactly half, rather than the whole cycle. Furthermore, the number of steps for the return to the start position for two vertices on cycles of different lengths is given by the least common multiple of those lengths. Note that we’ll overcount cycles by a factor that is equal to the length of the edge cycle. We illustrate with a sample computation.

Say the vertex permutation has the shape a32 a6 a7.

This shows that the corresponding edge permutation has the shape a36 a68 a73 a212 a42, and indeed 18 + 48 + 21 + 42 + 42 = 171 = 12 19 (19 - 1).

We now have all the ingredients to implement the graph enumeration algorithm. There will be four steps:

Note that we average the cycle index in the last step in order to be able to work with integers during the whole computation. Speaking of integers, our implementation will use the GMPInt wrapper class for GMP integers.

1.5.6 Concerning memory allocation

The computation that we undertake produces many intermediate results. These are: GMPInt values to represent coefficients of the products whose sum forms the cycle index; NSString objects that document the computation if the user activates debugging and NSMutableArray objects that hold partitions and product terms as well as polynomials in one variable that result from the substitution of 1 + z into the cycle index. All of these can use up a lot of memory very quickly, which is why we absolutely must implement garbage collection. This is done in the loop that iterates over the partitions and hence over the products that occur in the cycle index. We check whether the number of allocated critical objects has doubled, and empty the autorelease pool if it has.

1.5.7 Implementation I: auxiliary functions

There is a preliminary remark concerning our terminology. We use the term multiset to refer to partitions and products. This is because a partition like [1,1,1,2,3] or a13 a2 a3 is a multiset containing three copies of the value “1” and one copy each of the values “2” and “3.”

Our list of auxiliary functions starts with the basic GCD algorithm. The GCD g of p and q divides p = mq + r and q, so it also divides r < q. Hence replace (p,q) by (q,r) and repeat until r = 0. We need the GCD to compute the LCM of the lengths of two cycles of a vertex permutation. The LCM gives the length of the edge cycle of those edges that have one vertex on each of the two vertex cycles.

 
int gcd(int p, int q) 
{ 
    int r; 
 
    if(p<q){ 
        int s; 
 
        s = p; 
        p = q; 
        q = s; 
    } 
 
    while(r=p%q){ 
        p = q; 
        q = r; 
    } 
 
    return q; 
}

The function SumIt sums a multiset, where each element contributes according to its multiplicity and the first element has value one. It iterates over the elements in a loop. This is very useful for consistency checks. The cycle lengths of a vertex permutation must sum to n, and the cycle lengths of the corresponding edge permutation to 12n(n - 1).

 
int SumIt(NSArray *data) 
{ 
    int sum, pos, max = [data count]; 
 
    for(sum=0, pos=0; pos<max; pos++){ 
        sum += (pos+1)* 
            [[data objectAtIndex:pos] intValue]; 
    } 
 
    return sum; 
}

The next two functions are very important; they compute Pn,k and Pn, respectively. Start with the former. The base case comes first: the answer is {[1]} when n = 1. The data structure that we use is an array of arrays of integer NSNumber objects. The number is one summand from the partition, the inner array a single partition and the outer array a collection of partitions.

 
NSMutableArray *Partitionsk(int n, int k) 
{ 
    NSMutableArray *part, 
        *result = [NSMutableArray arrayWithCapacity:1]; 
    NSEnumerator *en; 
 
    if(n==1){ 
        part = 
            [NSMutableArray 
                arrayWithObject: 
                     [NSNumber numberWithInt:1]]; 
        [result addObject:part]; 
        return result; 
    }

The two recursions are next. Either the partition contains one or it doesn’t. There is a one-to-one correspondence with a permutation of Pn-1,k-1 in the former case, so obtain those and collect them, adding a summand with value one at the front of each summand array.

 
    if(k>1){ 
        en = [Partitionsk(n-1, k-1) objectEnumerator]; 
        while((part = [en nextObject])!=nil){ 
            [part insertObject:[NSNumber numberWithInt:1] 
                   atIndex:0]; 
            [result addObject:part]; 
        } 
    }

If the partition does not contain one and n - k is large enough for k summands, then we have a one-to-one correspondence with a permutation from Pn-k,k. We obtain these permutations and increment each summand of each partition, collecting the new partitions in the array result. These are the only possible cases and we are done.

 
    if(n-k>=k){ 
        en = [Partitionsk(n-k, k) objectEnumerator]; 
        while((part = [en nextObject])!=nil){ 
            NSMutableArray *npart = 
                [NSMutableArray arrayWithCapacity:k]; 
            int pos; 
            for(pos=0; pos<k; pos++){ 
                int val = [[part objectAtIndex:pos] intValue]; 
                [npart 
                     addObject: 
                         [NSNumber numberWithInt:val+1]]; 
            } 
            [result addObject:npart]; 
        } 
    } 
 
    return result; 
}

Obtaining all partitions of n is now easy: compute the union of Pn,k for 1 k n. This is done with a loop.

 
NSMutableArray *Partitions(int n) 
{ 
    int k; 
    NSMutableArray *result = 
        [NSMutableArray arrayWithCapacity:1]; 
 
    for(k=1; k<=n; k++){ 
        NSMutableArray *partsk = Partitionsk(n, k); 
        [result addObjectsFromArray:partsk]; 
    } 
 
    return result; 
}

We will need to turn partitions into multisets, i.e. vertex permutation shapes that represent product terms of the form k=1nakjk. This product correponds to a partition that contains j k instances of the value k. E.g. we want to turn [1,1,1,2,2,3] into a13 a22 a3, which is represented as the array [3,2,1]. First initialize the array with zero, i.e. in our example we would have [0,0,0] and the capacity of the array would be three.

 
NSMutableArray *PartitionToMSet(NSMutableArray *part) 
{ 
    int pos, max = [[part lastObject] intValue]; 
    NSMutableArray *result = 
        [NSMutableArray arrayWithCapacity:max]; 
 
    for(pos=0; pos<max; pos++){ 
        [result addObject:[NSNumber numberWithInt:0]]; 
    }

Then iterate over the partion and record a summand with value value (recall that these values are wrapped in NSNumber objects) at position value-1 of the multiset array. This is done by reading the old value, incrementing it, and writing it back into the array. This would yield the sequence [1,0,0], [2,0,0], [3,0,0], [3,1,0], [3,2,0], and [3,2,1] in the example.

 
    NSEnumerator *en = [part objectEnumerator]; 
    NSNumber *val; 
    while((val = [en nextObject])!=nil){ 
        int value = [val intValue], 
            count = [[result objectAtIndex:value-1] intValue]; 
        [result replaceObjectAtIndex:value-1 
                withObject:[NSNumber numberWithInt:count+1]]; 
    } 
 
    return result; 
}

Our program provides an extensive trace/debug facility that makes it possible for the user to follow the computation very closely. Hence it is imperative that we be able to convert from the internal representation of a multiset into a string representation, i.e. we must be able to turn [3,2,1] into the string a13 a22 a3. This is done by iterating over the multiset, reading the entry at that position, and storing a string representation of the term in the array factors. (Empty fields are skipped.)

 
NSString *MSetToProduct(NSMutableArray *mset) 
{ 
    NSNumber *cval; 
    NSMutableArray *factors = 
        [NSMutableArray arrayWithCapacity:1]; 
    int pos, max = [mset count]; 
 
    for(pos=0; pos<max; pos++){ 
        int count = [[mset objectAtIndex:pos] intValue];

The string representation of such a term has the form ak if if jk is one, and the form akjk otherwise.

 
        if(count){ 
            NSString *item = 
                [NSString stringWithFormat:@”a_%d”, 
                           pos+1]; 
            if(count>1){ 
                item = [item stringByAppendingFormat:@”ˆ%d”, 
                              count]; 
            } 
            [factors addObject:item];

We join all terms that have been collected by concatenating them. (They are separated by a single space in the result string.)

 
        } 
    } 
 
    return [factors componentsJoinedByString:@”_”]; 
}

Recall that the number of times a shape occurs in the symmetric group is given by the formula

    n!
∏n---kjkj!.
  k=1    k

The next procedure computes the denominator of this fraction. Set up a loop to iterate over the multiset and process nonzero entries only.

 
GMPInt *MSetToCoeffDenom(NSMutableArray *mset) 
{ 
    GMPInt *one = [GMPInt oneObj], 
        *res = one, *k = one; 
    int pos, max = [mset count]; 
 
    for(pos=0; pos<max; pos++){ 
        int count = [[mset objectAtIndex:pos] intValue]; 
        if(count){

The actual computation is to multiply the current denominator by kjk and j k!.

 
            GMPInt 
                *m1 = [k powUI:count],