Skip to content

Commit 2db05e3

Browse files
committed
complete bridge_guards additional_task
1 parent 8afd94e commit 2db05e3

File tree

5 files changed

+706
-0
lines changed

5 files changed

+706
-0
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
cmake_minimum_required(VERSION 3.20)
2+
3+
get_filename_component(PROJECT_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
4+
string(REPLACE " " "_" PROJECT_NAME ${PROJECT_NAME})
5+
project(${PROJECT_NAME} LANGUAGES CXX)
6+
7+
set(CMAKE_CXX_STANDARD 23)
8+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
9+
10+
file(GLOB_RECURSE SOURCE_LIST "src/*.cpp" "src/*.hpp")
11+
file(GLOB_RECURSE MAIN_SOURCE_LIST "src/main.cpp")
12+
set(TEST_SOURCE_LIST ${SOURCE_LIST})
13+
file(GLOB_RECURSE test_list "src/*test.cpp")
14+
15+
list(REMOVE_ITEM TEST_SOURCE_LIST ${MAIN_SOURCE_LIST})
16+
list(REMOVE_ITEM SOURCE_LIST ${test_list})
17+
18+
include_directories(${PROJECT_NAME} PUBLIC src)
19+
20+
find_library(Utils ../)
21+
22+
add_executable(${PROJECT_NAME} ${SOURCE_LIST})
23+
target_link_libraries(${PROJECT_NAME} PUBLIC Utils)
24+
25+
# Locate GTest
26+
enable_testing()
27+
find_package(GTest REQUIRED)
28+
include_directories(${GTEST_INCLUDE_DIRS})
29+
30+
# Link runTests with what we want to test and the GTest and pthread library
31+
add_executable(${PROJECT_NAME}_tests ${TEST_SOURCE_LIST})
32+
target_link_libraries(
33+
${PROJECT_NAME}_tests
34+
GTest::gtest_main
35+
Utils
36+
)
37+
38+
include(GoogleTest)
39+
gtest_discover_tests(${PROJECT_NAME}_tests)
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
# Задача: охранники речных мостов
2+
3+
Река имеет `N` контрольно-пропускных пунктов на левом берегу и `M` контрольно-пропускных пунктов на правом берегу.
4+
5+
`P` мостов построены, соединяя контрольно-пропускные пункты через реку. На контрольно-пропускных пунктах необходимо разместить охрану, и охрана может защищать все мосты, на которых присутствует этот КПП.
6+
7+
Для защиты одного моста может быть более одного охранника.
8+
9+
Найдите минимальное количество охранников, необходимое для защиты всех мостов через реку.
10+
11+
## Входные данные:
12+
13+
* Первая строка ввода состоит из 2 целых чисел, разделенных пробелами `N`
14+
и `M` − количество КПП на левом и правом берегу реки соответственно (1 ≤ `N`, `M` ≤ 100)
15+
* Вторая строка ввода состоит из одного целого числа `P` − общее количество мостов через реку (1 ≤ `P` ≤ 100).
16+
* Следующие `P` строк, каждая из которых состоит из 2 целых чисел, разделенных пробелами `u`, `v`, обозначающих, что между КПП `u` на левом берегу и КПП `v` на правом берегу есть мост (1 ≤ `u``N`) (1 ≤ `v``M`).
17+
18+
## Выходные данные:
19+
20+
* Одно целое число − минимальное количество охранников, необходимое для защиты всех мостов через реку.
21+
22+
## Пример:
23+
24+
### Вход:
25+
```
26+
4 3
27+
4
28+
1 3
29+
1 2
30+
2 2
31+
4 1
32+
```
33+
34+
### Выход:
35+
```
36+
3
37+
```
38+
39+
### Решение:
40+
41+
#### Введение и моделирование графом:
42+
43+
Для решения задачи о часовых на реке, смоделируем ситуацию с помощью двудольного графа.
44+
Одна доля графа представляла пункты наблюдения на левом берегу, а другая – на правом берегу.
45+
46+
Ребра между долями обозначали мосты. Целью было найти минимальный набор вершин (пунктов), которые ‘покрывают’ все ребра (мосты). Это классическая задача поиска минимального вершинного покрытия.
47+
48+
Известно, что в общем случае задача поиска минимального вершинного покрытия является NP-трудной, то есть не существует быстрого алгоритма для произвольных графов. Однако, для двудольных графов эту задачу можно решить за полиномиальное время, применив теорему Кёнига.
49+
50+
**Теорема Кёнига** утверждает, что в двудольном графе размер минимального вершинного покрытия равен размеру максимального паросочетания.
51+
52+
*Паросочетание* – это набор ребер, которые не имеют общих вершин, то есть ни одна вершина не является концом более чем одного ребра в паросочетании.
53+
54+
*Максимальное паросочетание* – это паросочетание с наибольшим возможным количеством ребер.
55+
56+
#### Максимальное паросочетание и алгоритм поиска максимального потока:
57+
58+
Для нахождения максимального паросочетания используем алгоритм поиска максимального потока.
59+
60+
Чтобы применить этот алгоритм, преобразуем двудольный граф в сеть, добавив искусственный источник (`s`) и сток (`t = s + 1`).
61+
62+
Источник (`s`) имел ребра единичной пропускной способности ко всем вершинам, представляющим пункты на левом берегу. Все вершины, представляющие пункты на правом берегу, имели ребра единичной пропускной способности к стоку (`t`). Рёбра между пунктами левого и правого берегов (представляющие мосты) также имели единичную пропускную способность.
63+
64+
Это преобразование позволило свести задачу поиска максимального паросочетания к задаче поиска максимального потока в созданной сети.
65+
66+
Для поиска максимального потока используем модификацию алгоритма Эдмондса-Карпа, реализуя обход в ширину (BFS) для поиска увеличивающего пути.
67+
68+
Функция `BFS` реализована следующим образом:
69+
```C++
70+
inline size_t BFS(size_t s, std::vector<ssize_t>& parent,
71+
std::vector<std::vector<size_t>>& adj_list,
72+
std::vector<std::vector<size_t>>& capacity) {
73+
parent[s] = -2;
74+
size_t n = parent.size();
75+
76+
std::vector<bool> visited(n, false);
77+
visited[s] = true;
78+
79+
std::queue<std::pair<size_t, size_t>> nodes;
80+
81+
nodes.push({s, LLONG_MAX});
82+
83+
while (!nodes.empty()) {
84+
std::pair<size_t, size_t> node_flow = nodes.front();
85+
size_t node = node_flow.first;
86+
size_t flow = node_flow.second;
87+
88+
nodes.pop();
89+
90+
for (size_t i = 0; i < adj_list[node].size(); i++) {
91+
size_t next = adj_list[node][i];
92+
93+
if (visited[next] || capacity[node][i] == 0) continue;
94+
95+
visited[next] = true;
96+
97+
parent[next] = node;
98+
99+
size_t new_flow = std::min(flow, capacity[node][i]);
100+
101+
if (next == s + 1) return new_flow;
102+
nodes.push({next, new_flow});
103+
}
104+
}
105+
106+
return 0;
107+
}
108+
```
109+
110+
В этой функции `parent` используется для отслеживания пути, `adj_list` - список смежности, `capacity` - матрица пропускных способностей. Функция возвращает пропускную способность найденного увеличивающего пути, или 0, если путь не найден.
111+
112+
#### Реализация алгоритма максимального потока:
113+
114+
Алгоритм максимального потока реализован в функции `MaxFlow`:
115+
```C++
116+
inline size_t MaxFlow(size_t s, std::vector<std::vector<size_t>>& adj_list,
117+
std::vector<std::vector<size_t>>& capacity) {
118+
size_t flow = 0;
119+
std::vector<ssize_t> parent(adj_list.size(), -1);
120+
121+
size_t new_flow = 0;
122+
123+
while ((new_flow = BFS(s, parent, adj_list, capacity))) {
124+
flow += new_flow;
125+
size_t curr = s + 1;
126+
127+
while (curr != s) {
128+
size_t prev = parent[curr];
129+
size_t idx = (find(adj_list[prev].begin(), adj_list[prev].end(), curr) -
130+
adj_list[prev].begin());
131+
capacity[prev][idx] -= new_flow;
132+
133+
idx = (find(adj_list[curr].begin(), adj_list[curr].end(), prev) -
134+
adj_list[curr].begin());
135+
capacity[curr][idx] += new_flow;
136+
137+
curr = prev;
138+
}
139+
}
140+
141+
return flow;
142+
}
143+
144+
```
145+
146+
Функция `MaxFlow` инициализирует поток в 0, и пока находит увеличивающие пути с помощью `BFS`, наращивает поток и обновляет пропускные способности остаточных ребер. Возвращает величину максимального потока.
147+
148+
#### Преобразование двудольного графа в сеть и нахождение максимального паросочетания
149+
150+
Преобразование двудольного графа в сеть и использование алгоритма максимального потока для нахождения максимального паросочетания реализовано в функции `MaximumBipartiteMatching`:
151+
```C++
152+
inline size_t MaximumBipartiteMatching(size_t n, size_t m,
153+
Graph<size_t>& bipartite_graph) {
154+
std::vector<std::vector<std::pair<size_t, size_t>>> bipartite_edges_stack(
155+
n + m + 3);
156+
std::vector<std::vector<size_t>> adj_list(n + m + 3);
157+
std::vector<std::vector<size_t>> capacity(n + m + 3);
158+
159+
for (size_t i = 0; i < bipartite_graph.EdgesAmount(); i++) {
160+
size_t u = StartVertFromTuple(bipartite_graph.Edges()[i]);
161+
size_t v = EndVertFromTuple(bipartite_graph.Edges()[i]);
162+
163+
v += n;
164+
165+
bipartite_edges_stack[u].push_back({v, 1});
166+
bipartite_edges_stack[v].push_back({u, 0});
167+
}
168+
169+
for (size_t i = 1; i <= n; i++) {
170+
bipartite_edges_stack[n + m + 1].push_back({i, 1});
171+
bipartite_edges_stack[i].push_back({n + m + 1, 0});
172+
}
173+
for (size_t i = 1; i <= m; i++) {
174+
bipartite_edges_stack[i + n].push_back({n + m + 2, 1});
175+
bipartite_edges_stack[n + m + 2].push_back({i + n, 0});
176+
}
177+
178+
for (size_t i = 1; i <= n + m + 2; i++)
179+
sort(bipartite_edges_stack[i].begin(), bipartite_edges_stack[i].end());
180+
181+
for (size_t i = 1; i <= n + m + 2; i++)
182+
for (size_t j = 0; j < bipartite_edges_stack[i].size(); j++) {
183+
adj_list[i].push_back(bipartite_edges_stack[i][j].first);
184+
capacity[i].push_back(bipartite_edges_stack[i][j].second);
185+
}
186+
187+
return MaxFlow(n + m + 1, adj_list, capacity);
188+
}
189+
190+
```
191+
192+
В этой функции двудольный граф `bipartite_graph` преобразуется в сеть с помощью `bipartite_edges_stack`, добавляется источник (`n + m + 1`) и сток (`n + m + 2`), затем строится матрица смежности `adj_list` и матрица пропускных способностей `capacity` и вызывается функция MaxFlow для вычисления максимального потока.
193+
194+
#### Вызов функции решения и заключение:
195+
196+
Функция `Solution` считывает входные данные и выводит результат:
197+
198+
```C++
199+
inline void Solution(std::istream& is = std::cin,
200+
std::ostream& os = std::cout) {
201+
size_t n, m;
202+
is >> n >> m;
203+
204+
size_t p;
205+
is >> p;
206+
207+
Graph<size_t> bipartite_graph;
208+
209+
for (size_t i = 0; i < p; i++) {
210+
size_t u, v;
211+
is >> u >> v;
212+
213+
bipartite_graph.AddEdge({u, v});
214+
}
215+
216+
os << MaximumBipartiteMatching(n, m, bipartite_graph) << std::endl;
217+
return;
218+
}
219+
```
220+
221+
В результате, величина максимального потока, возвращаемая функцией `MaximumBipartiteMatching` и являющаяся размером максимального паросочетания, дает размер минимального вершинного покрытия, что и являлось ответом к задаче – минимальное количество необходимых часовых.

0 commit comments

Comments
 (0)