Что такое union find

Обновлено: 06.05.2024

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

Говоря об этом Union-Find, его следует рассматривать как мой «алгоритм просвещения», потому что этот алгоритм был представлен в начале «Алгоритма 4», но он перевернул мое представление, он кажется таким изысканным! Позже я освежил LeetCode и проверил связанные темы алгоритмов. Это было очень интересно, и решение, данное «Алгоритмом 4», можно было бы дополнительно оптимизировать. Если добавить небольшую модификацию, временную сложность можно уменьшить до O (1).

Не несите чуши, просто доставляйте галантерею. Позвольте мне объяснить, что такое динамическое подключение.

1. Введение в проблему

Проще говоря, динамическое соединение можно абстрагировать до соединения изображения. Например, на следующем рисунке всего 10 узлов, они не связаны друг с другом и помечены цифрами 0-9:


Теперь наш алгоритм Union-Find в основном должен реализовывать эти два API:

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

1. Рефлексивность:узел p с участием p Подключен.

2. Симметрия: Если узел p с участием q Подключено, то q с участием p Тоже подключено.

3. Транзитивность: Если узел p с участием q Связано, q с участием r Подключено, то p с участием r Тоже подключено.

Например, на предыдущем рисунке любые два от 0 до 9разныеОчки не подключены, звоните connected Оба возвращают false, и есть 10 связанных компонентов.

Если ты позвонишь сейчас union(0, 1) , Тогда 0 и 1 соединяются, а количество связных компонент уменьшается до 9.

Позвони снова union(1, 2) , В это время подключены 0,1,2, звоните connected(0, 2) Он также возвращает истину, и подключенные компоненты становятся 8.


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

Таким образом, вы, вероятно, должны понять, что такое динамическое соединение. Ключом к алгоритму Union-Find является union с участием connected Эффективность функции. Итак, какая модель используется для представления связного состояния этой картины? Какая структура данных используется для реализации кода?

2. Основные идеи

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

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

Например, в графе с 10 узлами только что они не были связаны друг с другом в начале, вот и все:


Если два узла соединены, пусть корневой узел одного (любого) узла соединяется с корневым узлом другого узла.:


Таким образом, если узел p с участием q Если они подключены, они должны иметь один и тот же корневой узел.:


На этом алгоритм Union-Find в основном завершен. Разве это не потрясающе? Можно использовать массив для моделирования такого леса и так ловко решить эту более сложную задачу!

Так в чем же сложность этого алгоритма? Мы обнаружили, что основной API connected с участием union Сложность в find Функции, поэтому их сложность и find тем же.

find Основная функция - переход от узла к корню дерева, а его временная сложность - это высота дерева. Обычно мы можем думать, что высота дерева logN , Но это не обязательно так. logN Высота существует только в сбалансированном двоичном дереве. Для общего дерева может возникнуть крайний дисбаланс, в результате чего «дерево» почти выродится в «связанный список», а высота дерева может стать в худшем случае. N 。


Итак, приведенное выше решение, find , union , connected Временная сложность - O (N). Эта сложность очень неудовлетворительна. Вы хотите, чтобы теория графов решала проблемы с огромными объемами данных, такими как социальные сети. union с участием connected Звонки очень частые, и линейное время, необходимое для каждого звонка, совершенно невыносимо.

Ключ к проблеме - как найти способ избежать разбалансировки дерева.? Вам нужно только добавить промежуточный итог.

В-третьих, оптимизация баланса

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

Мы начали с простого и грубого p Где связано дерево q Ниже корневого узла дерева, где он находится, может возникнуть дисбаланс "с тяжелым верхом", например, следующая ситуация:


Со временем дерево может расти очень неравномерно. Мы действительно надеемся, что меньшее дерево будет связано с большим деревом, чтобы мы могли избежать тяжелой вершины и более сбалансированной 。 Решение - использовать дополнительный size Массив, который записывает количество узлов, содержащихся в каждом дереве, мы могли бы также назвать его «весом»:

