[英]Hash value for directed acyclic graph
如何将有向非循环图转换为哈希值,使得任何两个同构图哈希到相同的值? 这是可接受的,但两个同构图散列到不同的值是不可取的,这是我在下面的代码中所做的。 我们可以假设图中的顶点数最多为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))
要有效地测试图形同构,你需要使用nauty 。 特别是Python有包装pynauty ,但我无法证明它的质量(正确编译它我必须在它的setup.py
上做一些简单的修补)。 如果这个包装器正确地执行了所有操作,那么它会为您感兴趣的用途简化nauty,这只是散列pynauty.certificate(somegraph)
- 对于同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;
有向无环图的图同构仍然是GI完全的。 因此,目前还没有已知的(最坏情况下的亚指数)解决方案来保证两个同构有向无环图将产生相同的散列。 仅当已知不同图之间的映射时 - 例如,如果所有顶点都具有唯一标签 - 则可以有效地保证匹配哈希。
好吧,让我们对少数顶点强行执行此操作。 我们必须找到与输入中顶点排序无关的图形表示,从而保证同构图产生相同的表示。 此外,该表示必须确保没有两个非同构图产生相同的表示。
最简单的解决方案是为所有n构造邻接矩阵! 顶点的排列,只是将邻接矩阵解释为n 2位整数。 然后我们可以选择最小或最大的这个数字作为规范表示。 这个数字完全对图形进行编码,因此确保没有两个非同构图产生相同的数字 - 人们可以认为这个函数是一个完美的哈希函数 。 并且因为我们在顶点的所有可能排列下选择编码图的最小或最大数字,所以我们进一步确保同构图产生相同的表示。
在11个顶点的情况下,这有多好或坏? 那么,表示将有121位。 我们可以将其减少11位,因为表示循环的对角线在非循环图中将全部为零并且保留为110位。 从理论上讲,这个数字可以进一步减少; 并非所有2 110个剩余图表都是非循环的,每个图表最多可能有11个! - 大约2 25 - 同构表示,但在实践中这可能很难做到。 有谁知道如何计算具有n个顶点的不同有向无环图的数量?
找到这种表示需要多长时间? 天真11! 或39,916,800次迭代。 这不是什么都没有,可能已经不切实际但我没有实现和测试它。 但我们可能会加快这一点。 如果我们通过从左到右连接从上到下的行来将邻接矩阵解释为整数,我们希望在第一行左边的许多(零)获得大(小)数。 因此,我们选择具有最大(最小)度的一个(或一个顶点)作为第一个顶点(取决于表示的indegree或outdegree),然后选择在后续位置连接(未连接)到该顶点的顶点以带来那些(零) ) 向左转。
修剪搜索空间的可能性更大,但我不确定是否有足够的可以使其成为一个实用的解决方案。 也许有或者其他人至少可以根据这个想法建立一些东西。
哈希有多好? 我假设您不希望图表的完整序列化。 哈希很少保证没有第二个(但不同的)元素(图形)评估为相同的哈希。 如果它对您来说非常重要,那么同构图(在不同的表示中)具有相同的哈希值,那么只使用在表示变化下不变的值。 例如:
(i,j)
最多(max(indegree), max(outdegree))
具有(indegree, outdegree) = (i,j)
的节点总数(或者对于某些固定值(m,n)
元组限制(m,n)
) 所有这些信息都可以在O(#nodes)中收集[假设图表存储正确]。 连接它们并且你有一个哈希。 如果您愿意,可以在这些连接的信息中使用一些众所周知的哈希算法,例如sha
。 没有额外的散列,它是一个连续的散列 (它允许查找类似的图形),如果所选的散列算法具有这些属性,则使用额外的散列它是均匀的并且固定大小。
实际上,注册任何添加或删除的连接已经足够了。 它可能会错过已更改的连接( a -> c
而不是a -> b
)。
这种方法是模块化的,可以根据需要进行扩展。 包含的任何其他属性都将减少冲突次数,但会增加获取哈希值所需的工作量。 更多想法:
node->child->child
chain(= second order outdegree)可以达到的节点数,或者分别通过两个步骤到达给定节点的节点数。 xor
。 由于xor
,添加到散列的节点的特定顺序无关紧要。 你要求“一个独特的哈希值”,显然我不能给你一个。 但我认为术语“哈希”和“每个图形的唯一”是相互排斥的(当然不完全正确),并决定回答“哈希”部分,而不是“独特”部分。 “唯一散列”( 完美散列 )基本上需要是图的完整序列化(因为散列中存储的信息量必须反映图中的信息总量)。 如果这真的是你想要的,只需定义一些独特的节点顺序(例如,按自己的outdegree排序,然后是indegree,然后是outdegree of children等等,直到顺序明确)并以任何方式序列化图形(使用上述排序作为节点的索引)。
当然,这要复杂得多。
Imho,如果图形可以在拓扑上排序,则存在非常简单的解决方案。
我将描述一种算法来散列任意有向图,而不是考虑图是非循环的。 事实上,即使计算给定顺序的非循环图也是一项非常复杂的任务,我相信这只会使散列变得更加复杂,从而变得更慢。
邻域列表可以给出图的唯一表示。 对于每个顶点,创建一个包含所有邻居的列表。 将所有列表一个接一个地写入前面,将每个列表的邻居数量附加到前面。 还要保持邻居按升序排序,以使每个图表的表示都是唯一的。 例如,假设您有图表:
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是相应的邻居。 对于排序,首先比较sequnces (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}
的字母表 (可能的字母集合{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
。 现在,我们用这个字母表中的 N
字母的单词编码N
节点的每个有向图,因此可能的哈希码的数量精确地为: (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 位,所以可能是一个很大的数字。 我描述的算法仍然可以非常快速地工作,我相信你应该能够使用很多顶点来散列和“解开”图形。
还看看这个问题 。
我不确定它是100%工作,但这是一个想法:
让我们将图形编码为字符串,然后取其哈希值。
要在步骤3中进行连接之前为同构图生成相同的哈希,只需对哈希进行排序(例如,按字典顺序排序)。
对于图的哈希,只需获取其根的哈希值(或者如果有多个根则进行排序连接)。
编辑虽然我希望得到的字符串能描述没有碰撞的图形,但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;
}
当我看到这个问题时,我的想法与@example基本相同。 我编写了一个提供图形标记的函数,使得标记与两个同构图重合。
此标记由递增顺序的出度序列组成。 您可以使用您选择的字符串哈希函数来哈希此标记,以获取图表的哈希值。
编辑:我在@ NeilG原始问题的背景下表达了我的提议。 对他的代码进行的唯一修改是将hashkey
函数重新定义为:
def hashkey(self):
return tuple(sorted(map(len,self.lt.values())))
我假设在顶点或边上没有常见的标签,因为你可以将图形放在规范形式中,这本身就是一个完美的哈希。 因此,该提议仅基于同构。
为此,将DAG的哈希值与您想象的DAG简单聚合特征相结合,选择那些快速计算的特征。 这是一个入门名单:
加法让我更明确。 对于1,我们计算一组三元组<I,O;N>
(其中没有两个三元组具有相同的I
, O
值),表示存在具有度I
和出度O
N
节点。 你可以对这组三元组进行哈希处理,或者更好地使用按照规范顺序排列的整个集合,例如按字典顺序排序。 对于2,我们计算一组五元组<aI,aO,bI,bO;N>
表示从具有度aI
和out度aO
节点的N
边缘分别到具有bI
和bO
节点。 再次哈希这些五元组,或者按照规范顺序使用它们作为最终哈希的另一部分。
从此开始,然后查看仍然发生的碰撞可能会提供有关如何变得更好的见解。
多年前,我为这个问题创建了一个简单而灵活的算法(通过对它们进行散列来查找化学结构数据库中的重复结构)。
我把它命名为“Powerhash”,要创建算法,它需要两个见解。 第一个是功率迭代图算法,也用于PageRank。 第二个是能够用我们想要的任何东西替换功率迭代的内部步进功能。 我用一个函数替换它,在每个步骤和每个节点上执行以下操作:
在第一步,节点的哈希受其直接邻居的影响。 在第二步,节点的哈希受到距离它的2跳的邻域的影响。 在第N步,节点的散列将受到其周围的邻域N跳的影响。 所以你只需要继续运行Powerhash for N = graph_radius步骤。 最后,图中心节点的哈希将受到整个图的影响。
要生成最终哈希,请对最终步骤的节点哈希值进行排序并将它们连接在一起。 之后,您可以比较最终的哈希值,以查找两个图形是否同构。 如果您有标签,则将它们添加到您为每个节点(以及每个步骤)计算的内部哈希中。
有关这方面的更多信息,请点击此处查看我的帖子:
https://plus.google.com/114866592715069940152/posts/fmBFhjhQcZF
上面的算法是在“madIS”功能关系数据库中实现的。 您可以在此处找到算法的源代码:
https://github.com/madgik/madis/blob/master/src/functions/aggregate/graph.py
通过适当排序您的后代(如果您有一个单独的根节点,不是给定的,但具有合适的排序(可能包括虚拟根节点)),散列树的方法应该稍作修改。
此StackOverflow答案中的示例代码,修改将是在对父项进行散列之前以某种确定性顺序(增加散列?)对子项进行排序。
即使你有多个可能的根,你也可以创建一个合成的单根,所有的根都是子。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.