Хэш-значение для направленного ациклического графа

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

10 ответов


для эффективного тестирования изоморфизма графа вы захотите использовать 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;

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

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

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

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

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

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


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

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

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

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


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

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

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

конечно, это гораздо сложнее, хотя.


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

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

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

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

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) to (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. Теперь мы кодируем каждый направленный граф 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%, но вот идея:

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

  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;
}

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

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

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

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

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


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

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

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

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

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

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

  • сортировка хэшей соседей узла
  • хэш объединенные отсортированные хэши

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

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

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

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

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

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


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

пример кода этот ответ StackOverflow, модификация будет состоять в сортировке детей в некотором детерминированном порядке (увеличение хэша?) перед хешированием родителя.

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