An Algorithm for Enumerating All Spanning Trees of a Directed Graph Sanjiv Kapoor H. Rameshy Abstract We present an O(NV + V 3 ) time algorithm for enumerating all spanning trees of a directed graph. This improves the previous best known bound of O(NE + V + E ) ([1]) when V 2 = o(N ), which will be true for most graphs. Here, N refers to the number of spanning trees of a graph having V vertices and E edges. The algorithm is based on the technique of obtaining one spanning tree from another by a series of edge swaps. This result complements the result in the companion paper ([2]) which enumerates all spanning trees in an undirected graph in O(N + V + E ) time. Key words. spanning tree, directed graph, enumeration. 1 Introduction Spanning tree enumeration in directed graphs is an issue in some problems encountered in network and circuit analysis. Applications are given in ([3],pp 252-364). The previously best known algorithm due to Gabow and Myers ([1]) used a searching technique based on depth rst search and had a time complexity of O(NE + V + E ) and a space complexity of O(V + E ). Here N refers to the number of spanning trees of a directed graph with V vertices and E edges. The algorithm presented in this paper takes O(NV + V 3) time and O(V 2) space for enumerating all the spanning trees. While the space required is more than that in Gabow and Myers' algorithm ([1]), our algorithm is faster when V 2 = o(N ). Note that for most graphs, N will indeed be large. Our algorithm uses the paradigm followed in a companion paper ([2]) for enumerating spanning trees in undirected graphs. The main fact used is that a spanning tree can be obtained from another spanning tree by replacing edges in it by edges outside it. We rst obtain one spanning tree of the graph. Then edges external to it are characterized as back, cross and forward edges. A cross or forward edge may be exchanged for an edge in the spanning tree having the same tail, to result in a new spanning tree of the graph. Repeating this procedure for all cross and forward edges, gives all spanning trees which can be Indian y Indian Institute of Technology, Delhi. Institute of Science, Bangalore. Work done partly at the Indian Institute of Technology, Delhi. 1 obtained from the original spanning tree by an exchange of one edge. The entire procedure mentioned above is repeated with each of the new spanning trees obtained in order to generate all the spanning trees of the graph. To prevent repetitions, an inclusion/exclusion principle is used which eliminates certain edges from consideration while exchanging. The computation forest describing this procedure has a number of computation trees. Each tree in this forest enumerates arborescences rooted at one particular vertex of the graph. Each node in a particular computation tree represents an arborescence of the graph, and the arborescences associated with a node and its parent in the tree, dier in exactly one pair of edges. Our algorithm can output either all the spanning trees explicitly or in an implicit form (as in our companion paper on enumerating spanning trees in undirected graphs ([2])). The implicit form corresponds to outputting the rst spanning tree explicitly; the remaining spanning trees are then output as dierences from the previous spanning tree in the sequence of spanning trees to be output. The advantage of the implicit representation is that it is concise, i.e., has size O(N ). In either case, our algorithm has the same performance, as does the algorithm of Gabow and Myers ([1]). Section 2 gives some necessary denitions, section 3 gives an outline of the algorithm, section 4 describes the details of the scheme. 2 Denitions Let G be a directed graph with V vertices, E edges and N spanning trees (arborescences). A spanning tree of G rooted at v is a spanning tree of G having a unique path from v to every other vertex with edges directed away from the root v. Let N (v) denote the number P of spanning trees rooted at v. Then N = v G N (v). An exchange for a spanning tree T of G rooted at v is a pair of edges (e; f ) ,where e 2 T , f 2 E ? T and T ? feg [ ff g is a spanning tree rooted at v. An edge not in a spanning tree T is a back edge relative to T if its tail is an ancestor of its head in T , a forward edge if its tail is a descendant of its head in T , and a cross edge otherwise. Forward and cross edges are collectively referred to as non-back edges. The least common ancestor (lca) of two vertices in a tree is the last common vertex on the paths from the root to the two vertices. A vertex in a tree is considered to be an ancestor but not a proper ancestor of itself. Tail(f) and head(f) refer to the tail and head vertices of edge f in G, respectively. 2 3 Algorithm Outline In this section, we present an outline of the algorithm for generating all spanning trees of a directed graph. As in the rst part of our paper ([2]), a computation tree is generated as follows. The algorithm starts o with a directed spanning tree, T , rooted at particular vertex, v. All other spanning trees rooted at v are generated from T by exchanging non-tree edges with edges in T . All possible directed spanning trees may be generated by starting with directed spanning trees at each of the vertices. However note that the 2 exchanges in the directed case are more specic due to the nature of the non-tree edges as illustrated by the following properties. Property 1: Every non-back, non-tree edge, f , relative to a spanning tree T , may replace exactly one tree edge, e, in the spanning tree, namely the edge having the same tail, to result in a new spanning tree rooted at the same vertex as T . Property 2: A back edge can not be exchanged for any edge in the spanning tree to get a new spanning tree. To construct all spanning trees rooted at some particular vertex v, we consider the computation tree obtained by a search procedure using the principle of inclusion and exclusion. The search procedure starts o with a tree at the root of the computation tree. It then constructs sons of the node by the exchange property. >From properties 1 and 2, it follows that unlike the undirected case, the exchanges are limited to a single exchange for each non-back non-tree edge. More formally, let CD(G; v) be the computation tree which generates all spanning trees of the directed graph G with root v. At every node a of CD(G; v), there is a spanning tree SDa rooted at v. Node a has two sons b1 and b2. SDb1 is obtained from SDa by exchanging f with e, where f is a non-tree non-back edge and e is the unique tree edge with the same tail as f . SDb2 is the same as SDa. The signicance of b2 is that the subtree rooted at b2 will not include f in any spanning tree. This is captured by maintaining two sets, IN and OUT , with every node in the computation tree. INa is the set of all edges which must be present in the rooted spanning trees that correspond to nodes in the subtree rooted at the node a and OUTa represents all edges that are not present in the same spanning trees. The IN and OUT sets at sons b1 and b2 are obtained from the parent node a as follows: INb1 = INa [ ff g OUTb1 = OUTa [ feg [ fall edges in E incident upon tail(e)g ? ff g INb2 = INa OUTb2 = OUTa [ ff g The IN and OUT sets for the root node are empty. We show next that CD(G; v) suces to generate all directed spanning trees of G, rooted at vertex v. Lemma 3.1 CD(G; v) has at its nodes all directed spanning trees of G rooted at vertex v. Proof: The proof follows from induction and the inclusion/exclusion principle. Let a be the root node of the computation tree and let b1 and b2 be its left and right sons. Let SDb1 = SDa ? feg [ ff g. Then the computation subtree rooted at b2 forms the computation tree CD(G ? ff g; v). The computation subtree rooted at b1 generates all 3 directed spanning trees rooted at v, which include the edge f . Note that all edges in G with the same tail as f have been removed in this subtree. This removal is valid since no spanning tree rooted at v and including the edge f may include any of these edges e. 2 The algorithm for constructing the computation tree follows a recursive strategy. At the root we start with a directed spanning tree. In general at each node, a, we have a directed spanning tree. A non-back edge, f , is chosen to construct a new spanning tree by the exchange (e; f ) where e is the corresponding exchange edge given by property 1. The same procedure is repeated at the sons b1 and b2 of the node a in the computation tree as described above. Note that to construct the entire sub-computation tree rooted at each node a, one needs to nd the set of non- back, non-tree edges since every such edge leads to an exchange resulting in a new spanning tree. An exchange results in changing this set since some back edges may be converted to non-back or vice-versa. However we show that if we use a depth-rst tree as the starting spanning tree at the root node and order non-back edges by the postorder number of their tail vertices in the spanning tree then no non-back edges are converted to back-edges thus simplifying the changes that need to be determined. We use this strategy in our algorithm in the next section. 4 Algorithm Description. This section describes in detail an algorithm for generating all directed spanning trees of G rooted at a particular vertex r. The algorithm begins with a DFS tree of G (rooted at r) at the root of the computation tree CD(G; r). For each node a of the computation tree, two sets NB and B are maintained. NB is a set of those non-tree edges which are non-back w.r.t the directed spanning tree SDa and which are not included in OUTa . NB is maintained as a list of non-empty lists, with each list containing edges incident upon a particular vertex. The lists in NB are arranged in postorder number of the corresponding tail vertex in SDa. Note that insertion of a new edge in NB takes O(V ) time. B is a set of those back edges w.r.t SDa which are not included in OUTa and which may be useful, i.e., converted to non-back, at some proper descendant of a in the computation tree. At the root of CD(G; r), B contains all back edges w.r.t the spanning tree at the root. The data structure for B will be detailed later. Some of the back edges w.r.t SDa which have been identied at node a to be not useful in the above manner, constitute the set RBa . At every node in the computation tree, the rst edge in the rst list in NB is used as an exchange edge. The sets NB and B are updated at every exchange. The updates to these sets involve transferring edges from B to NB and removal of edges from B and NB . It is proved later that these changes suce and no changes from NB to B need to be made. The change of edges from B to NB is characterized by the following easily seen property: Property 3: Let spanning tree T be obtained from spanning tree T by applying the 0 exchange (e; f ). If x is a non-tree edge which is back w.r.t T and non-back w.r.t T , then 0 4 head(x) lies in the subtree of T rooted at tail(f ), and tail(x) is a vertex which is a proper ancestor of tail(f ) and a proper descendant of lca(head(f ); tail(f )) in T . Note that at a node a in the computation tree, the rst edge in the rst list in NB will have the tail with the least postorder number (with respect to the spanning tree at the root) among all edges in NB . The following claim will be important. It follows from the following 2 facts. First, the spanning tree at a diers from the spanning tree at the root in only that subtrees rooted at tails of edges used for exchanges at proper ancestors of a in the computation tree have been moved around. Second, none of the edges in NB at node a are incident on vertices in the above subtrees. Claim 1: At node a of the computation tree, the rst edge in the rst list in NB will have the tail with the least postorder number (with respect to the spanning tree at a) among all the edges in NB . We now give the algorithm. The algorithm ALGO Main computes a DFS tree T of the graph along with the sets NB and B . It then calls algorithm Gen to generate the entire computation tree in a depth-rst manner. Dropping subscripts, we use global sets IN and OUT . Their values at a node a of the computation tree will equal INa and OUTa, respectively. ALGO Main(G; r) T DFS tree of G rooted at vertex r; Initialize all data structures; Compute NB and B w.r.t T ; /* NB (B ) contains all edges that are non-back (back) w.r.t T */ IN ; OUT ; CHANGES ; If NB 6= then Gen(T ); End ALGO Main; As in ([2]), CHANGES is used to store the dierences from the last spanning tree generated. ALGO Gen(T ) /* The data structures B ,NB and CHANGES are global to this procedure, FL is local*/ While NB 6= do FL First list in NB ; f First edge in FL; Remove FL from NB ; e Unique exchange edge for f in T ; T T [ ff g ? feg; IN IN [ ff g; OUT OUT [ feg [ fFLg ? ff g; CHANGES CHANGES [ f(e; f )g; 0 5 Output CHANGES ; /*This outputs the dierences from the last tree generated*/ CHANGES ; Compute-back-to-non-back(f; T ); /*Computes edges in B which become non-back w.r.t T by virtue of the exchange (e; f ), removes them from B , inserts them into NB , stores the changes made to NB and B on STACK.*/ If NB 6= then Gen(T ); Undo changes to NB and B stored in STACK by the above call to Compute-back-to-non-back; If fFLg 6= then NB fFLg ? ff g [ NB ; /* FL with edge f removed is restored as the rst list of NB */ IN IN ? ff g; OUT OUT ? feg ? fedges in FLg [ ff g; CHANGES CHANGES [ f(f; e)g; End While; End ALGO Gen; Before we describe the procedure Compute-back-to-non-back which updates the sets NB and B when an exchange (e; f ) is made on T , we describe the data structures required to facilitate these updates. The data structure for NB was described earlier. We now describe the data structure for B . We maintain at each node v of G, a list B [v] of non-tree back edges in B having head vertex v. This list is ordered by decreasing depth of the tail vertices (i.e., in order of increasing distance from v) of these edges in T . Each list is indexed by an array A[v] such that the pth element of the array points to the rst edge in its list, if any, which is incident upon a proper ancestor of node p; if no such edge exists then A[v][p] is undened. Each array has a base vertex BASE [v] associated with it. Any edge in the list B [v] which is incident upon a descendant of BASE [v] is redundant for future computations in the current computation subtree. In the initialization step, ALGO Main constructs the data structures B and A and sets BASE [v] to v, for all vertices v. The procedure for updating B is as follows: Let f be the exchange edge (u; v). Let a be the lca of u and v in T . To compute all edges which have their heads in a subtree rooted at node v and tails on the path from a to v (a and v excluded) in T , the vertices in the subtree rooted at v are scanned and all the back edges satisfying the above property are removed from the back edge lists. This is done by accessing the array A[w] for each vertex w in the subtree rooted at v which satises the property that BASE [w] is a descendant of a in T . For each such vertex w, let jw be one of the two vertices, BASE [w] or v, whichever is closer to the root. Then B [w] is traversed starting from A[w][jw] till a back edge whose tail is an ancestor of a is found. All but the last of the edges traversed above need to be removed from B and transferred to NB . The removal of these edges is simply eected by setting BASE [w] to a if BASE [w] is not an ancestor of a. Note that in this process, back edges which occur before A[w][v] in the list B [w] are also implicitly removed from B . We note that these edges have both their head and tail in the subtree of T rooted at v. Since future descendant nodes of the computation tree are constructed by a postorder scan of 0 0 6 the spanning tree T , the subtree of T rooted at v is never used again for exchanges of tree edges. These edges are thus redundant and constitute the set RB at the current node of the computation tree. We further note that the edges in B are not removed explicitly but only by updating the base pointer. This helps in restoring the back edges at the end of the recursive step in the algorithm. Restoration is now done by simply resetting the base vertex to its previous value which is stored in STACK before recursion. To update NB whenever an exchange is made, each edge removed from B and having its tail on the path from v to a (both endpoints excluded) is added to NB in O(V ) time per edge; this insertion is performed by searching NB in the obvious way for the tail vertex of this edge. The consequent changes made to NB are also stored in STACK . We now give the procedure Compute-back-to-non-back. ALGO Compute-back-to-non-back(f; T ); /* Let f be the edge (u; v)*/ a lca(u; v) in T ; For every vertex w in the subtree of T rooted at v do If BASE [w] is a descendant of a in T then j higher numbered of the two vertices v; BASE [w] in T in the postorder scheme; Let e be the edge in list B [w] that is pointed to by A[w][j ]; While e is dened and tail(e) is not ancestor of a do Insert e into NB ; e Next edge in B [w]; End While; BASE [w] One of a, BASE [w], whichever has the higher postorder number; /* This deletes the new non-back edges from B */ End If; Store changes made above to B and NB on STACK ; End For; End Compute-back-to-non-back; A Remark on the Data Structures. Note that for each vertex v, the array A[v] and the list B [v] does not change explicitly over the course of the algorithm, only the variable BASE [v] does. It can also be seen that BASE [v] is always an ancestor of v in the spanning tree at every node x in the computation tree. Lemma 4.2 will show that just changing BASE [v] suces in maintaining B correctly. 4.1 Correctness We need the following property to prove correctness. Property 4: If f = (u; v) is an edge in NB and b is the lca of u and v in SDa then no edge in NB has its head in the subtree of SDa rooted at v and its tail on the path from b 7 to u. (b excluded) Proof. This property is essentially true because the depth rst nature of the tree is maintained at every node of CD(G; r) due to the order of selection of the exchange edge from NB . Dene an edge to be eligible at node x of the computation tree if it is not in SDx or OUTx and is non-back w.r.t SDx. It suces to show that at any node x of the computation tree, all eligible edges must connect a vertex with higher postorder number (w.r.t SDx ) to a vertex with lower postorder number. We show this by induction on the level of x. This is true for the root node of the computation tree because the spanning tree at that node is the DFS tree of G. Assume that this is true for any node x of the computation tree. First consider the left son b1 of x. Let f = (u; v) be the exchange edge at x and e be the edge in SDx incident upon v. By the choice of f and Claim 1, tail(f ) has the smallest postorder number (with respect to SDx) among the tails of all eligible edges at x. Hence, none of these edges can be incident upon the subtree of SDx rooted at v. After the exchange, the DFS tree changes since the subtree rooted at v in SDx moves its root. This aects the DFS numbering but leaves intact the relative ordering of all non-back edges since none of these edges are incident on vertices in the subtree of SDx rooted at v. Thus in SDb1 = SDx ? feg [ ff g, all edges that are eligible at both b1 and x satisfy the property that the postorder number of their tail is lower than the postorder number of their head w.r.t SDb1 . Further, the only edges that are eligible at b1 but not at x are those whose head lies in the subtree of SDx rooted at v and whose tails lie on the path P from v to lca(u; v) (Property 3). By the induction hypothesis, tail(f ) has lower postorder number than head(f ) in SDx . After e is exchanged for f , the postorder numbers of all vertices in the subtree of SDb1 rooted at v remains greater than the postorder number of the vertices in P . For the right son b2 of x, this is trivially true. This proves the property. 2 In order to prove correctness of our algorithm, we rst prove that the set of crucial edges, i.e, the set of non-back non-tree edges NB is maintained correctly at each node in the computation. This is shown by the following lemma: Lemma 4.1 During the construction of the computation tree the following changes to NB and B suce after an exchange at a node a in the computation tree:(a) changes from B to NB by property 3. (b) deletions from NB according to IN and OUT denitions. (c) removal of RBa from B . Proof: Property 4 implies that no edge needs to be transferred from NB to B following an exchange. To complete the proof of the lemma, observe that all possible changes to be made to B and NB , except the ones ruled out by property 4, are made in (a) (b) and (c). 2 The following property follows from the above lemma and the fact that at each node, the edge in NB with smallest postorder number of its tail vertex is chosen for swapping in. It justies the removal of RBx from B at any node x of the computation tree. Property 5: If an exchange (e; f ), f = (u; v) is made at a node x of the computation tree then the subtree of SDx rooted at v is preserved as such in each of the trees generated 8 at descendant nodes of x in the computation tree. So at node x, any edge in B incident upon a vertex in that subtree, is redundant for future computations at descendant nodes of x and may be removed from B . Lemma 4.2 Given NB and B for any node a in the computation tree, ALGO Gen correctly computes the sets NB and B for the 2 sons, b1 and b2, of a. Proof: We consider the sons b1 and b2 separately. For b1 it follows from Lemma 4.1 that the following changes need to be made to NB and B (a) Changes from NB to B by property 3. (b) Removal of edges from NB according to IN and OUT denitions. (c) Removal of RBb1 from B . Let SDb1 = SDa ? feg [ ff g. Let f = (u; v). We note that f has been chosen as the rst edge in NB , i.e. the rst edge in the postordering of the edges by their tail vertex. Compute-back-to-non-back uses property 3 to identify and remove from B the edges which are either converted from B to NB or are redundant for further computations. This is done correctly by the scan of the back edge list at each of the vertices in the subtree rooted at v. For the head vertex s of every edge which goes from B to NB , the value of BASE [s] is set to the lca of u and v, if it is an ancestor of the current value of BASE [s]. This implicitly deletes those edges from the data structure B [s] which either go into NB or into RBb1 . This is justied by properties 4 and 5 and Lemma 4.1 which ensure that no back edges lead from the subtree rooted at v to a vertex on the path from u to the lca of u and v. By the same justication, also, the array A[w] is correctly maintained for index values BASE [w] for all w. Note that only vertices which are ancestors of BASE [w] are considered when scanning back edges and BASE [w] only changes for vertices in the subtree rooted at v. Furthermore, a back edge incident onto a vertex which is an ancestor of BASE [w] remains a back edge. And, no new back edges are introduced. Thus no changes are required to A[w]. Also note that edges which switch from B to NB at node b1 are inserted into their correct postorder positions in NB . This is because the exchange (e; f ) does not change the relative postorder number of vertices outside the subtree of SDb1 rooted at v, and the edges inserted are not incident on a vertex in that subtree. Gen also implements the other changes required, i.e., the removal of those edges from NB which go into the OUT set of b1. For node b2, the only changes are deletions according to the IN and OUT denitions which are correctly implemented. 2 We can now state that ALGO Gen correctly computes all spanning trees without repetitions. Theorem 4.3 ALGO Main generates the entire computation tree CD(G; r) correctly. Proof: Follows from Lemma 4.2. 2 9 4.2 Complexity For the purpose of this section we dene a compressed version CD (G; r) of of CD(G; r). This is done to take care of the fact that the son b2 does not explicitly generate a tree but is used as a node which eliminates a non-back edge. In fact, starting at a node x of CD(G; r), the entire rightmost path has nodes of this nature. We can thus compress this path so that the left sons of the nodes along this path are now the sons of node x. We thus obtain at each node x, sons corresponding to all trees which can be obtained by one exchange from SDx , maintaining the IN and OUT set restrictions as required by the path. 0 Lemma 4.4 ALGO Main outputs the 2changes corresponding to the compressed compu- tation tree CD (G; r) in O(N (r)V + V ) operations. 0 Proof: At each node x of the computation tree, the major work that ALGO Gen performs is the conversion of back edges to non-back and removal of back edges. We let NBCx be the number of edges that are converted from back to non-back at node x. The time for manipulating the data structures NB , B and STACK (which includes the time for undoing changes after recursion) at node x is O(V (NBCx + 1)). This is because the procedure Compute-back-to-non-back takes just constant time for each vertex in the subtree rooted at v (for f = (u; v)), apart from the time to determine edges that are converted from back to non-back. The time for determining these edges is proportional to their number. The time for inserting each of these into NB is O(V ). All other operations in ALGO Gen, except the output operation, require O(V )Ptime. So the total time required by ALGO Gen minus the output operations equals O( (V NBCx)), the summation being over all nodes of the computation tree. Note that at any node x in the compressed computation tree, an edge which is converted from back to non-back is used as an exchange edge at some descendant of x and hence leads to a new spanning tree. Therefore, the the above summation gives an O(N (r)V ) time bound. Total output operations are O(N (r)) since only exchanges are output and the number of exchanges output is proportional to the size of the computation tree. Computing B and NB initially require O(V 2) time. Computing the DFS tree at the root of the computation tree requires O(V+E) time. The time bound follows. 2 Lemma 4.5 ALGO Main requires O(V 2) space. Proof: Follows from the size of the data structures involved. B requires O(V 2) space (in particular, the arrays A require this much space). NB requires O(V+E) space. CHANGES requires O(V ) space at most since any spanning tree may be obtained by any other spanning tree by at most V ? 1 exchanges. NB ,B and CHANGES are global to procedure Gen, hence are not stacked on recursion. At every node x of the computation tree, the size of the changes stored on STACK is O(V + NBCx) space. We note that the changes at a recursive step are undone and this eliminates need of copying data structures at the expense of a constant factor increase in number of operations. Since the computation tree CD(G; r) is generated in a depth rst manner and changes are added to STACK 10 only at the left branches, which are O(V ) in number along any path of the computation tree, the total stack space required is O(V 2 +NBCx) where the summation is taken over all nodes on the path to a leaf node. From Lemma 4.1 it follows that NBCx is O(E ) since no edge moves back from NB to B along the path. The theorem thus follows. 2 From the above lemmas the following result follows if the above procedure is repeated with each vertex in turn as root. Theorem2 4.6 All rooted directed spanning trees can be output in O(NV + V 3) operations and O(V ) space. 5 Conclusion An improved algorithm has been obtained for enumerating spanning trees in directed graphs. This algorithm takes O(NV + V 3) time and O(V 2) space for generating the computation tree and outputting relative changes between spanning trees of a directed graph. This betters the previous best time of O(NE ) achieved by Gabow and Myers' algorithm ([1]). Explicit enumeration, if desired, can be done by traversing the computation tree in an optimal O(NV ) time. Our algorithm is not optimal for constructing the computation tree itself because no lower bound other than the trivial O(N ) bound is known. The existence of an O(N ) algorithm for the same remains an open question. An improved algorithm was presented in a paper by the two authors along with another co-author in the Workshop on Algorithms and Data Structures, 1995 ([5]). However, this algorithm seems to have a bug which has not been removed as of now. 11 References ([1]) H. N. Gabow and E. W. Myers: Finding all spanning trees of directed and undirected graphs, SIAM J. Comp., Vol 7, No. 3, Aug 1978. ([2]) S. Kapoor and H. Ramesh: Algorithms for generating all spanning trees of undirected and weighted graphs, SIAM J. Comp., Vol 24, No. 2, 1995. ([3]) W. Mayeda: Graph Theory, John Wiley, NY 1972. ([4]) S. Shinoda: Finding all possible directed trees of a directed graph, Electron Commun., Japan, 51-A, 1968, pp. 45-47. ([5]) S. Kapoor, V. Kumar, H. Ramesh: An algorithm for generating all spanning trees of directed graphs, Proceedings of Workshop on Algorithms and Data Structures, 1995. 12
© Copyright 2026 Paperzz