Skip to content

Commit c16cb7c

Browse files
authored
Merge pull request #85 from algorithmics-blog/disjoint_set
disjoint set article
2 parents b8d6da4 + 1a1b162 commit c16cb7c

File tree

10 files changed

+352
-0
lines changed

10 files changed

+352
-0
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package disjoint_set
2+
3+
type DisjointSet struct {
4+
// Представление графа в виде слайса
5+
nodes []int
6+
}
7+
8+
func NewDisjointSet(size int) DisjointSet {
9+
// Создаем слайс заданного размера по количеству элементов
10+
nodes := make([]int, size)
11+
12+
for i := 0; i < size; i++ {
13+
// При инициализации считаем что каждый элемент обособлен и не соединен с другими.
14+
// В таком случае каждый элемент является рутовым сам для себя
15+
nodes[i] = i
16+
}
17+
18+
return DisjointSet{
19+
nodes: nodes,
20+
}
21+
}
22+
23+
func (uf *DisjointSet) find(node int) int {
24+
return uf.nodes[node]
25+
}
26+
27+
func (uf *DisjointSet) connected(x int, y int) bool {
28+
return uf.find(x) == uf.find(y)
29+
}
30+
31+
func (uf *DisjointSet) union(x int, y int) {
32+
rootX := uf.find(x)
33+
rootY := uf.find(y)
34+
35+
// Производим объединение только если два элемента находятся не в одном множестве и не имеют одинаковый рутовый узел.
36+
if rootY != rootX {
37+
for i := range uf.nodes {
38+
// Обновляем значение рутового элемента на новое у всех элементов второго множества
39+
if uf.nodes[i] == rootY {
40+
uf.nodes[i] = rootX
41+
}
42+
}
43+
}
44+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package disjoint_set
2+
3+
import (
4+
"github.com/stretchr/testify/assert"
5+
"testing"
6+
)
7+
8+
func Test_DisjointSet(t *testing.T) {
9+
set := NewDisjointSet(8)
10+
11+
set.union(0, 2)
12+
set.union(0, 1)
13+
set.union(2, 3)
14+
set.union(1, 3)
15+
16+
set.union(4, 5)
17+
set.union(4, 6)
18+
set.union(5, 6)
19+
20+
t.Run("Create disjoint set", func(t *testing.T) {
21+
assert.Equal(t, []int{0, 0, 0, 0, 4, 4, 4, 7}, set.nodes)
22+
})
23+
24+
t.Run("Find direct connection", func(t *testing.T) {
25+
assert.Equal(t, set.find(3), 0)
26+
})
27+
28+
t.Run("Find transitive connection", func(t *testing.T) {
29+
assert.Equal(t, set.find(5), 4)
30+
})
31+
32+
t.Run("Find in set with one element", func(t *testing.T) {
33+
assert.Equal(t, set.find(7), 7)
34+
})
35+
36+
t.Run("Check direct connection", func(t *testing.T) {
37+
assert.Equal(t, set.connected(4, 5), true)
38+
})
39+
40+
t.Run("Check transitive connection", func(t *testing.T) {
41+
assert.Equal(t, set.connected(0, 3), true)
42+
})
43+
44+
t.Run("Check root connection", func(t *testing.T) {
45+
assert.Equal(t, set.connected(7, 7), true)
46+
})
47+
48+
t.Run("Check not connected elements", func(t *testing.T) {
49+
assert.Equal(t, set.connected(0, 7), false)
50+
})
51+
52+
t.Run("Check union", func(t *testing.T) {
53+
set.union(3, 4)
54+
assert.Equal(t, set.nodes, []int{0, 0, 0, 0, 0, 0, 0, 7})
55+
assert.Equal(t, set.connected(0, 6), true)
56+
})
57+
}

content/blog/disjoint_set/index.md

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
---
2+
layout: layouts/post.njk
3+
title: Disjoint Set
4+
date: 2025-02-03
5+
preview_image: /images/previews/disjoint_set.webp
6+
tags:
7+
- data_structure
8+
- graph
9+
---
10+
11+
---
12+
13+
В этом посте мы с вами не будем решать конкретную задачу, а познакомимся с новой структурой данных.
14+
15+
Представим, что перед нами есть карта, на которой схематично обозначены 8 городов.
16+
При этом, города `0`, `1`, `2` и `3` соединены между собой дорогами.
17+
Города `4`, `5` и `6` также соединены между собой, а город `7` обособлен и не имеет сообщения с другими.
18+
19+
Таким образом города являются вершинами графов, а дороги — их ребрами.
20+
21+
![Схематичная карта городов](/images/resources/disjoint_set/map.png)
22+
23+
Города могут иметь прямое соединение, как например `4` и `6`, или же транзитивное как в случае `0` и `3`.
24+
В результате мы имеем три непересекающихся множества:
25+
26+
- `[0, 1, 2, 3]`
27+
- `[4, 5, 6]`
28+
- `[7]`
29+
30+
Теперь нам нужно построить эффективную структуру, которая позволит быстро понимать, соединены ли два города между собой, и
31+
даст возможность объединять два непересекающихся множества в одно.
32+
33+
Для подобных задач можно использовать структуру данных `Disjoint Set`, которую также иногда называют `Union Find`.
34+
35+
## Базовая реализация
36+
37+
Базово такая структура данных должна поддерживать три основных метода
38+
39+
- `find(x int) int` — находит рутовый элемент в множестве для указанного узла;
40+
- `union(x int, y int)` — объединяет два множества путем соединения двух вершин в них между собой;
41+
- `connected(x int, y int)` — проверяет соединены ли две вершины графа между собой, то есть входят ли они в одно
42+
множество.
43+
44+
Внутри структура будет хранить связи между вершинами графа в виде массива, где индекс массива обозначает номер города, а
45+
значение по индексу — индекс рутового узла в графе.
46+
47+
- В первом множестве решим, что рутовым узлом будет город с номером `0`. Он же является рутовым узлом для самого себя.
48+
- Во втором множестве выберем город с номером `4`. Он также является рутовым узлом для самого себя.
49+
- Семерка ни с чем не связана, поэтому она является рутовым узлом в своем множестве.
50+
51+
Таким образом для нашего случая массив должен иметь следующий вид.
52+
53+
![Схематичная карта городов](/images/resources/disjoint_set/array.png)
54+
55+
### Поиск
56+
57+
Теперь не сложно догадаться, что для реализации поиска достаточно получить индекс рутового узла для конкретного города
58+
по его индексу в массиве.
59+
60+
### Проверка связности
61+
62+
Проверка связности тоже реализуется крайне легко. Нужно получить рутовые элементы для города `x` и `y` при помощи метода `find` и сравнить их.
63+
Если два города имеют один и тот же рутовый город в графе, то они связаны либо напрямую, либо транзитивно.
64+
65+
### Объединение множеств
66+
67+
Теперь предположим, что нам нужно соединить города с номерами `3` и `4`, тем самым объединив первое и второе множества.
68+
69+
![Схематичная карта городов](/images/resources/disjoint_set/map_2.png)
70+
71+
Мы решаем, что у объединенного множества родительский элемент остается равным `0`.
72+
Это означает, что города с номерами `3`, `4` и `5` должны получить связь с ним, то есть обновить значение своего рутового узла на `0`.
73+
74+
Таким образом наш массив должен получить следующие значения.
75+
76+
![Схематичная карта городов](/images/resources/disjoint_set/array_2.png)
77+
78+
## Реализация
79+
80+
Теперь осталось имплементировать структуру данных по описанной логике.
81+
82+
{% renderFile "_includes/components/solution.njk", taskName = "disjoint_set" %}
83+
84+
А в качестве проверки нашей реализации напишем несколько простых тестов.
85+
86+
{% tabs %}
87+
{% tab "GO" %}
88+
89+
```go
90+
func Test_DisjointSet(t *testing.T) {
91+
set := NewDisjointSet(8)
92+
93+
// Наполняем структуру данными о связях между городами
94+
set.union(0, 2)
95+
set.union(0, 1)
96+
set.union(2, 3)
97+
set.union(1, 3)
98+
99+
set.union(4, 5)
100+
set.union(4, 6)
101+
set.union(5, 6)
102+
103+
t.Run("Create disjoint set", func(t *testing.T) {
104+
assert.Equal(t, []int{0, 0, 0, 0, 4, 4, 4, 7}, set.nodes)
105+
assert.Equal(t, 4, set.find(5))
106+
})
107+
108+
t.Run("Check union", func(t *testing.T) {
109+
set.union(3, 4)
110+
assert.Equal(t, set.nodes, []int{0, 0, 0, 0, 0, 0, 0, 7})
111+
assert.Equal(t, set.connected(0, 6), true)
112+
})
113+
}
114+
```
115+
116+
{% endtab %}
117+
118+
{% tab "Type Script" %}
119+
120+
```typescript
121+
describe('DisjointSet', () => {
122+
const set = new DisjointSet(8)
123+
124+
// Наполняем структуру данными о связях между городами
125+
set.union(0, 2)
126+
set.union(0, 1)
127+
set.union(2, 3)
128+
set.union(1, 3)
129+
130+
set.union(4, 5)
131+
set.union(4, 6)
132+
set.union(5, 6)
133+
134+
test("Create disjoint set", () => {
135+
expect(set.nodes).toEqual([0, 0, 0, 0, 4, 4, 4, 7]);
136+
expect(set.find(5)).toEqual(4);
137+
})
138+
139+
test("Check union", () => {
140+
set.union(3, 4)
141+
expect(set.nodes).toEqual([0, 0, 0, 0, 0, 0, 0, 7]);
142+
expect(set.connected(0, 6)).toEqual(true);
143+
})
144+
})
145+
```
146+
147+
{% endtab %}
148+
{% endtabs %}
149+
150+
## Оценка сложности
151+
152+
Теперь оценим сложность каждого метода созданной нами структуры данных.
153+
154+
### find
155+
156+
**По времени**
157+
158+
- `find` — сложность `O(1)`, так как мы получаем значение по индексу в массиве.
159+
- `connected` — сложность `O(1)`, так как мы дважды получаем значения по индексу в массиве.
160+
- `union` — сложность `O(n)`, так как нам нужно перебрать все элементы массива, чтобы обновить индексы родительских узлов.
161+
162+
**По памяти**
163+
164+
Сложность `O(n)` для всех методов, так как мы создаем массив `nodes` длинною `n` для хранения значений.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { DisjointSet } from "./solution";
2+
3+
describe('DisjointSet', () => {
4+
const set = new DisjointSet(8)
5+
6+
set.union(0, 2)
7+
set.union(0, 1)
8+
set.union(2, 3)
9+
set.union(1, 3)
10+
11+
set.union(4, 5)
12+
set.union(4, 6)
13+
set.union(5, 6)
14+
15+
test("Create disjoint set", () => {
16+
expect(set.nodes).toEqual([0, 0, 0, 0, 4, 4, 4, 7]);
17+
})
18+
19+
test("Find direct connection", () => {
20+
expect(set.find(3)).toEqual(0);
21+
})
22+
23+
test("Find transitive connection", () => {
24+
expect(set.find(5)).toEqual(4);
25+
})
26+
27+
test("Find in set with one element", () => {
28+
expect(set.find(7)).toEqual(7);
29+
})
30+
31+
test("Check direct connection", () => {
32+
expect(set.connected(4, 5)).toEqual(true);
33+
})
34+
35+
test("Check transitive connection", () => {
36+
expect(set.connected(0, 3)).toEqual(true);
37+
})
38+
39+
test("Check root connection", () => {
40+
expect(set.connected(7, 7)).toEqual(true);
41+
})
42+
43+
test("Check not connected elements", () => {
44+
expect(set.connected(0, 7)).toEqual(false);
45+
})
46+
47+
test("Check union", () => {
48+
set.union(3, 4)
49+
expect(set.nodes).toEqual([0, 0, 0, 0, 0, 0, 0, 7]);
50+
expect(set.connected(0, 6)).toEqual(true);
51+
})
52+
})
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
export class DisjointSet {
2+
readonly nodes: number[];
3+
4+
constructor(size: number) {
5+
// Создаем массив заданного размера по количеству элементов
6+
this.nodes = new Array(size);
7+
8+
for (let i = 0; i < size; i++) {
9+
// При инициализации каждый элемент является рутовым сам для себя
10+
this.nodes[i] = i;
11+
}
12+
}
13+
14+
find(node: number): number {
15+
return this.nodes[node];
16+
}
17+
18+
connected(x: number, y: number): boolean {
19+
return this.find(x) === this.find(y);
20+
}
21+
22+
union(x: number, y: number): void {
23+
const rootX = this.find(x);
24+
const rootY = this.find(y);
25+
26+
// Объединяем множества, если они не совпадают
27+
if (rootX !== rootY) {
28+
for (let i = 0; i < this.nodes.length; i++) {
29+
if (this.nodes[i] === rootY) {
30+
this.nodes[i] = rootX;
31+
}
32+
}
33+
}
34+
}
35+
}
108 KB
Loading
84.4 KB
Loading
90 KB
Loading
176 KB
Loading
185 KB
Loading

0 commit comments

Comments
 (0)