Объединение найти структуру данных - PullRequest
12 голосов
/ 28 ноября 2011

Для многих проблем, которые я вижу, рекомендуется использовать структуру данных union-find. Я попытался прочитать об этом и подумать о том, как это реализовано (с использованием C ++). В настоящее время я понимаю, что это не что иное, как список наборов. Таким образом, чтобы найти, к какому набору принадлежит элемент, нам нужно n*log n операций. И когда мы должны выполнить объединение, тогда мы должны найти два набора, которые нужно объединить, и сделать set_union для них. Это не выглядит ужасно эффективным для меня. Правильно ли я понимаю эту структуру данных или я что-то упустил?

Ответы [ 4 ]

17 голосов
/ 08 ноября 2015

Это довольно поздний ответ, но, вероятно, на стеке не было ответа в других местах, и, поскольку это самая верхняя страница для тех, кто ищет union-find, вот подробное решение.

Find-Union - очень быстрая операция, выполняемая в почти постоянное время. Это следует из представления Джереми о сжатии пути и отслеживании размеров набора. Сжатие пути выполняется для каждой операции поиска, тем самым занимая амортизированное время lg * (n). lg * подобен обратной функции Аккермана, которая растет настолько медленно, что редко превышает 5 (по крайней мере, до n <2 ^ 65535). Объединение / слияние наборов выполняется лениво, просто указывая один корень на другой, в частности корень меньшего набора на корень большего набора, что завершается за постоянное время. </p>

См. Код ниже https://github.com/kartikkukreja/blog-codes/blob/master/src/Union%20Find%20%28Disjoint%20Set%29%20Data%20Structure.cpp

class UF {
  int *id, cnt, *sz;
  public:
// Create an empty union find data structure with N isolated sets.
UF(int N) {
    cnt = N; id = new int[N]; sz = new int[N];
    for (int i = 0; i<N; i++)  id[i] = i, sz[i] = 1; }
~UF() { delete[] id; delete[] sz; }

// Return the id of component corresponding to object p.
int find(int p) {
    int root = p;
    while (root != id[root])    root = id[root];
    while (p != root) { int newp = id[p]; id[p] = root; p = newp; }
    return root;
}
// Replace sets containing x and y with their union.
void merge(int x, int y) {
    int i = find(x); int j = find(y); if (i == j) return;
    // make smaller root point to larger one
    if (sz[i] < sz[j]) { id[i] = j, sz[j] += sz[i]; }
    else { id[j] = i, sz[i] += sz[j]; }
    cnt--;
}
// Are objects x and y in the same set?
bool connected(int x, int y) { return find(x) == find(y); }
// Return the number of disjoint sets.
int count() { return cnt; }
};
5 голосов
/ 29 ноября 2011

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

Если я правильно помню, это можно показать (легко):

  • это сжатие пути (всякий раз, когда вы выполняете поиск «родителя» множества A, вы «сжимаете» путь, так что каждый будущий вызов этого будет обеспечивать родителя во времени O (1)) привести к сложности O (log n) на вызов;

  • это балансирование (вы приблизительно отслеживаете количество дочерних элементов в каждом наборе, а когда вам нужно «объединить» два набора, вы создаете тот, у которого меньше дочерних элементов, чем у того, у которого больше всего) приводит к сложности O (log n) на вызов.

Более сложное доказательство может показать, что когда вы объединяете обе оптимизации, вы получаете среднюю сложность, которая является обратной функцией Аккермана, написанной α (n), и это было основным изобретением Тарьяна для этой структуры.

Позже было показано, я полагаю, что для некоторых конкретных моделей использования эта сложность фактически постоянна (хотя для всех практических целей обратное значение значения ackermann составляет около 4). Согласно странице Википедии на сайте Union-Find, в 1989 году амортизированная стоимость на операцию любой эквивалентной структуры данных была показана как Ω (α (n)), что доказывает, что текущая реализация является асимптотически оптимальной.

2 голосов
/ 28 ноября 2011

Надлежащая структура данных объединения-поиска использует сжатие пути при каждом поиске. Это амортизирует стоимость, и каждая операция затем пропорциональна обратной функции ackermann, которая в основном делает ее постоянной (но не совсем).

Если вы реализуете его с нуля, я бы предложил использовать древовидный подход.

0 голосов
/ 11 декабря 2012

Простая структура с объединением устанавливает массив (element -> set), делая поиск, который устанавливает постоянное время;обновление их амортизируется по времени и конкатенации списков происходит постоянно.Не так быстро, как некоторые из вышеперечисленных подходов, но достаточно просто для программирования и более чем достаточно, чтобы улучшить время работы Big-O, скажем, алгоритма минимального связующего дерева Крускала.

...