Например size[3] = 5 Представлен узлом 3 Дерево, которое является корнем, имеет в общей сложности 5 Узлы. Итак, мы можем изменить это union метод:

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

В настоящее время, find , union , connected Временная сложность данных снижается до O (logN), даже если масштаб данных составляет сотни миллионов, необходимое время очень мало.

В-четвертых, сжатие пути

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


такой find Корневой узел определенного узла может быть найден за время O (1), соответственно, connected с участием union Сложность снижена до O (1).

Сделать это очень просто, достаточно лишь find Добавьте строку кода:

Эта операция немного странная. Достаточно взглянуть на GIF-файл, и вы поймете его роль (для наглядности это дерево более экстремальное):

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

Пять, заключительное резюме

Давайте сначала посмотрим на полный код:

Сложность алгоритма Union-Find можно проанализировать следующим образом: структура данных инициализации конструктора требует O (N) временной и пространственной сложности; соединение двух узлов union , Оцените возможность соединения двух узлов connected , Расчет связанных компонентов count Требуемая временная сложность - O (1).

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

Простейшая реализация: быстрое определение представителя


Система непересекающихся множеств в простейшей реализации

Определим массив id[], i-ая ячейка которого будет содержать представителя множества, которому принадлежит элемент i.

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

Определение представителя


Как показано выше, массив id[] содержит в своих ячейках значения представителей для всех элементов. Поэтому для вывода представителя в данной реализации достаточно вернуть значение соответствующей ячейки массива id[].

Можно видеть, что сложность операции определения представителя в этой реализации — O(1).

Объединение множеств


После слияния множеств должна сохраняться работоспособность операции find(), то есть все элементы объединённого множества должны иметь одного и того же представителя. Так как до объединения часть элементов имеет представителя a, а другая часть — представителя b, проще всего изменить все упоминания a на b:

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

Что такое union find

image


Всем привет. Недавно меня тоже настигла переломная веха студенческой жизни — бакалаврская дипломная работа. Среди многих формальных деталей этого замечательного явления особо заметным стоит нормоконтроль. Нет, я понимаю и всячески поддерживаю, что стандарты необходимы, в том числе стандарты на оформление академического текста. Просто наши стандарты, в отличие от западных, достаточно идиотичны. Они не экономят ни чернила, ни бумагу, они не упрощают поиск литературы по номенклатуре, а усложняют чтение названия. Не говоря уже о том, что текст стандарта спроектирован и описан людьми, работающими в редакторе Microsoft Word. Опять-таки, я не имею ничего против Word, это мощнейшая система. Но технический текст в нем набирать неудобно, и по гибкости он во много раз проигрывает бессмертному творению Дональда Кнута — LaTeX.

Под катом я полностью опишу процесс настройки каждой конкретной детали и использование их при написании, а также разные мелочи, упрощающие написание диплома еще больше. Сразу предупреждаю: где-то мои решения могут показаться костылями. Где-то они не слишком универсальны. Я это знаю, понимаю, принимаю и приветствую критику и предложения в комментариях ;-)

Задачи, рашаемые с помощью системы непересекающихся множеств

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

Алгоритм поиска объединения (Union-find), основные принципы, идеи кода, реализация на Java и упражнения LeetCode

Что такое Union-Find

Решите проблему такого рода: Сяохун и я - выпускники, Сяохун и Сяомин - выпускники, Сяомин и я - выпускники?

Фундаментальный

Обратитесь к отношениям эквивалентности в дискретной математике: рефлексивности, симметрии и транзитивности.
Рефлексивность: я и я - выпускники
Симметрия: мы с Сяохуном - выпускники, а я и Сяохун - выпускники.
Транзитивность: Сяохун и я - выпускники, Сяохун и Сяомин - выпускники, а мы с Сяомин - тоже выпускники. (Проблема решена)

Идея кода

