Подтвердить что ты не робот

Значение хеша для направленного ациклического графа

Как преобразовать направленный ациклический граф в хеш-значение, чтобы любые два изоморфных графа хэш равны одному значению? Это приемлемо, но нежелательно для двух изоморфных графов к хешу к разным значениям, что я и сделал в приведенном ниже коде. Мы можем предположить, что число вершин в графе не превышает 11.

Меня особенно интересует код Python.

Вот что я сделал. Если self.lt - это отображение из node для потомков (а не дочерних!), То я переписываю узлы в соответствии с модифицированной топологической сортировкой (которая сначала предпочитает упорядочивать элементы с большим количеством потомков). Затем я использую отсортированный словарь. Некоторые изоморфные графики будут хешировать до разных значений, особенно по мере роста числа узлов.

Я включил весь код, чтобы мотивировать мой прецедент. Я рассчитываю количество сравнений, необходимых для поиска медианы из 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))
4b9b3361

Ответ 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;

Ответ 2

Изоморфизм графов для направленных ациклических графов по-прежнему является GI-полным. Поэтому в настоящее время нет известного (наихудшего субэкспоненциального) решения, гарантирующего, что два изоморфных направленных ациклических графики будут давать один и тот же хеш. Только если отображение между разными графами известно - например, если все вершины имеют уникальные метки - можно эффективно гарантировать соответствие хэшей.

Хорошо, пусть это скопирует это для небольшого числа вершин. Нам нужно найти представление графика, не зависящего от упорядочения вершин на входе и, следовательно, гарантирует, что изоморфные графы дают одно и то же представление. Далее это представление должно гарантировать, что никакие два неизоморфных графа не дают то же представление.

Простейшим решением является построение матрицы смежности для всех n! перестановки вершин и просто интерпретировать матрицу смежности как n 2 битное целое число. Тогда мы можем просто выбрать наименьшее или самое большое из этих чисел как каноническое представление. Это число полностью кодирует граф и, следовательно, гарантирует, что никакие два неизоморфных графа не дают одинакового числа - можно было бы рассмотреть эту функцию a совершенную хэш-функцию. И поскольку мы выбираем наименьшее или наибольшее число, кодирующее граф при всех возможных перестановках вершин, мы также гарантируем, что изоморфные графики дают одно и то же представление.

Как хорошо или плохо это в случае 11 вершин? Ну, в представлении будет 121 бит. Мы можем уменьшить это на 11 бит, потому что диагональ, представляющая петли, будет всех нулей в ациклическом графе и останется с 110 битами. Это число теоретически может быть уменьшено; не все 2 110 оставшиеся графики ацикличны, и для каждого графика может быть до 11! - примерно 2 25 - изоморфные представления, но на практике это может быть довольно сложно сделать. Кто-нибудь знает, как вычислить количество различных направленных ациклических графов с n вершинами?

Сколько времени потребуется, чтобы найти это представление? Наивно 11! или 39,916,800 итераций. Это не что-то и, вероятно, уже непрактично, но я не реализовал и не тестировал его. Но мы можем немного ускорить это. Если мы интерпретируем матрицу смежности как целое, объединив строки сверху вниз слева направо, мы хотим, чтобы многие из них (нули) слева от первой строки получили большое (небольшое) число. Поэтому мы выбираем в качестве первой вершины одну (или одну из вершин) с наибольшей (наименьшей) степенью (не зависящей или не зависящей от представления) и вершинами, связанными (не связанными) с этой вершиной в последующих позициях, чтобы привести их (нули ) влево.

Вероятнее всего, есть возможности сократить пространство поиска, но я не уверен, достаточно ли для этого практического решения. Может быть, есть или, может быть, кто-то еще может хоть что-то придумать над этой идеей.

Ответ 3

Насколько хорош хэш? Я предполагаю, что вы не хотите полную сериализацию графика. Хэш редко гарантирует, что нет второго (но другого) элемента (графика), который оценивается с одним и тем же хэшем. Если это очень важно для вас, то изоморфные графы (в разных представлениях) имеют один и тот же хеш, а только используют значения, которые инвариантны относительно изменения представления. Например:.

  • общее количество узлов
  • общее количество (направленных) соединений
  • общее количество узлов с (indegree, outdegree) = (i,j) для любого набора (i,j) до (max(indegree), max(outdegree)) (или ограничено для кортежей до некоторого фиксированного значения (m,n))

