24

如何将有向无环图转换为哈希值,以便任何两个同构图哈希到相同的值?两个同构图散列到不同的值是可以接受的,但不可取,这就是我在下面的代码中所做的。我们可以假设图中的顶点数最多为 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))
4

10 回答 10

12

为了有效地测试图同构,您将需要使用nauty。专门针对 Python 有包装器pynauty,但我无法证明它的质量(为了正确编译它,我必须对其进行一些简单的修补setup.py)。如果这个包装器做的一切都是正确的,那么它为你感兴趣的用途简化了很多,它只是一个散列的问题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;
于 2013-01-29T01:42:01.117 回答
8

有向无环图的图同构仍然是 GI 完备的。因此,目前没有已知的(最坏情况下的次指数)解决方案来保证两个同构有向无环图将产生相同的散列。只有当不同图之间的映射是已知的——例如,如果所有顶点都有唯一的标签——才能有效地保证匹配的哈希值。

好的,让我们对少数顶点进行暴力破解。我们必须找到一个独立于输入中顶点顺序的图表示,从而保证同构图产生相同的表示。此外,这种表示必须确保没有两个非同构图产生相同的表示。

最简单的解决方案是为所有 n 构造邻接矩阵!顶点的排列,并将邻接矩阵解释为 n 2位整数。然后我们可以选择最小或最大的数字作为规范表示。这个数字完全编码了图,因此确保没有两个非同构图产生相同的数字——人们可以认为这个函数是一个完美的散列函数。并且因为我们选择在所有可能的顶点排列下对图进行编码的最小或最大数字,我们进一步确保同构图产生相同的表示。

在 11 个顶点的情况下,这有多好或多坏?好吧,表示将有 121 位。我们可以将其减少 11 位,因为表示循环的对角线在非循环图中将全为零,并剩下 110 位。这个数字理论上可以进一步减少;并非所有剩余的 2 110个图都是非循环的,每个图最多可能有 11 个!- 大约 2 25 - 同构表示,但实际上这可能很难做到。有人知道如何计算具有 n 个顶点的不同有向无环图的数量吗?

需要多长时间才能找到这个代表?天真11!或 39,916,800 次迭代。这不是什么,可能已经不切实际,但我没有实施和测试它。但我们可能可以加快一点速度。如果我们通过从左到右连接从上到下的行来将邻接矩阵解释为整数,我们希望第一行左侧有许多个(零)以获得大(小)数。因此,我们选择具有最大(最小)度(取决于表示的入度或出度)的一个(或一个顶点)作为第一个顶点,而不是在后续位置连接(未连接)到该顶点的顶点以使这些顶点(零) ) 向左转。

修剪搜索空间的可能性可能更多,但我不确定是否有足够的方法使其成为实用的解决方案。也许有或者也许其他人至少可以在这个想法的基础上建立一些东西。

于 2013-01-28T18:13:22.753 回答
3