Если мы хотим найти корреляцию в коллекции из N объектов, мы можем создать массив длиной N.
Элемент - это объект, а его значение в массиве - «маркер». Два соединенных объекта имеют одинаковую отметку.
Например:
My Little Red, Сяо Чжан, Сяо Ли, Сяо Мин
0 0 2 3 0
Значит, эта ситуация означает, что мы с Сяо Хун - выпускники. Всего в наборе 3 разных школы (кол-во == 3).

Реализация Union-find Java

Реализация взвешенного быстрого поиска в Java

В базовом Union Find наш алгоритм поиска имеет временную сложность O (N).
Для оптимизации существует алгоритм под названием «Быстрый поиск», в котором соединенные узлы образуют дерево, тогда наихудшая временная сложность поиска - это высота дерева.
Это еще не конец, у нас также есть "взвешенное быстрое объединение", то есть когда в объединении мы объединяем маленькое дерево в большое дерево, временная сложность объединения и поиска равна Уменьшить до O (log N).

Для оптимального решения достаточно добавить еще одну строку

Хотя я был очарован красотой улучшенного алгоритма поиска объединения, описанного выше, взвешенный QuickFind - не лучший случай, а быстрое объединение сжатия путей бесконечно близко к O (1).
В Union-find идеальный алгоритм - O (1), но его не существует.
Поэтому мы также называем быстрое объединение-поиск сжатия пути оптимальным решением.

Как сжать путь?
на самом деле очень прост. По пути мы ищем p, соединяем все узлы, через которые мы проходим, с root. На рисунке ниже мы должны пройти 1, 3 и 6, а затем достичь 9, поэтому в конце мы хотим подключить 1, 3 и 6 напрямую к D.

LeetCode:200. Number of Islands

Given a 2d grid map of '1’s (land) and '0’s (water), count the number of islands. An island is surrounded by water and is formed by connecting adjacent lands horizontally or vertically. You may assume all four edges of the grid are all surrounded by water.

Моноиды и их приложения: моноидальные вычисления в деревьях

Приветствую, Хабрахабр. Сегодня я хочу, в своём обычном стиле, устроить сообществу небольшой ликбез по структурам данных. Только на этот раз он будет гораздо более всеобъемлющ, а его применения и практичность — простираться далеко в самые разнообразные области программирования. Самые красивые применения, я, конечно же, покажу и опишу непосредственно в статье.

Итак, на повестке сегодняшнего дня — моноиды и их основное применение для кеширования вычислений в деревьях.

Моноид как концепция

Представьте себе множество чего угодно, множество, состоящее из объектов, которыми мы собираемся манипулировать. Назовём его M. На этом множестве мы вводим бинарную операцию, то есть функцию, которая паре элементов множества ставит в соответствие новый элемент. Здесь и далее эту абстрактную операцию мы будем обозначать "⊗", и записывать выражения в инфиксной форме: если a и b — элементы множества, то c = ab — тоже какой-то элемент этого множества.

Например, рассмотрим все строки, существующие на свете. И рассмотрим операцию конкатенации строк, традиционно обозначаемую в математике "◦", а в большинстве языков программирования "+": . Здесь множество M — строки, а "◦" выступает в качестве операции "⊗".
Или другой пример — функция fst, известная в функциональных языках при манипуляции с кортежами. Из двух своих аргументов она возвращает в качестве результата первый по порядку. Так, ; . Безразлично, на каком множестве рассматривать эту бинарную операцию, так что в вашей воле выбрать любое.

Далее мы на нашу операцию "⊗" накладываем ограничение ассоциативности. Это значит, что от неё требуется следующее: если с помощью "⊗" комбинируют последовательность объектов, то результат должен оставаться одинаковым вне зависимости от порядка применения "⊗". Более строго, для любых трёх объектов a, b и c должно иметь место:

