1"""
2An algorithm for finding if two undirected trees are isomorphic,
3and if so returns an isomorphism between the two sets of nodes.
4
5This algorithm uses a routine to tell if two rooted trees (trees with a
6specified root node) are isomorphic, which may be independently useful.
7
8This implements an algorithm from:
9The Design and Analysis of Computer Algorithms
10by Aho, Hopcroft, and Ullman
11Addison-Wesley Publishing 1974
12Example 3.2 pp. 84-86.
13
14A more understandable version of this algorithm is described in:
15Homework Assignment 5
16McGill University SOCS 308-250B, Winter 2002
17by Matthew Suderman
18http://crypto.cs.mcgill.ca/~crepeau/CS250/2004/HW5+.pdf
19"""
20
21from collections import defaultdict
22
23import networkx as nx
24from networkx.utils.decorators import not_implemented_for
25
26__all__ = ["rooted_tree_isomorphism", "tree_isomorphism"]
27
28
29@nx._dispatchable(graphs={"t1": 0, "t2": 2}, returns_graph=True)
30def root_trees(t1, root1, t2, root2):
31 """Create a single digraph dT of free trees t1 and t2
32 # with roots root1 and root2 respectively
33 # rename the nodes with consecutive integers
34 # so that all nodes get a unique name between both trees
35
36 # our new "fake" root node is 0
37 # t1 is numbers from 1 ... n
38 # t2 is numbered from n+1 to 2n
39 """
40
41 dT = nx.DiGraph()
42
43 newroot1 = 1 # left root will be 1
44 newroot2 = nx.number_of_nodes(t1) + 1 # right will be n+1
45
46 # may be overlap in node names here so need separate maps
47 # given the old name, what is the new
48 namemap1 = {root1: newroot1}
49 namemap2 = {root2: newroot2}
50
51 # add an edge from our new root to root1 and root2
52 dT.add_edge(0, namemap1[root1])
53 dT.add_edge(0, namemap2[root2])
54
55 for i, (v1, v2) in enumerate(nx.bfs_edges(t1, root1)):
56 namemap1[v2] = i + namemap1[root1] + 1
57 dT.add_edge(namemap1[v1], namemap1[v2])
58
59 for i, (v1, v2) in enumerate(nx.bfs_edges(t2, root2)):
60 namemap2[v2] = i + namemap2[root2] + 1
61 dT.add_edge(namemap2[v1], namemap2[v2])
62
63 # now we really want the inverse of namemap1 and namemap2
64 # giving the old name given the new
65 # since the values of namemap1 and namemap2 are unique
66 # there won't be collisions
67 namemap = {}
68 for old, new in namemap1.items():
69 namemap[new] = old
70 for old, new in namemap2.items():
71 namemap[new] = old
72
73 return (dT, namemap, newroot1, newroot2)
74
75
76@nx._dispatchable(graphs={"t1": 0, "t2": 2})
77def rooted_tree_isomorphism(t1, root1, t2, root2):
78 """
79 Return an isomorphic mapping between rooted trees `t1` and `t2` with roots
80 `root1` and `root2`, respectively.
81
82 These trees may be either directed or undirected,
83 but if they are directed, all edges should flow from the root.
84
85 It returns the isomorphism, a mapping of the nodes of `t1` onto the nodes
86 of `t2`, such that two trees are then identical.
87
88 Note that two trees may have more than one isomorphism, and this
89 routine just returns one valid mapping.
90 This is a subroutine used to implement `tree_isomorphism`, but will
91 be somewhat faster if you already have rooted trees.
92
93 Parameters
94 ----------
95 t1 : NetworkX graph
96 One of the trees being compared
97
98 root1 : node
99 A node of `t1` which is the root of the tree
100
101 t2 : NetworkX graph
102 The other tree being compared
103
104 root2 : node
105 a node of `t2` which is the root of the tree
106
107 Returns
108 -------
109 isomorphism : list
110 A list of pairs in which the left element is a node in `t1`
111 and the right element is a node in `t2`. The pairs are in
112 arbitrary order. If the nodes in one tree is mapped to the names in
113 the other, then trees will be identical. Note that an isomorphism
114 will not necessarily be unique.
115
116 If `t1` and `t2` are not isomorphic, then it returns the empty list.
117
118 Raises
119 ------
120 NetworkXError
121 If either `t1` or `t2` is not a tree
122 """
123
124 if not nx.is_tree(t1):
125 raise nx.NetworkXError("t1 is not a tree")
126 if not nx.is_tree(t2):
127 raise nx.NetworkXError("t2 is not a tree")
128
129 # get the rooted tree formed by combining them
130 # with unique names
131 (dT, namemap, newroot1, newroot2) = root_trees(t1, root1, t2, root2)
132
133 # Group nodes by their distance from the root
134 L = defaultdict(list)
135 for n, dist in nx.shortest_path_length(dT, source=0).items():
136 L[dist].append(n)
137
138 # height
139 h = max(L)
140
141 # each node has a label, initially set to 0
142 label = {v: 0 for v in dT}
143 # and also ordered_labels and ordered_children
144 # which will store ordered tuples
145 ordered_labels = {v: () for v in dT}
146 ordered_children = {v: () for v in dT}
147
148 # nothing to do on last level so start on h-1
149 # also nothing to do for our fake level 0, so skip that
150 for i in range(h - 1, 0, -1):
151 # update the ordered_labels and ordered_children
152 # for any children
153 for v in L[i]:
154 # nothing to do if no children
155 if dT.out_degree(v) > 0:
156 # get all the pairs of labels and nodes of children and sort by labels
157 # reverse=True to preserve DFS order, see gh-7945
158 s = sorted(((label[u], u) for u in dT.successors(v)), reverse=True)
159
160 # invert to give a list of two tuples
161 # the sorted labels, and the corresponding children
162 ordered_labels[v], ordered_children[v] = list(zip(*s))
163
164 # now collect and sort the sorted ordered_labels
165 # for all nodes in L[i], carrying along the node
166 forlabel = sorted((ordered_labels[v], v) for v in L[i])
167
168 # now assign labels to these nodes, according to the sorted order
169 # starting from 0, where identical ordered_labels get the same label
170 current = 0
171 for i, (ol, v) in enumerate(forlabel):
172 # advance to next label if not 0, and different from previous
173 if (i != 0) and (ol != forlabel[i - 1][0]):
174 current += 1
175 label[v] = current
176
177 # they are isomorphic if the labels of newroot1 and newroot2 are 0
178 isomorphism = []
179 if label[newroot1] == 0 and label[newroot2] == 0:
180 # now lets get the isomorphism by walking the ordered_children
181 stack = [(newroot1, newroot2)]
182 while stack:
183 curr_v, curr_w = stack.pop()
184 isomorphism.append((curr_v, curr_w))
185 stack.extend(zip(ordered_children[curr_v], ordered_children[curr_w]))
186
187 # get the mapping back in terms of the old names
188 # return in sorted order for neatness
189 isomorphism = [(namemap[u], namemap[v]) for (u, v) in isomorphism]
190
191 return isomorphism
192
193
194@not_implemented_for("directed")
195@not_implemented_for("multigraph")
196@nx._dispatchable(graphs={"t1": 0, "t2": 1})
197def tree_isomorphism(t1, t2):
198 """
199 Return an isomorphic mapping between two trees `t1` and `t2`.
200
201 If `t1` and `t2` are not isomorphic, an empty list is returned.
202 Note that two trees may have more than one isomorphism, and this routine just
203 returns one valid mapping.
204
205 Parameters
206 ----------
207 t1 : undirected NetworkX graph
208 One of the trees being compared
209
210 t2 : undirected NetworkX graph
211 The other tree being compared
212
213 Returns
214 -------
215 isomorphism : list
216 A list of pairs in which the left element is a node in `t1`
217 and the right element is a node in `t2`. The pairs are in
218 arbitrary order. If the nodes in one tree is mapped to the names in
219 the other, then trees will be identical. Note that an isomorphism
220 will not necessarily be unique.
221
222 If `t1` and `t2` are not isomorphic, then it returns the empty list.
223
224 Raises
225 ------
226 NetworkXError
227 If either `t1` or `t2` is not a tree
228
229 Notes
230 -----
231 This runs in ``O(n*log(n))`` time for trees with ``n`` nodes.
232 """
233 if not nx.is_tree(t1):
234 raise nx.NetworkXError("t1 is not a tree")
235 if not nx.is_tree(t2):
236 raise nx.NetworkXError("t2 is not a tree")
237
238 # To be isomorphic, t1 and t2 must have the same number of nodes and sorted
239 # degree sequences
240 if not nx.faster_could_be_isomorphic(t1, t2):
241 return []
242
243 # A tree can have either 1 or 2 centers.
244 # If the number doesn't match then t1 and t2 are not isomorphic.
245 center1 = nx.center(t1)
246 center2 = nx.center(t2)
247
248 if len(center1) != len(center2):
249 return []
250
251 # If there is only 1 center in each, then use it.
252 if len(center1) == 1:
253 return rooted_tree_isomorphism(t1, center1[0], t2, center2[0])
254
255 # If there both have 2 centers, then try the first for t1
256 # with the first for t2.
257 attempts = rooted_tree_isomorphism(t1, center1[0], t2, center2[0])
258
259 # If that worked we're done.
260 if len(attempts) > 0:
261 return attempts
262
263 # Otherwise, try center1[0] with the center2[1], and see if that works
264 return rooted_tree_isomorphism(t1, center1[0], t2, center2[1])