哈希必须有多好?我假设您想要图形的完整序列化。散列很少保证没有第二个(但不同的)元素(图)评估为相同的散列。如果对您来说非常重要的是,同构图(以不同的表示形式)具有相同的散列,那么只使用在表示变化下不变的值。例如:

  • 节点总数
  • (定向)连接的总数
  • (indegree, outdegree) = (i,j)任何元组的节点总数(i,j)不超过(max(indegree), max(outdegree))(或限制为不超过某个固定值的元组(m,n)

所有这些信息都可以在 O(#nodes) [假设图形存储正确] 中收集。将它们连接起来,你就有了一个哈希值。如果您愿意,您可以使用一些众所周知的哈希算法sha,例如这些连接信息。如果没有额外的散列,它是一个连续的散列(它允许找到相似的图),如果选择的散列算法具有这些属性,那么它是统一的并且大小固定。

事实上,注册任何添加或删除的连接已经足够好了。它可能会错过已更改的连接(a -> c而不是a -> b)。


这种方法是模块化的,可以根据需要进行扩展。包含的任何其他属性都将减少冲突的数量,但会增加获取哈希值所需的工作量。还有一些想法:

  • 与上述相同,但具有二阶进出度。IE。一条链可以到达的节点数node->child->child(=二阶外度)或分两步通向给定节点的节点数。
  • 或更一般的 n 阶进出度(可以在 O((average-number-of-connections) ^ (n-1) * #nodes) 中计算)
  • 偏心率= x的节点数(同样适用于任何 x)
  • 如果节点存储任何信息(除了它们的邻居),则使用xor所有节点内容的任何类型的散列。由于xor添加到散列的节点的特定顺序无关紧要。

您要求“唯一的哈希值”,显然我无法为您提供。但我认为术语“散列”和“每个图的唯一”是相互排斥的(当然不完全正确),并决定回答“散列”部分而不是“唯一”部分。一个“唯一散列”(完美散列)基本上需要是图的全序列化(因为散列中存储的信息量要反映图中的信息总量)。如果这确实是您想要的,只需定义一些唯一的节点顺序(例如,按自己的出度排序,然后按入度,然后按子项的出度等,直到顺序明确)并以任何方式序列化图形(使用位置在前面提到的排序作为节点的索引)。

当然,这要复杂得多。

于 2013-01-28T17:00:00.177 回答
2

几年前,我为这个问题创建了一个简单而灵活的算法(通过散列在化学结构数据库中找到重复的结构)。

我将它命名为“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

于 2017-03-20T10:45:47.147 回答
1

恕我直言,如果可以对图进行拓扑排序,则存在非常简单的解决方案。

  1. 对于索引为 i 的每个顶点,您可以构建他的(排序的)直接邻居的唯一哈希(例如,使用字符串的哈希技术)(如果顶点 1 有直接邻居 {43,23,2,7,12, 19,334} 散列函数应该散列 {2,7,12,19,23,43,334} 的数组)
  2. 对于整个 DAG,您可以创建一个哈希,作为每个节点的哈希字符串的哈希: Hash(DAG) = Hash(vertex_1) U Hash(vertex_2) U ..... Hash(vertex_N); 我认为这个过程的复杂性在最坏的情况下约为 (N*N)。如果图形无法进行拓扑排序,建议的方法仍然适用,但您需要以独特的方式对顶点进行排序(这是困难的部分)
于 2013-01-28T10:33:23.177 回答
1

我将描述一种算法来散列任意有向图,而不考虑该图是非循环的。事实上,即使计算给定顺序的无环图也是一项非常复杂的任务,我相信这只会使散列变得更加复杂,从而变得更慢。

图的唯一表示可以由邻域列表给出。为每个顶点创建一个包含所有邻居的列表。一个接一个地写下所有列表,并将每个列表的邻居数附加到前面。还要保持邻居按升序排序,以使每个图的表示唯一。因此,例如假设您有图表:

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}是由集合的两个元素组成的子集,即25

该集合的字母表(可能的字母集合){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)^NNN

在这种情况下,有一个线性算法可以在给定集合的所有子集的字典顺序中获得给定的子集编号{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位数字,所以可能是一个很大的数字。我描述的算法仍然会运行得非常快,我相信你应该能够散列和“取消散列”具有很多顶点的图。

也看看这个问题

于 2013-01-28T10:57:05.177 回答
1

我不确定它是否 100% 有效,但这是一个想法:

让我们将一个图编码成一个字符串,然后获取它的哈希值。

  1. 空图的哈希是“”
  2. 没有出边的顶点的哈希是“。”
  3. 带有出边的顶点的散列是每个子散列与一些分隔符的连接(例如“,”)

要在第 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;
}
于 2013-01-28T16:58:52.300 回答
1

当我看到这个问题时,我的想法与@example基本相同。我编写了一个函数,提供了一个图形标签,使得标签与两个同构图一致。

该标签由升序的出度序列组成。您可以使用您选择的字符串散列函数对该标签进行散列,以获得图形的散列。

编辑:我在@NeilG 的原始问题的背景下表达了我的建议。对他的代码进行的唯一修改是将hashkey函数重新定义为:

def hashkey(self): 
    return tuple(sorted(map(len,self.lt.values())))
于 2013-01-30T01:58:38.967 回答
1

我假设顶点或边上没有公共标签,因为那么您可以将图形置于规范形式中,这本身就是一个完美的散列。因此,该提议仅基于同构。

为此,尽可能多地为 DAG 的简单聚合特征组合哈希,选择那些可以快速计算的特征。这是一个入门列表:

  1. 节点进出度的二维直方图。
  2. 边缘 a->b 的 4d 直方图,其中 a 和 b 都以入/出度为特征。

我更明确一点。对于 1,我们将计算一组三元组<I,O;N>(其中没有两个三元组具有相同的I,O值),表示存在具有 in-degree和 out-degree的N节点。您可以散列这组三元组或更好地使用以某种规范顺序排列的整个集合,例如按字典顺序排序。对于 2,我们计算一组五元组,表示从具有 in degree和 out degree的节点到分别具有和的节点存在边。再次散列这些五元组,或者按规范顺序将它们原样用于最终散列的另一部分。IO<aI,aO,bI,bO;N>NaIaObIbO

从这个开始,然后查看仍然发生的冲突可能会提供有关如何变得更好的见解。

于 2013-01-30T20:44:40.230 回答
0

对你的后代进行适当的排序(如果你有一个单独的根节点,不是给定的,但有合适的排序(可能包括一个虚拟根节点)),散列树的方法应该稍微修改一下。

此 StackOverflow 答案中的示例代码,修改将是在散列父级之前以某种确定性顺序(增加哈希?)对子级进行排序。

即使您有多个可能的根,您也可以创建一个合成的单根,所有根都作为子根。

于 2013-01-28T17:54:49.460 回答