24

任意の 2 つの同型グラフが同じ値にハッシュされるように、有向非巡回グラフをハッシュ値に変換するにはどうすればよいですか? 2 つの同形グラフが異なる値にハッシュされることは許容されますが、望ましくありません。これは、以下のコードで実行したことです。グラフの頂点の数は最大で 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

グラフ同形性を効果的にテストするには、natyを使用します。具体的には Python にはラッパーpynautyがありますが、その品質を証明することはできません (正しくコンパイルするには、その に簡単なパッチを適用する必要がありましたsetup.py)。このラッパーがすべてを正しく実行している場合、関心のある用途について naty が大幅に簡素化され、ハッシュの問題だけにpynauty.certificate(somegraph)なります。これは、同形グラフの場合と同じ値になります。

いくつかの簡単なテストでpynautyは、すべてのグラフに対して同じ証明書が提供されていることが示されました (頂点の数は同じです)。しかし、これは、グラフを naty の形式に変換する際のラッパーのマイナーな問題によるものです。これを修正した後、うまくいきました(比較のために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完全です。したがって、現在、2つの同型有向非巡回グラフが同じハッシュを生成することを保証する既知の(最悪の場合のサブ指数)ソリューションはありません。異なるグラフ間のマッピングがわかっている場合(たとえば、すべての頂点に一意のラベルがある場合)にのみ、ハッシュの一致を効率的に保証できます。

さて、少数の頂点に対してブルートフォース攻撃をしましょう。入力内の頂点の順序に依存しないグラフの表現を見つける必要があるため、同型グラフが同じ表現を生成することが保証されます。さらに、この表現は、2つの非同型グラフが同じ表現を生成しないことを保証する必要があります。

最も簡単な解決策は、すべてのnの隣接行列を作成することです。頂点の順列であり、隣接行列をn2ビット整数として解釈します。次に、この数値の最小または最大を正規表現として選択できます。この数はグラフを完全にエンコードするため、2つの非同型グラフが同じ数を生成しないようにします。この関数は完全なハッシュ関数と見なすことができます。また、頂点のすべての可能な順列の下でグラフをエンコードする最小または最大の数を選択するため、同型グラフが同じ表現を生成することをさらに保証します。

11個の頂点の場合、これはどの程度良いですか、悪いですか?さて、表現は121ビットになります。非巡回グラフでは対角線を表すループがすべてゼロになり、110ビットが残るため、これを11ビット減らすことができます。この数は、理論的にはさらに減らすことができます。残りの2,110のグラフすべてが非循環であるわけではなく、グラフごとに最大11のグラフが存在する可能性があります。-おおよそ225-同型表現ですが、実際にはこれを行うのは非常に難しい場合があります。n個の頂点を持つ個別の有向非巡回グラフの数を計算する方法を知っている人はいますか?

この表現を見つけるのにどれくらい時間がかかりますか?素朴に11!または39,916,800回の反復。これは何もないわけではなく、おそらくすでに実用的ではありませんが、私はそれを実装してテストしませんでした。しかし、おそらくこれを少しスピードアップすることができます。行を上から下、左から右に連結して隣接行列を整数として解釈する場合、最初の行の左側にある多くの1(ゼロ)で大きな(小さな)数を取得する必要があります。したがって、最初の頂点として、最大(最小)次数(表現に応じて次数または次数)の頂点を選択し、後続の位置でこの頂点に接続されている(接続されていない)頂点よりも1つ(ゼロ) 左の方です。

検索スペースを整理する可能性はもっとあると思われますが、これを実用的な解決策にするのに十分かどうかはわかりません。たぶん、あるか、あるいは誰か他の人が少なくともこのアイデアに基づいて何かを構築することができます。

于 2013-01-28T18:13:22.753 に答える
3

ハッシュはどの程度良好でなければなりませんか? グラフの完全なシリアル化は必要ないと思います。ハッシュは、同じハッシュに評価される 2 番目の (ただし異なる) 要素 (グラフ) が存在しないことをほとんど保証しません。同形グラフ (異なる表現) が同じハッシュを持つことが非常に重要な場合は、表現の変更の下で不変な値のみを使用してください。例えば:

  • ノードの総数
  • (有向) 接続の総数
  • (indegree, outdegree) = (i,j)任意のタプルの(i,j)最大ノード数(max(indegree), max(outdegree))(またはタプルの場合は固定値までの制限(m,n))

これらの情報はすべて O(#nodes) [グラフが適切に保存されていると仮定して] に収集できます。それらを連結すると、ハッシュが得られます。sha必要に応じて、これらの連結された情報のようなよく知られたハッシュ アルゴリズムを使用できます。追加のハッシングなしでは連続ハッシュ(同様のグラフを見つけることができます) であり、追加のハッシングでは、選択されたハッシュ アルゴリズムがこれらのプロパティを持っている場合、均一でサイズが固定されます。

そのままで、追加または削除された接続を登録するのに十分です。a -> c(の代わりに)変更された接続を見逃す可能性がありa -> bます。


このアプローチはモジュール式で、好きなだけ拡張できます。含まれている追加のプロパティは、衝突の数を減らしますが、ハッシュ値を取得するために必要な労力を増やします。いくつかのアイデア:

  • 上記と同じですが、2 次の入次数と出次数があります。すなわち。チェーンによって到達できるノードの数node->child->child( = 2 次出次数)、またはそれぞれ 2 つのステップで特定のノードにつながるノードの数。
  • またはより一般的な n 次の入出次数 (O((平均接続数) ^ (n-1) * #nodes) で計算できます)
  • 離心率を持つノードの数= x (これも任意の x の場合)
  • ノードが何らかの情報を保存する場合 (近隣のもの以外) はxor、すべてのノード コンテンツの任意の種類のハッシュを使用します。ノードがハッシュに追加さxorれる特定の順序のため、重要ではありません。

あなたは「一意のハッシュ値」を要求しましたが、明らかに提供できません。しかし、「ハッシュ」と「すべてのグラフに固有」という用語は相互に排他的であると考えており(もちろん、完全に真実というわけではありません)、「固有」の部分ではなく「ハッシュ」の部分に答えることにしました。「一意のハッシュ」(完全なハッシュ)は、基本的にグラフの完全なシリアル化である必要があります(ハッシュに格納される情報量は、グラフ内の情報の総量を反映する必要があるため)。それが本当に必要な場合は、ノードの一意の順序を定義し(たとえば、順序が明確になるまで、自分の出次数、入次数、子の出次数などでソート)、任意の方法でグラフをシリアル化します(の位置を使用)ノードへのインデックスとして前述の順序付け)。

もちろん、これははるかに複雑ですが。

于 2013-01-28T17:00:00.177 に答える
1

イムホ、グラフをトポロジカルソートできれば、非常に簡単な解決策が存在します。

  1. インデックスiの頂点ごとに、(たとえば、文字列のハッシュ手法を使用して)彼の(ソートされた)直接隣接(頂点1に直接隣接がある場合はpe {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)です。

一意のハッシュを計算するには、これらの表現を何らかの方法で並べ替え、一意の番号を割り当てる必要があります。それを行うには、辞書編集のようなことをすることをお勧めします。ここで、c と a は各頂点の近傍の数で、 b_i_j(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)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 つの要素、つまり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は です。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

質問を見たとき、@example と本質的に同じ考えを持っていました。タグが2つの同形グラフで一致するように、グラフタグを提供する関数を作成しました。

このタグは、昇順の一連の出度で構成されます。このタグを任意の文字列ハッシュ関数でハッシュして、グラフのハッシュを取得できます。

編集: @NeilG の元の質問のコンテキストで提案を表明しました。彼のコードに加える唯一の変更は、hashkey関数を次のように再定義することです。

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

100%うまくいくかどうかはわかりませんが、アイデアは次のとおりです。

グラフを文字列にコーディングしてから、そのハッシュを取得しましょう。

  1. 空のグラフのハッシュは "" です
  2. 出辺のない頂点のハッシュは "." です。
  3. 発信エッジを持つ頂点のハッシュは、すべての子ハッシュを何らかの区切り文字 ("," など) で連結したものです。

ステップ 3 で連結する前に、同形グラフの同じハッシュを生成するには、ハッシュを並べ替えます (たとえば、辞書順)。

グラフのハッシュについては、そのルート (または複数のルートがある場合はソートされた連結) のハッシュを取得します。

編集結果の文字列が衝突のないグラフを表すことを望んでいましたが、hynekcerは、非同型グラフが同じハッシュを取得する場合があることを発見しました。これは、頂点に複数の親がある場合に発生し、親ごとに「複製」されます。たとえば、アルゴリズムは、「ダイヤモンド」{A->B->C,A->D->C} をケース {A->B->C,A->D->E} と区別しません。

私は Python に詳しくないので、この例でグラフがどのように格納されているかを理解するのは難しいですが、Python に簡単に変換できる可能性が高い C++ のコードを次に示します。

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

頂点やエッジに共通のラベルはないと仮定しています。その場合、グラフを標準形式にすることができ、それ自体が完全なハッシュになります。したがって、この提案は同型のみに基づいています。

このために、DAG の単純な集約特性のハッシュをできるだけ多く組み合わせて、計算が速いものを選択します。スターターリストは次のとおりです。

  1. ノードのイン度とアウト度の 2 次元ヒストグラム。
  2. エッジ a->b の 4 次元ヒストグラム。ここで、a と b は両方ともイン/アウトの程度によって特徴付けられます。

追加 もっとはっきりさせてください。1 の場合、in-degreeと out-degree を持つノードがあることを示す、トリプルのセットを計算します<I,O;N>(同じI,O値を持つトリプルは 2 つありません) 。このトリプルのセットをハッシュするか、辞書順でソートしたなど、標準的な順序で配置されたセット全体を使用することをお勧めします。2 については、in 次数と out 次数を持つノードからおよびを持つノードへのエッジがあることを示す5 組のセットをそれぞれ計算します。これらの 5 重を再度ハッシュするか、最終的なハッシュの別の部分にそのまま正規の順序で使用します。NIO<aI,aO,bI,bO;N>NaIaObIbO

これから始めて、まだ発生している衝突を調べると、改善方法についての洞察が得られるでしょう。

于 2013-01-30T20:44:40.230 に答える
0

子孫の適切な順序(および、指定されていない単一のルートノードがある場合、適切な順序(おそらく仮想ルートノードを含めることによる))を使用すると、ツリーをハッシュする方法を少し変更するだけで機能するはずです。

このStackOverflowの回答のコード例では、親をハッシュする前に、決定論的な順序で子を並べ替える(ハッシュを増やす?)ように変更します。

可能なルートが複数ある場合でも、すべてのルートを子として、合成の単一ルートを作成できます。

于 2013-01-28T17:54:49.460 に答える