有向无环图的哈希值
Posted
技术标签:
【中文标题】有向无环图的哈希值【英文标题】:Hash value for directed acyclic graph 【发布时间】:2013-01-10 00:40:44 【问题描述】:如何将有向无环图转换为散列值,以使任意两个同构图散列到相同的值?两个同构图散列到不同的值是可以接受的,但不可取,这就是我在下面的代码中所做的。我们可以假设图中的顶点数最多为 11 个。
我对 Python 代码特别感兴趣。
这就是我所做的。如果self.lt
是从节点到后代(不是孩子!)的映射,那么我会根据修改后的拓扑排序重新标记节点(如果可以的话,它更喜欢首先对具有更多后代的元素进行排序)。然后,我对排序后的字典进行哈希处理。一些同构图会散列成不同的值,尤其是随着节点数量的增长。
我已经包含了所有代码来激发我的用例。我正在计算找到 7 个数字的中位数所需的比较次数。同构图哈希到相同值的次数越多,需要重做的工作就越少。我考虑过先放置更大的连接组件,但没有看到如何快速做到这一点。
from tools.decorator import memoized # A standard memoization decorator
class Graph:
def __init__(self, n):
self.lt = i: set() for i in range(n)
def compared(self, i, j):
return j in self.lt[i] or i in self.lt[j]
def withedge(self, i, j):
retval = Graph(len(self.lt))
implied_lt = self.lt[j] | set([j])
for (s, lt_s), (k, lt_k) in zip(self.lt.items(),
retval.lt.items()):
lt_k |= lt_s
if i in lt_k or k == i:
lt_k |= implied_lt
return retval.toposort()
def toposort(self):
mapping =
while len(mapping) < len(self.lt):
for i, lt_i in self.lt.items():
if i in mapping:
continue
if any(i in lt_j or len(lt_i) < len(lt_j)
for j, lt_j in self.lt.items()
if j not in mapping):
continue
mapping[i] = len(mapping)
retval = Graph(0)
for i, lt_i in self.lt.items():
retval.lt[mapping[i]] = mapping[j]
for j in lt_i
return retval
def median_known(self):
n = len(self.lt)
for i, lt_i in self.lt.items():
if len(lt_i) != n // 2:
continue
if sum(1
for j, lt_j in self.lt.items()
if i in lt_j) == n // 2:
return True
return False
def __repr__(self):
return("[]".format(", ".join(": ".format(
i,
", ".join(str(x) for x in lt_i))
for i, lt_i in self.lt.items())))
def hashkey(self):
return tuple(sorted(k: tuple(sorted(v))
for k, v in self.lt.items().items()))
def __hash__(self):
return hash(self.hashkey())
def __eq__(self, other):
return self.hashkey() == other.hashkey()
@memoized
def mincomps(g):
print("Calculating:", g)
if g.median_known():
return 0
nodes = g.lt.keys()
return 1 + min(max(mincomps(g.withedge(i, j)),
mincomps(g.withedge(j, i)))
for i in nodes
for j in nodes
if j > i and not g.compared(i, j))
g = Graph(7)
print(mincomps(g))
【问题讨论】:
Goggling “图的哈希值”让我看到了 Ashish Kundu 和 Elisa Bertino 撰写的这篇有趣的论文,名为“On Hashing Graph”,关于哈希 DAG 的解决方案(使用 2 O(1) 操作) .我还没有达到可以将这篇论文提炼成你问题的答案的水平,但我读起来很开心:) 还有一个叫做“Merkle Signature Schemes”的东西,如果有帮助的话,Kundu & Bertino 论文网站可以作为起始解决方案 顶点或边上是否有标签?如果不是,同构图是否应该散列到相同的值? 需要散列唯一,还是通常唯一? (后者是 Python 对象散列函数所需的全部内容。) @NeilG 您是否暗示您正在使用一种可以有效检测两个图是否同构(GI)的算法?你知道不知道 GI 是在P
还是NP
(假设NP != P
),对吧?我不知道有什么比 nauty (cs.anu.edu.au/~bdm/nauty) 更正确的事情。我记得几年前的一些事情证明了 GI 在 P
中(作者还包括了一个 O(n^5)
算法),但是证明是有缺陷的,我不确定它是否最终被发布。
【参考方案1】:
为了有效地测试图同构,您需要使用nauty。专门针对 Python 有包装器pynauty,但我无法证明它的质量(为了正确编译它,我必须对其setup.py
进行一些简单的修补)。如果这个包装器一切正常,那么它会为您感兴趣的用途大大简化 nauty 并且只需散列 pynauty.certificate(somegraph)
- 这对于同构图将是相同的值。
一些快速测试表明pynauty
为每个图提供相同的证书(具有相同数量的顶点)。但这只是因为将图形转换为 nauty 格式时包装器中的一个小问题。修复此问题后,它对我有用(我还使用了http://funkybee.narod.ru/graphs.htm 的图表进行比较)。这是一个简短的补丁,它还考虑了setup.py
中所需的修改:
diff -ur pynauty-0.5-orig/setup.py pynauty-0.5/setup.py
--- pynauty-0.5-orig/setup.py 2011-06-18 20:53:17.000000000 -0300
+++ pynauty-0.5/setup.py 2013-01-28 22:09:07.000000000 -0200
@@ -31,7 +31,9 @@
ext_pynauty = Extension(
name = MODULE + '._pynauty',
- sources = [ pynauty_dir + '/' + 'pynauty.c', ],
+ sources = [ pynauty_dir + '/' + 'pynauty.c',
+ os.path.join(nauty_dir, 'schreier.c'),
+ os.path.join(nauty_dir, 'naurng.c')],
depends = [ pynauty_dir + '/' + 'pynauty.h', ],
extra_compile_args = [ '-O4' ],
extra_objects = [ nauty_dir + '/' + 'nauty.o',
diff -ur pynauty-0.5-orig/src/pynauty.c pynauty-0.5/src/pynauty.c
--- pynauty-0.5-orig/src/pynauty.c 2011-03-03 23:34:15.000000000 -0300
+++ pynauty-0.5/src/pynauty.c 2013-01-29 00:38:36.000000000 -0200
@@ -320,7 +320,7 @@
PyObject *adjlist;
PyObject *p;
- int i,j;
+ Py_ssize_t i, j;
int adjlist_length;
int x, y;
【讨论】:
我想我有责任选择赏金的获胜者,所以如果这个答案对@NeilG 有效,那么它对我也有效,但是对这个问题的总体回答对于我学习更高级的计算机科学主题(我最近一直在研究图表)。我觉得 SO 可以像任何主题的临时课堂讨论一样,我所花费的只是一周对其他问题的正确答案,我喜欢这个网站 :)【参考方案2】:Graph isomorphism for directed acyclic graphs is still GI-complete. 因此,目前没有已知的(最坏情况下的次指数)解决方案来保证两个同构有向无环图将产生相同的散列。只有当不同图之间的映射是已知的——例如,如果所有顶点都有唯一的标签——才能有效地保证匹配哈希。
好的,让我们对少数顶点进行暴力破解。我们必须找到一个独立于输入中顶点顺序的图表示,从而保证同构图产生相同的表示。此外,这种表示必须确保没有两个非同构图产生相同的表示。
最简单的解决方案是为所有 n 构造邻接矩阵!顶点的排列,并将邻接矩阵解释为 n2 位整数。然后我们可以选择最小或最大的数字作为规范表示。这个数字完全编码了图,因此确保没有两个非同构图产生相同的数字 - 可以将此函数视为perfect hash function。并且因为我们选择在所有可能的顶点排列下对图进行编码的最小或最大数字,我们进一步确保同构图产生相同的表示。
在 11 个顶点的情况下,这是好还是坏?好吧,表示将有 121 位。我们可以将其减少 11 位,因为表示循环的对角线在非循环图中将全为零,并剩下 110 位。这个数字理论上可以进一步减少;并非所有剩余的 2110 个图都是非循环的,每个图最多可能有 11 个! - 大约 225 - 同构表示,但实际上这可能很难做到。有人知道如何计算具有 n 个顶点的不同有向无环图的数量吗?
需要多长时间才能找到此表示?天真11!或 39,916,800 次迭代。这不是什么,可能已经不切实际,但我没有实施和测试它。但我们可能可以加快一点速度。如果我们通过从左到右连接从上到下的行来将邻接矩阵解释为整数,我们希望第一行左侧有很多个(零)以获得大(小)数。因此,我们选择具有最大(最小)度(取决于表示的入度或出度)的一个(或顶点之一)作为第一个顶点,而不是在后续位置连接(未连接)到该顶点的顶点以使这些顶点(零) ) 到左边。
修剪搜索空间的可能性可能更多,但我不确定是否有足够的方法使这成为一个实用的解决方案。也许有或者也许其他人至少可以在这个想法的基础上建立一些东西。
【讨论】:
谢谢,这是一个非常有用的答案。但是,如果顶点的数量最多为 11 个,那么指数时间就可以满足我的目的:)hash(input) = 1
诚然有很多冲突,但它是次指数的,任何两个同构的有向无环图都会产生相同的哈希。
由于有向无环图允许拓扑排序,您可以假设顶点是拓扑排序的,因此邻接矩阵在对角线下方只有零。为您迄今为止的工作 +1。
执行拓扑排序推断对顶点进行排序以最大化(最小化)表示。也许人们可以尝试仅考虑拓扑排序来找到最大(最小)表示,但我无法确定这种约束是否会使问题变得更容易或更难。
这更容易,因为您可以将空闲位数减少到 55。【参考方案3】:
哈希必须有多好?我假设您确实不想要图形的完整序列化。散列很少保证没有第二个(但不同的)元素(图)评估为相同的散列。如果对您来说非常重要的是,同构图(以不同的表示形式)具有相同的散列,那么只使用在表示变化下不变的值。例如:
节点总数 (定向)连接总数 对于任何元组(i,j)
到(max(indegree), max(outdegree))
的具有(indegree, outdegree) = (i,j)
的节点总数(或者对于不超过某个固定值(m,n)
的元组来说是有限的)
所有这些信息都可以在 O(#nodes) [假设图形存储正确] 中收集。将它们连接起来,你就有了一个哈希值。如果您愿意,可以在这些连接信息上使用一些众所周知的哈希算法,例如 sha
。如果没有额外的散列,它是一个连续散列(它允许找到相似的图),如果选择的散列算法具有这些属性,那么它是统一的并且大小固定。
事实上,注册任何添加或删除的连接已经足够好了。它可能会丢失已更改的连接(a -> c
而不是 a -> b
)。
这种方法是模块化的,可以根据需要进行扩展。包含的任何其他属性都将减少冲突的数量,但会增加获取哈希值所需的工作量。更多想法:
与上述相同,但具有二阶进出度。 IE。node->child->child
链可以到达的节点数(= 二阶外度)或分两步通向给定节点的节点数。
或更一般的 n 阶进出度(可以在 O((average-number-of-connections) ^ (n-1) * #nodes) 中计算)
eccentricity = x 的节点数(同样适用于任何 x)
如果节点存储任何信息(除了它们的邻居),则使用所有节点内容的任何类型哈希的xor
。由于xor
添加到哈希的节点的特定顺序无关紧要。
您要求“唯一的哈希值”,显然我无法为您提供。但我认为术语“散列”和“每个图的唯一”是相互排斥的(当然不完全正确),并决定回答“散列”部分而不是“唯一”部分。一个“唯一哈希”(perfect hash)基本上需要对图进行全序列化(因为哈希中存储的信息量必须反映图中的信息总量)。如果这确实是您想要的,只需定义一些唯一的节点顺序(例如,按自己的出度排序,然后按入度排序,然后按子项的出度等,直到顺序明确)并以任何方式序列化图形(使用位置在前面提到的排序作为节点的索引)。
当然,这要复杂得多。
【讨论】:
我显然没有很好地表达我的问题,我试图改写它以消除任何混淆。如果现在有意义,请告诉我…… @NeilG “两个同构图散列到不同的值是可以接受的,但不可取” - 但是两个不同的图具有相同的散列完全可以接受吗? @NeilG 好的。然后我会坚持我的答案=)【参考方案4】:几年前,我为这个问题创建了一个简单而灵活的算法(通过散列在化学结构数据库中找到重复的结构)。
我将其命名为“Powerhash”,创建算法需要两个见解。第一个是幂迭代图算法,也用于PageRank。第二个是能够用我们想要的任何东西替换幂迭代的内部阶跃函数。我将其替换为在每个步骤和每个节点上执行以下操作的函数:
对节点邻居的哈希进行排序 散列连接的排序散列在第一步中,节点的哈希值会受到其直接邻居的影响。第二步,一个节点的哈希值受到距离它 2 跳的邻居的影响。在第 N 步,节点的哈希值将受到其周围 N 跳的邻域影响。所以你只需要继续运行 N = graph_radius 步骤的 Powerhash。最终,图中心节点的哈希值会受到整个图的影响。
要生成最终哈希,请对最后一步的节点哈希进行排序并将它们连接在一起。之后,您可以比较最终的哈希值来确定两个图是否同构。如果您有标签,则将它们添加到您为每个节点(以及每个步骤)计算的内部哈希中。
关于这方面的更多信息,你可以在这里查看我的帖子:
https://plus.google.com/114866592715069940152/posts/fmBFhjhQcZF
上面的算法是在“madIS”函数关系数据库中实现的。你可以在这里找到算法的源代码:
https://github.com/madgik/madis/blob/master/src/functions/aggregate/graph.py
【讨论】:
【参考方案5】:恕我直言,如果可以对图形进行拓扑排序,那么存在非常简单的解决方案。
-
对于索引为 i 的每个顶点,您可以构建他的(排序的)直接邻居的唯一哈希(例如,使用字符串的哈希技术)(如果顶点 1 有直接邻居 43,23,2,7,则为 pe) ,12,19,334 散列函数应该散列 2,7,12,19,23,43,334 的数组)
对于整个 DAG,您可以创建一个哈希,作为每个节点的哈希字符串的哈希: Hash(DAG) = Hash(vertex_1) U Hash(vertex_2) U ..... Hash(vertex_N);
我认为这个过程的复杂性在最坏的情况下约为 (N*N)。如果图形无法进行拓扑排序,建议的方法仍然适用,但您需要以独特的方式对顶点进行排序(这是困难的部分)
【讨论】:
这就是我所做的,但是拓扑排序并不是唯一的。例如,如果有两个断开连接的组件,它们的节点可以以任何方式交错。【参考方案6】:我将描述一种算法来散列任意有向图,而不考虑该图是非循环的。事实上,即使计算给定顺序的无环图也是一项非常复杂的任务,我相信这只会使散列变得更加复杂,从而变得更慢。
图的唯一表示可以由邻域列表给出。为每个顶点创建一个包含所有邻居的列表。一个接一个地写下所有列表,并将每个列表的邻居数附加到前面。还要保持邻居按升序排序,以使每个图的表示唯一。因此,例如假设您有图表:
1->2, 1->5
2->1, 2->4
3->4
5->3
我的建议是将其转换为(2,2,5, 2,1,4, 1,4, 0, 1,3)
,这里的大括号只是为了可视化表示,而不是 python 语法的一部分。所以列表实际上是:(2,2,5, 2,1,4, 1,4, 0, 1,3)
。
现在要计算唯一哈希,您需要以某种方式对这些表示进行排序并为它们分配一个唯一编号。我建议你做一些类似字典排序的事情来做到这一点。假设您有两个序列(a1, b1_1, b_1_2,...b_1_a1,a2, b_2_1, b_2_2,...b_2_a2,...an, b_n_1, b_n_2,...b_n_an)
和(c1, d1_1, d_1_2,...d_1_c1,c2, d_2_1, d_2_2,...d_2_c2,...cn, d_n_1, d_n_2,...d_n_cn)
,这里 c 和 a 是每个顶点的邻居数,b_i_j 和 d_k_l 是相应的邻居。对于排序,首先比较序列(a1,a2,...an)
和(c1,c2, ...,cn)
,如果它们不同,请使用它来比较序列。如果这些序列不同,则从左到右比较列表,首先按字典顺序比较 (b_1_1, b_1_2...b_1_a1)
和 (d_1_1, d_1_2...d_1_c1)
,依此类推,直到第一个不匹配。
事实上,我建议将N
大小的单词的字典序号在由1,2,3,...N
元素子集的所有可能选择形成的字母表上用作散列。给定顶点的邻域列表是该字母表上的一个字母,例如2,2,5
是由集合的两个元素组成的子集,即2
和5
。
1,2,3
集合的字母(可能的字母集合)将是(按字典顺序排列):
0, 1,1, 1,2, 1,3, 2, 1, 2, 2, 1, 3, 2, 2, 3, 3, 1, 2, 3
上面的第一个数字是给定子集中元素的数量,其余数字是子集本身。因此,从这个字母表中形成所有 3 个字母词,你将得到所有可能的具有 3 个顶点的有向图。
现在集合1,2,3,....N
的子集数是2^N
,因此这个字母表的字母 数是2^N
。现在我们用一个 word 对每个 N
节点的有向图进行编码,其中恰好来自这个 alphabet 的 N
letters,因此可能的数量哈希码正是:(2^N)^N
。这是为了表明哈希码随着N
的增加而真正快速增长。这也是具有N
节点的可能不同有向图的数量,所以我建议的是最佳散列,因为它是双射并且没有更小的散列可以是唯一的。
有一个线性算法可以在给定集合的所有子集的字典顺序中获取给定的子集编号,在本例中为1,2,....N
。这是我为编码/解码数字子集而编写的代码,反之亦然。它是用C++
写的,但我希望很容易理解。对于散列,您只需要代码函数,但由于我建议的散列是可逆的,我添加了解码函数 - 您将能够从散列重建图形,我认为这非常酷:
typedef long long ll;
// Returns the number in the lexicographical order of all combinations of n numbers
// of the provided combination.
ll code(vector<int> a,int n)
sort(a.begin(),a.end()); // not needed if the set you pass is already sorted.
int cur = 0;
int m = a.size();
ll res =0;
for(int i=0;i<a.size();i++)
if(a[i] == cur+1)
res++;
cur = a[i];
continue;
else
res++;
int number_of_greater_nums = n - a[i];
for(int j = a[i]-1,increment=1;j>cur;j--,increment++)
res += 1LL << (number_of_greater_nums+increment);
cur = a[i];
return res;
// Takes the lexicographical code of a combination of n numbers and returns the
// combination
vector<int> decode(ll kod, int n)
vector<int> res;
int cur = 0;
int left = n; // Out of how many numbers are we left to choose.
while(kod)
ll all = 1LL << left;// how many are the total combinations
for(int i=n;i>=0;i--)
if(all - (1LL << (n-i+1)) +1 <= kod)
res.push_back(i);
left = n-i;
kod -= all - (1LL << (n-i+1)) +1;
break;
return res;
此代码还将结果存储在long long
变量中,这仅适用于少于 64 个元素的图形。具有 64 个节点的图的所有可能散列将是 (2^64)^64
。这个数字大约有 1280 个数字,所以可能是一个很大的数字。我描述的算法仍然会运行得非常快,我相信您应该能够散列和“取消散列”具有大量顶点的图。
也可以看看this question。
【讨论】:
两个同构图会有不同的哈希值。只要至少一个节点的邻域列表不同,哈希就会不同。 不就是对邻接字典进行哈希处理吗? 实际上是一个列表,但是是的。所以顶点是根据它们的编号进行编号和排序的,并且每个顶点的列表都是按顺序打印的。 好的。看看我对这个问题的编辑。我本可以省略拓扑排序并散列邻接字典。问题在于我使用 dag 作为动态编程的关键。同构图散列到相同值的次数越多,重做的工作就越少。【参考方案7】:我不确定它是否 100% 工作,但这里有一个想法:
让我们将一个图形编码成一个字符串,然后获取它的哈希值。
-
空图的哈希是“”
没有出边的顶点的哈希是“.”
具有出边的顶点的散列是每个子散列与某些分隔符(例如“,”)的串联
要在步骤 3 中连接之前为同构图生成相同的哈希,只需对哈希进行排序(例如按字典顺序)。
对于图的哈希,只需取其根的哈希(或排序连接,如果有多个根)。
edit 虽然我希望生成的字符串能够描述没有冲突的图,但 hynekcer 发现有时非同构图会获得相同的哈希值。当一个顶点有几个父母时会发生这种情况 - 然后它为每个父母“复制”。例如,该算法不区分“钻石”A->B->C,A->D->C 和情况 A->B->C,A->D->E。
我不熟悉 Python,我很难理解示例中图形的存储方式,但这里有一些 C++ 代码,很可能可以轻松转换为 Python:
THash GetHash(const TGraph &graph)
return ComputeHash(GetVertexStringCode(graph,FindRoot(graph)));
std::string GetVertexStringCode(const TGraph &graph,TVertexIndex vertex)
std::vector<std::string> childHashes;
for(auto c:graph.GetChildren(vertex))
childHashes.push_back(GetVertexStringCode(graph,*c));
std::sort(childHashes.begin(),childHashes.end());
std::string result=".";
for(auto h:childHashes)
result+=*h+",";
return result;
【讨论】:
有趣。您可以尝试通过更改哈希函数来查看它是否优于我的解决方案,然后查看您的版本打印了多少行。 不幸的是,我的 Python 不够流利,无法在您的解决方案中插入相关代码。我能做的最好的就是提出一个说明性的 C++ 代码。 恐怕它不适用于不是树的图,例如“钻石” (A->B->D, A->C->D) 将具有与树相同的哈希 (A->B->D, A->C->E) 你也可以有一个反转树,每个顶点可以有更多的父母,但只有一个孩子,或混合。图片en.wikipedia.org/wiki/Directed_acyclic_graph @hynekcer,谢谢。实际上,这是哈希的冲突(比 MD5 更有可能:))。我在答案中添加了一条注释,不幸的是,我没有找到快速解决问题的方法(不会使算法过于复杂)。 我无法测试你的算法。你更了解它,你可以确认或拒绝我猜想的问题。我不是指 MD5、SHA 的冲突……而是对它们的相同输入。良好的散列算法的冲突是可以接受的,因为对象的每 _ 小变化都会修改其散列值,并且发现冲突不应该是微不足道的。将两个分支合并为一个是一个小变化。【参考方案8】:我假设顶点或边上没有公共标签,因为这样您就可以将图形置于规范形式中,这本身就是一个完美的散列。因此,该提议仅基于同构。
为此,尽可能多地为 DAG 的简单聚合特征组合哈希,并选择那些计算速度快的特征。这是一个入门列表:
-
节点进出度的二维直方图。
边缘 a->b 的 4d 直方图,其中 a 和 b 均以入/出度为特征。
加法
让我更明确一点。对于 1,我们将计算一组三元组 <I,O;N>
(其中没有两个三元组具有相同的 I
、O
值),表示有 N
节点具有入度 I
和出度学位O
。您可以散列这组三元组或更好地使用以某种规范顺序排列的整个集合,例如按字典顺序排序。对于 2,我们计算了一组五元组<aI,aO,bI,bO;N>
,表示从具有aI
和出度aO
的节点到具有bI
和bO
的节点分别有N
边。再次对这些五元组进行哈希处理,或者按规范顺序将它们按原样用于最终哈希的另一部分。
从这个开始,然后查看仍然发生的冲突可能会提供有关如何变得更好的见解。
【讨论】:
【参考方案9】:当我看到这个问题时,我的想法与@example基本相同。我编写了一个函数,提供了一个图形标签,使得标签与两个同构图一致。
这个标签由升序的出度序列组成。您可以使用您选择的字符串散列函数对该标签进行散列,以获得图形的散列。
编辑:我在@NeilG 的原始问题的背景下表达了我的建议。对他的代码进行的唯一修改是将hashkey
函数重新定义为:
def hashkey(self):
return tuple(sorted(map(len,self.lt.values())))
【讨论】:
很好,你用我的代码试试看是否表现更好? 说实话,我没看懂你的代码。你可以试试这个功能,或者多评论你的代码吗?谢谢。self.lt
是Graph
中从节点到后代的映射。顺便说一句,您的组件数量有问题,因为单个组件可以有许多没有任何入边的节点。
@NeilG 谢谢,组件计数绝对错误。我编辑了答案,现在我只使用出度。下次我会更小心的。
@NeilG 我已经在你的代码中包含了这个哈希策略,并且我进行了计时测试,但我没有看到任何改进(我看到几乎相同的执行时间,不到一分钟)。您确定执行过程中有很多同构图吗?顺便说一句,我所做的唯一更改是def hashkey(self): return tuple(sorted(map(len,self.lt.values())))
。【参考方案10】:
如果你的后代有合适的顺序(如果你有一个单独的根节点,不是给定的,但有合适的顺序(可能包括一个虚拟根节点)),散列树的方法应该稍微有点修改。
this *** answer 中的示例代码,修改将是在散列父级之前以某种确定的顺序(增加哈希?)对子级进行排序。
即使您有多个可能的根,您也可以创建一个合成的单个根,并将所有根作为子根。
【讨论】:
正如你所暗示的,链接问题中的算法需要一个唯一可识别的根节点。否则它可以以不同的方式散列同构图。 @alexis 如果你没有唯一可识别的根,你可以合成一个,但所有的根都是孩子。以上是关于有向无环图的哈希值的主要内容,如果未能解决你的问题,请参考以下文章