\datethis \input epsf \let\possiblyflakyepsfbox=\epsfbox \def\epsfbox#1{\hbox{\possiblyflakyepsfbox{#1}}} \def\adj{\mathrel{\!\mathrel-\mkern-8mu\mathrel-\mkern-8mu\mathrel-\!}} % adjacent vertices @* Introduction. This program generates all spanning trees of a given series-parallel graph, changing only one edge at a time, using an interesting algorithm. The given graph is specified using a simple right-Polish syntax $$G\,\to\,\.-\,\mid\,G\,G\,\,\.s\,\mid\,G\,G\,\,\.p$$ so that, for example, the specifications \.{----ps-sp--sp} and \.{----p-ss--spp} both denote the graph $$\epsfbox{spspan.1}$$ which can also be represented as a tree: $$\epsfbox{spspan.2}$$ Branch nodes of the tree are either $S$ nodes or $P$ nodes, alternating from level to level. As we do the computation, we count the total number of spanning trees that were generated and the total number of memory references that were needed. @d o mems++ @d oo mems+=2 @d ooo mems+=3 @d oooo mems+=4 @d call oo /* let's say that a subroutine call costs two mems */ @# @d verbose (argc>2) /* show the edges of each spanning tree */ @d extraverbose (argc>3) /* show inner workings of the program */ @c #include @@; @@; unsigned int trees,mems; @@; main (int argc, char*argv[]) { register int j,k; if (argc==1) { fprintf(stderr,"Usage: %s SPformula [[gory] details]\n", argv[0]);@+ exit(0); } @; @; printf(" (%u mems to get started)\n",mems);@+mems=0; @; printf("Altogether %u spanning trees, %u additional mems.\n",trees,mems); } @*Parsing and preparation. We begin by converting the Polish notation into a binary tree. In the following code, we have scanned $j$ binary operators and there are $k$~items on the stack. @d abort(mess) {@+fprintf(stderr,"Parsing error: %.*s|%s, %s!\n", p-argv[1],argv[1],p,mess);@+exit(-1);@+} @= { register char*p=argv[1]; for (j=k=0; *p; p++) if (*p=='-') @@; else if (*p=='s' || *p=='p') @@; else abort("bad symbol"); if (k!=1) abort("disconnected graph"); @; } @ @d maxn 1000 /* the maximum number of leaves; {\it not checked\/} */ @= int stack[maxn]; /* stack for parsing */ int llink[maxn],rlink[maxn]; /* binary subtrees */ @ Mems are not counted in this phase of the operation, because the program is essentially assumed to begin with the graph represented as a tree. @= stack[k++]=0; @ @= { if (k<2) abort("missing operand"); rlink[++j]=stack[--k]; llink[j]=stack[k-1]; stack[k-1]=(*p=='s'? 0x100: 0)+j; } @ Now we convert the binary tree to the desired working tree, whose branch nodes appear in preorder. @= typedef struct node_struct { int typ; /* 1 for series nodes, otherwise 0 */ struct node_struct *lchild; /* leftmost child; |NULL| for a leaf */ struct node_struct *rchild; /* rightmost child; |NULL| for a leaf */ struct node_struct *rsib; /* right sibling; wraps around cyclically */ @@; } node; @ The first half of |nodelist| contains up to |maxn| leaves; the other half contains up to |maxn| branches. @= node nodelist[maxn+maxn]; /* nodes of the tree */ node *curleaf; /* the leftmost not-yet-allocated leaf node */ node *curnode; /* the rightmost allocated branch node */ node *root, *topnode; /* root of the tree and its parent */ @ A recursive subroutine called |build| will govern the construction process. @d isleaf(p) ((p)= curleaf=nodelist; topnode=curnode=nodelist+maxn; curnode->typ=2; /* special |typ| code for the outer level */ root=build(stack[0],curnode); root->rsib=root; /* unnecessary but tidy */ @ When we |build| a leaf node, we simply allocate it. When we |build| a branch node, we link its children together via their sibling links. Only one complication arises: We must prevent serial nodes from having serial children and parallel nodes from having parallel children. In such cases the child's family is merged with that of the parent, and the child goes away. @= node* build(int stackitem, node* par) { register node *p,*l,*r,*lc,*rc; register int t,j; if (stackitem==0) return curleaf++; t=stackitem>>8, j=stackitem&0xff; /* type and location of a binary op */ if (t!=par->typ) p=++curnode, p->typ=t; else p=par; l=build(llink[j],p), lc=l->lchild, rc=l->rchild, r=build(rlink[j],p); if (l==p) @@; else if (r==p) @@; else p->lchild=l, p->rchild=r, l->rsib=r, r->rsib=l; return p; } @ @= r=p->lchild, p->lchild=l, l->rsib=r, p->rchild->rsib=l; @ @= if (r==p) @@; else p->rchild=r, rc->rsib=r, r->rsib=lc; @ @= rc->rsib=p->lchild, p->lchild=lc, p->rchild->rsib=lc; @ OK, the tree has been set up; our next goal is to decorate it. First let's take a closer look at the problem we're trying to solve. Each node of the tree corresponds to a series-parallel graph between two vertices $u$ and~$v$, in a straightforward way: A leaf is a single edge $u\adj v$. A nonleaf node~|p| corresponds to a superedge'' formed from the edges or superedges $u_1\adj v_1$, \dots, $u_k\adj v_k$ of its $k\ge2$ children. If |p| is a series node, its children are joined so that $v_j=u_{j+1}$ for 1\le jval| to each leaf node~|p|, specifying whether the corresponding edge is present or absent in the current spanning tree being considered. The |p->val| field of a branch node, similarly, will specify whether the corresponding superedge currently has a spanning tree or a near-spanning tree. In the following algorithm every branch node |p| has a designated child, |p->des|, with the property that |p->val=p->des->val|. Only certain combinations of values are legal; the legal ones, according to the discussion above, are characterized by two rules: \indent\indent All non-designated children of a series node have value 1; \indent\indent All non-designated children of a parallel node have value 0. \noindent In other words, if |q| is the parent of node |p|, \hbox{|p->val|}=\cases{\hbox{|q->val|},&if |p=q->des|;\cr \noalign{\smallskip} \hbox{|q->typ|},&if |p!=q->des|.\cr} For any choice of the designated children, we obtain a unique spanning tree or near-spanning tree for node~|p| by setting |p->val| to 1 or~0, respectively, and using this equation to propagate values down to the leaves. Thus we can generate all the spanning trees of the graph (namely the spanning trees corresponding to the |root| node) by setting |root->val=1| and considering all possible settings of designated children |p->des| throughout the tree. However, many settings of the |p->des| pointers will produce the same result: The value of |p->des| is irrelevant for serial nodes of value~1 and for parallel nodes of value~0. We will return to this problem later; meanwhile let's put the necessary information into our data structure. @= int val; /* 0 = off, open, near-spanning; 1 = on, closed, spanning */ struct node_struct *des; /* the designated child */ @ To start things off, we might as well designate each node's leftmost child. Mems are computed under the assumption that a node's |typ| and |val| can be fetched and stored in a single operation. @= o,topnode->typ=1; call,init_tree(root,topnode); trees=1; if (verbose) @; @ A few amendments to the data structure will be desirable later, but we're ready now to write most of the tree-initializing routine. @= void init_tree(node* p,node* par) /* |par| is the parent of |p| */ { register node*q; ooo,p->val=(par->des==p? par->val: par->typ); if (isleaf(p)) @@; else { oo,p->des=p->lchild; for (q=p->lchild;;q=q->rsib) { call, init_tree(q,p); if (o,q->rsib==p->lchild) break; } @; } } @* Diagnostic routines. Several simple subroutines are used to print all or part of our data structure, as aids to debugging and/or when the user wants to examine all the spanning trees. We name the leaves \.a, \.b, \.c, etc., and the branches \.A, \.B, \.C, etc., as in the example at the beginning of this program. When I'm debugging this program I plan to save keystrokes and mental energy by typing, say, |xx('A')| when I want a pointer to node \.A. @d leafname(p) ('a'+((p)-nodelist)) @d branchname(p) ('A'+((p)-root)) @d nodename(p) (isleaf(p)? leafname(p): branchname(p)) @= node*xx(char c) { if (c>='a') return nodelist+(c-'a'); return nodelist+maxn+(c-'@@'); } @ @= void printleaf(node* p) { printf("%c:%c rsib=%c\n", leafname(p),p->val+'0',nodename(p->rsib)); } @# void printbranch(node* p) { printf("%c:%c rsib=%c lchild=%c des=%c rchild=%c", branchname(p),p->val+'0',nodename(p->rsib), nodename(p->lchild),nodename(p->des),nodename(p->rchild)); @; printf("\n"); } @# void printnode(node* p) { if (isleaf(p)) printleaf(p); else printbranch(p); } @ @= void printtree(node* p,int indent) { register node* q; register int k; for (k=0;klchild;;q=q->rsib) { printtree(q,indent+1); if (q->rsib==p->lchild) break; } } @ @= void printedges(node*p) /* print the leaves whose value is 1 */ { register node* q; if (isleaf(p)) { if (p->val) printf("%c",leafname(p)); }@+else@+ for (q=p->lchild;;q=q->rsib) { printedges(q); if (q->rsib==p->lchild) break; } } @ @= { if (extraverbose) printtree(root,0); printf("The first spanning tree is "); printedges(root); printf(".\n"); } @*Overview of the algorithm. A branch node |p| will be called {\it easy\/} if |p->val=p->typ|. In such cases the designated child |p->des| has no effect on the spanning tree or near-spanning tree, because all children have the same value. Let's say for convenience that the {\it configs\/} of |p| are its spanning trees if |p->val=1|, its near-spanning trees if |p->val=0|. Our problem is to generate all configs of the root. If |p| is easy, its configs are the Cartesian product of the configs of its children. But if |p| is uneasy, its configs are the union of such Cartesian products, taken over all possible choices of |p->des|. Easy nodes are relatively rare: At most one child of an uneasy node (namely the designated child) can be easy, and all children of easy nodes are uneasy unless they are leaves. @d easy(p) o,p->typ==p->val @ Cartesian products of configurations are easily generated in Gray-code order, using essentially a mixed-radix Gray code forn$-tuples. (See Section 7.2.1.1 of {\sl The Art of Computer Programming}.) In this program I'm using a modular'' code instead of a reflected'' one, because the modular code requires only |rsib| links to cycle through the possible choices of |p->des|. Let's include a new field |p->leaf| in each node, pointing to the leaf that lies at the end of the path from |p| to its designated descendants |p->des|, |p->des->des|, etc. All the |val| fields on this path are the same as |p->val|. When |p->des| is changed from one child to another, say from |q| to |r|, only two edge values of the overall spanning tree are affected. Namely, we have |q->typ!=p->typ| and |r->typ=p->typ|, so |q->leaf->val| becomes |r->typ| and |r->leaf->val| becomes |q->typ|. Therefore such a change is pleasantly Gray.'' @= struct node_struct *leaf; /* the end of the designated path */ struct node_struct *parent; /* parent of this node */ @ These considerations lead us to the following algorithm to generate all spanning trees: Begin with all uneasy branch nodes active. Then repeatedly \smallskip\itemitem{1)} Select the rightmost active node, |p|, in preorder. \smallskip\itemitem{2)} Change |p->des| to |p->des->rsib|, update all values of the tree, and visit the new spanning tree. \smallskip\itemitem{3)} Activate all uneasy nodes to the right of |p|. \smallskip\itemitem{4)} If |p->des| has run through all children of |p| since |p| last became active, make node~|p| passive. \smallskip\noindent A field |p->done| is introduced in order to implement step (4): Node~|p| becomes passive when |p->des=p->done|, and at such a time we reset |p->done| to the previous value of |p->des|. Actually |p->done| is initially equal to |p->rchild|, and the |rchild| pointers are not needed by the main algorithm. So we can equate |p->done| with |p->rchild|. @d done rchild /* the new meaning of the |rchild| field */ @ For example, let's apply the algorithm to the series-parallel graph illustrated in the introduction. Since |A| is a parallel node and since each leftmost child is initially designated, |init_tree| sets |A->val=1|, |B->val=0|, |C->val=1|, |D->val=0|, and the first spanning tree consists of edges$\it aceg. All four branch nodes are initially uneasy. (That's just a coincidence, not a general rule.) The current state of the algorithm can be indicated by writing each designated child as a subscript, by enclosing easy nodes in parentheses, and by placing a hat over passive nodes. With these conventions, the algorithm proceeds as follows: \vcenter{\halign{\hbox to2.1em{\hfil#\hfil}& \hbox to2.1em{\hfil#\hfil}& \hbox to2.1em{\hfil#\hfil}& \hbox to2.1em{\hfil#\hfil}& \hskip5em \hbox to.6em{\hfil#\hfil}& \hbox to.6em{\hfil#\hfil}& \hbox to.6em{\hfil#\hfil}& \hbox to.6em{\hfil#\hfil}\cr \multispan4\hidewidth branch node states\hidewidth& \multispan4\qquad\qquad spanning tree\cr \noalign{\smallskip} A_a&B_b&C_c&D_f&a&c&e&g\cr A_a&B_b&C_c&\widehat D_g&a&c&e&f\cr A_a&B_b&\widehat C_d&D_g&a&d&e&f\cr A_a&B_b&\widehat C_d&\widehat D_f&a&d&e&g\cr A_a&B_C&(C_d)&D_f&a&b&e&g\cr A_a&B_C&(C_d)&\widehat D_g&a&b&e&f\cr A_a&\widehat B_e&C_d&D_g&a&b&d&f\cr A_a&\widehat B_e&C_d&\widehat D_f&a&b&d&g\cr A_a&\widehat B_e&\widehat C_c&D_f&a&b&c&g\cr A_a&\widehat B_e&\widehat C_c&\widehat D_g&a&b&c&f\cr A_B&(B_e)&C_c&D_g&b&c&e&f\cr A_B&(B_e)&C_c&\widehat D_f&b&c&e&g\cr A_B&(B_e)&\widehat C_d&D_f&b&d&e&g\cr A_B&(B_e)&\widehat C_d&\widehat D_g&b&d&e&f\cr \widehat A_D&B_e&C_d&(D_g)&b&d&f&g\cr \widehat A_D&B_e&\widehat C_c&(D_g)&b&c&f&g\cr \widehat A_D&B_b&C_c&(D_g)&c&e&f&g\cr \widehat A_D&B_b&\widehat C_d&(D_g)&d&e&f&g\cr \widehat A_D&\widehat B_C&(C_d)&(D_g)&b&e&f&g\cr }} Thus, we first change |D->des| from |f| to |g| and passivate~|D|; then we change |C->des| from |c| to |d| and passivate~|C|. After four steps we change |B->des| from |b| to |C|, making |C| easy; and so on. @ So-called focus pointers'' can be used to implement steps (1) and (3) very efficiently, as discussed in Algorithm 7.2.1.1L. We set |p->focus=p| except when |p| is an uneasy node such that the nearest uneasy node to its right is active. We also imagine that an artificial uneasy active node appears to the right of |curnode|, which is the rightmost branch node of the entire tree in preorder. Then the simple operations $$\hbox{|p=r->focus|, \ |r->focus=r|}$$ implement (1) and (3), when |r| is the rightmost uneasy node---in spite of the fact that step (2) changes some nodes from easy to uneasy and vice versa(!). Furthermore, we can passivate node |p| in step (4) by the simple operations $$\hbox{|p->focus=l->focus|, \ |l->focus=l|}$$ when |l| is the rightmost uneasy node to the left of |p|. We imagine that |topnode|, which lies to the left of everything in preorder, is always uneasy and active; therefore |l| always exists. Step~(1) stops if |p=topnode|, since we have then generated all the spanning trees. @= struct node_struct *focus; /* the magical Gray-oriented focus pointer */ @ We can easily incorporate the new fields into our initialization routine. It will turn out that the algorithm doesn't really have to look at |leaf| or |parent| pointers, so no mems are charged for the cost of computing them. @= p->leaf=p, p->parent=par; @ @= p->leaf=p->des->leaf, p->parent=par; o,p->focus=p; @ @= printf(" leaf=%c",leafname(p->leaf)); if (p->focus!=p) printf(" focus=%c",branchname(p->focus)); @*Doing it. Let's go ahead now and implement the algorithm just sketched. @= topnode->focus=topnode; while (1) { register node *p, *q, *l, *r; for (r=curnode;easy(r);r--); /* find the rightmost uneasy node */ oo,p=r->focus, r->focus=r; /* steps (1) and (3) */ if (p==topnode) break; @des| and visit a new spanning tree@>; if (o,p->des==p->done) @; } @ All uneasy nodes to the right of |p| are now active, and |l| is the former |p->des|. @= { o,p->done=l; for (l=p-1;easy(l);l--); /* find the first uneasy node to the left */ ooo,p->focus=l->focus, l->focus=l; } @ If the user has asked for |verbose| output, we print only the edge that has entered the spanning tree and the edge that has left. @des| and visit a new spanning tree@>= oo,l=p->des, r=l->rsib; o,k=p->val; /* |k=l->val!=r->val| */ for (q=l;;o,q=q->des) { o,q->val=k^1; if (isleaf(q)) break; } if (verbose) printf(" %c%c",k? '-': '+',leafname(q)); for (q=r;;o,q=q->des) { o,q->val=k; if (isleaf(q)) break; } if (verbose) printf("%c%c\n",k? '+': '-',leafname(q)); o,p->des=r, trees++; /* visiting'' */ for (q=p; q->des==r; r=q,q=q->parent) q->leaf=r->leaf; /* that loop was optional, so it costs no mems */ if (extraverbose) { printedges(root); printf("; now %c->leaf=%c\n",branchname(r),leafname(r->leaf)); } @* A loopless version. The algorithm implemented here contains four loops. Two of them skip over easy nodes when finding |r| and |l| in the list of branches; two of them go down from branches to leaves when changing the |val| fields. The amortized cost of those loops is constant per new spanning tree. But it can be instructive to search for an algorithm that is entirely loopless, in the sense that the number of operations per new tree is bounded (once the algorithm has initialized itself in linear time). Loopless algorithms tend to run slower than their loopy counterparts, especially in cases like the present where the additional overhead needed to avoid looping appears to be substantial. So the search for a loopless implementation is strictly academic. Yet it still was fascinating enough to keep me working on it for three days during my recent vacation. I believe I see how to do it. But I don't have time to carry through the details, so I've decided just to sketch them here. Maybe somebody else will be inspired to work them out and to compare the loopless mem-counts with those of the present implementation. The first two loops can be avoided by changing the tree dynamically, so that the designated child is always the leftmost. In such cases it's easy to see that no two easy nodes can be consecutive in preorder. My planned implementation swaps the rightmost child into the leftmost position when |p->des| is supposed to change. This swapping causes two adjacent substrings of the preordered node list to change places. The node list should be doubly linked; to do the swap, we need a new field |p->scope| that points to the rightmost branch that is descended from |p| in the current list. The other two loops can be avoided if we update the |val| fields lazily, starting at the bottom. But then the pointer |p->leaf| becomes crucial, not optional, because the leaf nodes are encountered first, and because we need to know both |p->leaf| and |p->rchild->leaf| when reporting the edges that enter and leave the spanning tree. Of course the introduction of two required fields |p->scope| and |p->leaf| means that we must maintain them, and that seems to require additional loops that were not needed in the present implementation. Fortunately we don't have to update them instantly; they only have to be valid when |p| is the critical node in step~(2) of the algorithm. My solution is to introduce two additional fields for registration.'' Consider a sequence of nodesp_1$,$p_2$, \dots,$p_k$where$p_{j+1}$is the rightmost child of$p_j$for$1\le jscope| and we're done. Or if |q=u->registry|, where |registry| is a new field to be discussed further, again we update |q->scope|. Otherwise we conclude that |q| is not the top of the food chain to |p|. When the critical node |p| becomes passive, after a case where |q->scope| has been updated, we set |p->registry=q|, and |u->registry=NULL| in the case that |q=u->registry|. This handshaking passes the required information down the tree, and doesn't leave spurious non-null |registry| values that could lead to false diagnoses. A similar method works to maintain the |leaf| pointers, which are similar but based on leftmost instead of rightmost children. Instead of |p->registry|, I should have spoken of |p->scope_registry| and |p->leaf_registry|. (Whew.) @*Index.