Легко увидеть, что конкатенация строк ассоциативна: не важно, какое склеивание в последовательности строк выполнять раньше, а какое позже, в итоге все равно получится общая склейка всех строк в последовательности. То же касается и функции fst, ибо:
fst(fst(a, b), c) = a
fst(a, fst(b, c)) = a
Цепочка применений fst к последовательности в любом порядке всё равно выдаст её головной элемент.

И последнее, что мы потребуем: в множестве M по отношению к операции должен существовать нейтральный элемент, или единица операции. Это такой объект, который можно комбинировать с любым элементом множества, и это не изменит последний. Формально выражаясь, если e — нейтральный элемент, то для любого a из множества имеет место:
ae = ea = a
В примере со строками нейтральным элементом выступает пустая строка "" : с какой стороны к какой строке её ни приклеивай, строка не поменяется. А вот fst в этом отношении нам устроит подлянку: нейтральный элемент для неё придумать невозможно. Ведь всегда, и если , то свойство нейтральности мы теряем. Можно, конечно, рассмотреть fst на множестве из одного элемента, но кому такая скука нужна? :)

Каждую такую тройку <M, ⊗, e> мы и будем торжественно называть моноидом. Зафиксируем это знание в коде:

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

Вторая реализация: быстрое объединение


Система непересекающихся множеств: реализация для быстрого объединения

Идея данной реализации в том, что при объединении множеств изменяется лишь один элемент массива id[]. Чтобы это стало возможным, прежде всего необходимо определить несколько условий:

  • Теперь элементы множества будут представлены в некоторой многоуровневой древовидной структуре;
  • i-ая ячейка массива id[] будет содержать значение элемента, к которому в этой структуре присоединён элемент i;
  • Если некоторый элемент i не присоединён к другому элементу, то выполняется id[i] == i. Очевидно, что в каждой древовидной структуре найдётся элемент, обладающий таким свойством — это корень древовидной структуры. Важно, что от любого элемента структуры можно дойти до корневого элемента через некоторое конечное число «подъёмов» по массиву id[]. Эта особенность позволяет использовать корень древовидной структуры в качестве представителя множества.

Конструктор системы непересекающихся множеств остаётся прежним, так как в одноэлементном множестве единственный элемент будет являться корнем и в массиве id[] будет указывать на себя.

Определение представителя


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

Определение представителя в худшем случае следует по массиву id[] от элемента-листа до корня древовидной струтуры, поэтому её сложность прямо пропорциональна высоте древовидной структуры. Ниже показано, что эта сложность составляет O(N).

Объединение множеств


В рассмотренных ограничениях объединение множеств представляет собой объединение их древовидных структур. Для выполнения последнего достаточно присоединить корень одной структуры к элементу другой структуры; в целях минимального увеличения высоты результирующей структуры наиболее выгодно присоединять корень одной из структур к корню другой. Как упоминалось ранее, это потребует корректировки всего одного элемента массива id[]:

Несмотря на то, что операция объединения содержит лишь одно обращение к массиву id[], она использует вызовы операции find(), поэтому её сложность также зависит от высоты древовидной структуры. В том случае, когда происходит постоянное подвешивание корня структуры к единственному элементу другой структуры, результат вырождается в односвязный список, а его высота становится равной N. Поэтому операция объединения, равно как операция определения представителя, в данной реализации имеет сложность O(N).

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

Система непересекающихся множеств

Система неперескающихся множеств (англ. disjointed set union, иногда union-find) — специфическая структура данных, содержащая информацию о наборе множеств, которая позволяет объединять множества и отвечать на вопрос, принадлежат ли указанные элементы к одному множеству.

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

Изначально рассматриваются N различных элементов, каждый из которых представляет собой самостоятельное множество. Далее любые два множества допустимо объединять, при этом все элементы обоих множеств становятся элементами результирующего множества. Очевидно, что из начального состояния (N одноэлементных множеств) через (N-1) слияние будет получено состояние, при котором все элементы объединены в одно множество. Как можно будет убедиться в дальнейшем, реализации системы непересекающихся множеств достаточно просто дополнить операцией добавления нового одноэлементного множества (то есть добавления (N+1)-го элемента, формирующего самостоятельное множество).

