Пиксели на цифровой фотографии, компьютеры в сети, люди в социальной сети - все это связанные объекты.

Учитывая набор из N объектов, соедините два объекта или спросите, связаны ли два объекта (напрямую или напрямую).

Проблема в том, чтобы иметь возможность реализовать две команды; объединение и соединение.

union(2, 5)        // connect object 2 with object 5
connected(1 , 6)   // is object 1 connected to object 6?

Свойства подключения

Мы предполагаем, что «связан с» является отношением эквивалентности . Отношение эквивалентности разделяет объекты на классы эквивалентности или связанные компоненты.

  • Возвратное → p связано с p.
  • Симметричный → Если p соединен с q, то q соединен с p.
  • Транзитивный → Если p связан с q, а q связан с r, то p связан с r.

Осуществление операций

  • Найти: в каком компоненте находится объект p?
  • Связано: находятся ли объекты p и q в одном компоненте?
  • Объединение: соедините два компонента, содержащие объекты p и q (если это еще не сделано).

Связанные компоненты - это подмножества, каждое из которых состоит из связанных объектов.

Тип данных Union-Find

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

Это может быть реализовано несколькими разными алгоритмами. Мы собираемся изучить два алгоритма; Быстрый поиск и Быстрое объединение.

Быстрый поиск

Это алгоритм решения проблемы динамического подключения, также называемый «энергичным подходом».

Инициализировать

Каждый объект в массиве имеет значение или идентификатор, и изначально идентификатор является его индексом. Элемент p связан с элементом q, если у них одинаковый идентификатор.

for (int i = 0; i < n; i++)
    id[i] = i;               // set id of the object at index i

Союз

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

if (pID == qID) return;   // already connected?
for (int i = 0; i < id.length; i++)
    if (id[i] == pID) 
        id[i] = qID;

Подключено

Два объекта связаны тогда и только тогда, когда у них одинаковый идентификатор.

return id[p] == id[q];

Анализируя

Он быстро проверяет наличие соединения, медленно создает массив объектов и соединяет объекты.

  • Инициализировать и объединить → O (N)
  • Подключено → O (1)

Быстрый союз

Это алгоритм решения проблемы динамического подключения, также называемый «ленивым подходом».

Инициализировать

Массив представляет родительский элемент каждого объекта. Изначально родителем объекта является сам объект; каждый объект является корнем.

Теперь мы можем представить, что структура данных состоит из деревьев, каждое дерево имеет связанные объекты, и деревья могут не быть связаны друг с другом.

for (int i = 0; i < n; i++)
    parent[i] = i;           // set parent of the object at index i

Корень

Получить корень объекта. Это просто вспомогательный метод, который будет использоваться в командах union и connected.

while (p != parent[p])
    p = parent[p];
return p;

Союз

При соединении двух предметов p и q соедините корни. Итак, измените родительский элемент корня p на корень q или наоборот. Теперь корень p стал потомком корня q (как если бы мы соединяли два дерева или подмножества).

int rootP = root(p);
int rootQ = root(q);
if (rootP == rootQ) return;    // already connected?
parent[rootP] = rootQ;

Связаны

Два объекта связаны, если их корень одинаковый. Итак, мы должны проверить родителей каждого объекта до корня.

return root(p) == root(q);

Анализируя

Он медленно проверяет наличие соединения, медленно создает массив объектов и соединяет объекты. Соединение двух объектов может занять O (N), когда глубина дерева равна N.

  • Инициализировать и объединить и подключить → O (N).

Улучшения Quick Union

1. Взвешенный быстрый союз

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

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

// union by size (initially size[i] = 1)
if (size[rootP] < size[rootQ]) {
    parent[rootP] = rootQ;
    size[rootQ] += size[rootP];
} else {
    parent[rootQ] = rootP;
    size[rootP] += size[rootQ];
}
// union by depth (initially depth[i] = 0)
if (depth[rootP] < depth[rootQ]) 
    parent[rootP] = rootQ;
else if (depth[rootP] > depth[rootQ]) 
    parent[rootQ] = rootP;
else {
      parent[rootQ] = rootP;
      depth[rootP]++;
}

Анализируя

Время, затрачиваемое на объединение и связанные команды, не превышает глубины самого высокого дерева, которое составляет не более O (LogN).

Доказательство: когда T1 объединяется с T2, где T1 ≤T2, глубина T1 увеличивается на 1. Теперь глубина T2 (после объединения) увеличится на 1, если объединить с T3, где Т3 ≥ Т2. Итак, каждый раз нам нужно дерево X ≥ tree Y, чтобы увеличить глубину дерева Y на 1. Следовательно, размер T1 может увеличиваться не более чем на LogN.

  • Union & Connected → O (Вход в систему)

2. Сжатие пути

Расправьте дерево; Всякий раз, когда вы хотите перейти к корню от объекта, назначьте родительский элемент текущего объекта его прародителю (или корню). Это правило применяется до самого дерева.

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

// path compression by linking to the root
int root = p;
while (root != parent[root])
    root = parent[root];
while (p != root) {
    int newp = parent[p];
    parent[p] = root;
    p = newp;
}
// path compression by linking to grandparent(halving the depth)
while (p != parent[p]) { 
    parent[p] = parent[parent[p]];
    p = parent[p];
}

Анализируя

Амортизированная стоимость одной операции, как известно, является логарифмической.

  • Union & Connected → O (LogN) (амортизируется)

Резюме

  • Быстрый поиск → O (1) для связного и O (N) для объединения.
  • Быстрое соединение → O (N).
  • Взвешенное быстрое объединение → O (LogN).
  • Quick Union и Path Compression → O (LogN) (амортизировано).
  • Взвешенное быстрое объединение со сжатием пути → O (Log * N) (амортизировано).

Log * N - повторный логарифм N, обычно читается как «логарифм».

Другие связанные проблемы (просачивание)

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

Задача перколяции задается сеткой из N x N элементов, окрашенных в белый цвет, если он открыт, синий, если он соединен с любым элементом в верхнем ряду, и черный, если он закрыт. Система просачивается? Другими словами, соединяется ли верхний ряд с нижним через открытые элементы ?.

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

С сеткой из N x N элементов, чтобы проверить, связан ли какой-либо элемент в первой строке с каким-либо элементом внизу. Для этого потребуется N² вызовов команды connected.

Уловка, которую мы можем использовать, состоит в том, чтобы соединить все элементы в первой строке с «виртуальным» элементом, и мы сделаем то же самое для всех элементов в нижней строке.

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

Остерегайтесь этой ошибки!

Если элементы в нижнем ряду соединены с «виртуальным» нижним элементом, а элемент A внизу соединяется с верхним виртуальным узлом (стал синим).

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

Решения

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

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