Все эти данные могут быть собраны в O (#nodes) [при условии, что график хранится правильно]. Объедините их, и у вас есть хеш. Если вы предпочитаете, вы можете использовать какой-то известный хеш-алгоритм, например sha для этих конкатенированных данных. Без дополнительного хэширования это непрерывный хеш (он позволяет находить похожие графики), с дополнительным хэшированием он является однородным и фиксированным по размеру, если выбранный алгоритм хэширования обладает этими свойствами.

Как бы то ни было, он уже достаточно хорошо зарегистрировал любое добавленное или удаленное соединение. Он может пропустить соединения, которые были изменены (a -> c вместо a -> b).


Этот подход является модульным и может быть расширен настолько, насколько вам нравится. Любое добавленное свойство, которое будет включено, уменьшит количество столкновений, но увеличит усилия, необходимые для получения значения хэш-функции. Еще несколько идей:

  • то же, что и выше, но со вторым и вторым порядком. То есть. количество узлов, которые могут быть достигнуты цепочкой node->child->child (= outdegree второго порядка) или соответственно количеством узлов, которые приводят к заданному node в два этапа.
  • или более общий n-й порядок входов и выходов (может быть вычислен в O ((среднее число соединений) ^ (n-1) * #nodes))
  • количество узлов с eccentricity= x (снова для любого x)
  • если узлы хранят какую-либо информацию (кроме своих соседей), используйте xor любого вида хэша всех node -контентов. Из-за xor конкретного порядка, в котором узлы, добавленные в хэш, не имеют значения.

Вы запросили "уникальное значение хэш-функции", и я не могу предложить вам его. Но я считаю термины "хеш" и "уникальными для каждого графа" взаимоисключающими (не совсем верно, конечно) и решил ответить на "хеш-часть", а не "уникальную" часть. "Уникальный хеш" (идеальный хэш) в основном должен быть полной сериализацией графика (поскольку объем информации, хранящейся в хэше, должен отражают общий объем информации на графике). Если это действительно то, что вы хотите, просто определите какой-то уникальный порядок узлов (например, отсортированный по собственной устаревшей, затем не зависящей, затем превзойти детей и т.д., Пока порядок не будет однозначным) и сериализуйте график любым способом (используя позицию в вышеупомянутый порядок в качестве индекса для узлов).

Конечно, это намного сложнее.

Ответ 4

Imho. Если граф может быть топологически отсортирован, существует очень простое решение.

  • Для каждой вершины с индексом я вы можете построить уникальный хеш (например, используя метод хэширования для строк) его (отсортированных) прямых соседей (pe, если вершина 1 имеет прямые соседние точки {43, 23, 2,7, 12,19,334} хэш-функции должны иметь хэш-массив из {2,7,12,19,23,43,334})
  • Для всей DAG вы можете создать хеш, как хэш строки хэшей для каждого node: Hash (DAG) = Hash (vertex_1) U Hash (vertex_2) U..... Hash (vertex_N ); Я думаю, что сложность этой процедуры вокруг (N * N) в худшем случае. Если граф не может быть топологически отсортирован, предлагаемый подход все еще применим, но вам нужно упорядочить вершины уникальным образом (и это сложная часть).

Ответ 5

Я опишу алгоритм хеширования произвольного направленного графа, не учитывая, что граф ацикличен. Фактически даже подсчет ациклических графов данного порядка является очень сложной задачей, и я считаю, что здесь это только сделает хэширование значительно более сложным и, следовательно, медленнее.

Единственное представление графа может быть задано списком окрестностей. Для каждой вершины создайте список со всеми соседями. Напишите все списки один за другим, добавив количество соседей для каждого списка на передний план. Также держите соседей отсортированными в порядке возрастания, чтобы сделать представление уникальным для каждого графика. Например, предположим, что у вас есть график:

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} будет (упорядочен лексикографически):

{0}, {1,1}, {1,2}, {1,3}, {2, 1, 2}, {2, 1, 3}, {2, 2, 3}, {3, 1, 2, 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 цифр, поэтому, возможно, это большое число. Тем не менее алгоритм, который я описываю, будет работать очень быстро, и я считаю, что вы должны иметь возможность хэширования и "развязать" графики с большим количеством вершин.

Также посмотрите этот вопрос.

Ответ 6

Я не уверен, что он работает на 100%, но вот идея:

Пусть закодирует граф в строку, а затем возьмет его хэш.

  • хэш пустого графа - ""
  • хеш вершины без исходящих ребер - это. "
  • хэш вершины с исходящими ребрами является конкатенацией каждого дочернего хеша с некоторым разделителем (например, "," )

Чтобы создать тот же хеш для изоморфных графов до конкатенации на шаге 3, просто сортируйте хэши (например, в лексикографическом порядке).

Для хэша графа просто возьмите хэш его корня (или отсортированную конкатенацию, если есть несколько корней).

edit. Хотя я надеялся, что результирующая строка будет описывать график без столкновений, hynekcer обнаружил, что иногда неизоморфные графики получат одинаковый хеш. Это происходит, когда в вершине есть несколько родителей - затем он "дублируется" для каждого родителя. Например, алгоритм не дифференцирует "алмаз" {A- > B- > C, A- > D- > C} из случая {A- > B- > C, A- > D- > E}.

Я не знаком с Python, и мне трудно понять, как хранится граф в примере, но вот какой-то код в С++, который, вероятно, легко конвертируется в 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;
}

Ответ 7

Когда я увидел этот вопрос, у меня была по существу та же идея, что и @example. Я написал функцию, предоставляющую тег графа, так что тег совпадает для двух изоморфных графов.

Этот тег состоит из последовательности выходов в возрастающем порядке. Вы можете использовать этот тег с помощью хеш-функции по вашему выбору, чтобы получить хэш графика.

Изменить: Я выразил свое предложение в контексте исходного вопроса @NeilG. Единственной модификацией, сделанной для его кода, является переопределение функции hashkey как:

def hashkey(self): 
    return tuple(sorted(map(len,self.lt.values())))

Ответ 8

Я предполагаю, что на вершинах или ребрах нет общих меток, так как тогда вы могли бы поместить график в каноническую форму, которая сама по себе была бы идеальным хешем. Поэтому это предложение основано только на изоморфизме.

Для этого объедините хэши для как можно большего количества простых совокупных характеристик DAG, выбирая те, которые быстро вычисляются. Вот стартовый список:

  • 2d гистограмма входов и выходов узлов.
  • 4d гистограмма ребер a- > b, где a и b оба характеризуются степенью in/out.

Добавление Позвольте мне быть более явным. Для 1 мы вычислили набор троек <I,O;N> (где ни один из двух троек не имеет одинаковых значений I, O), что означает, что есть N узлы со степенью I и out- степень O. Вы сделали бы этот набор троек или еще лучше, используя весь набор, расположенный в некотором каноническом порядке, например. лексикографически отсортированы. Для 2 мы вычисляем набор пятикратных значений <aI,aO,bI,bO;N>, означающий, что есть ребра N из узлов с степенью aI и степенью aO, в узлы с bI и bO соответственно. Опять же хэш этих пятикратных или использовать их в каноническом порядке как есть для другой части финального хеша.

Начиная с этого, а затем глядя на возникающие конфликты, возможно, вы узнаете, как лучше.

Ответ 9

Несколько лет назад я создал простой и гибкий алгоритм для этой задачи (поиск дублирующих структур в базе данных химических структур путем их хэширования).

Я назвал его "Powerhash", и для создания алгоритма ему потребовались две идеи. Первый - это алгоритм графа итераций мощности, также используемый в PageRank. Во-вторых, это возможность заменить итерацию мощности внутри шаговой функции тем, что мы хотим. Я заменил его функцией, выполняющей следующие действия на каждом шаге, и для каждого node:

  • Сортировка хэшей node соседей
  • Хеширование конкатенированных сортированных хэшей

На первом этапе хеш node зависит от его прямых соседей. На втором этапе хеш node зависит от окрестности 2-х прыжков от него. На N-м шаге на хеш node будет влиять окрестность N-хэпов вокруг него. Поэтому вам нужно продолжить выполнение шагов Powerhash для N = graph_radius. В итоге на весь граф будет влиять хеш графа node.

Чтобы создать окончательный хеш, соберите последний шаг node хэшей и объедините их вместе. После этого вы можете сравнить финальные хэши, чтобы найти, являются ли два графика изоморфными. Если у вас есть метки, добавьте их во внутренние хеши, которые вы вычисляете для каждого node (и на каждом шаге).

Подробнее об этом вы можете посмотреть здесь:

https://plus.google.com/114866592715069940152/posts/fmBFhjhQcZF

Алгоритм, описанный выше, был реализован в функциональной реляционной базе данных madIS. Здесь вы можете найти исходный код алгоритма:

https://github.com/madgik/madis/blob/master/src/functions/aggregate/graph.py

Ответ 10

С подходящим упорядочением ваших потомков (и если у вас есть один корень node, а не заданный, но с подходящим упорядочением (возможно, включив виртуальный корень node)), метод для хэширования дерева должен работа с небольшой модификацией.

Пример кода в fooobar.com/info/128725/..., модификация будет состоять в том, чтобы сортировать дочерние элементы в некотором детерминированном порядке (увеличивая хэш?) перед хешированием родителя.

Даже если у вас есть несколько возможных корней, вы можете создать синтетический единственный корень со всеми корнями в виде дочерних элементов.