Любое множество может быть уникальным образом идентифицировано с помощью одного из своих элементов: два множества не могут содержать один и тот же элемент по определению (этот факт отражён словом «непересекающихся» в названии структуры данных). Такой элемент называется представителем множества (англ. representative element).

Оптимальная реализация

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

Эвристика объединения по рангу


Как было показано ранее, время работы реализации «быстрое объединение» ухудшается с ростом высоты древовидной структуры. Сама же высота может стремительно деградировать до N при неудачно подобранных входных данных (когда структура большего размера присоединяется к корню структуры меньшго размера). Данная оптимизация служит для исключения подобных ситуаций и вводит новое правило: структура большего размера не может быть присоедина к структуре меньшего размера.

Для отслеживания размера структур определяется массив size[], элементы которого в конструкторе инициализируются единицами (так как исходные множества являются одноэлементными):

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

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

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

Эвристика сжатия путей


Дополнительно уменьшить высоту дерева можно, если в операции find() перенаправлять все посещённые вершины непосредственно на найденный корень:

Данная оптимизация вкупе с предыдущей делает дерево практически плоским, что обеспечивает асимптотику операций, почти равную O(1).

Ниже приведён полный код оптимальной реализации системы непересекающихся множеств.

Некоторые вопросы для размышления:

  • Каким образом реализуется операция makeSet(), добавляющая в систему новое одноэлементное множество?
  • Как реализовать эвристику объединения по рангу, использующую в качестве критерия высоту древовидных структур?
  • Обратите внимание: после выполнения метода find(), использующего эвристику сжатия путей, некоторые элементы массива size[] могут содержать неверную (неактуальную) информацию. Влияет ли это на правильность объединения по рангу? Нужно ли исправлять это несоответствие?

Интерфейс

Здесь и далее будем предполагать, что элементами множеств являются целые числа. Система непересекающихся множеств должна обеспечивать следующие операции:

Система непересекающихся множеств и её применения

Добрый день, Хабрахабр. Это еще один пост в рамках моей программы по обогащению базы данных крупнейшего IT-ресурса информацией по алгоритмам и структурам данных. Как показывает практика, этой информации многим не хватает, а необходимость встречается в самых разнообразных сферах программистской жизни.
Я продолжаю преимущественно выбирать те алгоритмы/структуры, которые легко понимаются и для которых не требуется много кода — а вот практическое значение сложно недооценить. В прошлый раз это было декартово дерево. В этот раз — система непересекающихся множеств. Она же известна под названиями disjoint set union (DSU) или Union-Find.

Условие

Поставим перед собой следующую задачу. Пускай мы оперируем элементами N видов (для простоты, здесь и далее — числами от 0 до N-1). Некоторые группы чисел объединены в множества. Также мы можем добавить в структуру новый элемент, он тем самым образует множество размера 1 из самого себя. И наконец, периодически некоторые два множества нам потребуется сливать в одно.

Формализируем задачу: создать быструю структуру, которая поддерживает следующие операции:

MakeSet(X) — внести в структуру новый элемент X, создать для него множество размера 1 из самого себя.
Find(X) — возвратить идентификатор множества, которому принадлежит элемент X. В качестве идентификатора мы будем выбирать один элемент из этого множества — представителя множества. Гарантируется, что для одного и того же множества представитель будет возвращаться один и тот же, иначе невозможно будет работать со структурой: не будет корректной даже проверка принадлежности двух элементов одному множеству if (Find(X) == Find(Y)) .
Unite(X, Y) — объединить два множества, в которых лежат элементы X и Y, в одно новое.

На рисунке я продемонстрирую работу такой гипотетической структуры.

Русские Блоги

Демонстрация работы

Читайте также: