diff --git a/CMakeLists.txt b/CMakeLists.txt index 06c6f26..23b3f71 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,21 +1,21 @@ -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.20) -project(homeworks) +project(homeworks LANGUAGES CXX) -add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/lib) +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_FLAGS "-Wall -Wextra -pedantic -std=c++23 -O2") +add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/lib) add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/sandbox) - add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/additional_tasks) -file(GLOB_RECURSE tasks_dirs LIST_DIRECTORIES true ".") +file(GLOB_RECURSE SUBFOLDERS LIST_DIRECTORIES true ".") -foreach(dir ${tasks_dirs}) - IF(IS_DIRECTORY ${dir}) - IF(${dir} MATCHES "task_0[0-9]$" AND NOT ${dir} MATCHES "build") - add_subdirectory(${dir}) - ENDIF() - ELSE() - CONTINUE() - ENDIF() +foreach(FOLDER ${SUBFOLDERS}) + if(IS_DIRECTORY ${FOLDER}) + if(${FOLDER} MATCHES "task_0[0-9]$" AND NOT ${FOLDER} MATCHES "build") + add_subdirectory(${FOLDER}) + endif() + endif() endforeach() diff --git a/README.md b/README.md index 5b185df..1884dc7 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ # Домашнее задание для 2 семестра алгоритмов и структур данных +## (Homework for second semester algorithms and data structures on MIPT DAFE/RSE) ### Для удобства можно пользоваться папкой lib, все файлы из этой папки будут подключаться к любой задаче -### Можно получить дополнительные баллы, если добавить интересные текстовые задачи. Необходимы текст задачи, решение и тесты. Каждая задача отдельный ПР, полчуть дополнительные баллы можно только если пулл реквест замержен в основную ветку. +Можно получить дополнительные баллы, если добавить интересные текстовые задачи. Необходимы текст задачи, решение и тесты. Каждая задача отдельный - Pull Request, получить дополнительные баллы можно только если PR замерджен в основную ветку. -### Можно получить дополнительные баллы, если добавить теорию в папку doc. Делается в отдельном ПР, полчуть дополнительные баллы можно только если пулл реквест замержен в основную ветку. +Можно получить дополнительные баллы, если добавить теорию в папку doc. Делается в отдельном PR, только если PR замерджен в основную ветку. -### Код должен быть отформатирован clang-format'ом со стилем Google +Код должен быть отформатирован clang-format'ом со стилем Google. diff --git a/additional_tasks/CMakeLists.txt b/additional_tasks/CMakeLists.txt index 9fc7aad..7647cf9 100644 --- a/additional_tasks/CMakeLists.txt +++ b/additional_tasks/CMakeLists.txt @@ -1,15 +1,13 @@ -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.20) -project(additional_tasks) +project(additional_tasks LANGUAGES CXX) -file(GLOB_RECURSE tasks_dirs LIST_DIRECTORIES true ".") +file(GLOB_RECURSE SUBFOLDERS LIST_DIRECTORIES true ".") -foreach(dir ${tasks_dirs}) - IF(IS_DIRECTORY ${dir}) - IF(NOT ${dir} MATCHES ".*src.*") - add_subdirectory(${dir}) - ENDIF() - ELSE() - CONTINUE() - ENDIF() +foreach(FOLDER ${SUBFOLDERS}) + if(IS_DIRECTORY ${FOLDER}) + if(NOT ${FOLDER} MATCHES ".*src.*") + add_subdirectory(${FOLDER}) + endif() + endif() endforeach() diff --git a/additional_tasks/bridge_guards/CMakeLists.txt b/additional_tasks/bridge_guards/CMakeLists.txt new file mode 100644 index 0000000..7e3bf1f --- /dev/null +++ b/additional_tasks/bridge_guards/CMakeLists.txt @@ -0,0 +1,39 @@ +cmake_minimum_required(VERSION 3.20) + +get_filename_component(PROJECT_NAME ${CMAKE_CURRENT_LIST_DIR} NAME) +string(REPLACE " " "_" PROJECT_NAME ${PROJECT_NAME}) +project(${PROJECT_NAME} LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +file(GLOB_RECURSE SOURCE_LIST "src/*.cpp" "src/*.hpp") +file(GLOB_RECURSE MAIN_SOURCE_LIST "src/main.cpp") +set(TEST_SOURCE_LIST ${SOURCE_LIST}) +file(GLOB_RECURSE test_list "src/*test.cpp") + +list(REMOVE_ITEM TEST_SOURCE_LIST ${MAIN_SOURCE_LIST}) +list(REMOVE_ITEM SOURCE_LIST ${test_list}) + +include_directories(${PROJECT_NAME} PUBLIC src) + +find_library(Utils ../) + +add_executable(${PROJECT_NAME} ${SOURCE_LIST}) +target_link_libraries(${PROJECT_NAME} PUBLIC Utils) + +# Locate GTest +enable_testing() +find_package(GTest REQUIRED) +include_directories(${GTEST_INCLUDE_DIRS}) + +# Link runTests with what we want to test and the GTest and pthread library +add_executable(${PROJECT_NAME}_tests ${TEST_SOURCE_LIST}) +target_link_libraries( + ${PROJECT_NAME}_tests + GTest::gtest_main + Utils +) + +include(GoogleTest) +gtest_discover_tests(${PROJECT_NAME}_tests) diff --git a/additional_tasks/bridge_guards/README.md b/additional_tasks/bridge_guards/README.md new file mode 100644 index 0000000..b577909 --- /dev/null +++ b/additional_tasks/bridge_guards/README.md @@ -0,0 +1,221 @@ +# Задача: охранники речных мостов + +Река имеет `N` контрольно-пропускных пунктов на левом берегу и `M` контрольно-пропускных пунктов на правом берегу. + +`P` мостов построены, соединяя контрольно-пропускные пункты через реку. На контрольно-пропускных пунктах необходимо разместить охрану, и охрана может защищать все мосты, на которых присутствует этот КПП. + +Для защиты одного моста может быть более одного охранника. + +Найдите минимальное количество охранников, необходимое для защиты всех мостов через реку. + +## Входные данные: + +* Первая строка ввода состоит из 2 целых чисел, разделенных пробелами `N` +и `M` − количество КПП на левом и правом берегу реки соответственно (1 ≤ `N`, `M` ≤ 100) +* Вторая строка ввода состоит из одного целого числа `P` − общее количество мостов через реку (1 ≤ `P` ≤ 100). +* Следующие `P` строк, каждая из которых состоит из 2 целых чисел, разделенных пробелами `u`, `v`, обозначающих, что между КПП `u` на левом берегу и КПП `v` на правом берегу есть мост (1 ≤ `u` ≤ `N`) (1 ≤ `v` ≤ `M`). + +## Выходные данные: + +* Одно целое число − минимальное количество охранников, необходимое для защиты всех мостов через реку. + +## Пример: + +### Вход: +``` +4 3 +4 +1 3 +1 2 +2 2 +4 1 +``` + +### Выход: +``` +3 +``` + +### Решение: + +#### Введение и моделирование графом: + +Для решения задачи о часовых на реке, смоделируем ситуацию с помощью двудольного графа. +Одна доля графа представляла пункты наблюдения на левом берегу, а другая – на правом берегу. + +Ребра между долями обозначали мосты. Целью было найти минимальный набор вершин (пунктов), которые ‘покрывают’ все ребра (мосты). Это классическая задача поиска минимального вершинного покрытия. + +Известно, что в общем случае задача поиска минимального вершинного покрытия является NP-трудной, то есть не существует быстрого алгоритма для произвольных графов. Однако, для двудольных графов эту задачу можно решить за полиномиальное время, применив теорему Кёнига. + +**Теорема Кёнига** утверждает, что в двудольном графе размер минимального вершинного покрытия равен размеру максимального паросочетания. + +*Паросочетание* – это набор ребер, которые не имеют общих вершин, то есть ни одна вершина не является концом более чем одного ребра в паросочетании. + +*Максимальное паросочетание* – это паросочетание с наибольшим возможным количеством ребер. + +#### Максимальное паросочетание и алгоритм поиска максимального потока: + +Для нахождения максимального паросочетания используем алгоритм поиска максимального потока. + +Чтобы применить этот алгоритм, преобразуем двудольный граф в сеть, добавив искусственный источник (`s`) и сток (`t = s + 1`). + +Источник (`s`) имел ребра единичной пропускной способности ко всем вершинам, представляющим пункты на левом берегу. Все вершины, представляющие пункты на правом берегу, имели ребра единичной пропускной способности к стоку (`t`). Рёбра между пунктами левого и правого берегов (представляющие мосты) также имели единичную пропускную способность. + +Это преобразование позволило свести задачу поиска максимального паросочетания к задаче поиска максимального потока в созданной сети. + +Для поиска максимального потока используем модификацию алгоритма Эдмондса-Карпа, реализуя обход в ширину (BFS) для поиска увеличивающего пути. + +Функция `BFS` реализована следующим образом: +```C++ +inline size_t BFS(size_t s, std::vector& parent, + std::vector>& adj_list, + std::vector>& capacity) { + parent[s] = -2; + size_t n = parent.size(); + + std::vector visited(n, false); + visited[s] = true; + + std::queue> nodes; + + nodes.push({s, LLONG_MAX}); + + while (!nodes.empty()) { + std::pair node_flow = nodes.front(); + size_t node = node_flow.first; + size_t flow = node_flow.second; + + nodes.pop(); + + for (size_t i = 0; i < adj_list[node].size(); i++) { + size_t next = adj_list[node][i]; + + if (visited[next] || capacity[node][i] == 0) continue; + + visited[next] = true; + + parent[next] = node; + + size_t new_flow = std::min(flow, capacity[node][i]); + + if (next == s + 1) return new_flow; + nodes.push({next, new_flow}); + } + } + + return 0; +} +``` + +В этой функции `parent` используется для отслеживания пути, `adj_list` - список смежности, `capacity` - матрица пропускных способностей. Функция возвращает пропускную способность найденного увеличивающего пути, или 0, если путь не найден. + +#### Реализация алгоритма максимального потока: + +Алгоритм максимального потока реализован в функции `MaxFlow`: +```C++ +inline size_t MaxFlow(size_t s, std::vector>& adj_list, + std::vector>& capacity) { + size_t flow = 0; + std::vector parent(adj_list.size(), -1); + + size_t new_flow = 0; + + while ((new_flow = BFS(s, parent, adj_list, capacity))) { + flow += new_flow; + size_t curr = s + 1; + + while (curr != s) { + size_t prev = parent[curr]; + size_t idx = (find(adj_list[prev].begin(), adj_list[prev].end(), curr) - + adj_list[prev].begin()); + capacity[prev][idx] -= new_flow; + + idx = (find(adj_list[curr].begin(), adj_list[curr].end(), prev) - + adj_list[curr].begin()); + capacity[curr][idx] += new_flow; + + curr = prev; + } + } + + return flow; +} + +``` + +Функция `MaxFlow` инициализирует поток в 0, и пока находит увеличивающие пути с помощью `BFS`, наращивает поток и обновляет пропускные способности остаточных ребер. Возвращает величину максимального потока. + +#### Преобразование двудольного графа в сеть и нахождение максимального паросочетания + +Преобразование двудольного графа в сеть и использование алгоритма максимального потока для нахождения максимального паросочетания реализовано в функции `MaximumBipartiteMatching`: +```C++ +inline size_t MaximumBipartiteMatching(size_t n, size_t m, + Graph& bipartite_graph) { + std::vector>> bipartite_edges_stack( + n + m + 3); + std::vector> adj_list(n + m + 3); + std::vector> capacity(n + m + 3); + + for (size_t i = 0; i < bipartite_graph.EdgesAmount(); i++) { + size_t u = StartVertFromTuple(bipartite_graph.Edges()[i]); + size_t v = EndVertFromTuple(bipartite_graph.Edges()[i]); + + v += n; + + bipartite_edges_stack[u].push_back({v, 1}); + bipartite_edges_stack[v].push_back({u, 0}); + } + + for (size_t i = 1; i <= n; i++) { + bipartite_edges_stack[n + m + 1].push_back({i, 1}); + bipartite_edges_stack[i].push_back({n + m + 1, 0}); + } + for (size_t i = 1; i <= m; i++) { + bipartite_edges_stack[i + n].push_back({n + m + 2, 1}); + bipartite_edges_stack[n + m + 2].push_back({i + n, 0}); + } + + for (size_t i = 1; i <= n + m + 2; i++) + sort(bipartite_edges_stack[i].begin(), bipartite_edges_stack[i].end()); + + for (size_t i = 1; i <= n + m + 2; i++) + for (size_t j = 0; j < bipartite_edges_stack[i].size(); j++) { + adj_list[i].push_back(bipartite_edges_stack[i][j].first); + capacity[i].push_back(bipartite_edges_stack[i][j].second); + } + + return MaxFlow(n + m + 1, adj_list, capacity); +} + +``` + +В этой функции двудольный граф `bipartite_graph` преобразуется в сеть с помощью `bipartite_edges_stack`, добавляется источник (`n + m + 1`) и сток (`n + m + 2`), затем строится матрица смежности `adj_list` и матрица пропускных способностей `capacity` и вызывается функция MaxFlow для вычисления максимального потока. + +#### Вызов функции решения и заключение: + +Функция `Solution` считывает входные данные и выводит результат: + +```C++ +inline void Solution(std::istream& is = std::cin, + std::ostream& os = std::cout) { + size_t n, m; + is >> n >> m; + + size_t p; + is >> p; + + Graph bipartite_graph; + + for (size_t i = 0; i < p; i++) { + size_t u, v; + is >> u >> v; + + bipartite_graph.AddEdge({u, v}); + } + + os << MaximumBipartiteMatching(n, m, bipartite_graph) << std::endl; + return; +} +``` + +В результате, величина максимального потока, возвращаемая функцией `MaximumBipartiteMatching` и являющаяся размером максимального паросочетания, дает размер минимального вершинного покрытия, что и являлось ответом к задаче – минимальное количество необходимых часовых. \ No newline at end of file diff --git a/additional_tasks/bridge_guards/src/bridge_guards.hpp b/additional_tasks/bridge_guards/src/bridge_guards.hpp new file mode 100644 index 0000000..b091362 --- /dev/null +++ b/additional_tasks/bridge_guards/src/bridge_guards.hpp @@ -0,0 +1,189 @@ +#include +#include + +#include "graph.hpp" + +/** + * @brief Функция BFS (поиск в ширину) для нахождения увеличивающего пути в + * сети. + * + * @param s: начальная вершина (источник) в сети. + * @param parent: массив, хранящий родительские вершины для восстановления пути. + * @param adj_list: список смежности графа. + * @param capacity: матрица пропускных способностей ребер. + * + * @return `size_t`: пропускная способность найденного увеличивающего пути, или + * 0, если путь не найден. + */ +inline size_t BFS(size_t s, std::vector& parent, + std::vector>& adj_list, + std::vector>& capacity) { + // инициализация родительского массива и массива посещенных вершин + + parent[s] = -2; // отмечаем начальную вершину как посещенную, значение -2 + // используется для обозначения источника + size_t n = parent.size(); + + std::vector visited(n, false); + visited[s] = true; + + // используем очередь для обхода графа в ширину + std::queue> nodes; + + // добавляем начальную вершину с максимальным потоком + nodes.push({s, LLONG_MAX}); + + // обходим граф до тех пор, пока очередь не пуста + while (!nodes.empty()) { + // извлекаем вершину и её текущий поток из очереди + std::pair node_flow = nodes.front(); + size_t node = node_flow.first; + size_t flow = node_flow.second; + + nodes.pop(); + + // проверяем все ребра, исходящие из текущей вершины + for (size_t i = 0; i < adj_list[node].size(); i++) { + size_t next = adj_list[node][i]; // следующая вершина + + // если следующая вершина уже посещена или пропускная способность ребра + // равна 0, то пропускаем ребро + if (visited[next] || capacity[node][i] == 0) continue; + + // отмечаем следующую вершину как посещенную + visited[next] = true; + + // записываем родительскую вершину + parent[next] = node; + + // вычисляем новый поток как min из текущего потока и пропускной + // способности ребра + size_t new_flow = std::min(flow, capacity[node][i]); + + // если достигли стока, то возвращаем найденный поток + if (next == s + 1) return new_flow; + // иначе добавляем следующую вершину в очередь с новым потоком + nodes.push({next, new_flow}); + } + } + + // увеличивающий путь не найден, возвращаем 0 + return 0; +} + +/** + * @brief Функция для вычисления максимального потока в сети. + * + * @param s: начальная вершина (источник) в сети. + * @param adj_list: список смежности графа. + * @param capacity: матрица пропускных способностей ребер. + * + * @return `size_t`: величина максимального потока в сети. + */ +inline size_t MaxFlow(size_t s, std::vector>& adj_list, + std::vector>& capacity) { + size_t flow = 0; // инициализируем максимальный поток нулем + std::vector parent(adj_list.size(), -1); // родительский массив + + size_t new_flow = 0; + + // повторяем поиск увеличивающих путей до тех пор, пока они находятся + while ((new_flow = BFS(s, parent, adj_list, capacity))) { + // добавляем найденный поток к максимальному потоку + flow += new_flow; + + size_t curr = s + 1; // начинаем с стока + + // обновляем пропускные способности ребер по найденному пути + while (curr != s) { + size_t prev = parent[curr]; + size_t idx = (find(adj_list[prev].begin(), adj_list[prev].end(), curr) - + adj_list[prev].begin()); + capacity[prev][idx] -= new_flow; + + idx = (find(adj_list[curr].begin(), adj_list[curr].end(), prev) - + adj_list[curr].begin()); + capacity[curr][idx] += new_flow; + + curr = prev; + } + } + + return flow; // максимальный поток +} + +/** + * @brief Функция для вычисления максимального паросочетания в двудольном графе. + * + * @param n: количество вершин в первой доле графа. + * @param m: количество вершин во второй доле графа. + * @param bipartite_graph: объект, представляющий двудольный граф. + * + * @return `size_t`: размер максимального паросочетания. + */ +inline size_t MaximumBipartiteMatching(size_t n, size_t m, + Graph& bipartite_graph) { + // вспомогательные структуры данных для алгоритма Форда-Фалкерсона + std::vector>> bipartite_edges_stack( + n + m + 3); + std::vector> adj_list(n + m + 3); + std::vector> capacity(n + m + 3); + + // преобразуем двудольный граф в сеть + + for (size_t i = 0; i < bipartite_graph.EdgesAmount(); i++) { + size_t u = StartVertFromTuple(bipartite_graph.Edges()[i]); + size_t v = EndVertFromTuple(bipartite_graph.Edges()[i]); + + v += n; // смещаем номера вершин второй доли + + bipartite_edges_stack[u].push_back({v, 1}); + bipartite_edges_stack[v].push_back( + {u, 0}); // обратное ребро с пропускной способностью 0 + } + + // добавляем источник и сток + for (size_t i = 1; i <= n; i++) { + bipartite_edges_stack[n + m + 1].push_back({i, 1}); + bipartite_edges_stack[i].push_back({n + m + 1, 0}); + } + for (size_t i = 1; i <= m; i++) { + bipartite_edges_stack[i + n].push_back({n + m + 2, 1}); + bipartite_edges_stack[n + m + 2].push_back({i + n, 0}); + } + + // сортируем ребра для удобства работы + for (size_t i = 1; i <= n + m + 2; i++) + sort(bipartite_edges_stack[i].begin(), bipartite_edges_stack[i].end()); + + // списки смежности и пропускных способностей + for (size_t i = 1; i <= n + m + 2; i++) + for (size_t j = 0; j < bipartite_edges_stack[i].size(); j++) { + adj_list[i].push_back(bipartite_edges_stack[i][j].first); + capacity[i].push_back(bipartite_edges_stack[i][j].second); + } + + // максимальный поток, который равен максимальному паросочетанию + return MaxFlow(n + m + 1, adj_list, capacity); +} + +inline void Solution(std::istream& is = std::cin, + std::ostream& os = std::cout) { + size_t n, m; + is >> n >> m; + + size_t p; + is >> p; + + Graph bipartite_graph; + + for (size_t i = 0; i < p; i++) { + size_t u, v; + is >> u >> v; + + bipartite_graph.AddEdge({u, v}); + } + + os << MaximumBipartiteMatching(n, m, bipartite_graph) << std::endl; + return; +} \ No newline at end of file diff --git a/additional_tasks/bridge_guards/src/main.cpp b/additional_tasks/bridge_guards/src/main.cpp new file mode 100644 index 0000000..b62c7c5 --- /dev/null +++ b/additional_tasks/bridge_guards/src/main.cpp @@ -0,0 +1,6 @@ +#include "bridge_guards.hpp" + +int main() { + Solution(); + return 0; +} diff --git a/additional_tasks/bridge_guards/src/test.cpp b/additional_tasks/bridge_guards/src/test.cpp new file mode 100644 index 0000000..7953edb --- /dev/null +++ b/additional_tasks/bridge_guards/src/test.cpp @@ -0,0 +1,251 @@ +#include + +#include "bridge_guards.hpp" + +TEST(BridgeGuardsTest, SimpleTest) { + std::stringstream ss; + ss << "1 1\n" + << "1\n" + << "1 1\n"; + + std::stringstream output; + + Solution(ss, output); + EXPECT_EQ(output.str(), "1\n"); +} + +TEST(BridgeGuardsTest, Test_1) { + std::stringstream ss; + ss << "4 3\n" + << "4\n" + << "1 3\n" + << "1 2\n" + << "2 1\n" + << "2 3\n"; + + std::stringstream output; + + Solution(ss, output); + EXPECT_EQ(output.str(), "2\n"); +} + +TEST(BridgeGuardsTest, Test_2) { + std::stringstream ss; + ss << "4 3\n" + << "4\n" + << "1 3\n" + << "1 2\n" + << "2 2\n" + << "4 1\n"; + + std::stringstream output; + + Solution(ss, output); + EXPECT_EQ(output.str(), "3\n"); +} + +TEST(BridgeGuardsTest, Test_3) { + std::stringstream ss; + ss << "4 4\n" + << "4\n" + << "1 1\n" + << "2 2\n" + << "3 3\n" + << "4 4\n"; + + std::stringstream output; + + Solution(ss, output); + EXPECT_EQ(output.str(), "4\n"); +} + +TEST(BridgeGuardsTest, Test_4) { + std::stringstream ss; + ss << "5 2\n" + << "5\n" + << "1 1\n" + << "2 1\n" + << "3 1\n" + << "4 2\n" + << "5 2\n"; + + std::stringstream output; + + Solution(ss, output); + EXPECT_EQ(output.str(), "2\n"); +} + +TEST(BridgeGuardsTest, HardTest) { + std::stringstream ss; + ss << "10 6\n" + << "12\n" + << "1 1\n" + << "2 1\n" + << "3 1\n" + << "4 3\n" + << "5 3\n" + << "6 2\n" + << "7 3\n" + << "8 3\n" + << "9 4\n" + << "9 5\n" + << "10 5\n" + << "10 6\n"; + + std::stringstream output; + + Solution(ss, output); + EXPECT_EQ(output.str(), "5\n"); +} + +TEST(BridgeGuardsTest, EmptyGraph) { + std::stringstream ss; + ss << "0 0\n" // 0 вершин в обеих долях + << "0\n"; // 0 мостов + + std::stringstream output; + + Solution(ss, output); + EXPECT_EQ(output.str(), "0\n"); +} + +TEST(BridgeGuardsTest, NoEdges) { + std::stringstream ss; + ss << "5 3\n" // вершин в первой доле, 3 во второй + << "0\n"; // 0 мостов + + std::stringstream output; + + Solution(ss, output); + EXPECT_EQ(output.str(), "0\n"); +} + +TEST(BridgeGuardsTest, OneSidedGraph) { + std::stringstream ss; + ss << "5 0\n" // 5 вершин в первой доле, 0 во второй + << "0\n"; // 0 мостов + + std::stringstream output; + + Solution(ss, output); + EXPECT_EQ(output.str(), "0\n"); +} + +TEST(BridgeGuardsTest, OneSidedGraph2) { + std::stringstream ss; + ss << "0 3\n" // 0 вершин в первой доле, 3 во второй + << "0\n"; // 0 мостов + + std::stringstream output; + + Solution(ss, output); + EXPECT_EQ(output.str(), "0\n"); +} + +TEST(BridgeGuardsTest, LargeGraphSmallEdges) { + std::stringstream ss; + ss << "100 100\n" // 100 вершин в каждой доле + << "5\n" // 5 мостов + << "1 1\n" + << "2 2\n" + << "3 3\n" + << "4 4\n" + << "5 5\n"; + + std::stringstream output; + + Solution(ss, output); + EXPECT_EQ(output.str(), "5\n"); +} + +TEST(BridgeGuardsTest, CompleteBipartiteGraph) { + std::stringstream ss; + ss << "3 3\n" + << "9\n" + << "1 1\n" + << "1 2\n" + << "1 3\n" + << "2 1\n" + << "2 2\n" + << "2 3\n" + << "3 1\n" + << "3 2\n" + << "3 3\n"; + + std::stringstream output; + + Solution(ss, output); + EXPECT_EQ(output.str(), "3\n"); +} + +TEST(BridgeGuardsTest, StarShapedGraph) { + std::stringstream ss; + ss << "5 1\n" + << "5\n" + << "1 1\n" + << "2 1\n" + << "3 1\n" + << "4 1\n" + << "5 1\n"; + + std::stringstream output; + Solution(ss, output); + EXPECT_EQ(output.str(), "1\n"); +} + +TEST(BridgeGuardsTest, StarShapedGraph2) { + std::stringstream ss; + ss << "1 5\n" + << "5\n" + << "1 1\n" + << "1 2\n" + << "1 3\n" + << "1 4\n" + << "1 5\n"; + + std::stringstream output; + Solution(ss, output); + EXPECT_EQ(output.str(), "1\n"); +} + +TEST(BridgeGuardsTest, DisconnectedGraph) { + std::stringstream ss; + ss << "6 6\n" + << "4\n" + << "1 1\n" + << "2 2\n" + << "3 3\n" + << "4 4\n"; + + std::stringstream output; + Solution(ss, output); + EXPECT_EQ(output.str(), "4\n"); +} + +TEST(BridgeGuardsTest, Test_5) { + std::stringstream ss; + ss << "3 2\n" + << "4\n" + << "1 1\n" + << "1 2\n" + << "2 1\n" + << "3 1\n"; + + std::stringstream output; + Solution(ss, output); + EXPECT_EQ(output.str(), "2\n"); +} + +TEST(BridgeGuardsTest, Test_6) { + std::stringstream ss; + ss << "2 3\n" + << "4\n" + << "1 1\n" + << "1 2\n" + << "2 2\n" + << "2 3\n"; + + std::stringstream output; + Solution(ss, output); + EXPECT_EQ(output.str(), "2\n"); +} diff --git a/additional_tasks/chem_experiments_chain/CMakeLists.txt b/additional_tasks/chem_experiments_chain/CMakeLists.txt new file mode 100644 index 0000000..7e3bf1f --- /dev/null +++ b/additional_tasks/chem_experiments_chain/CMakeLists.txt @@ -0,0 +1,39 @@ +cmake_minimum_required(VERSION 3.20) + +get_filename_component(PROJECT_NAME ${CMAKE_CURRENT_LIST_DIR} NAME) +string(REPLACE " " "_" PROJECT_NAME ${PROJECT_NAME}) +project(${PROJECT_NAME} LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +file(GLOB_RECURSE SOURCE_LIST "src/*.cpp" "src/*.hpp") +file(GLOB_RECURSE MAIN_SOURCE_LIST "src/main.cpp") +set(TEST_SOURCE_LIST ${SOURCE_LIST}) +file(GLOB_RECURSE test_list "src/*test.cpp") + +list(REMOVE_ITEM TEST_SOURCE_LIST ${MAIN_SOURCE_LIST}) +list(REMOVE_ITEM SOURCE_LIST ${test_list}) + +include_directories(${PROJECT_NAME} PUBLIC src) + +find_library(Utils ../) + +add_executable(${PROJECT_NAME} ${SOURCE_LIST}) +target_link_libraries(${PROJECT_NAME} PUBLIC Utils) + +# Locate GTest +enable_testing() +find_package(GTest REQUIRED) +include_directories(${GTEST_INCLUDE_DIRS}) + +# Link runTests with what we want to test and the GTest and pthread library +add_executable(${PROJECT_NAME}_tests ${TEST_SOURCE_LIST}) +target_link_libraries( + ${PROJECT_NAME}_tests + GTest::gtest_main + Utils +) + +include(GoogleTest) +gtest_discover_tests(${PROJECT_NAME}_tests) diff --git a/additional_tasks/chem_experiments_chain/README.md b/additional_tasks/chem_experiments_chain/README.md new file mode 100644 index 0000000..1895985 --- /dev/null +++ b/additional_tasks/chem_experiments_chain/README.md @@ -0,0 +1,73 @@ +# Задача: обратимые состояния в цепочке химических экспериментов + +В прогрессивной Московской химической лаборатории используют новые методы описания цепочки экспериментов: лаборанты изображают их в виде графов, вершинами которого являются состояния вещества, а эксперименты - ребрами. + +Таким образом компании, разрабатывающей программное обеспечение, используя методы хемоинформатики, очень удобно использовать эксперименты этой лаборатории в своих наработках. В одном из проектов им потребовалось найти исключительно полезные состояния, обладающие свойством обратимости в цепочке. + +К обратимым экспериментам относят те, результирующее состояние вещества которых можно вернуть к исходному, через другие процессы в той же цепочке; в таком случае результирующее состояния вещества также называют обратимым. + +Найдите по известной цепочке экспериментов, оформленной в виде графа, эти обратимые состояния. + +## Входные данные: + +* Первая строка содержит целое число `N` (`N` > 0) — количество экспериментов в графе. +* Следующие `N` строк содержат по два поля, разделенных пробелом: `A` `B`, где `A` и `B` — это строковые имена вершин (состояния веществ). Ребро идет от вершины `A` к вершине `B`. Имена вершин могут содержать любые символы, кроме пробела, переноса строки и пр. + +## Выходные данные: + +* Выходные данные представляют собой строку, содержащую через пробел имена всех обратимых состояний (порядок вывода обратимых состояний не важен, состояния выводятся без повторений) +* Если обратимых состояний нет, то вывод должен быть пустым. + +# Используемый алгоритм: нахождение компонент сильной связности алгоритмом Тарьяна + +## Как работает алгоритм Тарьяна: + +Алгоритм Тарьяна - это подход к решению задачи поиска компонент сильной связности (СС) в ориентированном графе. Он работает за линейное время (`O(|V|+|E|)`, где `V` - количество вершин, а `E` - количество ребер) и использует рекурсию для эффективного обхода графа. + +Алгоритм Тарьяна основывается на том, что вершина `v` является корнем СС тогда и только тогда, когда ее “низкая связь” (`low_links`) равна ее “времени входа” (`indexes`). “Низкая связь” - это минимальное “время входа” всех вершин, которые можно достичь из `v` по пути рекурсии. + +Иначе говоря, вершины рассматриваются в обратном топологическом порядке, поэтому в конце рекурсивной функции для исходной вершины не будет встречено ни одной вершины из той же компоненты сильной связности, так как все вершины, достижимые из исходной, уже обработаны, и обратные связи в дереве дают второй путь из одной вершины в другую и связывают компоненты сильной связности в одну. + +## Объяснение кода: + +```C++ +template +static void StronglyConnectedComponentsStep( + const vert_t& v, size_t& curr_index, std::stack& verts_stack, + std::unordered_map& indexes, + std::unordered_map& low_links, + std::unordered_map& is_on_stack, + std::unordered_map>& adj_list, + std::vector>& strongly_connected_components); +``` + +С помощью функции выше выполняется рекурсивный обход. + +Для каждой вершины `v`, которая еще не была посещена, выполняется рекурсивный обход. При первом посещении вершины `v`, `indexes[v]` устанавливается равным `curr_index`, `low_links[v]` также устанавливается равным `curr_index`. `curr_index` увеличивается на 1. Вершина `v` помещается в стек `verts_stack`. + +Для каждой смежной вершины `w` вершине `v` выполняется следующее: +1. Если w не была посещена (```indexes[w] == 0```): выполняется рекурсивный вызов StronglyConnectedComponentsStep для вершины `w`, `low_links[v]` обновляется минимальным значением между текущим значением и `low_links[w]`. +2. Если w уже находится в стеке (`is_on_stack[w]`): +`low_links[v]` обновляется минимальным значением между текущим значением и `low_links[w]`. + +После обработки всех смежных вершин, если ```low_links[v] == indexes[v]```, это означает, что `v` является корнем СС. +В этом случае все вершины, которые находятся в стеке от v до вершины, которая была обработана ранее, также принадлежат этой СС. +Вершины удаляются из стека, пока v не будет удален. Эти удаленные вершины образуют СС, которая добавляется в список СС. + +```C++ +/** + * @brief Поиск компонент сильной связности в ориентированного графа по + * алгоритму Тарьяна + * @tparam vert_t: тип вершин + * @tparam weight_t: тип весов + * @param graph: исходный граф + * @throw std::invalid_argument("StronglyConnectedComponents: graph is not + * directed."); + * @return std::vector>: компоненты сильной связности + */ +template +std::vector> StronglyConnectedComponents( + Graph graph); +``` + +Основная функция выше вызывает ```StronglyConnectedComponentsStep(...)``` для каждой вершины графа и возвращает список компонент. \ No newline at end of file diff --git a/additional_tasks/chem_experiments_chain/src/chem_experiments_chain.hpp b/additional_tasks/chem_experiments_chain/src/chem_experiments_chain.hpp new file mode 100644 index 0000000..1205b9a --- /dev/null +++ b/additional_tasks/chem_experiments_chain/src/chem_experiments_chain.hpp @@ -0,0 +1,26 @@ +#include "tarjan_algorithm.hpp" + +/// @brief Решает задачу: "обратимые состояния в цепочке химических +/// экспериментов" +void Solution(std::istream& is = std::cin, std::ostream& os = std::cout) { + size_t experiments_amount; + is >> experiments_amount; + + Graph graph; + graph.MakeDirected(); + + for (size_t i = 0; i < experiments_amount; i++) { + std::string u, v; + is >> u >> v; + + graph.AddEdge({u, v}); + graph.AddVert(u); + graph.AddVert(v); + } + + for (const auto& component : StronglyConnectedComponents(graph)) + if (component.size() != 1 && !component.empty()) + for (const auto& elem : component) os << elem << " "; + + os << std::endl; +} diff --git a/additional_tasks/chem_experiments_chain/src/main.cpp b/additional_tasks/chem_experiments_chain/src/main.cpp new file mode 100644 index 0000000..9af7009 --- /dev/null +++ b/additional_tasks/chem_experiments_chain/src/main.cpp @@ -0,0 +1,6 @@ +#include "chem_experiments_chain.hpp" + +int main() { + Solution(); + return 0; +} diff --git a/additional_tasks/chem_experiments_chain/src/tarjan_algorithm.hpp b/additional_tasks/chem_experiments_chain/src/tarjan_algorithm.hpp new file mode 100644 index 0000000..e4ee3ae --- /dev/null +++ b/additional_tasks/chem_experiments_chain/src/tarjan_algorithm.hpp @@ -0,0 +1,108 @@ +#pragma once + +#include +#include +#include + +#include "graph.hpp" + +namespace { + +template +inline void StronglyConnectedComponentsStep( + const vert_t& vert, size_t& curr_index, std::stack& verts_stack, + std::unordered_map& indexes, + std::unordered_map& low_links, + std::unordered_map& is_on_stack, + std::unordered_map>& adj_list, + std::vector>& strongly_connected_components) { + // в curr_index храним количество ранее обработанных вершин, + // indexes[vert] - это "время входа" в вершину vert + indexes[vert] = low_links[vert] = curr_index++; + + verts_stack.push(vert); + + // is_on_stack нужно, чтобы проверять принадлежность вершины стеку за O(1) + is_on_stack[vert] = true; + + // перебираем рёбра, исходящие из vert + for (auto& u_vert : adj_list[vert]) { + if (indexes[u_vert] == 0) { + // вершина u_vert ранее не посещалась; запускаемся из неё рекурсивно + StronglyConnectedComponentsStep(u_vert, curr_index, verts_stack, indexes, + low_links, is_on_stack, adj_list, + strongly_connected_components); + + low_links[vert] = std::min(low_links[vert], low_links[u_vert]); + } else if (is_on_stack[u_vert]) + // вершина u_vert находится в стеке, значит, принадлежит той же компоненте + // сильной связности, что и vert + + // если u_vert не в стеке, значит, ребро (vert, u_vert) ведёт в ранее + // обработанную компоненту сильной связности и должна быть проигнорирована + low_links[vert] = std::min(low_links[vert], low_links[u_vert]); + } + + // вершина vert - корень текущей компоненты сильной связности, + // все вершины в стеке от vert и выше образуют эту компоненту + if (low_links[vert] == indexes[vert]) { + vert_t u_vert; + std::vector strongly_connected_component; + + do { + u_vert = verts_stack.top(); + verts_stack.pop(); + + is_on_stack[u_vert] = false; + strongly_connected_component.push_back(u_vert); + } while (u_vert != vert); + + strongly_connected_components.push_back(strongly_connected_component); + } +} + +} // namespace + +/** + * @brief Поиск компонент сильной связности в ориентированного графа по + * алгоритму Тарьяна. + * @tparam vert_t: тип вершин + * @tparam weight_t: тип весов + * @param graph: исходный граф + * @throw `std::invalid_argument("StronglyConnectedComponents: graph is not + * directed.")`. + * @return `std::vector>`: компоненты сильной связности + */ +template +std::vector> StronglyConnectedComponents( + const Graph& graph) { + if (!graph.IsDirected()) + throw std::invalid_argument( + "StronglyConnectedComponents: graph is not directed."); + + if (graph.Verts().empty()) return {}; + + std::vector> strongly_connected_component; + + std::stack verts_stack; + size_t curr_index = 0; + + std::unordered_map> adj_list = graph.GetAdjList(); + + std::unordered_map indexes; + std::unordered_map low_links; + std::unordered_map is_on_stack; + + for (const auto& vert : graph.Verts()) { + indexes[vert] = low_links[vert] = 0; + is_on_stack[vert] = false; + } + + for (const auto& vert : graph.Verts()) + if (indexes[vert] == 0) + StronglyConnectedComponentsStep(vert, curr_index, verts_stack, indexes, + low_links, is_on_stack, adj_list, + strongly_connected_component); + + return strongly_connected_component; +} diff --git a/additional_tasks/chem_experiments_chain/src/test.cpp b/additional_tasks/chem_experiments_chain/src/test.cpp new file mode 100644 index 0000000..8845c94 --- /dev/null +++ b/additional_tasks/chem_experiments_chain/src/test.cpp @@ -0,0 +1,172 @@ +#include + +#include "tarjan_algorithm.hpp" + +TEST(SCCSTA_Test, Simple) { + std::unordered_map> adj_list_dict = { + {"A", {"B"}}, {"B", {"C", "D"}}, {"C", {"A", "D"}}, {"D", {"E"}}, + {"E", {"D"}}, {"F", {"E", "G"}}, {"G", {"F", "H"}}, {"H", {"E", "G"}}}; + + auto graph = Graph::GraphFromAdjList(adj_list_dict); + + std::vector> answer = { + {"D", "E"}, {"H", "G", "F"}, {"A", "C", "B"}}; + + ASSERT_EQ(StronglyConnectedComponents(graph), answer); +} + +TEST(SCCSTA_Test, EmptyGraph) { + Graph graph; + auto components = StronglyConnectedComponents(graph); + + ASSERT_EQ(components.size(), 0); +} + +TEST(SCCSTA_Test, SingleVertex) { + Graph graph; + graph.AddVert(1); + + auto components = StronglyConnectedComponents(graph); + + ASSERT_EQ(components.size(), 1); + + ASSERT_EQ(components[0], std::vector({1})); +} + +TEST(SCCSTA_Test, TwoVerticesNoEdges) { + Graph graph; + graph.AddVert(1); + graph.AddVert(2); + + auto components = StronglyConnectedComponents(graph); + ASSERT_EQ(components.size(), 2); + + ASSERT_EQ(components[0], std::vector({1})); + ASSERT_EQ(components[1], std::vector({2})); +} + +TEST(SCCSTA_Test, SimpleCycle) { + Graph graph; + graph.AddVert(1); + graph.AddVert(2); + graph.AddEdge({1, 2}); + graph.AddEdge({2, 1}); + auto components = StronglyConnectedComponents(graph); + ASSERT_EQ(components.size(), 1); + ASSERT_EQ(components[0], std::vector({1, 2})); +} + +TEST(SCCSTA_Test, MultipleCycles) { + Graph graph; + graph.AddVert(1); + graph.AddVert(2); + graph.AddVert(3); + graph.AddVert(4); + graph.AddEdge({1, 2}); + graph.AddEdge({2, 1}); + graph.AddEdge({3, 4}); + graph.AddEdge({4, 3}); + + auto components = StronglyConnectedComponents(graph); + + ASSERT_EQ(components.size(), 2); + + std::sort(components[0].begin(), components[0].end()); + std::sort(components[1].begin(), components[1].end()); + + ASSERT_EQ(components[0], std::vector({1, 2})); + ASSERT_EQ(components[1], std::vector({3, 4})); +} + +TEST(SCCSTA_Test, ComplexGraph) { + Graph graph; + graph.AddVert(1); + graph.AddVert(2); + graph.AddVert(3); + graph.AddVert(4); + graph.AddVert(5); + graph.AddVert(6); + graph.AddEdge({1, 2}); + graph.AddEdge({2, 3}); + graph.AddEdge({3, 1}); + graph.AddEdge({4, 5}); + graph.AddEdge({5, 6}); + graph.AddEdge({6, 4}); + graph.AddEdge({2, 4}); + + auto components = StronglyConnectedComponents(graph); + + ASSERT_EQ(components.size(), 2); + + std::sort(components.begin(), components.end()); + + std::sort(components[0].begin(), components[0].end()); + std::sort(components[1].begin(), components[1].end()); + + ASSERT_EQ(components[0], std::vector({1, 2, 3})); + ASSERT_EQ(components[1], std::vector({4, 5, 6})); +} + +TEST(SCCSTA_Test, UndirectedGraph) { + Graph graph; + graph.AddVert(1); + graph.AddVert(2); + graph.AddEdge({1, 2}); + + graph.MakeUndirected(); + + ASSERT_THROW(StronglyConnectedComponents(graph), std::invalid_argument); +} + +TEST(SCCSTA_Test, SelfLoop) { + Graph graph; + graph.AddVert(1); + graph.AddEdge({1, 1}); + + auto components = StronglyConnectedComponents(graph); + + ASSERT_EQ(components.size(), 2); + + ASSERT_EQ(components[0], std::vector({1})); + ASSERT_EQ(components[1], std::vector({1})); +} + +TEST(SCCSTA_Test, MultipleSelfLoops) { + Graph graph; + graph.AddVert(1); + graph.AddVert(2); + graph.AddEdge({1, 1}); + graph.AddEdge({2, 2}); + + auto components = StronglyConnectedComponents(graph); + + ASSERT_EQ(components.size(), 3); + + ASSERT_EQ(components[0], std::vector({1})); + ASSERT_EQ(components[2], std::vector({2})); +} + +TEST(SCCSTA_Test, ConnectedComponents) { + Graph graph; + graph.AddVert(1); + graph.AddVert(2); + graph.AddVert(3); + graph.AddVert(4); + graph.AddEdge({1, 2}); + graph.AddEdge({2, 1}); + graph.AddEdge({3, 4}); + graph.AddEdge({4, 3}); + graph.AddEdge({1, 3}); + + auto components = StronglyConnectedComponents(graph); + + ASSERT_EQ(components.size(), 2); + + std::sort(components.begin(), components.end()); + + std::sort(components[0].begin(), components[0].end()); + std::sort(components[1].begin(), components[1].end()); + + ASSERT_EQ(components[0], std::vector({1, 2})); + ASSERT_EQ(components[1], std::vector({3, 4})); +} diff --git a/additional_tasks/find_word_chains/CMakeLists.txt b/additional_tasks/find_word_chains/CMakeLists.txt new file mode 100644 index 0000000..7e3bf1f --- /dev/null +++ b/additional_tasks/find_word_chains/CMakeLists.txt @@ -0,0 +1,39 @@ +cmake_minimum_required(VERSION 3.20) + +get_filename_component(PROJECT_NAME ${CMAKE_CURRENT_LIST_DIR} NAME) +string(REPLACE " " "_" PROJECT_NAME ${PROJECT_NAME}) +project(${PROJECT_NAME} LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +file(GLOB_RECURSE SOURCE_LIST "src/*.cpp" "src/*.hpp") +file(GLOB_RECURSE MAIN_SOURCE_LIST "src/main.cpp") +set(TEST_SOURCE_LIST ${SOURCE_LIST}) +file(GLOB_RECURSE test_list "src/*test.cpp") + +list(REMOVE_ITEM TEST_SOURCE_LIST ${MAIN_SOURCE_LIST}) +list(REMOVE_ITEM SOURCE_LIST ${test_list}) + +include_directories(${PROJECT_NAME} PUBLIC src) + +find_library(Utils ../) + +add_executable(${PROJECT_NAME} ${SOURCE_LIST}) +target_link_libraries(${PROJECT_NAME} PUBLIC Utils) + +# Locate GTest +enable_testing() +find_package(GTest REQUIRED) +include_directories(${GTEST_INCLUDE_DIRS}) + +# Link runTests with what we want to test and the GTest and pthread library +add_executable(${PROJECT_NAME}_tests ${TEST_SOURCE_LIST}) +target_link_libraries( + ${PROJECT_NAME}_tests + GTest::gtest_main + Utils +) + +include(GoogleTest) +gtest_discover_tests(${PROJECT_NAME}_tests) diff --git a/additional_tasks/find_word_chains/README.md b/additional_tasks/find_word_chains/README.md new file mode 100644 index 0000000..1a2e60d --- /dev/null +++ b/additional_tasks/find_word_chains/README.md @@ -0,0 +1,32 @@ +# Задача: можно ли объединить список слов в цепочку + +Дан набор слов. Требуется определить, можно ли расположить эти слова в таком порядке, чтобы последняя буква каждого слова совпадала с первой буквой следующего слова, образуя замкнутую цепочку. + +Например, слова “for”, “rig”, “geek”, “kaf” можно соединить в цепочку (for -> rig -> geek -> kaf -> for), в то время как для слов “aab”, “abb” это невозможно. + +## Входные данные: + +* Первая строка содержит целое число `N` (`N` > 0) — количество слов. +* Следующая строка содержит `N` строковых литералов, описывающих набор **слов**. + +## Выходные данные: + +* Выходные данные представляют собой строку, содержащую `Yes`, если слова образуют цепочку и `No`, если нет. + +## Решение: + +Для решения задачи используется подход, основанный на теории графов. Набор слов преобразуется в ориентированный граф следующим образом: + +1. **Построение графа:** Каждая буква английского алфавита (a-z) представляет собой вершину графа. Для каждого слова создаётся ориентированное ребро, соединяющее первую букву слова с его последней буквой. Например, для слова "for" будет создано ребро от вершины 'f' к вершине 'r'. + +2. **Проверка на Эйлеров цикл:** Задача сводится к проверке существования Эйлерова цикла в построенном графе. Эйлеров цикл — это циклический путь, проходящий через каждое ребро графа ровно один раз. Для существования эйлерова цикла необходимо и достаточно выполнение следующих условий: + + * **Степень вершин:** В ориентированном графе для каждой вершины количество входящих ребер должно быть равно количеству исходящих ребер. + * **Связность:** Граф должен быть сильно связным, то есть из любой вершины должна быть достижима любая другая вершина, двигаясь по ребрам. + +3. **Алгоритм проверки:** Алгоритм реализует проверку условий существования эйлерова цикла: + + * **Подсчет степеней:** Для каждой вершины подсчитываются входящая и исходящая степени. Если хотя бы для одной вершины эти степени не равны, эйлеров цикл невозможен. + * **Проверка на связность:** Используется алгоритм поиска в глубину (Depth-First Search, DFS). Начинается обход с произвольной вершины, имеющей исходящее ребро. Если после DFS не все вершины с ненулевой степенью были посещены, значит граф не сильно связный, а значит, и эйлерова цикла нет. + +Если оба условия выполнены, алгоритм возвращает "Yes", в противном случае — "No". diff --git a/additional_tasks/find_word_chains/src/euler_path.hpp b/additional_tasks/find_word_chains/src/euler_path.hpp new file mode 100644 index 0000000..866e14b --- /dev/null +++ b/additional_tasks/find_word_chains/src/euler_path.hpp @@ -0,0 +1,148 @@ +#pragma once + +#include "graph.hpp" + +namespace { + +/** + * @brief + * + * @tparam vert_t + * @tparam weight_t + * @param v + * @param graph + * @param visited + */ + +/** + * @brief Вспомогательная рекурсивная функция для обхода графа в глубину (DFS). + * + * @details Проверяет связность графа, начиная с вершины `v`. Эта функция + * используется внутри `HasEulerPath` для проверки связности после проверки + * количества вершин с нечётной степенью. + * + * @tparam vert_t: тип вершин графа. + * @tparam weight_t: тип весов рёбер графа. + * @param v: начальная вершина для DFS. + * @param graph: граф, в котором выполняется обход. + * @param visited: посещенные вершины. + */ +template +inline void HasEulerPathStep(const vert_t& v, + const Graph& graph, + std::unordered_map& visited) { + visited[v] = true; + + // получаем соседнюю вершину (откуда идёт ребро), если соседняя вершина ещё не + // посещена, рекурсивно вызываем DFS для неё. + + for (const auto& edge_tuple : graph.Edges()) { + if (StartVertFromTuple(edge_tuple) == v) { + vert_t neighbor = EndVertFromTuple(edge_tuple); + + if (!visited[neighbor]) HasEulerPathStep(neighbor, graph, visited); + } + + if (EndVertFromTuple(edge_tuple) == v && !graph.IsDirected()) { + vert_t neighbor = StartVertFromTuple(edge_tuple); + + if (!visited[neighbor]) HasEulerPathStep(neighbor, graph, visited); + } + } +} + +/** + * @brief Вычисляет исходящую степень вершины в графе. + * + * @tparam vert_t: тип вершин графа. + * @tparam weight_t: тип весов рёбер графа. + * @param graph: граф, в котором вычисляется исходящая степень. + * @param v: вершина, для которой вычисляется исходящая степень. + * + * @return size_t: исходящая степень вершины (количество исходящих ребер). + */ +template +inline size_t OutDeg(const Graph& graph, const vert_t& v) { + return graph.GetAdjList()[v].size(); +} + +/** + * @brief Вычисляет входящую степень вершины в графе. + * + * @tparam vert_t: тип вершин графа. + * @tparam weight_t: тип весов рёбер графа. + * @param graph: граф, в котором вычисляется входящая степень. + * @param v: вершина, для которой вычисляется входящая степень. + * + * @return size_t: входящая степень вершины (количество входящих ребер). + */ +template +inline size_t InDeg(const Graph& graph, const vert_t& v) { + // для неориентированных графов входящая степень равна исходящей + if (!graph.IsDirected()) return OutDeg(graph, v); + + size_t res = 0; + + auto adj_list = graph.GetAdjList(); + + for (const auto& u : graph.Verts()) + if (Contains(adj_list[u], v)) res++; + + return res; +} + +} // namespace + +/** + * @brief Проверяет, существует ли в графе эйлеров путь. + * + * @details Эйлеров путь — это путь в графе, который проходит через каждое ребро + * ровно один раз. Функция сначала проверяет необходимое, но недостаточное + * условие существования эйлерова пути: количество вершин с нечетной степенью + * должно быть не больше двух. Затем она проверяет связность графа с помощью + * DFS. + * + * @tparam vert_t: тип вершин графа. + * @tparam weight_t: тип весов рёбер графа. + * @param graph: граф, для которого проверяется наличие эйлерова пути. + * + * @return `true`, если эйлеров путь существует + * @return `false` в противном случае. + */ +template +inline bool HasEulerPath(const Graph& graph) { + // считаем количество вершин с нечетной степенью + size_t odd_vert_count = 0; + + for (const auto& v : graph.Verts()) + if (OutDeg(graph, v) != InDeg(graph, v)) + odd_vert_count++; + + else if (!graph.IsDirected() && OutDeg(graph, v) % 2 != 0) + odd_vert_count++; + + // если вершин с нечетной степенью больше двух, то эйлерова пути нет + if (odd_vert_count > 2) return false; + + std::unordered_map visited; + for (const auto& vert : graph.Verts()) visited[vert] = false; + + std::unordered_map degree; + for (const auto& vert : graph.Verts()) + degree[vert] = graph.IsDirected() ? InDeg(graph, vert) + OutDeg(graph, vert) + : OutDeg(graph, vert); + + // находим первую вершину с ненулевой степенью и начинаем DFS + for (const auto& v : graph.Verts()) + if (degree[v] > 0) { + HasEulerPathStep(v, graph, visited); + break; + } + + // проверяем, все ли вершины с ненулевой степенью были посещены. + for (const auto& v : graph.Verts()) + if (degree[v] > 0 && !visited[v]) return false; + + // все вершины с ненулевой степенью были посещены, эйлеров путь существует + return true; +} diff --git a/additional_tasks/find_word_chains/src/find_word_chains.hpp b/additional_tasks/find_word_chains/src/find_word_chains.hpp new file mode 100644 index 0000000..32e34ca --- /dev/null +++ b/additional_tasks/find_word_chains/src/find_word_chains.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include "euler_path.hpp" + +/** + * @brief Проверяет, можно ли составить цепочку из заданных слов так, чтобы + * последняя буква каждого слова совпадала с первой буквой следующего. + * + * @param words вектор строк (слов), которые нужно проверить. + * + * @return `true`: слова можно объединить в цепочку + * @return `false`: в противном случае + */ +bool CanBeChained(const std::vector& words) { + if (words.empty()) return true; + + Graph chars_graph; + + for (const std::string& s : words) { + if (s.empty()) continue; // пропускаем пустые строки + + chars_graph.AddEdge({s[0], s.back()}); + } + + // дополнительная проверка на связность: + if (chars_graph.VertsAmount() > 0 && chars_graph.EdgesAmount() == 0) + return false; // есть изолированная вершина + + // проверка на Эйлеров цикл + return HasEulerPath(chars_graph); +} + +/// @brief Решает задачу: "" +void Solution(std::istream& is = std::cin, std::ostream& os = std::cout) { + size_t words_amount; + is >> words_amount; + + std::vector words(words_amount); + for (auto& word : words) is >> word; + + if (CanBeChained(words)) + os << "Yes"; + else + os << "No"; + + os << std::endl; +} diff --git a/additional_tasks/find_word_chains/src/main.cpp b/additional_tasks/find_word_chains/src/main.cpp new file mode 100644 index 0000000..de055f3 --- /dev/null +++ b/additional_tasks/find_word_chains/src/main.cpp @@ -0,0 +1,6 @@ +#include "find_word_chains.hpp" + +int main() { + Solution(); + return 0; +} \ No newline at end of file diff --git a/additional_tasks/find_word_chains/src/test.cpp b/additional_tasks/find_word_chains/src/test.cpp new file mode 100644 index 0000000..edb5c96 --- /dev/null +++ b/additional_tasks/find_word_chains/src/test.cpp @@ -0,0 +1,93 @@ +#include + +#include "find_word_chains.hpp" + +TEST(CanBeChainedTest, EmptyInput) { + std::vector words = {}; + ASSERT_TRUE(CanBeChained(words)); +} + +TEST(CanBeChainedTest, SingleWord) { + std::vector words = {"abc"}; + ASSERT_TRUE(CanBeChained(words)); +} + +TEST(CanBeChainedTest, SimpleChain) { + std::vector words = {"for", "rig", "geek", "kaf"}; + ASSERT_TRUE(CanBeChained(words)); +} + +TEST(CanBeChainedTest, SimpleChain2) { + std::vector words = {"abc", "cba"}; + ASSERT_TRUE(CanBeChained(words)); +} + +TEST(CanBeChainedTest, NoChain2) { + std::vector words = {"abc", "def"}; + ASSERT_FALSE(CanBeChained(words)); +} + +TEST(CanBeChainedTest, SameStartEnd) { + std::vector words = {"aba", "cba", "acc"}; + ASSERT_TRUE(CanBeChained(words)); +} + +TEST(CanBeChainedTest, MultipleSameWords) { + std::vector words = {"aba", "aba", "aca"}; + ASSERT_TRUE(CanBeChained(words)); +} + +TEST(CanBeChainedTest, WordsWithSameStartEnd) { + std::vector words = {"aaa", "bbb", "ccc"}; + ASSERT_FALSE(CanBeChained(words)); +} + +TEST(CanBeChainedTest, MultipleWordsWithSameStartEnd) { + std::vector words = {"aaa", "aba", "aca"}; + ASSERT_TRUE(CanBeChained(words)); +} + +TEST(CanBeChainedTest, MultipleWordsWithSameStartEnd2) { + std::vector words = {"aaa", "abb", "aba"}; + ASSERT_TRUE(CanBeChained(words)); +} + +TEST(CanBeChainedTest, ComplexChain) { + std::vector words = {"aba", "bab", "bba"}; + ASSERT_FALSE(CanBeChained(words)); +} + +TEST(CanBeChainedTest, ComplexChain2) { + std::vector words = {"abc", "cde", "efg", "gba"}; + ASSERT_TRUE(CanBeChained(words)); +} + +TEST(CanBeChainedTest, ComplexNoChain) { + std::vector words = {"abc", "cde", "efg", "hbl"}; + ASSERT_FALSE(CanBeChained(words)); +} + +TEST(CanBeChainedTest, OneWordMultipleSameLetters) { + std::vector words = {"aaaa"}; + ASSERT_TRUE(CanBeChained(words)); +} + +TEST(CanBeChainedTest, TwoWordsSameStartEnd) { + std::vector words = {"aaa", "bbb"}; + ASSERT_FALSE(CanBeChained(words)); +} + +TEST(CanBeChainedTest, EmptyWords) { + std::vector words = {"abc", "", "def"}; + ASSERT_FALSE(CanBeChained(words)); +} + +TEST(CanBeChainedTest, EmptyWordsOnly) { + std::vector words = {"", "", ""}; + ASSERT_TRUE(CanBeChained(words)); +} + +TEST(CanBeChainedTest, LongChain) { + std::vector words = {"a", "ab", "bc", "cd", "de", "ef", "fa"}; + ASSERT_TRUE(CanBeChained(words)); +} \ No newline at end of file diff --git a/additional_tasks/graph/CMakeLists.txt b/additional_tasks/graph/CMakeLists.txt new file mode 100644 index 0000000..7e3bf1f --- /dev/null +++ b/additional_tasks/graph/CMakeLists.txt @@ -0,0 +1,39 @@ +cmake_minimum_required(VERSION 3.20) + +get_filename_component(PROJECT_NAME ${CMAKE_CURRENT_LIST_DIR} NAME) +string(REPLACE " " "_" PROJECT_NAME ${PROJECT_NAME}) +project(${PROJECT_NAME} LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +file(GLOB_RECURSE SOURCE_LIST "src/*.cpp" "src/*.hpp") +file(GLOB_RECURSE MAIN_SOURCE_LIST "src/main.cpp") +set(TEST_SOURCE_LIST ${SOURCE_LIST}) +file(GLOB_RECURSE test_list "src/*test.cpp") + +list(REMOVE_ITEM TEST_SOURCE_LIST ${MAIN_SOURCE_LIST}) +list(REMOVE_ITEM SOURCE_LIST ${test_list}) + +include_directories(${PROJECT_NAME} PUBLIC src) + +find_library(Utils ../) + +add_executable(${PROJECT_NAME} ${SOURCE_LIST}) +target_link_libraries(${PROJECT_NAME} PUBLIC Utils) + +# Locate GTest +enable_testing() +find_package(GTest REQUIRED) +include_directories(${GTEST_INCLUDE_DIRS}) + +# Link runTests with what we want to test and the GTest and pthread library +add_executable(${PROJECT_NAME}_tests ${TEST_SOURCE_LIST}) +target_link_libraries( + ${PROJECT_NAME}_tests + GTest::gtest_main + Utils +) + +include(GoogleTest) +gtest_discover_tests(${PROJECT_NAME}_tests) diff --git a/additional_tasks/graph/README.md b/additional_tasks/graph/README.md new file mode 100644 index 0000000..a8a967f --- /dev/null +++ b/additional_tasks/graph/README.md @@ -0,0 +1,5 @@ +# Класс `Graph` от Криворучко Дмитрия Игоревича + +Ненормальный класс `Graph`, содержащий класс `Edge` в качестве приватного поля, но не имеющий отдельного класса Vertice (допускается использование только `std::string` и целочисленных типов (`int`, `short`, `std::size_t`) в качестве названия вершин, подставляемых шаблонами) с восемью статическими методами — псевдоконструкторами, различными вспомогательными методами для удаления и поиска ребра или вершины. + +Может использоваться для решения специфических задач, где граф надо хранить не как список смежности, а как списки вершин и рёбер. \ No newline at end of file diff --git a/additional_tasks/graph/src/main.cpp b/additional_tasks/graph/src/main.cpp new file mode 100644 index 0000000..ba6549d --- /dev/null +++ b/additional_tasks/graph/src/main.cpp @@ -0,0 +1,175 @@ +#include + +#include "graph.hpp" + +template +void PrintGraph(const Graph& graph) { + std::cout << std::endl << graph << std::endl << std::endl; + graph.PrintAdjList(); + std::cout << std::endl << graph.GetAdjList() << std::endl; +} + +int main() { + // ------------------------- Graph from pairs ------------------------- + + std::cout << std::endl + << "------------------------------" + " Graph from pairs " + "------------------------------" + << std::endl + << std::endl; + + const std::vector> edges_pairs = { + {0, 1}, {1, 2}, {2, 0}}; + + auto graph = Graph::GraphNonWeighted(edges_pairs); + + std::cout << "Pairs: " << edges_pairs << std::endl; + + PrintGraph(graph); + + // ------------------- Graph from pairs and weights ------------------- + + std::cout << std::endl + << "------------------------------" + " Graph from pairs and weights " + "------------------------------" + << std::endl + << std::endl; + + const std::vector weights = {1, 2, 3}; + + graph = Graph::GraphWeighted(edges_pairs, weights); + + std::cout << "Pairs: " << edges_pairs << std::endl + << "Weights: " << weights << std::endl; + + PrintGraph(graph); + + // ------------------------- Graph from tuples ------------------------ + + std::cout << std::endl + << "------------------------------" + " Graph from tuples " + "------------------------------" + << std::endl + << std::endl; + + const std::vector> edges_tuples = { + {0, 1, 5}, {1, 2, 10}, {2, 0, 3}}; + + graph = Graph::GraphWeighted(edges_tuples); + + std::cout << "Tuples: " << edges_tuples << std::endl; + + PrintGraph(graph); + + // ------------------------ Graph from strings ------------------------ + + std::cout << std::endl + << "------------------------------" + " Graph from strings " + "------------------------------" + << std::endl + << std::endl; + + const std::vector edges_strs = {"0->1", "2->1", "3->2", "1->3"}; + + graph = Graph::GraphFromStrs(edges_strs); + + std::cout << "Strings: " << edges_strs << std::endl; + + PrintGraph(graph); + + // -------------------------- Graph from dict ------------------------- + + std::cout << std::endl + << "------------------------------" + " Graph from dict " + "------------------------------" + << std::endl + << std::endl; + + const std::unordered_map edges_dict = { + {"0->1", 5}, {"2->1", 1}, {"3->2", 2}, {"1->3", 3}}; + + graph = Graph::GraphFromMap(edges_dict); + + std::cout << "Dicts: " << edges_dict << std::endl; + + PrintGraph(graph); + + // ----------------------- Graph from AdjMatrix ----------------------- + + std::cout << std::endl + << "------------------------------" + " Graph from AdjMatrix " + "------------------------------" + << std::endl + << std::endl; + + std::vector> adj_matrix = {{0, 1, 0}, {1, 0, 1}, {0, 1, 0}}; + + graph = Graph::GraphFromAdjMatrix(adj_matrix); + + std::cout << "AdjMatrix: " << adj_matrix << std::endl; + + PrintGraph(graph); + + std::cout << std::endl << "WEIGHTED VERSION:" << std::endl << std::endl; + + adj_matrix = {{0, 1, 0}, {1, 0, 5}, {0, 7, 0}}; + graph = Graph::GraphFromAdjMatrix(adj_matrix, true); + + std::cout << "AdjMatrix: " << adj_matrix << std::endl; + + PrintGraph(graph); + + // ------------------------ Graph from AdjList ------------------------ + + std::cout << std::endl + << "------------------------------" + " Graph from AdjList " + "------------------------------" + << std::endl + << std::endl; + + const std::vector> adj_list = {{1}, {0, 2}, {1}}; + + graph = Graph::GraphFromAdjList(adj_list); + + std::cout << "AdjList: " << adj_list << std::endl; + + PrintGraph(graph); + + // ------------------- Graph from AdjList with keys ------------------- + + std::cout << std::endl + << "------------------------------" + " Graph from AdjList with keys " + "------------------------------" + << std::endl + << std::endl; + + const std::unordered_map> adj_list_dict = { + {0, {1}}, {1, {0, 2}}, {2, {1}}}; + + graph = Graph::GraphFromAdjList(adj_list_dict); + + std::cout << "AdjList: " << adj_list_dict << std::endl; + + PrintGraph(graph); + + std::cout << std::endl << "STRING VERSION:" << std::endl << std::endl; + + std::unordered_map> adj_list_dict_2 = { + {"A", {"B"}}, {"B", {"A", "C"}}, {"C", {"B"}}}; + + auto graph_2 = Graph::GraphFromAdjList(adj_list_dict_2); + + std::cout << "AdjList: " << adj_list_dict_2 << std::endl; + + PrintGraph(graph_2); + + return 0; +} diff --git a/additional_tasks/graph/src/test.cpp b/additional_tasks/graph/src/test.cpp new file mode 100644 index 0000000..bdc8ee6 --- /dev/null +++ b/additional_tasks/graph/src/test.cpp @@ -0,0 +1,540 @@ +#include + +#include "graph.hpp" + +TEST(GraphTest_size_t, CreateNonWeightedGraph) { + std::vector> edges_pairs = {{0, 1}, {1, 2}, {2, 0}}; + Graph graph = + Graph::GraphNonWeighted(edges_pairs); + + ASSERT_EQ(graph.VertsAmount(), 3); + ASSERT_EQ(graph.EdgesAmount(), 3); + + ASSERT_FALSE(graph.IsWeighted()); + ASSERT_TRUE(graph.IsDirected()); +} + +TEST(GraphTest_size_t, CreateWeightedGraph) { + std::vector> edges_pairs = {{0, 1}, {1, 2}, {2, 0}}; + std::vector weights = {1, 2, 3}; + Graph graph = + Graph::GraphWeighted(edges_pairs, weights); + + ASSERT_EQ(graph.VertsAmount(), 3); + ASSERT_EQ(graph.EdgesAmount(), 3); + + ASSERT_TRUE(graph.IsWeighted()); + ASSERT_TRUE(graph.IsDirected()); +} + +TEST(GraphTest_size_t, CreateGraphFromAdjacencyMatrix) { + std::vector> adj_matrix = {{0, 1, 0}, {1, 0, 1}, {0, 1, 0}}; + Graph graph = + Graph::GraphFromAdjMatrix(adj_matrix); + + graph.MakeUndirected(); + + ASSERT_EQ(graph.VertsAmount(), 3); + ASSERT_EQ(graph.EdgesAmount(), 2); + + ASSERT_FALSE(graph.IsWeighted()); +} + +TEST(GraphTest_size_t, CreateGraphFromAdjacencyMatrix2) { + std::vector> adj_matrix = {{0, 1, 0}, {1, 0, 1}, {0, 1, 0}}; + Graph graph = + Graph::GraphFromAdjMatrix(adj_matrix); + + ASSERT_EQ(graph.VertsAmount(), 3); + ASSERT_EQ(graph.EdgesAmount(), 4); + + ASSERT_FALSE(graph.IsWeighted()); +} + +TEST(GraphTest_size_t, CreateWeightedGraphFromAdjacencyMatrix) { + std::vector> adj_matrix = {{0, 1, 0}, {1, 0, 5}, {0, 7, 0}}; + Graph graph = + Graph::GraphFromAdjMatrix(adj_matrix, true); + + graph.MakeUndirected(); + + ASSERT_EQ(graph.VertsAmount(), 3); + ASSERT_EQ(graph.EdgesAmount(), 2); + + ASSERT_TRUE(graph.IsWeighted()); +} + +TEST(GraphTest_size_t, CreateWeightedGraphFromAdjacencyMatrix2) { + std::vector> adj_matrix = {{0, 1, 0}, {1, 0, 9}, {0, 6, 0}}; + Graph graph = + Graph::GraphFromAdjMatrix(adj_matrix, true); + + ASSERT_EQ(graph.VertsAmount(), 3); + ASSERT_EQ(graph.EdgesAmount(), 4); + + ASSERT_TRUE(graph.IsWeighted()); +} + +TEST(GraphTest_size_t, CreateGraphFromAdjacencyList) { + std::vector> adj_list = {{1}, {0, 2}, {1}}; + Graph graph = Graph::GraphFromAdjList(adj_list); + + graph.MakeUndirected(); + + ASSERT_EQ(graph.VertsAmount(), 3); + ASSERT_EQ(graph.EdgesAmount(), 2); + + ASSERT_FALSE(graph.IsWeighted()); + + graph.MakeDirected(); + ASSERT_TRUE(graph.IsDirected()); +} + +TEST(GraphTest_size_t, MakeUndirected) { + std::vector> edges = {{0, 1}, {1, 2}, {2, 0}}; + Graph g = Graph::GraphNonWeighted(edges); + + ASSERT_TRUE(g.IsDirected()); + g.MakeUndirected(); + ASSERT_FALSE(g.IsDirected()); +} + +TEST(GraphTest_size_t, CreateGraphWithEmptyEdges) { + std::vector> edges; + Graph g = Graph::GraphNonWeighted(edges); + + ASSERT_EQ(g.VertsAmount(), 0); + ASSERT_EQ(g.EdgesAmount(), 0); + ASSERT_FALSE(g.IsWeighted()); + ASSERT_TRUE(g.IsDirected()); +} + +TEST(GraphTest_size_t, CreateGraphWithDuplicateEdges) { + std::vector> edges = { + {0, 1}, {1, 2}, {0, 1}, {2, 1}, {1, 2}}; + // для скорости работы по умолчанию дубликаты допускаются + Graph g = Graph::GraphNonWeighted(edges); + + ASSERT_EQ(g.VertsAmount(), 3); + ASSERT_EQ(g.EdgesAmount(), 3); + ASSERT_FALSE(g.IsWeighted()); + ASSERT_TRUE(g.IsDirected()); + + g.MakeUndirected(); + + ASSERT_EQ(g.VertsAmount(), 3); + ASSERT_EQ(g.EdgesAmount(), 2); + ASSERT_FALSE(g.IsWeighted()); + ASSERT_FALSE(g.IsDirected()); +} + +TEST(GraphTest_size_t, CreateGraphFromAdjMatrixWithInvalidSize) { + std::vector> adj_matrix = {{0, 1}, {1, 0, 1}, {1, 0}}; + + EXPECT_THROW((Graph::GraphFromAdjMatrix(adj_matrix)), + std::invalid_argument); +} + +TEST(GraphTest_size_t, GraphFromMapTest) { + std::unordered_map edges_dict = { + {"0->1", 5}, {"2->1", 1}, {"3->2", 2}, {"1->3", 3}}; + Graph graph = Graph::GraphFromMap(edges_dict); + + ASSERT_EQ(graph.VertsAmount(), 4); + ASSERT_EQ(graph.EdgesAmount(), 4); + + ASSERT_TRUE(graph.IsWeighted()); + ASSERT_TRUE(graph.IsDirected()); + + std::unordered_map empty_edges_dict; + Graph empty_graph = + Graph::GraphFromMap(empty_edges_dict); + + ASSERT_EQ(empty_graph.VertsAmount(), 0); + ASSERT_EQ(empty_graph.EdgesAmount(), 0); + + ASSERT_FALSE(empty_graph.IsWeighted()); + ASSERT_TRUE(empty_graph.IsDirected()); + + std::unordered_map invalid_edges_dict = { + {"0-1", 0}, {"2-1", 1}, {"3->2", 2}, {"1>3", 3}}; + + EXPECT_THROW((Graph::GraphFromMap(invalid_edges_dict)), + std::invalid_argument); + + edges_dict = {{"0->14", 5}, {"25->1", 1}, {"3->2", 2}, {"1->3", 3}}; + + graph = Graph::GraphFromMap(edges_dict); + + ASSERT_EQ(graph.VertsAmount(), 26); + ASSERT_EQ(graph.EdgesAmount(), 4); + + ASSERT_TRUE(graph.IsWeighted()); + ASSERT_TRUE(graph.IsDirected()); +} + +TEST(GraphTest_size_t, GraphFromStrsTest) { + std::vector edges_strs = {"0->1", "2->1", "3->2", "1->3"}; + Graph graph = Graph::GraphFromStrs(edges_strs); + + ASSERT_EQ(graph.VertsAmount(), 4); + ASSERT_EQ(graph.EdgesAmount(), 4); + + ASSERT_FALSE(graph.IsWeighted()); + ASSERT_TRUE(graph.IsDirected()); + + std::vector empty_edges_strs; + Graph empty_graph = + Graph::GraphFromStrs(empty_edges_strs); + + ASSERT_EQ(empty_graph.VertsAmount(), 0); + ASSERT_EQ(empty_graph.EdgesAmount(), 0); + + ASSERT_FALSE(empty_graph.IsWeighted()); + ASSERT_TRUE(empty_graph.IsDirected()); + + std::vector invalid_edges_strs = {"0-1", "2-1", "3->2", "1>3"}; + EXPECT_THROW((Graph::GraphFromStrs(invalid_edges_strs)), + std::invalid_argument); + + invalid_edges_strs = {"0->vert", "2->1", "3->2", "1->3"}; + EXPECT_THROW((Graph::GraphFromStrs(invalid_edges_strs)), + std::invalid_argument); +} + +TEST(GraphTest_size_t, GraphWeighted_EmptyInput) { + std::vector> empty_input; + Graph graph = Graph::GraphWeighted(empty_input); + + ASSERT_EQ(graph.VertsAmount(), 0); + ASSERT_EQ(graph.EdgesAmount(), 0); +} + +TEST(GraphTest_size_t, GraphWeighted_SingleEdge) { + std::vector> input = {{0, 1, 5}}; + Graph graph = Graph::GraphWeighted(input); + + ASSERT_EQ(graph.VertsAmount(), 2); + ASSERT_EQ(graph.EdgesAmount(), 1); + ASSERT_TRUE(graph.IsWeighted()); +} + +TEST(GraphTest_size_t, GraphWeighted_MultipleEdges) { + std::vector> input = { + {0, 1, 5}, {1, 2, 10}, {2, 0, 3}}; + Graph graph = Graph::GraphWeighted(input); + + ASSERT_EQ(graph.VertsAmount(), 3); + ASSERT_EQ(graph.EdgesAmount(), 3); + ASSERT_TRUE(graph.IsWeighted()); +} + +TEST(GraphTest_size_t, Edges_EmptyGraph) { + std::vector> empty_input; + Graph graph = Graph::GraphWeighted(empty_input); + auto edges = graph.Edges(); + + ASSERT_TRUE(edges.empty()); +} + +TEST(GraphTest_size_t, Edges_SingleEdge) { + std::vector> input = {{0, 1, 5}}; + Graph graph = Graph::GraphWeighted(input); + auto edges = graph.Edges(); + + ASSERT_EQ(edges.size(), 1); + ASSERT_EQ(StartVertFromTuple(edges[0]), 0); + ASSERT_EQ(EndVertFromTuple(edges[0]), 1); + ASSERT_EQ(WeightFromTuple(edges[0]), 5); +} + +TEST(GraphTest_size_t, Edges_MultipleEdges) { + std::vector> input = { + {0, 1, 5}, {1, 2, 10}, {2, 0, 3}}; + Graph graph = Graph::GraphWeighted(input); + auto edges = graph.Edges(); + + ASSERT_EQ(edges.size(), 3); + + ASSERT_EQ(StartVertFromTuple(edges[0]), 0); + ASSERT_EQ(EndVertFromTuple(edges[0]), 1); + ASSERT_EQ(WeightFromTuple(edges[0]), 5); + + ASSERT_EQ(StartVertFromTuple(edges[1]), 1); + ASSERT_EQ(EndVertFromTuple(edges[1]), 2); + ASSERT_EQ(WeightFromTuple(edges[1]), 10); + + ASSERT_EQ(StartVertFromTuple(edges[2]), 2); + ASSERT_EQ(EndVertFromTuple(edges[2]), 0); + ASSERT_EQ(WeightFromTuple(edges[2]), 3); +} + +TEST(GraphTest_size_t, Edges_ConstAccess) { + std::vector> input = { + {0, 1, 5}, {1, 2, 10}, {2, 0, 3}}; + Graph graph = Graph::GraphWeighted(input); + const auto& edges = graph.Edges(); + + ASSERT_EQ(edges.size(), 3); + + ASSERT_EQ(StartVertFromTuple(edges[0]), 0); + ASSERT_EQ(EndVertFromTuple(edges[0]), 1); + ASSERT_EQ(WeightFromTuple(edges[0]), 5); +} + +TEST(GraphTest_size_t, ContainsEdgeWeighted) { + Graph g = + Graph::GraphWeighted({{0, 1, 5}, {1, 2, 10}, {2, 0, 3}}); + + ASSERT_TRUE(g.ContainsEdge({0, 1, 5})); + ASSERT_TRUE(g.ContainsEdge({1, 2, 10})); + ASSERT_TRUE(g.ContainsEdge({2, 0, 3})); + + ASSERT_FALSE(g.ContainsEdge({0, 2, 5})); + ASSERT_FALSE(g.ContainsEdge({1, 0, 10})); + ASSERT_FALSE(g.ContainsEdge({2, 1, 3})); +} + +TEST(GraphTest_size_t, ContainsEdgeNonWeighted) { + Graph g = + Graph::GraphNonWeighted({{0, 1}, {1, 2}, {2, 0}}); + + ASSERT_TRUE(g.ContainsEdge({0, 1})); + ASSERT_TRUE(g.ContainsEdge({1, 2})); + ASSERT_TRUE(g.ContainsEdge({2, 0})); + + ASSERT_FALSE(g.ContainsEdge({0, 2})); + ASSERT_FALSE(g.ContainsEdge({1, 0})); + ASSERT_FALSE(g.ContainsEdge({2, 1})); +} + +TEST(GraphTest_size_t, ContainsEdgeThrowsOnNonWeightedGraph) { + Graph g = + Graph::GraphNonWeighted({{0, 1}, {1, 2}, {2, 0}}); + ASSERT_THROW(g.ContainsEdge({0, 1, 5}), std::logic_error); +} + +TEST(GraphTest_size_t, GetWeightOfExistingEdge) { + Graph graph = Graph::GraphWeighted( + {{0, 1, 250L}, {1, 2, 370L}, {2, 0, 120L}}); + + ASSERT_EQ(graph.GetEdgeWeight({0, 1}), 250L); + ASSERT_EQ(graph.GetEdgeWeight({1, 2}), 370L); + ASSERT_EQ(graph.GetEdgeWeight({2, 0}), 120L); +} + +TEST(GraphTest_size_t, GetWeightOfNonExistingEdge) { + Graph graph = Graph::GraphWeighted( + {{0, 1, 250L}, {1, 2, 370L}, {2, 0, 120L}}); + + ASSERT_THROW(graph.GetEdgeWeight({0, 2}), std::invalid_argument); + ASSERT_THROW(graph.GetEdgeWeight({1, 0}), std::invalid_argument); +} + +TEST(GraphTest_size_t, GetWeightOfEdgeInNonWeightedGraph) { + Graph non_weighted_graph = + Graph::GraphNonWeighted({{0, 1}, {1, 2}, {2, 0}}); + + ASSERT_THROW(non_weighted_graph.GetEdgeWeight({0, 1}), std::logic_error); +} + +TEST(GraphTest_size_t, AddVert) { + Graph g; + + // Тест добавления новой вершины + g.AddVert(0); + EXPECT_EQ(g.VertsAmount(), 1); + EXPECT_TRUE(Contains(g.Verts(), size_t(0))); + + // Тест добавления уже существующей вершины + g.AddVert(0); + EXPECT_EQ(g.VertsAmount(), 1); + EXPECT_TRUE(Contains(g.Verts(), size_t(0))); +} + +TEST(GraphTest_size_t, AddEdgeUnweighted) { + Graph g; + + g.AddEdge({0, 1}); + EXPECT_EQ(g.EdgesAmount(), 1); + EXPECT_TRUE(g.ContainsEdge({0, 1})); + + g.AddEdge({0, 1}); + EXPECT_EQ(g.EdgesAmount(), 1); // теперь в графе только уникальные ребра + EXPECT_TRUE(g.ContainsEdge({0, 1})); + + g.AddEdge({2, 3}); + EXPECT_EQ(g.VertsAmount(), 4); + EXPECT_TRUE(Contains(g.Verts(), size_t(2))); + EXPECT_TRUE(Contains(g.Verts(), size_t(3))); +} + +TEST(GraphTest_size_t, AddEdgeWeighted) { + Graph g; + + g.AddEdge({0, 1, 10}); + EXPECT_EQ(g.EdgesAmount(), 1); + EXPECT_TRUE(g.ContainsEdge({0, 1, 10})); + EXPECT_EQ(g.GetEdgeWeight({0, 1}), 10); + + g.AddEdge({0, 1, 15}); + + EXPECT_EQ(g.EdgesAmount(), 2); + EXPECT_TRUE(g.ContainsEdge({0, 1, 10})); + EXPECT_EQ(g.GetEdgeWeight({0, 1}), 10); + + // Warning + EXPECT_NO_THROW(g.AddEdge({2, 3})); +} + +TEST(GraphTest_size_t, RemoveVert) { + Graph g; + g.AddVert(1); + g.AddVert(2); + g.AddVert(3); + g.AddEdge({1, 2, 5}); + g.AddEdge({2, 3, 7}); + + ASSERT_NO_THROW(g.RemoveVert(2)); + EXPECT_FALSE(Contains(g.Verts(), size_t(2))); + EXPECT_FALSE(g.ContainsEdge({1, 2})); + EXPECT_FALSE(g.ContainsEdge({2, 3})); + + ASSERT_THROW(g.RemoveVert(4), std::invalid_argument); +} + +TEST(GraphTest_size_t, RemoveEdgeByPair) { + Graph g; + g.AddVert(1); + g.AddVert(2); + g.AddVert(3); + g.AddEdge({1, 2, 5}); + g.AddEdge({2, 3, 7}); + + ASSERT_NO_THROW(g.RemoveEdge({1, 2})); + EXPECT_FALSE(g.ContainsEdge({1, 2})); + EXPECT_TRUE(g.ContainsEdge({2, 3})); + + ASSERT_THROW(g.RemoveEdge({1, 3}), std::invalid_argument); +} + +TEST(GraphTest_size_t, RemoveEdgeByTuple) { + Graph g; + g.AddVert(1); + g.AddVert(2); + g.AddVert(3); + g.AddEdge({1, 2, 5}); + g.AddEdge({2, 3, 7}); + + ASSERT_NO_THROW(g.RemoveEdge({2, 3, 7})); + EXPECT_FALSE(g.ContainsEdge({2, 3, 7})); + EXPECT_TRUE(g.ContainsEdge({1, 2, 5})); + + ASSERT_THROW(g.RemoveEdge({1, 3, 10}), std::invalid_argument); +} + +TEST(GraphTest_size_t, AddAndRemoveVerts) { + Graph g; + EXPECT_EQ(g.VertsAmount(), 0); + + g.AddVert(1); + g.AddVert(2); + g.AddVert(3); + EXPECT_EQ(g.VertsAmount(), 3); + EXPECT_TRUE(Contains(g.Verts(), size_t(1))); + EXPECT_TRUE(Contains(g.Verts(), size_t(2))); + EXPECT_TRUE(Contains(g.Verts(), size_t(3))); + + g.RemoveVert(2); + EXPECT_EQ(g.VertsAmount(), 2); + EXPECT_TRUE(Contains(g.Verts(), size_t(1))); + EXPECT_FALSE(Contains(g.Verts(), size_t(2))); + EXPECT_TRUE(Contains(g.Verts(), size_t(3))); +} + +TEST(GraphTest_size_t, AddAndRemoveEdges) { + Graph g; + g.AddVert(1); + g.AddVert(2); + g.AddVert(3); + + g.AddEdge({1, 2, 5}); + g.AddEdge({2, 3, 7}); + EXPECT_EQ(g.EdgesAmount(), 2); + EXPECT_TRUE(g.ContainsEdge({1, 2, 5})); + EXPECT_TRUE(g.ContainsEdge({2, 3, 7})); + + g.RemoveEdge({1, 2, 5}); + EXPECT_EQ(g.EdgesAmount(), 1); + EXPECT_FALSE(g.ContainsEdge({1, 2, 5})); + EXPECT_TRUE(g.ContainsEdge({2, 3, 7})); +} + +TEST(GraphTest_size_t, ContainsAndGetVerts) { + Graph g; + g.AddVert(1); + g.AddVert(2); + g.AddVert(3); + + EXPECT_TRUE(Contains(g.Verts(), size_t(1))); + EXPECT_TRUE(Contains(g.Verts(), size_t(2))); + EXPECT_TRUE(Contains(g.Verts(), size_t(3))); + EXPECT_FALSE(Contains(g.Verts(), size_t(4))); + + std::vector verts = g.Verts(); + EXPECT_EQ(verts.size(), 3); + EXPECT_TRUE(std::find(verts.begin(), verts.end(), 1) != verts.end()); + EXPECT_TRUE(std::find(verts.begin(), verts.end(), 2) != verts.end()); + EXPECT_TRUE(std::find(verts.begin(), verts.end(), 3) != verts.end()); +} + +TEST(GraphTest_size_t, RemoveEdgeFromDirectedGraph) { + Graph graph_directed{}; + graph_directed.AddVert(0); + graph_directed.AddVert(1); + graph_directed.AddVert(2); + graph_directed.AddEdge({0, 1, 1}); + graph_directed.AddEdge({1, 2, 2}); + + ASSERT_NO_THROW(graph_directed.RemoveEdge({0, 1})); + ASSERT_FALSE(graph_directed.ContainsEdge({0, 1})); + ASSERT_TRUE(graph_directed.ContainsEdge({1, 2})); +} + +TEST(GraphTest_size_t, RemoveEdgeFromUndirectedGraph) { + Graph graph_undirected{}; + graph_undirected.AddVert(0); + graph_undirected.AddVert(1); + graph_undirected.AddVert(2); + graph_undirected.AddEdge({0, 1, 1}); + graph_undirected.AddEdge({1, 2, 2}); + + graph_undirected.MakeUndirected(); + + ASSERT_NO_THROW(graph_undirected.RemoveEdge({0, 1})); + ASSERT_FALSE(graph_undirected.ContainsEdge({0, 1})); + ASSERT_FALSE(graph_undirected.ContainsEdge({1, 0})); + ASSERT_TRUE(graph_undirected.ContainsEdge({1, 2})); +} + +TEST(GraphTest_size_t, RemoveNonExistentEdgeThrowsException) { + Graph graph_directed{}; + graph_directed.AddVert(0); + graph_directed.AddVert(1); + graph_directed.AddVert(2); + graph_directed.AddEdge({0, 1, 1}); + graph_directed.AddEdge({1, 2, 2}); + + ASSERT_THROW(graph_directed.RemoveEdge({0, 2}), std::invalid_argument); + + Graph graph_undirected{}; + graph_undirected.AddVert(0); + graph_undirected.AddVert(1); + graph_undirected.AddVert(2); + graph_undirected.AddEdge({0, 1, 1}); + graph_undirected.AddEdge({1, 2, 2}); + + graph_undirected.MakeUndirected(); + + ASSERT_THROW(graph_undirected.RemoveEdge({0, 2}), std::invalid_argument); +} diff --git a/additional_tasks/petya_and_vasya_labyrinth/CMakeLists.txt b/additional_tasks/petya_and_vasya_labyrinth/CMakeLists.txt new file mode 100644 index 0000000..7e3bf1f --- /dev/null +++ b/additional_tasks/petya_and_vasya_labyrinth/CMakeLists.txt @@ -0,0 +1,39 @@ +cmake_minimum_required(VERSION 3.20) + +get_filename_component(PROJECT_NAME ${CMAKE_CURRENT_LIST_DIR} NAME) +string(REPLACE " " "_" PROJECT_NAME ${PROJECT_NAME}) +project(${PROJECT_NAME} LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +file(GLOB_RECURSE SOURCE_LIST "src/*.cpp" "src/*.hpp") +file(GLOB_RECURSE MAIN_SOURCE_LIST "src/main.cpp") +set(TEST_SOURCE_LIST ${SOURCE_LIST}) +file(GLOB_RECURSE test_list "src/*test.cpp") + +list(REMOVE_ITEM TEST_SOURCE_LIST ${MAIN_SOURCE_LIST}) +list(REMOVE_ITEM SOURCE_LIST ${test_list}) + +include_directories(${PROJECT_NAME} PUBLIC src) + +find_library(Utils ../) + +add_executable(${PROJECT_NAME} ${SOURCE_LIST}) +target_link_libraries(${PROJECT_NAME} PUBLIC Utils) + +# Locate GTest +enable_testing() +find_package(GTest REQUIRED) +include_directories(${GTEST_INCLUDE_DIRS}) + +# Link runTests with what we want to test and the GTest and pthread library +add_executable(${PROJECT_NAME}_tests ${TEST_SOURCE_LIST}) +target_link_libraries( + ${PROJECT_NAME}_tests + GTest::gtest_main + Utils +) + +include(GoogleTest) +gtest_discover_tests(${PROJECT_NAME}_tests) diff --git a/additional_tasks/petya_and_vasya_labyrinth/README.md b/additional_tasks/petya_and_vasya_labyrinth/README.md new file mode 100644 index 0000000..c28204d --- /dev/null +++ b/additional_tasks/petya_and_vasya_labyrinth/README.md @@ -0,0 +1,218 @@ +# Задача: Петя и Вася в страшном лабиринте + +Петя и Вася - школьные друзья. И как все школьные друзья, они тесно общаются с охранником Валерием. + +Валерий за работой часто решает различные лабиринты, однако водить ручкой по бумаге ему уже надоело и он начал строить свои собственные лабиринты из фанеры на заднем дворе. Однажды зимним вечером он предложил ребятам пройти его последний лабиринт, где он сделал 2 входа и несколько выходов. + +Но ребята схитрили и еще днём успели узнать, как выглядит карта лабиринта. Валерий прознал об их выходке, и когда друзья уже начали его проходить, резко выключил всё освещение заднего двора. Теперь ребятам, которые всё еще боятся темноты, необходимо как можно скорее выбраться из него, не оборачиваясь назад. + +Кому первым из них удастся это сделать, если они идут по кратчайшему пути? (использовать вход школьного друга как выход - можно) + +## Входные данные: + + Лабиринт в текстовом многострочном формате: + * `#` - стена + * `0` - проход + * `V` - точка входа Васи + * `P` - точка входа Пети + + оканчивается пустой строкой + +Примеры: +``` +##V######## +#000000000# +#0#####0#0# +#0#00000#0# +#####0###0# +P0#000#000# +#0###0#0#0# +#0#000#0#0# +#0#0###0#0# +#000#000#0# +#0###0##### + +``` + +``` +##V######## +#000000000# +#0#####0#0# +#0#00000#0# +#####0###0# +P0#000#000# +#0###0#0#0# +#0#000#0#0# +#0#0###0#0# +#000#000#0# +#####0##### + +``` + +``` +###V######### +#00000000#00# +#0####0####0# +00#0#0000000# +#0#0#######0# +#0#0#0000000# +#0#0#0###0### +#0#00000#000# +#0#########0# +#0000#000000# +####0##P##### + +``` + +``` +#########V## +#000000#000# +#0####0#0#0# +#0#00#0#0#0# +00#0##0#0#0# +#0#0#00#0#0# +#0#0#0##0#0# +#0#00000000# +#0########## +#00000000000 +##P######### + +``` + +``` +##V### +#00#0P +#0##0# +#0000# +###### + +``` + +## Выходные данные: + + * В единственную строке имя победителя и длина его кратчайшего маршрута. + * В случае ничьи вывести "Draw!" и длину кратчайшего пути. + * В случае, когда из лабиринта не удается выйти, вывести "Standoff! Valery!", тем самым обозначая, что охранник провел друзей. + + +# Используемый алгоритм: построение дерева кратчайших путей: DAG Relaxation (для этой задачи подходит очень не очень) + +Алгоритм релаксации является стандартным алгоритмом для нахождения кратчайших путей в направленном ацикличном графе (DAG). Алгоритм работает, последовательно обновляя расстояния до каждой вершины, используя информацию о расстояниях до её предков. Он работает за линейное время (`O(|V|+|E|)`, где `V` - количество вершин, а `E` - количество ребер). + +## Теория алгоритма +`𝛿(𝑠, 𝑡)` = −∞ 𝑖𝑓 𝑡ℎ𝑒𝑟𝑒 𝑖𝑠 𝑎 𝑝𝑎𝑡ℎ 𝑓𝑟𝑜𝑚 𝑠 𝑡𝑜 𝑡 𝑐𝑜𝑛𝑡𝑎𝑖𝑛𝑠 +𝑎 𝑛𝑒𝑔𝑎𝑡𝑖𝑣𝑒-𝑤𝑒𝑖𝑔ℎ𝑡 𝑐𝑦𝑐𝑙𝑒 + +Если известны `δ(s, v)` для всех вершин `v ∈ V`, то можем +построить дерево кратчайших путей за `O(|V|+|E|)`. + +`𝛿(𝑠, 𝑡)` − кратчайший путь +`d(𝑠, 𝑡)` − оценка расстояния + +Поддерживаем `d(s, v)` (изначально ∞) для каждой `v ∈ V`, +которая всегда ограничивает сверху `δ(s, v)`, постепенно +понижая `d`, пока не достигнем: `d(s, v) = δ(s, v)`. + +Когда понижаем оценку? + +Неравенство треугольника: вес кратчайшего пути из `u` в `v` +не может быть больше чем кратчайший путь из `u` в `v` через +другую вершину `x`. +`𝛿(𝑢,𝑣) ≤ 𝛿(𝑢,𝑥) + 𝛿(𝑥,𝑣) 𝑓𝑜𝑟 𝑎𝑙𝑙 𝑢, 𝑣, 𝑥 ∈ 𝑉` + +## Объяснение кода: + +```C++ +/** + * @brief Вычисляет кратчайшие пути от заданной начальной вершины до всех других + * вершин в направленном ациклическом графе (DAG) с помощью релаксации. + * @tparam vert_t: тип вершины в графе + * @tparam weight_t: тип веса в графе + * @param graph: граф, для которого необходимо вычислить кратчайшие пути. + * @param start_vert: начальная вершина, от которой вычисляются расстояния. + * @throw std::invalid_argument("DAGRelaxation: there is no such start + * vertice.") + * @throw std::invalid_argument("DAGRelaxation: graph is not directed."); + * @return std::unordered_map: словарь, где ключ - вершина, а + * значение - кратчайшее расстояние от start_vert до этой вершины + * (если до вершины нет пути, то значение будет равно + * std::numeric_limits::max()) + */ +template +std::unordered_map DAGRelaxation( + const Graph& graph, vert_t start); +``` + +1. Проверяет, что граф направленный и что начальная вершина существует. +2. Инициализирует словарь (хеш-таблицу) `dists`, где хранятся расстояния от начальной вершины до каждой. Все расстояния изначально бесконечны, кроме расстояния до самой начальной вершины, которое равно нулю. +3. Сортирует вершины графа по топологическому порядку (чтобы посетить вершину только после посещения всех ее предков). +4. Проходит по каждой вершине в этом порядке и обновляет расстояния до всех смежных вершин, если текущее расстояние до смежной вершины больше, чем сумма расстояния до текущей вершины и веса ребра между ними. + +В итоге `dists` будет содержать кратчайшие расстояния от начальной вершины до всех остальных вершин в графе. + +### Основные моменты: +* Топологическая сортировка важна, чтобы гарантировать, что мы не будем обновлять расстояния до вершины, пока не найдем кратчайший путь к ее предку. +* Релаксация - это процесс обновления расстояний до вершин, используя информацию о расстояниях до их предков. +* Код работает только для DAG, так как для циклических графов может не сходиться. + +# Используемый алгоритм: нахождение кратчайшего расстояния между двумя вершинами: A* (для этой задачи подходит лучше) + +Алгоритм A* — это эвристический алгоритм поиска пути, который находит кратчайший путь между начальной и конечной вершинами в графе. Он использует эвристическую функцию для оценки стоимости пути до конечной вершины, что позволяет ему эффективно обходить пространство поиска. В отличие от алгоритмов полного перебора, A* ориентирован на поиск наиболее перспективных путей. + +## Теория алгоритма: + +A* основывается на концепции функции стоимости `f(n) = g(n) + h(n)`, где: + +`n` — текущая вершина. + +`g(n)` — фактическая стоимость пути от начальной вершины до `n`. + +`h(n)` — эвристическая оценка стоимости пути от n до конечной вершины. Эта функция должна быть допустимой (никогда не завышает фактическую стоимость) и монотонной (для улучшения эффективности, хотя это не является обязательным условием корректности). + +Алгоритм A* использует приоритетную очередь для хранения вершин, которые ещё нужно посетить, отсортированных по возрастанию значения `f(n)`. На каждом шаге выбирается вершина с наименьшим `f(n)`, и её соседи рассматриваются. Если стоимость пути до соседа через текущую вершину меньше, чем текущая лучшая оценка стоимости до соседа, то оценка обновляется, и сосед добавляется в приоритетную очередь (или его приоритет изменяется). + +Процесс повторяется до тех пор, пока не будет найдена конечная вершина, или приоритетная очередь не опустеет (в последнем случае путь не существует). + +## Объяснение кода: +```C++ +/** + * @brief Вычисляет кратчайший путь между двумя вершинами с помощью A*. + * @tparam vert_t: тип вершины в графе + * @tparam weight_t: тип веса в графе + * @param start: начальная вершина. + * @param goal: конечная вершина. + * @param graph: граф, для которого необходимо вычислить кратчайшие пути. + * @param heuristic_range: функция эвристической оценки расстояния от + * произвольной вершины до конечной вершины. Должна принимать две вершины в + * качестве аргументов и возвращать вес (оценку расстояния). + * @throw `std::invalid_argument("AStar: there is no such start vertice in + * graph.")`. + * @throw `std::invalid_argument("AStar: there is no such goal vertice in + * graph.")`. + * @return `weight_t`: стоимость кратчайшего пути от `start` до `goal` + * (если до вершины нет пути, то значение будет равно + * `std::numeric_limits::max()`). + */ +template +weight_t AStar( + const vert_t& start, const vert_t& goal, + const Graph& graph, + std::function heuristic_range); +``` + +Код реализует алгоритм A* с использованием приоритетной очереди (`std::priority_queue`) и словарей для хранения стоимости пути от начальной вершины (`cost_from_start`) и суммы стоимости пути и эвристики (`range_plus_cost`). + +1. Проверка входных данных: Код проверяет наличие начальной и конечной вершин в графе. +2. Инициализация: Создаётся приоритетная очередь `visited_verts`, отсортированная по возрастанию стоимости `f(n)`. Словари `cost_from_start` и `range_plus_cost` инициализируются. Стоимость пути до начальной вершины устанавливается в 0, а до всех остальных — в бесконечность. +3. Поиск пути: Пока приоритетная очередь не пуста, алгоритм извлекает вершину с наименьшей оценкой `f(n)`. Если это конечная вершина, то алгоритм возвращает стоимость пути. В противном случае, алгоритм рассматривает соседей текущей вершины, обновляет оценки стоимости пути и добавляет соседей в приоритетную очередь. + +Возврат результата: Если конечная вершина не найдена после обработки всех вершин, алгоритм возвращает бесконечность, указывая на отсутствие пути. + +### Основные моменты: + +1. Эвристическая функция: качество работы A* сильно зависит от качества эвристической функции. Более точная эвристика приводит к более быстрому поиску. +2. Допустимость и монотонность: эвристическая функция должна быть допустимой (не завышать фактическую стоимость) для гарантии нахождения кратчайшего пути. Монотонность (согласованность) эвристики улучшает производительность, но не является необходимым условием корректности. +3. Сложность: сложность A* зависит от размера графа и качества эвристики. В худшем случае она может быть экспоненциальной, но на практике часто работает значительно быстрее, чем полный перебор. + +Этот алгоритм является одним из наиболее эффективных алгоритмов поиска кратчайшего пути в графах, особенно когда пространство поиска велико. Выбор эвристики является ключевым фактором, влияющим на производительность алгоритма. + diff --git a/additional_tasks/petya_and_vasya_labyrinth/src/a_star.hpp b/additional_tasks/petya_and_vasya_labyrinth/src/a_star.hpp new file mode 100644 index 0000000..032cb03 --- /dev/null +++ b/additional_tasks/petya_and_vasya_labyrinth/src/a_star.hpp @@ -0,0 +1,95 @@ +#pragma once + +#include +#include +#include +#include + +#include "graph.hpp" + +/** + * @brief Вычисляет кратчайший путь между двумя вершинами с помощью A*. + * @tparam vert_t: тип вершины в графе + * @tparam weight_t: тип веса в графе + * @param start: начальная вершина. + * @param goal: конечная вершина. + * @param graph: граф, для которого необходимо вычислить кратчайшие пути. + * @param heuristic_range: функция эвристической оценки расстояния от + * произвольной вершины до конечной вершины. Должна принимать две вершины в + * качестве аргументов и возвращать вес (оценку расстояния). + * @throw `std::invalid_argument("AStar: there is no such start vertice in + * graph.")`. + * @throw `std::invalid_argument("AStar: there is no such goal vertice in + * graph.")`. + * @return `weight_t`: стоимость кратчайшего пути от `start` до `goal` + * (если до вершины нет пути, то значение будет равно + * `std::numeric_limits::max()`). + */ +template +weight_t AStar( + const vert_t& start, const vert_t& goal, + const Graph& graph, + std::function heuristic_range) { + if (!graph.ContainsVert(start)) + throw std::invalid_argument( + "AStar: there is no such start vertice in graph."); + + if (!graph.ContainsVert(goal)) + throw std::invalid_argument( + "AStar: there is no such goal vertice in graph."); + + std::priority_queue, + std::vector>, + std::greater>> + /** + * @brief приоритетная очередь для вершин, которые нужно посетить + * @details пары (стоимость, вершина), упорядоченные по стоимости. + */ + visited_verts; + + // @brief стоимости пути от стартовой вершины до каждой вершины + std::unordered_map cost_from_start; + + // @brief суммы стоимостей пути от стартовой вершины и эвристики до конечной + std::unordered_map range_plus_cost; + + for (const auto& v : graph.Verts()) + cost_from_start[v] = std::numeric_limits::max(); + + // начальные значения + cost_from_start[start] = 0; + range_plus_cost[start] = + cost_from_start[start] + heuristic_range(start, goal); + visited_verts.push({range_plus_cost[start], start}); + + while (!visited_verts.empty()) { + // посещенная вершина с минимальным значением range_plus_cost + const vert_t current = visited_verts.top().second; + visited_verts.pop(); + + // достигнута конечная вершина, уже нашли путь до нужной вершины + if (current == goal) return cost_from_start[goal]; + + const auto neighbors = graph.GetAdjList()[current]; + for (const auto& neighbor : neighbors) { + // @brief предполагаемая стоимость пути до соседа + weight_t tentative_score = + cost_from_start[current] + + ((graph.IsWeighted()) ? graph.GetEdgeWeight({current, neighbor}) : 1); + + // предполагаемая стоимость меньше текущей, + // обновляем стоимость и добавляем соседа в очередь + if (tentative_score < cost_from_start[neighbor]) { + cost_from_start[neighbor] = tentative_score; + + // range_plus_cost для новой вершины (другого соседа) + range_plus_cost[neighbor] = + cost_from_start[neighbor] + heuristic_range(neighbor, goal); + visited_verts.push({range_plus_cost[neighbor], neighbor}); + } + } + } + + // путь не был найден + return std::numeric_limits::max(); +} diff --git a/additional_tasks/petya_and_vasya_labyrinth/src/dag_relaxation.hpp b/additional_tasks/petya_and_vasya_labyrinth/src/dag_relaxation.hpp new file mode 100644 index 0000000..7356837 --- /dev/null +++ b/additional_tasks/petya_and_vasya_labyrinth/src/dag_relaxation.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include + +#include "topological_sort.hpp" + +/** + * @brief Вычисляет кратчайшие пути от заданной начальной вершины до всех других + * вершин в направленном ациклическом графе (DAG) с помощью релаксации. + * @tparam vert_t: тип вершины в графе. + * @tparam weight_t: тип веса в графе. + * @param graph: граф, для которого необходимо вычислить кратчайшие пути. + * @param start: начальная вершина, от которой вычисляются расстояния. + * @throw `std::invalid_argument("DAGRelaxation: there is no such start + * vertice.")`. + * @throw `std::invalid_argument("DAGRelaxation: graph is not directed.")`. + * @return `std::unordered_map`: словарь, где ключ - вершина, + * а значение - кратчайшее расстояние от start до этой вершины (если до вершины + * нет пути, то значение будет равно `std::numeric_limits::max()`). + */ +template +std::unordered_map DAGRelaxation( + const Graph& graph, const vert_t& start) { + if (!graph.ContainsVert(start)) + throw std::invalid_argument( + "DAGRelaxation: there is no such start vertice in graph."); + + if (!graph.IsDirected()) + throw std::invalid_argument("DAGRelaxation: graph is not directed."); + + /// @brief хеш-таблица расстояний от start до каждой вершины + std::unordered_map dists; + + // инициализация расстояний от start до каждой вершины бесконечностями + for (const auto& vert : graph.Verts()) + dists[vert] = std::numeric_limits::max(); + + // расстояние от начальной вершины до самой себя равно 0 + dists[start] = 0; + + std::vector sorted_verts = TopologicalSort(graph); + + for (const auto& u_vert : sorted_verts) + for (size_t i = 0; i < graph.GetAdjList()[u_vert].size(); i++) { + auto vert = graph.GetAdjList()[u_vert][i]; + + // (нас не интересует бесконечное расстояние от start до u_vert) + if (dists[u_vert] == std::numeric_limits::max()) continue; + + // если граф не взвешен, то задаем расстояние единицей + weight_t u_v_dist = 1; + if (graph.IsWeighted()) u_v_dist = graph.GetEdgeWeight({u_vert, vert}); + + // релаксируем ребро, если текущее расстояние до vert больше, чем + // расстояние до u_vert + расстояние между вершинами (вес их ребра) + if (dists[vert] > dists[u_vert] + u_v_dist) + dists[vert] = dists[u_vert] + u_v_dist; + } + + return dists; +} diff --git a/additional_tasks/petya_and_vasya_labyrinth/src/main.cpp b/additional_tasks/petya_and_vasya_labyrinth/src/main.cpp new file mode 100644 index 0000000..e209d2c --- /dev/null +++ b/additional_tasks/petya_and_vasya_labyrinth/src/main.cpp @@ -0,0 +1,7 @@ +#include "petya_and_vasya_labyrinth.hpp" + +int main() { + // ComplicatedBadSolution(); + GoodSolution(); + return 0; +} diff --git a/additional_tasks/petya_and_vasya_labyrinth/src/petya_and_vasya_labyrinth.hpp b/additional_tasks/petya_and_vasya_labyrinth/src/petya_and_vasya_labyrinth.hpp new file mode 100644 index 0000000..2d6abd8 --- /dev/null +++ b/additional_tasks/petya_and_vasya_labyrinth/src/petya_and_vasya_labyrinth.hpp @@ -0,0 +1,290 @@ +#include +#include +#include +#include + +#include "a_star.hpp" +#include "dag_relaxation.hpp" + +namespace { + +/** + * @brief Находит ячейку в заданном векторе строк, соответствующую определенному + * суффиксу. + * @param strings: вектор строк для поиска. + * @param i: индекс. + * @param j: индекс. + * @return std::string: строка, содержащая суффикс, или пустая строка, если + * совпадений не найдено. + */ +inline std::string FindCell(const std::vector& strings, size_t i, + size_t j) { + std::string suffix = "_" + std::to_string(i) + "_" + std::to_string(j); + + auto it = + std::find_if(strings.begin(), strings.end(), [&](const std::string& str) { + return str.find(suffix) != std::string::npos; + }); + + return it == strings.end() ? "" : *it; +} + +/** + * @brief Добавляет ребра в граф на основе смежности ячеек. + * @param graph: граф, в который нужно добавить ребра (НЕНАПРАВЛЕННЫЙ). + */ +inline void AddEdgesNonDirected(Graph& graph) { + for (const auto& vert : graph.Verts()) { + size_t j = std::stoll(vert.substr(vert.rfind('_') + 1)); + size_t i = std::stoll(vert.substr( + vert.rfind('_', vert.rfind('_') - 1) + 1, + vert.rfind('_') - vert.rfind('_', vert.rfind('_') - 1) - 1)); + + std::string up_vert = "", down_vert = ""; + auto right_vert = FindCell(graph.Verts(), i + 1, j); + auto left_vert = FindCell(graph.Verts(), i, j + 1); + + if (i != 0 && j != 0) { + up_vert = FindCell(graph.Verts(), i - 1, j); + down_vert = FindCell(graph.Verts(), i, j - 1); + } + + for (const auto& another_vert : {up_vert, down_vert, right_vert, left_vert}) + if (!another_vert.empty() && !graph.ContainsEdge({another_vert, vert})) + graph.AddEdge({vert, another_vert}); + } +} + +/** + * @brief Итеративно добавляет ребра в граф на основе смежности ячеек. + * @param graph: граф, в который нужно добавить ребра (НАПРАВЛЕННЫЙ). + * @param vert: начальная вершина для обхода. + */ +inline void AddEdgesIterative(Graph& graph, + const std::string& vert) { + std::stack s; + s.push(vert); + + while (!s.empty()) { + std::string current_vert = s.top(); + s.pop(); + + size_t j = std::stoll(current_vert.substr(current_vert.rfind('_') + 1)); + size_t i = std::stoll(current_vert.substr( + current_vert.rfind('_', current_vert.rfind('_') - 1) + 1, + current_vert.rfind('_') - + current_vert.rfind('_', current_vert.rfind('_') - 1) - 1)); + + std::string up_vert = "", down_vert = ""; + auto right_vert = FindCell(graph.Verts(), i + 1, j); + auto left_vert = FindCell(graph.Verts(), i, j + 1); + + if (i != 0 && j != 0) { + up_vert = FindCell(graph.Verts(), i - 1, j); + down_vert = FindCell(graph.Verts(), i, j - 1); + } + + for (const auto& another_vert : {up_vert, down_vert, right_vert, left_vert}) + if (!another_vert.empty() && + !graph.ContainsEdge({another_vert, current_vert})) { + graph.AddEdge({current_vert, another_vert}); + if (another_vert[0] != 'E') s.push(another_vert); + } + } +} + +} // namespace + +struct PetyaAndVasyaGraphs { + struct { + Graph graph; + std::string vert; + std::vector exits; + } petya, vasya; +}; + +enum class SolutionType { ComplicatedBad, Good }; + +/** + * @brief Разбирает входные данные лабиринта из стандартного ввода и создает + * направленный граф. + * @return `PetyaAndVasyaGraphs`, содержащий графы для обоих игроков. + */ +inline PetyaAndVasyaGraphs ParseMaze( + std::istream& is = std::cin, + SolutionType sol_type = SolutionType::ComplicatedBad) { + Graph graph; + std::vector console_maze; + std::string line; + + PetyaAndVasyaGraphs maze; + size_t petya_start_i = -1, petya_start_j = -1, vasya_start_i = -1, + vasya_start_j = -1; + + // считывание лабиринта + while (std::getline(is, line) && !line.empty()) console_maze.push_back(line); + + // добавление вершин + for (size_t i = 0; i < console_maze.size(); i++) { + const auto& line = console_maze[i]; + + for (size_t j = 0; j < line.size(); j++) { + const auto& cell = line[j]; + std::stringstream cell_name_stream; + + if (cell == 'P') { + petya_start_i = i, petya_start_j = j; + maze.petya.vert = "E_" + std::to_string(i) + "_" + std::to_string(j); + maze.vasya.exits.push_back(maze.petya.vert); + + cell_name_stream << 'E'; + + } else if (cell == 'V') { + vasya_start_i = i, vasya_start_j = j; + maze.vasya.vert = "E_" + std::to_string(i) + "_" + std::to_string(j); + maze.petya.exits.push_back(maze.vasya.vert); + + cell_name_stream << 'E'; + } + + else if (cell == '0') { + if (j == 0 || i == 0 || j == (line.size() - 1) || + i == (console_maze.size() - 1)) { + auto vert = "E_" + std::to_string(i) + "_" + std::to_string(j); + + maze.vasya.exits.push_back(vert); + maze.petya.exits.push_back(vert); + + cell_name_stream << 'E'; + } + + else + cell_name_stream << 'C'; + } + + if (!cell_name_stream.str().empty()) { + cell_name_stream << "_" << i << "_" << j; + + graph.AddVert(cell_name_stream.str()); + } + } + } + + // добавление ребер + maze.petya.graph = graph; + maze.vasya.graph = graph; + + switch (sol_type) { + case SolutionType::ComplicatedBad: { + AddEdgesIterative(maze.petya.graph, + FindCell(graph.Verts(), petya_start_i, petya_start_j)); + + AddEdgesIterative(maze.vasya.graph, + FindCell(graph.Verts(), vasya_start_i, vasya_start_j)); + + maze.petya.graph.MakeDirected(); + maze.vasya.graph.MakeDirected(); + break; + } + + case SolutionType::Good: { + AddEdgesNonDirected(maze.petya.graph); + maze.petya.graph.MakeUndirected(); + + maze.vasya.graph = maze.petya.graph; + break; + } + } + + return maze; +} + +/// @brief Хорошо решает задачу: "Петя и Вася в страшном лабиринте". +void GoodSolution(std::istream& is = std::cin, std::ostream& os = std::cout) { + try { + auto maze = ParseMaze(is, SolutionType::Good); + + long petya_min = std::numeric_limits::max(); + long vasya_min = std::numeric_limits::max(); + + auto manhattan_heuristic = [](const std::string& vert_1, + const std::string& vert_2) { + auto extract_coords = [](const std::string& vert) { + size_t j = std::stoll(vert.substr(vert.rfind('_') + 1)); + size_t i = std::stoll(vert.substr( + vert.rfind('_', vert.rfind('_') - 1) + 1, + vert.rfind('_') - vert.rfind('_', vert.rfind('_') - 1) - 1)); + + return std::make_pair(i, j); + }; + + // в матрице (лабиринте) координаты положительные, модули не нужны + return extract_coords(vert_1).first - extract_coords(vert_2).first + + extract_coords(vert_1).second - extract_coords(vert_2).second; + }; + + // (размеры будут одинаковые) + for (size_t i = 0; i < maze.petya.exits.size(); i++) { + petya_min = std::min( + petya_min, + AStar(maze.petya.vert, maze.petya.exits[i], maze.petya.graph, + std::function( + manhattan_heuristic))); + + vasya_min = std::min( + vasya_min, + AStar(maze.vasya.vert, maze.vasya.exits[i], maze.vasya.graph, + std::function( + manhattan_heuristic))); + } + + if (vasya_min < petya_min) + os << "Vasya! with " << vasya_min << std::endl; + + else if (vasya_min == petya_min) + if (vasya_min != std::numeric_limits::max()) + os << "Draw! with " << vasya_min << std::endl; + else + os << "Deadlock! Valery!" << std::endl; + + else + os << "Petya! with " << petya_min << std::endl; + + } catch (const std::exception&) { + throw std::invalid_argument("Invalid enter format!"); + } +} + +/// @brief Сложно и плохо решает задачу: "Петя и Вася в страшном лабиринте". +void ComplicatedBadSolution(std::istream& is = std::cin, + std::ostream& os = std::cout) { + try { + auto maze = ParseMaze(is, SolutionType::ComplicatedBad); + + auto petya_relax = DAGRelaxation(maze.petya.graph, maze.petya.vert); + auto vasya_relax = DAGRelaxation(maze.vasya.graph, maze.vasya.vert); + + long petya_min = std::numeric_limits::max(); + long vasya_min = std::numeric_limits::max(); + + // (размеры будут одинаковые) + for (size_t i = 0; i < maze.petya.exits.size(); i++) { + petya_min = std::min(petya_min, petya_relax[maze.petya.exits[i]]); + vasya_min = std::min(vasya_min, vasya_relax[maze.vasya.exits[i]]); + } + + if (vasya_min < petya_min) + os << "Vasya! with " << vasya_min << std::endl; + + else if (vasya_min == petya_min) + if (vasya_min != std::numeric_limits::max()) + os << "Draw! with " << vasya_min << std::endl; + else + os << "Deadlock! Valery!" << std::endl; + + else + os << "Petya! with " << petya_min << std::endl; + } catch (const std::exception&) { + throw std::invalid_argument("Invalid enter format!"); + } +} diff --git a/additional_tasks/petya_and_vasya_labyrinth/src/test.cpp b/additional_tasks/petya_and_vasya_labyrinth/src/test.cpp new file mode 100644 index 0000000..79f91ee --- /dev/null +++ b/additional_tasks/petya_and_vasya_labyrinth/src/test.cpp @@ -0,0 +1,528 @@ +#include + +#include "dag_relaxation.hpp" +#include "petya_and_vasya_labyrinth.hpp" + +#define LONG_INF std::numeric_limits::max() + +namespace { + +template +inline bool operator==(const std::unordered_map& lhs, + const std::unordered_map& rhs) { + if (lhs.size() != rhs.size()) return false; + + for (const auto& [key, value] : lhs) + if (rhs.find(key) == rhs.end() || rhs.at(key) != value) return false; + + // else + return true; +} + +template +inline bool operator!=(const std::unordered_map& lhs, + const std::unordered_map& rhs) { + return !(lhs == rhs); +} + +} // namespace + +TEST(DAGRelaxationTest, SimpleDAG_A) { + Graph graph; + graph.AddEdge({"A", "B", 5}); + graph.AddEdge({"A", "C", 3}); + graph.AddEdge({"B", "D", 2}); + graph.AddEdge({"C", "D", 4}); + graph.AddEdge({"C", "E", 1}); + graph.AddEdge({"D", "F", 7}); + graph.AddEdge({"E", "F", 2}); + + auto distances = DAGRelaxation(graph, std::string("A")); + + std::unordered_map expected_distances = { + {"A", 0}, {"B", 5}, {"C", 3}, {"D", 7}, {"E", 4}, {"F", 6}}; + + ASSERT_EQ(distances, expected_distances); +} + +TEST(DAGRelaxationTest, SimpleDAG_B) { + Graph graph; + graph.AddEdge({"A", "B", 5}); + graph.AddEdge({"A", "C", 3}); + graph.AddEdge({"B", "D", 2}); + graph.AddEdge({"C", "D", 4}); + graph.AddEdge({"C", "E", 1}); + graph.AddEdge({"D", "F", 7}); + graph.AddEdge({"E", "F", 2}); + + auto distances = DAGRelaxation(graph, std::string("B")); + + std::unordered_map expected_distances = { + {"F", 9}, {"E", LONG_INF}, {"B", 0}, + {"D", 2}, {"C", LONG_INF}, {"A", LONG_INF}}; + + ASSERT_EQ(distances, expected_distances); +} + +TEST(DAGRelaxationTest, SimpleDAG_C) { + Graph graph; + graph.AddEdge({"A", "B", 5}); + graph.AddEdge({"A", "C", 3}); + graph.AddEdge({"B", "D", 2}); + graph.AddEdge({"C", "D", 4}); + graph.AddEdge({"C", "E", 1}); + graph.AddEdge({"D", "F", 7}); + graph.AddEdge({"E", "F", 2}); + + auto distances = DAGRelaxation(graph, std::string("C")); + + std::unordered_map expected_distances = { + {"F", 3}, {"E", 1}, {"B", LONG_INF}, {"D", 4}, {"C", 0}, {"A", LONG_INF}}; + + ASSERT_EQ(distances, expected_distances); +} + +TEST(DAGRelaxationTest, SimpleDAG_D) { + Graph graph; + graph.AddEdge({"A", "B", 5}); + graph.AddEdge({"A", "C", 3}); + graph.AddEdge({"B", "D", 2}); + graph.AddEdge({"C", "D", 4}); + graph.AddEdge({"C", "E", 1}); + graph.AddEdge({"D", "F", 7}); + graph.AddEdge({"E", "F", 2}); + + auto distances = DAGRelaxation(graph, std::string("D")); + + std::unordered_map expected_distances = { + {"F", 7}, {"E", LONG_INF}, {"B", LONG_INF}, + {"D", 0}, {"C", LONG_INF}, {"A", LONG_INF}}; + + ASSERT_EQ(distances, expected_distances); +} + +TEST(DAGRelaxationTest, SimpleDAG_E) { + Graph graph; + graph.AddEdge({"A", "B", 5}); + graph.AddEdge({"A", "C", 3}); + graph.AddEdge({"B", "D", 2}); + graph.AddEdge({"C", "D", 4}); + graph.AddEdge({"C", "E", 1}); + graph.AddEdge({"D", "F", 7}); + graph.AddEdge({"E", "F", 2}); + + auto distances = DAGRelaxation(graph, std::string("E")); + + std::unordered_map expected_distances = { + {"F", 2}, {"E", 0}, {"B", LONG_INF}, + {"D", LONG_INF}, {"C", LONG_INF}, {"A", LONG_INF}}; + + ASSERT_EQ(distances, expected_distances); +} + +TEST(DAGRelaxationTest, SimpleDAG_F) { + Graph graph; + graph.AddEdge({"A", "B", 5}); + graph.AddEdge({"A", "C", 3}); + graph.AddEdge({"B", "D", 2}); + graph.AddEdge({"C", "D", 4}); + graph.AddEdge({"C", "E", 1}); + graph.AddEdge({"D", "F", 7}); + graph.AddEdge({"E", "F", 2}); + + auto distances = DAGRelaxation(graph, std::string("F")); + + std::unordered_map expected_distances = { + {"F", 0}, {"E", LONG_INF}, {"B", LONG_INF}, + {"D", LONG_INF}, {"C", LONG_INF}, {"A", LONG_INF}}; + + ASSERT_EQ(distances, expected_distances); +} + +TEST(DAGRelaxationTest, UnweightedDAG) { + Graph graph; + graph.AddEdge({"A", "B"}); + graph.AddEdge({"A", "C"}); + graph.AddEdge({"B", "D"}); + graph.AddEdge({"C", "D"}); + graph.AddEdge({"C", "E"}); + graph.AddEdge({"D", "F"}); + graph.AddEdge({"E", "F"}); + + auto distances = DAGRelaxation(graph, std::string("A")); + + std::unordered_map expected_distances = { + {"A", 0}, {"B", 1}, {"C", 1}, {"D", 2}, {"E", 2}, {"F", 3}}; + + ASSERT_EQ(distances, expected_distances); +} + +TEST(DAGRelaxationTest, NonDirectedGraph) { + Graph graph; + graph.AddEdge({"A", "B"}); + graph.AddEdge({"A", "C"}); + graph.AddEdge({"B", "D"}); + graph.AddEdge({"C", "D"}); + + graph.MakeUndirected(); + + ASSERT_THROW(DAGRelaxation(graph, std::string("A")), std::invalid_argument); +} + +TEST(DAGRelaxationTest, NonExistingStartVertex) { + Graph graph; + graph.AddEdge({"A", "B"}); + graph.AddEdge({"A", "C"}); + graph.AddEdge({"B", "D"}); + graph.AddEdge({"C", "D"}); + + graph.MakeUndirected(); + + ASSERT_THROW(DAGRelaxation(graph, std::string("E")), std::invalid_argument); +} + +TEST(DAGRelaxationTest, DAGWithNoIncomingEdges) { + Graph graph; + graph.AddEdge({"A", "B", 5}); + graph.AddEdge({"A", "C", 3}); + graph.AddEdge({"B", "D", 2}); + graph.AddEdge({"C", "D", 4}); + graph.AddEdge({"C", "E", 1}); + graph.AddEdge({"D", "F", 7}); + graph.AddEdge({"E", "F", 2}); + + graph.AddEdge({"G", "F", 3}); + + auto distances = DAGRelaxation(graph, std::string("A")); + + std::unordered_map expected_distances = { + {"A", 0}, {"B", 5}, {"C", 3}, {"D", 7}, + {"E", 4}, {"F", 6}, {"G", LONG_INF}}; + + ASSERT_EQ(distances, expected_distances); +} + +TEST(DAGRelaxationTest, OneVertDAG) { + Graph graph; + graph.AddVert("A"); + + auto distances = DAGRelaxation(graph, std::string("A")); + + std::unordered_map expected_distances = {{"A", 0}}; + + ASSERT_EQ(distances, expected_distances); +} + +TEST(DAGRelaxationTest, TwoVertsDAG) { + Graph graph; + graph.AddEdge({"A", "B", 5}); + + auto distances = DAGRelaxation(graph, std::string("A")); + + std::unordered_map expected_distances = {{"A", 0}, + {"B", 5}}; + + ASSERT_EQ(distances, expected_distances); +} + +TEST(DAGRelaxationTest, MultiplePathsToOneVertex) { + Graph graph; + + graph.AddEdge({"A", "B", 1}); + graph.AddEdge({"A", "C", 4}); + graph.AddEdge({"B", "D", 2}); + graph.AddEdge({"C", "D", 1}); + + auto distances = DAGRelaxation(graph, std::string("A")); + std::unordered_map expected_distances = { + {"A", 0}, {"B", 1}, {"C", 4}, {"D", 3}}; + + ASSERT_EQ(distances, expected_distances); +} + +TEST(DAGRelaxationTest, IntVerticesAndWeights) { + Graph graph; + + graph.AddEdge({1, 2, 5}); + graph.AddEdge({1, 3, 2}); + graph.AddEdge({2, 3, 1}); + + auto distances = DAGRelaxation(graph, 1); + std::unordered_map expected_distances = {{1, 0}, {2, 5}, {3, 2}}; + + ASSERT_EQ(distances, expected_distances); +} + +// Тест с символами в качестве вершин и дробными значениями в качестве весов +TEST(DAGRelaxationTest, CharVerticesAndDoubleWeights) { + Graph graph; + + graph.AddEdge({"A", "B", 3.5}); + graph.AddEdge({"A", "C", 1.5}); + graph.AddEdge({"B", "C", 2.5}); + + auto distances = DAGRelaxation(graph, std::string("A")); + + std::unordered_map expected_distances = { + {"A", 0.0}, {"B", 3.5}, {"C", 1.5}}; + + ASSERT_EQ(distances, expected_distances); +} + +// Тест со строками в качестве вершин и весами (long) +TEST(DAGRelaxationTest, StringVerticesAndLongWeights) { + Graph graph; + + graph.AddEdge({"A", "B", 10}); + graph.AddEdge({"A", "C", 5}); + graph.AddEdge({"B", "C", 2}); + + auto distances = DAGRelaxation(graph, std::string("A")); + + std::unordered_map expected_distances = { + {"A", 0}, {"B", 10}, {"C", 5}}; + + ASSERT_EQ(distances, expected_distances); +} + +TEST(ComplicatedBadSolutionTest, PetyaWinsSimple) { + std::stringstream ss("P0#V"); + std::stringstream output; + + ComplicatedBadSolution(ss, output); + EXPECT_EQ(output.str(), "Petya! with 1\n"); +} + +TEST(ComplicatedBadSolutionTest, VasyaWinsSimple) { + std::stringstream ss("V0#P"); + std::stringstream output; + + ComplicatedBadSolution(ss, output); + EXPECT_EQ(output.str(), "Vasya! with 1\n"); +} + +TEST(ComplicatedBadSolutionTest, DrawSimple) { + std::stringstream ss("P0#V0"); + std::stringstream output; + + ComplicatedBadSolution(ss, output); + EXPECT_EQ(output.str(), "Draw! with 1\n"); +} + +TEST(ComplicatedBadSolutionTest, ValeryWinsSimple) { + std::stringstream ss("V#P\n"); + std::stringstream output; + + ComplicatedBadSolution(ss, output); + EXPECT_EQ(output.str(), "Deadlock! Valery!\n"); +} + +TEST(ComplicatedGoodBadSolutionTest, PetyaWins) { + std::stringstream ss; + ss << "##V########\n" + << "#000000000#\n" + << "#0#####0#0#\n" + << "#0#00000#0#\n" + << "#####0###0#\n" + << "P0#000#000#\n" + << "#0###0#0#0#\n" + << "#0#000#0#0#\n" + << "#0#0###0#0#\n" + << "#000#000#0#\n" + << "#0###0#####\n\n"; + + auto maze = ss.str(); + + std::stringstream output; + + ComplicatedBadSolution(ss, output); + EXPECT_EQ(output.str(), "Petya! with 6\n"); + + ss << maze; + + std::stringstream output_12; + + GoodSolution(ss, output_12); + EXPECT_EQ(output_12.str(), "Petya! with 6\n"); + + std::stringstream ss_2; + ss_2 << "#########V##\n" + << "#000000#000#\n" + << "#0####0#0#0#\n" + << "#0#00#0#0#0#\n" + << "00#0##0#0#0#\n" + << "#0#0#00#0#0#\n" + << "#0#0#0##0#0#\n" + << "#0#00000000#\n" + << "#0##########\n" + << "#00000000000\n" + << "##P#########\n\n"; + + maze = ss_2.str(); + + std::stringstream output_2; + + ComplicatedBadSolution(ss_2, output_2); + EXPECT_EQ(output_2.str(), "Petya! with 8\n"); + + ss_2 << maze; + + std::stringstream output_22; + + GoodSolution(ss_2, output_22); + EXPECT_EQ(output_22.str(), "Petya! with 8\n"); +} + +TEST(ComplicatedGoodBadSolutionTest, VasyaWins) { + std::stringstream ss; + ss << "##V########\n" + << "#000000000#\n" + << "#0#####0#0#\n" + << "#0#00000#0#\n" + << "#####0###0#\n" + << "P0#000#000#\n" + << "#0###0#0#0#\n" + << "#0#000#0#0#\n" + << "#0#0###0#0#\n" + << "#000#000#0#\n" + << "#####0#####\n\n"; + + auto maze = ss.str(); + + std::stringstream output; + + ComplicatedBadSolution(ss, output); + EXPECT_EQ(output.str(), "Vasya! with 21\n"); + + ss << maze; + + std::stringstream output_12; + + GoodSolution(ss, output_12); + EXPECT_EQ(output_12.str(), "Vasya! with 21\n"); + + std::stringstream ss_2; + ss_2 << "###V#########\n" + << "#00000000#00#\n" + << "#0####0####0#\n" + << "00#0#0000000#\n" + << "#0#0#######0#\n" + << "#0#0#0000000#\n" + << "#0#0#0###0###\n" + << "#0#00000#000#\n" + << "#0#########0#\n" + << "#0000#000000#\n" + << "####0##P#####\n\n"; + + maze = ss_2.str(); + + std::stringstream output_2; + + ComplicatedBadSolution(ss_2, output_2); + EXPECT_EQ(output_2.str(), "Vasya! with 6\n"); + + ss_2 << maze; + + std::stringstream output_22; + + GoodSolution(ss_2, output_22); + EXPECT_EQ(output_22.str(), "Vasya! with 6\n"); +} + +TEST(ComplicatedBadSolutionTest, PetyaWinsHardDAGRelaxation) { + std::stringstream ss; + ss << "###P####\n" + << "#000000#\n" + << "#000000#\n" + << "#000000#\n" + << "#0000000\n" + << "#000000#\n" + << "#000000#\n" + << "#000000#\n" + << "#000000#\n" + << "#000000#\n" + << "####V###\n\n"; + + std::stringstream output; + + ComplicatedBadSolution(ss, output); + EXPECT_EQ(output.str(), "Petya! with 8\n"); +} + +TEST(ComplicatedGoodSolutionTest, PetyaWinsHardAStar) { + std::stringstream ss; + ss << "###P####\n" + << "#000000#\n" + << "#000000#\n" + << "#000000#\n" + << "#0000000\n" + << "#000000#\n" + << "#000000#\n" + << "#000000#\n" + << "#000000#\n" + << "#000000#\n" + << "####V###\n\n"; + + std::stringstream output; + + GoodSolution(ss, output); + EXPECT_EQ(output.str(), "Petya! with 8\n"); +} + +TEST(ComplicatedGoodBadSolutionTest, Draw) { + std::stringstream ss; + ss << "##V########\n" + << "#000000000#\n" + << "#0#####0#0#\n" + << "#0#00000#0#\n" + << "#####0###0#\n" + << "P0#000#000#\n" + << "#0###0#0#0#\n" + << "#0#000#0#0#\n" + << "#0#0###0#0#\n" + << "#000#000#0#\n" + << "###########\n\n"; + + auto maze = ss.str(); + + std::stringstream output; + + ComplicatedBadSolution(ss, output); + EXPECT_EQ(output.str(), "Draw! with 25\n"); + + ss << maze; + + std::stringstream output_12; + + GoodSolution(ss, output_12); + EXPECT_EQ(output_12.str(), "Draw! with 25\n"); +} + +TEST(ComplicatedGoodBadSolutionTest, ValeryWins) { + std::stringstream ss; + ss << "##V########\n" + << "#000000000#\n" + << "#0#######0#\n" + << "#0#00000#0#\n" + << "#####0###0#\n" + << "P0#000#000#\n" + << "#0###0#0#0#\n" + << "#0#000#0#0#\n" + << "#0#0###0#0#\n" + << "#000#000#0#\n" + << "###########\n\n"; + + auto maze = ss.str(); + + std::stringstream output; + + ComplicatedBadSolution(ss, output); + EXPECT_EQ(output.str(), "Deadlock! Valery!\n"); + + ss << maze; + + std::stringstream output_12; + + GoodSolution(ss, output_12); + EXPECT_EQ(output_12.str(), "Deadlock! Valery!\n"); +} diff --git a/additional_tasks/petya_and_vasya_labyrinth/src/topological_sort.hpp b/additional_tasks/petya_and_vasya_labyrinth/src/topological_sort.hpp new file mode 100644 index 0000000..3863cdc --- /dev/null +++ b/additional_tasks/petya_and_vasya_labyrinth/src/topological_sort.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include "graph.hpp" + +namespace { + +template +void TopologicalSortStep( + const vert_t& u_vert, std::unordered_map& visited, + std::unordered_map>& adj_list, + std::vector& topological_order) { + visited[u_vert] = true; + + for (const auto& vert : adj_list[u_vert]) + if (!visited[vert]) + TopologicalSortStep(vert, visited, adj_list, topological_order); + + topological_order.push_back(u_vert); +} + +} // namespace + +/** + * @brief Производит топологическую сортировку вершин на основе обхода в глубину + * (DFS) + * @tparam vert_t: тип вершины в графе + * @tparam weight_t: тип веса в графе + * @param graph: сортируемый граф + * @throw `std::invalid_argument("TopologicalSort: graph is not directed.")`. + * @return `std::vector`: список отсортированных вершин + */ +template +std::vector TopologicalSort(const Graph& graph) { + if (!graph.IsDirected()) + throw std::invalid_argument("TopologicalSort: graph is not directed."); + if (graph.Verts().empty()) return {}; + + std::vector topological_order; + + std::unordered_map visited; + for (const auto& vert : graph.Verts()) visited[vert] = false; + + std::unordered_map> adj_list = graph.GetAdjList(); + + for (const auto& vert : graph.Verts()) + if (!visited[vert]) + TopologicalSortStep(vert, visited, adj_list, topological_order); + + std::reverse(topological_order.begin(), topological_order.end()); + + return topological_order; +} diff --git a/additional_tasks/template_task/CMakeLists.txt b/additional_tasks/template_task/CMakeLists.txt index cb87577..7e3bf1f 100644 --- a/additional_tasks/template_task/CMakeLists.txt +++ b/additional_tasks/template_task/CMakeLists.txt @@ -1,35 +1,34 @@ -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.20) get_filename_component(PROJECT_NAME ${CMAKE_CURRENT_LIST_DIR} NAME) string(REPLACE " " "_" PROJECT_NAME ${PROJECT_NAME}) -project(${PROJECT_NAME} C CXX) +project(${PROJECT_NAME} LANGUAGES CXX) set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) -file(GLOB_RECURSE source_list "src/*.cpp" "src/*.hpp") -file(GLOB_RECURSE lib_source_list "../../lib/src/*.cpp" "../../lib/src/*.hpp") -file(GLOB_RECURSE main_source_list "src/main.cpp") -file(GLOB_RECURSE test_source_list "src/*.cpp") +file(GLOB_RECURSE SOURCE_LIST "src/*.cpp" "src/*.hpp") +file(GLOB_RECURSE MAIN_SOURCE_LIST "src/main.cpp") +set(TEST_SOURCE_LIST ${SOURCE_LIST}) file(GLOB_RECURSE test_list "src/*test.cpp") -list(REMOVE_ITEM test_source_list ${main_source_list}) -list(REMOVE_ITEM source_list ${test_list}) +list(REMOVE_ITEM TEST_SOURCE_LIST ${MAIN_SOURCE_LIST}) +list(REMOVE_ITEM SOURCE_LIST ${test_list}) include_directories(${PROJECT_NAME} PUBLIC src) -add_executable(${PROJECT_NAME} ${source_list} ${lib_source_list}) +find_library(Utils ../) + +add_executable(${PROJECT_NAME} ${SOURCE_LIST}) +target_link_libraries(${PROJECT_NAME} PUBLIC Utils) # Locate GTest enable_testing() find_package(GTest REQUIRED) include_directories(${GTEST_INCLUDE_DIRS}) -find_library(Utils ../) -target_link_libraries(${PROJECT_NAME} PUBLIC Utils) - # Link runTests with what we want to test and the GTest and pthread library -add_executable(${PROJECT_NAME}_tests ${test_source_list}) +add_executable(${PROJECT_NAME}_tests ${TEST_SOURCE_LIST}) target_link_libraries( ${PROJECT_NAME}_tests GTest::gtest_main diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 98320e1..7a00ab2 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -1,5 +1,6 @@ -cmake_minimum_required(VERSION 3.10) -project(Utils) +cmake_minimum_required(VERSION 3.20) + +project(Utils LANGUAGES CXX) set(CMAKE_CXX_STANDARD 23) @@ -7,4 +8,5 @@ file(GLOB_RECURSE lib_source_list "src/*.cpp" "src/*.hpp") add_library(${PROJECT_NAME} ${lib_source_list}) +set_target_properties(${PROJECT_NAME} PROPERTIES LINKER_LANGUAGE CXX) target_include_directories(${PROJECT_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src) diff --git a/lib/src/graph.hpp b/lib/src/graph.hpp new file mode 100644 index 0000000..09faa3f --- /dev/null +++ b/lib/src/graph.hpp @@ -0,0 +1,873 @@ +#pragma once + +#include "utils.hpp" + +// MARK: Concepts + +template +concept AllowedVertType = + std::is_arithmetic_v || std::is_same_v; + +template +concept AllowedWeightType = std::is_arithmetic_v; + +// MARK: VertFromTuple + +template +inline vert_t StartVertFromTuple( + const std::tuple& edge) { + return std::get<0>(edge); +} + +template +inline vert_t EndVertFromTuple( + const std::tuple& edge) { + return std::get<1>(edge); +} + +template +inline weight_t WeightFromTuple( + const std::tuple& edge) { + return std::get<2>(edge); +} + +// MARK: Graph + +/** + * @brief Класс графа (может быть взвешенным и ориентированным). + * + * @tparam vert_t: тип вершин. + * @tparam weight_t: тип весов. + */ +template +class Graph { + public: + /// @brief Инициализирует новый экземпляр Graph. + Graph() : edges_(), verts_() {} + + /** + * @brief Копирующий конструктор. + * + * @param other: граф, который нужно скопировать. + */ + Graph(const Graph& other) = default; + + /** + * @brief Оператор копирующего присваивания. + * + * @param other: граф, значения которого нужно присвоить. + * + * @return `Graph&`: ссылка на текущий объект. + */ + Graph& operator=(const Graph& other) = default; + + /** + * @brief Перемещающий конструктор. Перемещает ресурсы из другого графа. + * + * @param other: граф, ресурсы которого нужно переместить. + */ + Graph(Graph&& other) noexcept + : edges_(std::move(other.edges_)), + verts_(std::move(other.verts_)), + is_direct_{other.is_direct_} {} + + /** + * @brief Оператор перемещающего присваивания. Перемещает ресурсы из другого. + * графа. + * + * @param other: граф, ресурсы которого нужно переместить. + * + * @return `Graph&`: ссылка на текущий объект. + */ + Graph& operator=(Graph&& other) noexcept { + if (this != &other) { + edges_ = std::move(other.edges_); + verts_ = std::move(other.verts_); + is_direct_ = other.is_direct_; + } + + return *this; + } + + /** + * @brief Создает новый экземпляр Graph по ребрам, + * представленными вектором std::pair (НЕВЗВЕШЕННЫЙ). + * + * @param edges_pairs: ребра графа. + * + * @return `Graph`: новый экземпляр Graph. + */ + static Graph GraphNonWeighted( + const std::vector>& edges_pairs) { + if (edges_pairs.empty()) return Graph(); + + std::vector edges{}; + edges.reserve(edges_pairs.size()); + + for (const auto& edge : edges_pairs) edges.push_back(edge); + + return Graph(edges); + } + + /** + * @brief Создает новый экземпляр Graph по ребрам, + * представленными вектором std::pair и weight_t (ВЗВЕШЕННЫЙ). + * + * @param edges_pairs: ребра графа. + * @param weights: веса ребер. + * + * @return `Graph`: новый экземпляр Graph. + * + * @throw `std::invalid_argument("GraphWeighted: the sizes of the edges and + * weights vectors do not match.")`. + */ + static Graph GraphWeighted( + const std::vector>& edges_pairs, + const std::vector& weights) { + if (edges_pairs.empty() && weights.empty()) return Graph(); + + std::vector edges{}; + edges.reserve(edges_pairs.size()); + + if (edges_pairs.size() != weights.size()) + throw std::invalid_argument( + "GraphWeighted: the sizes of the edges and weights vectors do not " + "match."); + + for (size_t i = 0; i < weights.size(); i++) + edges.push_back(Edge(edges_pairs[i], weights[i])); + + return Graph(edges); + } + + /** + * @brief Создает новый экземпляр Graph по ребрам. + * представленными вектором std::tuple (ВЗВЕШЕННЫЙ). + * + * @param edges_tuples: ребра графа. + * + * @return `Graph`: новый экземпляр Graph. + */ + static Graph GraphWeighted( + const std::vector>& edges_tuples) { + if (edges_tuples.empty()) return Graph(); + + std::vector edges; + + for (const auto& [start, end, weight] : edges_tuples) + edges.emplace_back(start, end, weight); + + return Graph(edges); + } + + /** + * @brief Создает новый экземпляр Graph по ребрам. + * представленными вектором std::string (НЕВЗВЕШЕННЫЙ). + * + * @param edges_strs: ребра графа. + * + * @return `Graph`: новый экземпляр Graph. + */ + static Graph GraphFromStrs(const std::vector& edges_strs) { + if (edges_strs.empty()) return Graph(); + + std::vector::Edge> edges; + + for (const auto& edge_str : edges_strs) { + vert_t start_vert, end_vert; + std::tie(start_vert, end_vert) = ParseEdgeString_(edge_str); + + edges.emplace_back(start_vert, end_vert); + } + + return Graph(edges); + } + + /** + * @brief Создает новый экземпляр Graph по ребрам. + * представленными словарем из std::string и weight_t (ВЗВЕШЕННЫЙ). + * + * @param edges_dict: ребра графа. + * + * @return `Graph`: новый экземпляр Graph. + */ + static Graph GraphFromMap( + const std::unordered_map& edges_dict) { + if (edges_dict.empty()) return Graph(); + + std::vector::Edge> edges; + + for (const auto& [edge_str, weight] : edges_dict) { + vert_t start_vert, end_vert; + std::tie(start_vert, end_vert) = ParseEdgeString_(edge_str); + + edges.emplace_back(start_vert, end_vert, weight); + } + + return Graph(edges); + } + + /** + * @brief Создает новый экземпляр Graph по матрице смежности. + * + * @param adj_matrix: матрица смежности. + * @param is_weighted: взвешен ли граф. + * + * @return `Graph`: новый экземпляр Graph. + * + * @throw `std::invalid_argument("GraphFromAdjMatrix: AdjacencyMatrix is not + * squared.")`. + * @throw `std::invalid_argument("GraphFromAdjMatrix: AdjacencyMatrix is not + * squared [row problem].")`. + * @throw `std::logic_error("GraphFromAdjMatrix: this method (constructor) is + * deleted for std::string.")`. + */ + static Graph GraphFromAdjMatrix( + const std::vector>& adj_matrix, + bool is_weighted = false) { + if constexpr (std::is_arithmetic_v) { + if (adj_matrix.empty()) return Graph(); + + std::vector edges{}; + + if (adj_matrix.size() != adj_matrix[0].size()) + throw std::invalid_argument( + "GraphFromAdjMatrix: AdjacencyMatrix is not squared."); + + for (size_t row = 0; row < adj_matrix.size(); row++) { + if (row != 0) + if (adj_matrix[row].size() != adj_matrix[row - 1].size()) + throw std::invalid_argument( + "GraphFromAdjMatrix: AdjacencyMatrix is not squared [row " + "problem]."); + + for (size_t col = 0; col < adj_matrix[row].size(); col++) { + if (adj_matrix[row][col] == 0) continue; + + if (is_weighted) + edges.push_back(Edge(col, row, adj_matrix[col][row])); + else + edges.push_back(Edge(col, row)); + } + } + + return Graph(edges); + + } else if constexpr (std::is_same_v) + throw std::logic_error( + "GraphFromAdjMatrix: this method (constructor) " + "is deleted for std::string."); + } + + /** + * @brief Создает новый экземпляр Graph. + * по списку смежности (НЕВЗВЕШЕННЫЙ). + * + * @param adj_list: список смежности. + * + * @return `Graph`: новый экземпляр Graph. + * + * @throw std::logic_error("GraphFromAdjList: this method (constructor) is + * deleted for std::string."). + */ + static Graph GraphFromAdjList( + const std::vector>& adj_list) { + if constexpr (std::is_arithmetic_v) { + if (adj_list.empty()) return Graph(); + + std::vector edges{}; + + for (size_t row = 0; row < adj_list.size(); row++) + for (size_t col = 0; col < adj_list[row].size(); col++) + edges.push_back(Edge(row, adj_list[row][col])); + + return Graph(edges); + + } else if constexpr (std::is_same_v) + throw std::logic_error( + "GraphFromAdjList: this method (constructor) " + "is deleted for std::string."); + } + + /** + * @brief Создает новый экземпляр Graph. + * по списку смежности с указанием вершины-ключа (НЕВЗВЕШЕННЫЙ). + * + * @param adj_list_dict: список смежности с указанием вершины-ключа. + * + * @return `Graph`: новый экземпляр Graph. + */ + static Graph GraphFromAdjList( + const std::unordered_map>& adj_list_dict) { + if (adj_list_dict.empty()) return Graph(); + + std::vector edges{}; + + for (const auto& vert_str_pair : adj_list_dict) { + auto vert = vert_str_pair.first; + for (const auto& vert_2 : vert_str_pair.second) + edges.push_back(Edge(vert, vert_2)); + } + + return Graph(edges); + } + + /// @brief Проверяет, взвешен ли граф. + bool IsWeighted() const { return is_weighted_; } + + /// @return `size_t`: кол-во вершин. + size_t VertsAmount() const { return verts_.size(); } + + /// @return `vert_t`: кол-во ребер. + size_t EdgesAmount() const { return edges_.size(); } + + /// @return `const std::vector&`: вершины. + const std::vector& Verts() const { return verts_; } + + /// @return `std::vector>`: ребра. + std::vector> Edges() const { + if (edges_.empty()) return {}; + + std::vector> edges_tuples( + edges_.size()); + std::transform(edges_.begin(), edges_.end(), edges_tuples.begin(), + [](const Edge& edge) { + return std::make_tuple(edge.StartVert(), edge.EndVert(), + edge.Weight()); + }); + + return edges_tuples; + } + + /** + * @brief Выводит в поток список вершин. + * + * @param os: входной поток. + * + * @return std::ostream&: выходной поток. + */ + std::ostream& PrintVerts(std::ostream& os = std::cout) const { + os << Verts(); + return os; + } + + /** + * @brief Выводит в поток список ребер. + * + * @param os: входной поток. + * + * @return `std::ostream&`: выходной поток. + */ + std::ostream& PrintEdges(std::ostream& os = std::cout) const { + os << edges_; + return os; + } + + /** + * @brief Выводит в поток список смежности. + * + * @param os: входной поток. + * + * @return `std::ostream&`: выходной поток. + */ + std::ostream& PrintAdjList(std::ostream& os = std::cout) const { + for (const auto& vert : Verts()) { + os << vert << ": "; + + for (const auto& neighbor : edges_) { + if (neighbor.StartVert() == vert) os << neighbor.EndVert() << "; "; + if (!IsDirected()) + if (neighbor.EndVert() == vert) os << neighbor.StartVert() << "; "; + } + + os << "\n"; + } + + return os; + } + + /// @brief Делает граф ненаправленным (удаляет лишние ребра). + void MakeUndirected() { + std::unordered_set seen_edges; + std::vector unique_edges; + unique_edges.reserve(EdgesAmount()); + + for (size_t i = 0; i < EdgesAmount(); i++) { + if (seen_edges.count(i) != 0) continue; + + for (size_t j = i + 1; j < EdgesAmount(); j++) + if (edges_[i].StartVert() == edges_[j].EndVert() && + edges_[j].StartVert() == edges_[i].EndVert()) { + seen_edges.insert(j); + break; + } + + unique_edges.push_back(edges_[i]); + } + + edges_ = std::move(unique_edges); + is_direct_ = false; + } + + /// @brief Делает граф направленным (ничего). + void MakeDirected() { is_direct_ = true; } + + /// @brief Проверяет, направлен ли граф. + bool IsDirected() const { return is_direct_; } + + /** + * @return `std::vector>`: список смежности. + * @throw `std::logic_error("GetAdjListWithoutKeys: this method is deleted + * for std::string.")`. + */ + std::vector> GetAdjListWithoutKeys() const { + if constexpr (std::is_arithmetic_v) { + std::vector> adj_list( + *std::max_element(Verts().begin(), Verts().end()) + 1); + + for (const auto& edge : edges_) { + adj_list[edge.StartVert()].push_back(edge.EndVert()); + if (!IsDirected()) adj_list[edge.EndVert()].push_back(edge.StartVert()); + } + + return adj_list; + } + + else if constexpr (std::is_same_v) + throw std::logic_error( + "GetAdjListWithoutKeys: this method is deleted for std::string."); + } + + /// @return `std::unordered_map>`: список + /// смежности с указанием вершины-ключа. + std::unordered_map> GetAdjList() const { + auto adj_list_dict = std::unordered_map>(); + + for (const auto& edge : edges_) { + adj_list_dict[edge.StartVert()].push_back(edge.EndVert()); + if (!IsDirected()) + adj_list_dict[edge.EndVert()].push_back(edge.StartVert()); + } + + return adj_list_dict; + } + + /** + * @return `std::vector>`: матрица смежности. + * @throw `std::logic_error("GetAdjMatrix: this method is deleted for + * std::string.")`. + */ + std::vector> GetAdjMatrix() const { + if constexpr (std::is_arithmetic_v) { + std::vector> adj_matrix( + VertsAmount(), std::vector(VertsAmount(), 0)); + + for (const auto& edge : edges_) + if (edge.IsWeighted()) { + adj_matrix[edge.StartVert()][edge.EndVert()] = edge.Weight(); + if (!IsDirected()) + adj_matrix[edge.EndVert()][edge.StartVert()] = edge.Weight(); + } else { + adj_matrix[edge.StartVert()][edge.EndVert()] = 1; + if (!IsDirected()) adj_matrix[edge.EndVert()][edge.StartVert()] = 1; + } + + return adj_matrix; + } + + else if constexpr (std::is_same_v) + throw std::logic_error( + "GetAdjMatrix: this method is deleted for std::string."); + } + + /** + * @brief Проверяет, содержится ли вершина в графе. + * + * @param vert: вершина. + * + * @return `true`: содержится. + * @return `false`: не содержится. + */ + bool ContainsVert(const vert_t& vert) const { + return std::find(Verts().begin(), Verts().end(), vert) != Verts().end(); + } + + /** + * @brief Проверяет, содержится ли ребро в графе (ВЗВЕШЕННЫЙ). + * + * @param edge: ребро. + * + * @return `true`: содержится. + * @return `false`: не содержится. + * + * @throw `std::logic_error("ContainsEdge: graph is not weighted.")`. + */ + bool ContainsEdge(const std::tuple& edge) const { + if (!IsWeighted()) + throw std::logic_error("ContainsEdge: graph is not weighted."); + + return GetEdgeIter_(edge) != edges_.end(); + } + + /** + * @brief Проверяет, содержится ли ребро в графе (НЕВЗВЕШЕННЫЙ). + * + * @param edge: ребро. + * + * @return `true`: содержится. + * @return `false`: не содержится. + */ + bool ContainsEdge(const std::pair& edge) const { + return GetEdgeIter_(edge) != edges_.end(); + } + + /** + * @brief Находит вес ребра в взвешенном графе. + * + * @param edge: ребро. + * + * @return `weight_t`: вес. + * + * @throw `std::logic_error("GetEdgeWeight: graph is not weighted.")`. + * @throw `std::invalid_argument("GetEdgeWeight: there is no such edge:")`. + */ + weight_t GetEdgeWeight(const std::pair& edge) const { + if (!IsWeighted()) + throw std::logic_error("GetEdgeWeight: graph is not weighted."); + + auto it = GetEdgeIter_(edge); + + if (it == edges_.end()) + throw std::invalid_argument("GetEdgeWeight: there is no such edge: " + + Edge(edge).Name()); + + return it->Weight(); + } + + /** + * @brief Меняет вес ребра в взвешенном графе. + * + * @param edge: ребро. + * @param new_weight: вес. + * + * @throw `std::logic_error("SetEdgeWeight: graph is not weighted.")`. + * @throw `std::invalid_argument("SetEdgeWeight: there is no such edge:")`. + */ + void SetEdgeWeight(const std::pair& edge, + weight_t new_weight) { + if (!IsWeighted()) + throw std::logic_error("SetEdgeWeight: graph is not weighted."); + + auto it = GetEdgeIter_(edge); + + if (it == edges_.end()) + throw std::invalid_argument("SetEdgeWeight: there is no such edge: " + + Edge(edge).Name()); + + it->SetWeight(new_weight); + } + + void AddVert(const vert_t& vert) { + if (!Contains(verts_, vert)) verts_.push_back(vert); + } + + /// @warning `"AddEdge: weighted graph must consist of weighted edges.` + void AddEdge(const std::tuple& edge_tuple, + bool ignore_warning = false) { + if (WeightFromTuple(edge_tuple) == 0) + AddEdge({StartVertFromTuple(edge_tuple), EndVertFromTuple(edge_tuple)}, + ignore_warning); + else + AddEdge_(edge_tuple); + } + + /// @warning `"AddEdge: weighted graph must consist of weighted edges.` + void AddEdge(const std::pair& edge_pair, + bool ignore_warning = false) { + if (IsWeighted() && !ignore_warning) + std::cerr << "Warning! AddEdge: weighted graph should consist of " + "weighted edges." + << std::endl; + + AddEdge_(Edge(edge_pair.first, edge_pair.second, static_cast(0))); + } + + /// @throw `std::invalid_argument("RemoveVert: there is no such vert:")`. + void RemoveVert(const vert_t& vert) { + if constexpr (std::is_arithmetic_v) { + if (!Contains(Verts(), vert)) + throw std::invalid_argument( + "RemoveVert: there is no such vert in graph: " + + std::to_string(vert)); + } + + else if constexpr (std::is_same_v) + if (!Contains(Verts(), vert)) + throw std::invalid_argument( + "RemoveVert: there is no such vert in graph: " + vert); + + verts_.erase(std::remove(verts_.begin(), verts_.end(), vert), verts_.end()); + + edges_.erase(std::remove_if(edges_.begin(), edges_.end(), + [vert](const Edge& edge) { + return edge.StartVert() == vert || + edge.EndVert() == vert; + }), + edges_.end()); + } + + /// @throw `std::invalid_argument("RemoveEdge: there is no such edge:")`. + void RemoveEdge(const std::pair& edge_pair) { + if (!ContainsEdge(edge_pair)) + throw std::invalid_argument( + "RemoveEdge: there is no such edge in graph: " + + Edge(edge_pair).Name()); + + edges_.erase(std::remove_if(edges_.begin(), edges_.end(), + [&edge_pair, this](const Edge& e) { + return (e == Edge(edge_pair)) || + (!IsDirected() && + Edge(e.EndVert(), e.StartVert()) == + Edge(edge_pair)); + }), + edges_.end()); + } + + /// @throw `std::invalid_argument("RemoveEdge: there is no such edge:")`. + void RemoveEdge(const std::tuple& edge_tuple) { + if (!ContainsEdge(edge_tuple)) + throw std::invalid_argument( + "RemoveEdge: there is no such edge in graph: " + + Edge(edge_tuple).Name()); + + edges_.erase( + std::remove_if( + edges_.begin(), edges_.end(), + [&edge_tuple, this](const Edge& e) { + return (e == Edge(edge_tuple)) || + (!IsDirected() && + e == Edge(std::make_tuple(EndVertFromTuple(edge_tuple), + StartVertFromTuple(edge_tuple), + WeightFromTuple(edge_tuple)))); + }), + edges_.end()); + } + + private: + class Edge { + public: + Edge() = delete; + + Edge(const vert_t start_vert, const vert_t& end_vert) + : start_vert_{start_vert}, end_vert_{end_vert} {} + + Edge(const vert_t& start_vert, vert_t end_vert, weight_t weight) + : start_vert_{start_vert}, end_vert_{end_vert}, weight_{weight} {} + + Edge(const std::pair& edge_pair) + : start_vert_{edge_pair.first}, end_vert_{edge_pair.second} {} + + Edge(const std::pair& edge_pair, weight_t weight) + : start_vert_{edge_pair.first}, + end_vert_{edge_pair.second}, + weight_{weight} {} + + Edge(const std::tuple& edge_tuple) + : start_vert_{StartVertFromTuple(edge_tuple)}, + end_vert_{EndVertFromTuple(edge_tuple)}, + weight_{WeightFromTuple(edge_tuple)} {} + + bool IsWeighted() const { return weight_ != 0; } + + vert_t StartVert() const { return start_vert_; } + vert_t EndVert() const { return end_vert_; } + + weight_t Weight() const { return weight_; } + + void SetWeight(weight_t new_weight) { weight_ = new_weight; } + + bool operator==(const Edge& rhs) const { + if (StartVert() != rhs.StartVert() || EndVert() != rhs.EndVert()) + return false; + + if (IsWeighted() && rhs.IsWeighted()) return Weight() == rhs.Weight(); + + return true; + } + + bool operator!=(const Edge& rhs) const { return !(*this == rhs); } + + auto operator<=>(const Edge& rhs) const { return weight_ <=> rhs.Weight(); } + + const std::string& Name() const { + static std::string name; + + if constexpr (std::is_arithmetic_v) { + if (IsWeighted()) + name = "[" + std::to_string(StartVert()) + "->" + + std::to_string(EndVert()) + + ", w: " + std::to_string(Weight()) + "]"; + else + name = "[" + std::to_string(StartVert()) + "->" + + std::to_string(EndVert()) + "]"; + + // example: "[4->5]" + + } else if constexpr (std::is_same_v) { + if (IsWeighted()) + name = "['" + StartVert() + "'->'" + EndVert() + + "', w: " + std::to_string(Weight()) + "]"; + else + name = "['" + StartVert() + "'->'" + EndVert() + "']"; + + // example: "["Paris"->"Berlin"]" + } + + return name; + } + + private: + vert_t start_vert_; + vert_t end_vert_; + weight_t weight_ = 0; + }; + + std::vector edges_; + std::vector verts_; + + bool is_direct_ = true; + bool is_weighted_ = false; + + public: + friend std::ostream& operator<<(std::ostream& os, + const Graph::Edge& edge) { + os << edge.Name(); + return os; + } + + private: + Graph(const std::vector& edges) { + if (edges.empty()) return; + + for (const auto& edge : edges) { + if (edge.IsWeighted()) is_weighted_ = true; + AddEdge_(edge); + } + + if constexpr (std::is_arithmetic_v) { + // кол-во вершин = максимальная вершина среди ребер, т.е. в этом случае + // происходит заполнение вершин до наибольшей из них в списке ребер + vert_t max_vert = edges[0].StartVert(); + + for (const auto& edge : edges_) { + max_vert = std::max(max_vert, edge.StartVert()); + max_vert = std::max(max_vert, edge.EndVert()); + } + + verts_.resize(max_vert + 1); + std::iota(verts_.begin(), verts_.end(), 0); + + } else if constexpr (std::is_same_v) + for (const auto& edge : edges_) { + if (!Contains(Verts(), edge.StartVert())) + verts_.push_back(edge.StartVert()); + + if (!Contains(Verts(), edge.EndVert())) + verts_.push_back(edge.EndVert()); + } + } + + void AddEdge_(const Edge& edge) { + AddVert(edge.StartVert()); + AddVert(edge.EndVert()); + + if (!Contains(edges_, edge)) edges_.emplace_back(edge); + + if (edge.Weight() != 0) is_weighted_ = true; + } + + static std::pair ParseEdgeString_( + const std::string& edge_str) { + const size_t pos = edge_str.find("->"); + vert_t start_vert; + vert_t end_vert; + + if (pos == std::string::npos) + throw std::invalid_argument("EdgeString: invalid edge string format: " + + edge_str); + + if constexpr (std::is_arithmetic_v) { + try { + start_vert = std::stoul(edge_str.substr(0, pos)); + end_vert = std::stoul(edge_str.substr(pos + 2)); + } + + catch (const std::exception&) { + throw std::invalid_argument( + "EdgeString: invalid edge string format " + "(vertices should be numbers): " + + edge_str); + } + } + + else if constexpr (std::is_same_v) { + start_vert = edge_str.substr(0, pos); + end_vert = edge_str.substr(pos + 2); + } + + return {start_vert, end_vert}; + } + + auto GetEdgeIter_(const std::pair& edge) const { + auto [start_vert, end_vert] = edge; + + return std::find_if( + edges_.begin(), edges_.end(), + [start_vert, end_vert, this](const auto& e) { + return (e.StartVert() == start_vert && e.EndVert() == end_vert) || + (!IsDirected() && e.StartVert() == end_vert && + e.EndVert() == start_vert); + }); + } + + auto GetEdgeIter_(const std::pair& edge) { + auto [start_vert, end_vert] = edge; + + return std::find_if( + edges_.begin(), edges_.end(), + [start_vert, end_vert, this](const auto& e) { + return (e.StartVert() == start_vert && e.EndVert() == end_vert) || + (!IsDirected() && e.StartVert() == end_vert && + e.EndVert() == start_vert); + }); + } + + auto GetEdgeIter_(const std::tuple& edge) const { + auto [start_vert, end_vert, weight] = edge; + + return std::find_if( + edges_.begin(), edges_.end(), + [start_vert, end_vert, weight, this](const auto& e) { + return (e.StartVert() == start_vert && e.EndVert() == end_vert && + e.Weight() == weight) || + (!IsDirected() && e.StartVert() == end_vert && + e.EndVert() == start_vert && e.Weight() == weight); + }); + } +}; + +// MARK: operator<< + +template +inline std::ostream& operator<<(std::ostream& os, + const Graph& graph) { + os << "Edges:\n "; + graph.PrintEdges(os); + + os << "\n"; + + os << "Vertices:\n "; + graph.PrintVerts(os); + return os; +} diff --git a/lib/src/util.cpp b/lib/src/util.cpp deleted file mode 100644 index 81e15bd..0000000 --- a/lib/src/util.cpp +++ /dev/null @@ -1 +0,0 @@ -#include "util.hpp" diff --git a/lib/src/util.hpp b/lib/src/util.hpp deleted file mode 100644 index e69de29..0000000 diff --git a/lib/src/utils.hpp b/lib/src/utils.hpp new file mode 100644 index 0000000..59853a4 --- /dev/null +++ b/lib/src/utils.hpp @@ -0,0 +1,179 @@ +#pragma once + +// std libs: +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using std::size_t; + +/** + * @brief Выводит все элементы пары в поток + * @tparam Type1: тип, возможный к выводу в консоль + * @tparam Type2: тип, возможный к выводу в консоль + * @param os: ссылка на поток, в который надо вывести (мод.) + * @param pair: пара элементов произвольного типа + * @return std::ostream&: ссылка на поток, в который вывели + */ +template +inline std::ostream& operator<<(std::ostream& os, + const std::pair& pair) { + return os << "{" << pair.first << "; " << pair.second << "}"; +} + +/** + * @brief Выводит все элементы std::tuple в поток + * @tparam I: текущий индекс, обрабатываемый в кортеже + * @tparam Ts: типы элементов в кортеже + * @param os: выходной поток, в который будут записаны элементы кортежа + * @param t: кортеж, который нужно распечатать + * @return std::ostream&: модифицированный выходной поток + */ +template +static std::ostream& PrintTuple(std::ostream& os, const std::tuple& t) { + if constexpr (I < sizeof...(Ts)) { + if (I != 0) os << "; "; + + os << std::get(t); + return PrintTuple(os, t); + } else + return os; +} + +/** + * @brief Выводит все элементы std::tuple в поток + * @tparam Ts: типы элементов в кортеже. + * @param os: выходной поток, в который будет записан кортеж. + * @param t: кортеж, который нужно распечатать. + * @return std::ostream&: модифицированный выходной поток + */ +template +std::ostream& operator<<(std::ostream& os, const std::tuple& t) { + os << "{"; + PrintTuple(os, t); + return os << "}"; +} + +/** + * @brief Выводит все элементы вектора в поток + * @tparam Type: тип, возможный к выводу в консоль + * @param os: ссылка на поток, в который надо вывести (мод.) + * @param vec: вектор элементов произвольного типа + * @return std::ostream&: ссылка на поток, в который вывели + */ +template +inline std::ostream& operator<<(std::ostream& os, + const std::vector& vec) { + os << "{"; + + for (std::size_t i = 0; i < vec.size(); i++) { + os << vec[i]; + if (i != vec.size() - 1) os << "; "; + } + + return os << "}"; +} + +/** + * @brief Выводит все элементы std::unordered_map в выходной поток + * @tparam K: тип ключей в неупорядоченной карте + * @tparam V: тип значений в неупорядоченной карте + * @param os: выходной поток, в который будет записан словарь + * @param map: словарь, который нужно распечатать + * @return std::ostream&: модифицированный выходной поток + */ +template +std::ostream& operator<<(std::ostream& os, + const std::unordered_map& map) { + os << "{"; + + bool first = true; + for (const auto& [key, value] : map) { + if (!first) os << "; "; + + os << key << ": " << value; + first = false; + } + + return os << "}"; +} + +/** + * @brief функция, которая обрезает незнач. нули float при преобр. в строку + * @param number: число типа float + * @return std::string: итоговое число, записанное в строку + */ +inline std::string ErasedZerosStr(float number) { + std::string str = std::to_string(number); + + // удаляем незначащие нули + str.erase(str.find_last_not_of('0') + 1, std::string::npos); + + // если последний символ - десятичная точка, удаляем + if (str.back() == '.') str.pop_back(); + + return str; +} + +/** + * @brief перегрузка, которая вводит все элементы вектора из потока + * (работает исключительно с консолью, так как + * (вывод о текущем состоянии происходит туда) + * @tparam Type: тип, возможный к выводу в консоль + * @param is: ссылка на поток, из которого надо ввести (мод.) + * @param vec: вектор элементов типа Type (мод.) + * @return std::istream&: ссылка на поток, из которого ввели + */ +template +inline std::istream& operator>>(std::istream& is, std::vector& vec) { + // @brief размер вектора + long size = 0; + + std::cout << "Enter array size: "; + while (size <= 0) { + is >> size; + if (!is) { + std::cerr << "Invalid size input." << std::endl; + return is; + } + + if (size <= 0) std::cout << "Invalid size input. Try again: "; + } + + // @brief текущий элемент + Type curr; + + vec.clear(); // (для перезаписи нужна отчистка) + std::cout << "Enter array elements: "; + for (std::size_t i = 0; i < std::size_t(size); i++) { + is >> curr; + if (!is) { + std::cerr << "Invalid array input. The entry is incorrect." << std::endl; + return is; + } + + vec.push_back(curr); + } + + return is; +} + +/** + * @brief Проверяет наличие элемента в векторе + * @tparam T: тип элемента + * @param vec: исходный вектор + * @param value: искомое значение + * @return true: элемент найден + * @return false: элемент не найден + */ +template +inline bool Contains(const std::vector& vec, const T& value) { + return std::find(vec.begin(), vec.end(), value) != vec.end(); +} diff --git a/sandbox/CMakeLists.txt b/sandbox/CMakeLists.txt index 8bf8f4b..0838c39 100644 --- a/sandbox/CMakeLists.txt +++ b/sandbox/CMakeLists.txt @@ -1,15 +1,13 @@ -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.20) -project(sendbox) +project(sandbox LANGUAGES CXX) -file(GLOB_RECURSE tasks_dirs LIST_DIRECTORIES true ".") +file(GLOB_RECURSE SUBFOLDERS LIST_DIRECTORIES true ".") -foreach(dir ${tasks_dirs}) - IF(IS_DIRECTORY ${dir}) - IF(NOT ${dir} MATCHES ".*src.*") - add_subdirectory(${dir}) - ENDIF() - ELSE() - CONTINUE() - ENDIF() +foreach(FOLDER ${SUBFOLDERS}) + if(IS_DIRECTORY ${FOLDER}) + if(NOT ${FOLDER} MATCHES ".*src.*") + add_subdirectory(${FOLDER}) + endif() + endif() endforeach() diff --git a/sandbox/template/CMakeLists.txt b/sandbox/template/CMakeLists.txt index f7ef8a9..7e3bf1f 100644 --- a/sandbox/template/CMakeLists.txt +++ b/sandbox/template/CMakeLists.txt @@ -1,35 +1,34 @@ -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.20) get_filename_component(PROJECT_NAME ${CMAKE_CURRENT_LIST_DIR} NAME) string(REPLACE " " "_" PROJECT_NAME ${PROJECT_NAME}) -project(${PROJECT_NAME} C CXX) +project(${PROJECT_NAME} LANGUAGES CXX) set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) -file(GLOB_RECURSE source_list "src/*.cpp" "src/*.hpp") -file(GLOB_RECURSE main_source_list "src/main.cpp") -file(GLOB_RECURSE test_source_list "src/*.cpp") +file(GLOB_RECURSE SOURCE_LIST "src/*.cpp" "src/*.hpp") +file(GLOB_RECURSE MAIN_SOURCE_LIST "src/main.cpp") +set(TEST_SOURCE_LIST ${SOURCE_LIST}) file(GLOB_RECURSE test_list "src/*test.cpp") -list(REMOVE_ITEM test_source_list ${main_source_list}) -list(REMOVE_ITEM source_list ${test_list}) +list(REMOVE_ITEM TEST_SOURCE_LIST ${MAIN_SOURCE_LIST}) +list(REMOVE_ITEM SOURCE_LIST ${test_list}) include_directories(${PROJECT_NAME} PUBLIC src) -add_executable(${PROJECT_NAME} ${source_list}) +find_library(Utils ../) + +add_executable(${PROJECT_NAME} ${SOURCE_LIST}) +target_link_libraries(${PROJECT_NAME} PUBLIC Utils) # Locate GTest enable_testing() find_package(GTest REQUIRED) include_directories(${GTEST_INCLUDE_DIRS}) -find_library(Utils ../) -target_link_libraries(${PROJECT_NAME} PUBLIC Utils) - # Link runTests with what we want to test and the GTest and pthread library -add_executable(${PROJECT_NAME}_tests ${test_source_list}) - +add_executable(${PROJECT_NAME}_tests ${TEST_SOURCE_LIST}) target_link_libraries( ${PROJECT_NAME}_tests GTest::gtest_main diff --git a/task_01/CMakeLists.txt b/task_01/CMakeLists.txt index 6612735..7e3bf1f 100644 --- a/task_01/CMakeLists.txt +++ b/task_01/CMakeLists.txt @@ -1,35 +1,34 @@ -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.20) get_filename_component(PROJECT_NAME ${CMAKE_CURRENT_LIST_DIR} NAME) string(REPLACE " " "_" PROJECT_NAME ${PROJECT_NAME}) -project(${PROJECT_NAME} C CXX) +project(${PROJECT_NAME} LANGUAGES CXX) set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) -file(GLOB_RECURSE source_list "src/*.cpp" "src/*.hpp") -file(GLOB_RECURSE main_source_list "src/main.cpp") -file(GLOB_RECURSE test_source_list "src/*.cpp") +file(GLOB_RECURSE SOURCE_LIST "src/*.cpp" "src/*.hpp") +file(GLOB_RECURSE MAIN_SOURCE_LIST "src/main.cpp") +set(TEST_SOURCE_LIST ${SOURCE_LIST}) file(GLOB_RECURSE test_list "src/*test.cpp") -list(REMOVE_ITEM test_source_list ${main_source_list}) -list(REMOVE_ITEM source_list ${test_list}) +list(REMOVE_ITEM TEST_SOURCE_LIST ${MAIN_SOURCE_LIST}) +list(REMOVE_ITEM SOURCE_LIST ${test_list}) include_directories(${PROJECT_NAME} PUBLIC src) -add_executable(${PROJECT_NAME} ${source_list}) +find_library(Utils ../) + +add_executable(${PROJECT_NAME} ${SOURCE_LIST}) +target_link_libraries(${PROJECT_NAME} PUBLIC Utils) # Locate GTest enable_testing() find_package(GTest REQUIRED) - include_directories(${GTEST_INCLUDE_DIRS}) -find_library(Utils ../) -target_link_libraries(${PROJECT_NAME} PUBLIC Utils) - # Link runTests with what we want to test and the GTest and pthread library -add_executable(${PROJECT_NAME}_tests ${test_source_list}) +add_executable(${PROJECT_NAME}_tests ${TEST_SOURCE_LIST}) target_link_libraries( ${PROJECT_NAME}_tests GTest::gtest_main diff --git a/task_02/CMakeLists.txt b/task_02/CMakeLists.txt index 7c1cb30..7e3bf1f 100644 --- a/task_02/CMakeLists.txt +++ b/task_02/CMakeLists.txt @@ -1,34 +1,34 @@ -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.20) get_filename_component(PROJECT_NAME ${CMAKE_CURRENT_LIST_DIR} NAME) string(REPLACE " " "_" PROJECT_NAME ${PROJECT_NAME}) -project(${PROJECT_NAME} C CXX) +project(${PROJECT_NAME} LANGUAGES CXX) set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) -file(GLOB_RECURSE source_list "src/*.cpp" "src/*.hpp") -file(GLOB_RECURSE main_source_list "src/main.cpp") -file(GLOB_RECURSE test_source_list "src/*.cpp") +file(GLOB_RECURSE SOURCE_LIST "src/*.cpp" "src/*.hpp") +file(GLOB_RECURSE MAIN_SOURCE_LIST "src/main.cpp") +set(TEST_SOURCE_LIST ${SOURCE_LIST}) file(GLOB_RECURSE test_list "src/*test.cpp") -list(REMOVE_ITEM test_source_list ${main_source_list}) -list(REMOVE_ITEM source_list ${test_list}) +list(REMOVE_ITEM TEST_SOURCE_LIST ${MAIN_SOURCE_LIST}) +list(REMOVE_ITEM SOURCE_LIST ${test_list}) include_directories(${PROJECT_NAME} PUBLIC src) -add_executable(${PROJECT_NAME} ${source_list}) +find_library(Utils ../) + +add_executable(${PROJECT_NAME} ${SOURCE_LIST}) +target_link_libraries(${PROJECT_NAME} PUBLIC Utils) # Locate GTest enable_testing() find_package(GTest REQUIRED) include_directories(${GTEST_INCLUDE_DIRS}) -find_library(Utils ../) -target_link_libraries(${PROJECT_NAME} PUBLIC Utils) - # Link runTests with what we want to test and the GTest and pthread library -add_executable(${PROJECT_NAME}_tests ${test_source_list}) +add_executable(${PROJECT_NAME}_tests ${TEST_SOURCE_LIST}) target_link_libraries( ${PROJECT_NAME}_tests GTest::gtest_main diff --git a/task_03/CMakeLists.txt b/task_03/CMakeLists.txt index 7c1cb30..7e3bf1f 100644 --- a/task_03/CMakeLists.txt +++ b/task_03/CMakeLists.txt @@ -1,34 +1,34 @@ -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.20) get_filename_component(PROJECT_NAME ${CMAKE_CURRENT_LIST_DIR} NAME) string(REPLACE " " "_" PROJECT_NAME ${PROJECT_NAME}) -project(${PROJECT_NAME} C CXX) +project(${PROJECT_NAME} LANGUAGES CXX) set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) -file(GLOB_RECURSE source_list "src/*.cpp" "src/*.hpp") -file(GLOB_RECURSE main_source_list "src/main.cpp") -file(GLOB_RECURSE test_source_list "src/*.cpp") +file(GLOB_RECURSE SOURCE_LIST "src/*.cpp" "src/*.hpp") +file(GLOB_RECURSE MAIN_SOURCE_LIST "src/main.cpp") +set(TEST_SOURCE_LIST ${SOURCE_LIST}) file(GLOB_RECURSE test_list "src/*test.cpp") -list(REMOVE_ITEM test_source_list ${main_source_list}) -list(REMOVE_ITEM source_list ${test_list}) +list(REMOVE_ITEM TEST_SOURCE_LIST ${MAIN_SOURCE_LIST}) +list(REMOVE_ITEM SOURCE_LIST ${test_list}) include_directories(${PROJECT_NAME} PUBLIC src) -add_executable(${PROJECT_NAME} ${source_list}) +find_library(Utils ../) + +add_executable(${PROJECT_NAME} ${SOURCE_LIST}) +target_link_libraries(${PROJECT_NAME} PUBLIC Utils) # Locate GTest enable_testing() find_package(GTest REQUIRED) include_directories(${GTEST_INCLUDE_DIRS}) -find_library(Utils ../) -target_link_libraries(${PROJECT_NAME} PUBLIC Utils) - # Link runTests with what we want to test and the GTest and pthread library -add_executable(${PROJECT_NAME}_tests ${test_source_list}) +add_executable(${PROJECT_NAME}_tests ${TEST_SOURCE_LIST}) target_link_libraries( ${PROJECT_NAME}_tests GTest::gtest_main diff --git a/task_04/CMakeLists.txt b/task_04/CMakeLists.txt index 7c1cb30..7e3bf1f 100644 --- a/task_04/CMakeLists.txt +++ b/task_04/CMakeLists.txt @@ -1,34 +1,34 @@ -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.20) get_filename_component(PROJECT_NAME ${CMAKE_CURRENT_LIST_DIR} NAME) string(REPLACE " " "_" PROJECT_NAME ${PROJECT_NAME}) -project(${PROJECT_NAME} C CXX) +project(${PROJECT_NAME} LANGUAGES CXX) set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) -file(GLOB_RECURSE source_list "src/*.cpp" "src/*.hpp") -file(GLOB_RECURSE main_source_list "src/main.cpp") -file(GLOB_RECURSE test_source_list "src/*.cpp") +file(GLOB_RECURSE SOURCE_LIST "src/*.cpp" "src/*.hpp") +file(GLOB_RECURSE MAIN_SOURCE_LIST "src/main.cpp") +set(TEST_SOURCE_LIST ${SOURCE_LIST}) file(GLOB_RECURSE test_list "src/*test.cpp") -list(REMOVE_ITEM test_source_list ${main_source_list}) -list(REMOVE_ITEM source_list ${test_list}) +list(REMOVE_ITEM TEST_SOURCE_LIST ${MAIN_SOURCE_LIST}) +list(REMOVE_ITEM SOURCE_LIST ${test_list}) include_directories(${PROJECT_NAME} PUBLIC src) -add_executable(${PROJECT_NAME} ${source_list}) +find_library(Utils ../) + +add_executable(${PROJECT_NAME} ${SOURCE_LIST}) +target_link_libraries(${PROJECT_NAME} PUBLIC Utils) # Locate GTest enable_testing() find_package(GTest REQUIRED) include_directories(${GTEST_INCLUDE_DIRS}) -find_library(Utils ../) -target_link_libraries(${PROJECT_NAME} PUBLIC Utils) - # Link runTests with what we want to test and the GTest and pthread library -add_executable(${PROJECT_NAME}_tests ${test_source_list}) +add_executable(${PROJECT_NAME}_tests ${TEST_SOURCE_LIST}) target_link_libraries( ${PROJECT_NAME}_tests GTest::gtest_main diff --git a/task_05/CMakeLists.txt b/task_05/CMakeLists.txt index 7c1cb30..7e3bf1f 100644 --- a/task_05/CMakeLists.txt +++ b/task_05/CMakeLists.txt @@ -1,34 +1,34 @@ -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.20) get_filename_component(PROJECT_NAME ${CMAKE_CURRENT_LIST_DIR} NAME) string(REPLACE " " "_" PROJECT_NAME ${PROJECT_NAME}) -project(${PROJECT_NAME} C CXX) +project(${PROJECT_NAME} LANGUAGES CXX) set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) -file(GLOB_RECURSE source_list "src/*.cpp" "src/*.hpp") -file(GLOB_RECURSE main_source_list "src/main.cpp") -file(GLOB_RECURSE test_source_list "src/*.cpp") +file(GLOB_RECURSE SOURCE_LIST "src/*.cpp" "src/*.hpp") +file(GLOB_RECURSE MAIN_SOURCE_LIST "src/main.cpp") +set(TEST_SOURCE_LIST ${SOURCE_LIST}) file(GLOB_RECURSE test_list "src/*test.cpp") -list(REMOVE_ITEM test_source_list ${main_source_list}) -list(REMOVE_ITEM source_list ${test_list}) +list(REMOVE_ITEM TEST_SOURCE_LIST ${MAIN_SOURCE_LIST}) +list(REMOVE_ITEM SOURCE_LIST ${test_list}) include_directories(${PROJECT_NAME} PUBLIC src) -add_executable(${PROJECT_NAME} ${source_list}) +find_library(Utils ../) + +add_executable(${PROJECT_NAME} ${SOURCE_LIST}) +target_link_libraries(${PROJECT_NAME} PUBLIC Utils) # Locate GTest enable_testing() find_package(GTest REQUIRED) include_directories(${GTEST_INCLUDE_DIRS}) -find_library(Utils ../) -target_link_libraries(${PROJECT_NAME} PUBLIC Utils) - # Link runTests with what we want to test and the GTest and pthread library -add_executable(${PROJECT_NAME}_tests ${test_source_list}) +add_executable(${PROJECT_NAME}_tests ${TEST_SOURCE_LIST}) target_link_libraries( ${PROJECT_NAME}_tests GTest::gtest_main diff --git a/task_06/CMakeLists.txt b/task_06/CMakeLists.txt index 7c1cb30..7e3bf1f 100644 --- a/task_06/CMakeLists.txt +++ b/task_06/CMakeLists.txt @@ -1,34 +1,34 @@ -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.20) get_filename_component(PROJECT_NAME ${CMAKE_CURRENT_LIST_DIR} NAME) string(REPLACE " " "_" PROJECT_NAME ${PROJECT_NAME}) -project(${PROJECT_NAME} C CXX) +project(${PROJECT_NAME} LANGUAGES CXX) set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) -file(GLOB_RECURSE source_list "src/*.cpp" "src/*.hpp") -file(GLOB_RECURSE main_source_list "src/main.cpp") -file(GLOB_RECURSE test_source_list "src/*.cpp") +file(GLOB_RECURSE SOURCE_LIST "src/*.cpp" "src/*.hpp") +file(GLOB_RECURSE MAIN_SOURCE_LIST "src/main.cpp") +set(TEST_SOURCE_LIST ${SOURCE_LIST}) file(GLOB_RECURSE test_list "src/*test.cpp") -list(REMOVE_ITEM test_source_list ${main_source_list}) -list(REMOVE_ITEM source_list ${test_list}) +list(REMOVE_ITEM TEST_SOURCE_LIST ${MAIN_SOURCE_LIST}) +list(REMOVE_ITEM SOURCE_LIST ${test_list}) include_directories(${PROJECT_NAME} PUBLIC src) -add_executable(${PROJECT_NAME} ${source_list}) +find_library(Utils ../) + +add_executable(${PROJECT_NAME} ${SOURCE_LIST}) +target_link_libraries(${PROJECT_NAME} PUBLIC Utils) # Locate GTest enable_testing() find_package(GTest REQUIRED) include_directories(${GTEST_INCLUDE_DIRS}) -find_library(Utils ../) -target_link_libraries(${PROJECT_NAME} PUBLIC Utils) - # Link runTests with what we want to test and the GTest and pthread library -add_executable(${PROJECT_NAME}_tests ${test_source_list}) +add_executable(${PROJECT_NAME}_tests ${TEST_SOURCE_LIST}) target_link_libraries( ${PROJECT_NAME}_tests GTest::gtest_main