From 208bb12a53569a56c3389d4eec725f2d1631c855 Mon Sep 17 00:00:00 2001 From: everysoftware Date: Sat, 21 Jun 2025 21:19:42 +0300 Subject: [PATCH 1/2] feat: add greed stone --- README.md | 3 ++- tests/test_graphs/test_dfs.py | 2 +- tests/test_graphs/test_greed_stone.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b38199e..155f368 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ## Ключевые особенности -- **13** разобранных тем, к каждой теме легкий конспект +- **14** разобранных тем, к каждой теме легкий конспект - **70+** практических задач с LeetCode, тренировок от Яндекса и реальных собеседований - Решение **к каждой задаче** на лаконичном Python с комментариями - **800+** автоматизированных тестов для проверки решений @@ -26,6 +26,7 @@ | 11 | Теория чисел | Продвинутые подходы | `k_number_theory` | | 12 | 2D Динамическое программирование | Продвинутые подходы | `l_dp2` | | 13 | Деревья | Продвинутые структуры данных | `m_trees` | +| 14 | Графы | Продвинутые структуры данных | `n_trees` | Каждая тема содержит: diff --git a/tests/test_graphs/test_dfs.py b/tests/test_graphs/test_dfs.py index 48cdbd1..f23f805 100644 --- a/tests/test_graphs/test_dfs.py +++ b/tests/test_graphs/test_dfs.py @@ -2,7 +2,7 @@ import pytest -from src.o_graphs.dfs import dfs_graph, dfs_iterative +from src.n_graphs.dfs import dfs_graph, dfs_iterative tree = { "A": ["B", "C"], diff --git a/tests/test_graphs/test_greed_stone.py b/tests/test_graphs/test_greed_stone.py index e69a43a..2019b54 100644 --- a/tests/test_graphs/test_greed_stone.py +++ b/tests/test_graphs/test_greed_stone.py @@ -1,6 +1,6 @@ import pytest -from src.o_graphs.greed_stone import greed_stone +from src.n_graphs.greed_stone import greed_stone @pytest.mark.parametrize( From e62a6e16cc538acc2daacd32a5f3fd8984bbc90f Mon Sep 17 00:00:00 2001 From: everysoftware Date: Mon, 14 Jul 2025 16:01:13 +0300 Subject: [PATCH 2/2] feat: add abstract on graphs --- src/n_graphs/ABSTRACT.md | 103 ++++++++++++++++++++++++++++++++++ src/n_graphs/README.md | 50 +++++++++++++++++ src/n_graphs/__init__.py | 0 src/n_graphs/greed_stone.py | 46 +++++++++++++++ tests/test_graphs/test_dfs.py | 44 --------------- 5 files changed, 199 insertions(+), 44 deletions(-) create mode 100644 src/n_graphs/ABSTRACT.md create mode 100644 src/n_graphs/README.md create mode 100644 src/n_graphs/__init__.py create mode 100644 src/n_graphs/greed_stone.py delete mode 100644 tests/test_graphs/test_dfs.py diff --git a/src/n_graphs/ABSTRACT.md b/src/n_graphs/ABSTRACT.md new file mode 100644 index 0000000..2c811fd --- /dev/null +++ b/src/n_graphs/ABSTRACT.md @@ -0,0 +1,103 @@ +# Графы + +## Графы + +**Граф** - это абстрактная структура данных, которая состоит из множества вершин (или узлов) и множества рёбер (или +дуг), соединяющих эти вершины. Графы могут быть ориентированными или неориентированными, взвешенными или невзвешенными. +Графы широко используются в различных областях, таких как компьютерные сети, социальные сети, транспортные системы и +многие другие. + +## Основные понятия + +- **Вершина (узел)** - это базовый элемент графа, который может представлять объект или точку интереса. +- **Ребро (дуга)** - это связь между двумя вершинами, которая может быть направленной или ненаправленной. +- **Ориентированный (направленный) граф** - это граф, в котором рёбра имеют направление, то есть они соединяют вершины в + определённом порядке. +- **Неориентированный (ненаправленный) граф** - это граф, в котором рёбра не имеют направления, то есть связь между + вершинами двусторонняя. +- **Взвешенный граф** - это граф, в котором каждому ребру присвоено значение (вес), которое может представлять + стоимость, расстояние или другую метрику. +- **Невзвешенный граф** - это граф, в котором рёбра не имеют весов, то есть все связи между вершинами равнозначны. +- **Цикл** - это последовательность вершин, в которой первая и последняя вершины совпадают, и каждая пара соседних + вершин соединена ребром. +- **Путь** - это последовательность вершин, в которой каждая пара соседних вершин соединена ребром, и первая и последняя + вершины могут быть разными. +- **Связный граф** - это граф, в котором существует путь между любой парой вершин. +- **Компонента связности** - это максимальный связный подграф, то есть подграф, в котором существует путь между любой + парой вершин, и который не может быть расширен добавлением других вершин. +- **Дерево** - это связный ациклический граф, в котором существует ровно один путь между любой парой вершин. +- **Лес** - это множество деревьев, то есть связный ациклический граф, который может состоять из нескольких компонент + связности. +- **Подграф** - это граф, который состоит из части вершин и рёбер исходного графа, то есть подграф может быть получен + путём удаления некоторых вершин и рёбер из исходного графа. +- **Изоморфизм графов** - это отношение между двумя графами, при котором существует взаимно однозначное соответствие + между их вершинами и рёбрами, сохраняющее структуру графов. + +## Представления графов + +Существует несколько способов представления графов в памяти компьютера. Наиболее распространённые из них: + +- **Матрица смежности** - это двумерный массив, где элемент в строке `i` и столбце `j` указывает на наличие или + отсутствие ребра между вершинами `i` и `j`. Если граф взвешенный, то вместо булевого значения хранится вес ребра. +- **Список смежности** - это список, где для каждой вершины хранится список её соседей. Если граф взвешенный, то + вместо списка соседей хранится список пар (сосед, вес). +- **Список рёбер** - это список, где каждое ребро представлено парой вершин (или тройкой из двух вершин и веса), которые + оно соединяет. Этот способ удобен для хранения графов с небольшим количеством рёбер. +- **Объектно-ориентированное представление** - это способ, при котором вершины и рёбра представлены как объекты, + что позволяет использовать методы и свойства для работы с графом. Например, вершина может содержать список своих + соседей, а ребро может содержать информацию о весе. + +Пример ООП-представления графа на Python: + +```python +from __future__ import annotations + +from dataclasses import dataclass, field + + +@dataclass +class Node: + key: str + children: list[Node] = field(default_factory=list) + + def add_child(self, child_node: Node) -> None: + self.children.append(child_node) + + +# Пример графа +node_a = Node('A') +node_a.add_child(Node('B')) +node_a.add_child(Node('C')) +node_a.children[0].add_child(Node('D')) +node_a.children[0].add_child(Node('E')) +node_a.children[1].add_child(Node('F')) + +# В графе могут быть циклы +node_g = Node('G') +node_a.add_child(node_g) +node_g.add_child(node_a) + +# В графе могут быть изолированные вершины +node_x = Node('X') +``` + +## Обход в глубину + +**Обход в глубину** (Depth-First Search, DFS) - это алгоритм обхода графа, который начинает с заданной вершины и +посещает как можно глубже, прежде чем вернуться назад. Он используется для поиска всех достижимых вершин от +начальной вершины. DFS может быть реализован с помощью стека или рекурсии. + +В отличие от обхода деревьев, обход в глубину графов может посещать вершины несколько раз, если граф содержит циклы. +Более того возможна ситуация, когда DFS не посещает все вершины графа, если граф не связный. + +## Обход в ширину + +**Обход в ширину** (Breadth-First Search, BFS) - это алгоритм обхода графа, который начинает с заданной вершины и +посещает все соседние вершины на текущем уровне, прежде чем перейти к следующему уровню. Он используется для +поиска кратчайшего пути в невзвешенных графах. BFS может быть реализован с помощью очереди. + +## Алгоритм Дейкстры + +**Алгоритм Дейкстры** - это алгоритм для поиска кратчайшего пути от одной вершины до всех остальных вершин во +взвешенном графе с неотрицательными весами рёбер. Он работает, выбирая вершину с наименьшим расстоянием от начальной +вершины и обновляя расстояния до её соседей. Алгоритм повторяется, пока не будут обработаны все вершины. diff --git a/src/n_graphs/README.md b/src/n_graphs/README.md new file mode 100644 index 0000000..6d2d804 --- /dev/null +++ b/src/n_graphs/README.md @@ -0,0 +1,50 @@ +# Задачи по графам + +## A. Камень жадности + +| Поле | Значение | +|-----------|-----------------------------------------| +| Сложность | Сложная | +| Источник | https://stepik.org/lesson/287380/step/1 | + +В одной из мультивселенных могущественный титан Фанос собрал все камни конечности, победил храбрую команду +Блюстителей и поверг мир во тьму. Путешествуя по благодарному миру, Фанос забрел в портовый город у границы +с одной из людских колоний, называемой Р. Местные жители поведали герою легенду о самом большом богатстве +во вселенной – седьмом камне конечности, называемом камень жадности. По слухам, камень открывался только +тому, кто посетит все поселения Р, побывав в каждом ровно один раз (кроме того, в котором начинается +путешествие - в него нужно вернуться) и заплатив при этом самую низкую цену (да, путешествия по Р тоже +имеют свою цену). + +Фанос узнал, что на территории Р работает только один из камней конечности - камень времени. И работает +он специфическим образом – переносит своего владельца в последний момент времени, когда обладатель находился +вне территории Р. Более того, энергия камня жадности настолько сильна, что как только Фанос попадает в поселения +Р, **каждое свое перемещение между поселениями он делает максимально дешевым из возможных**. Однако, находясь +вне Р, Фанос все еще может использовать камень пространства, чтобы мгновенно очутиться в любом из поселений. + +После столь длинной истории наша задача проста – вычислить самый дешевый путь между поселениями Р, +который удалось найти Фаносу. + +Формат входа. Входные данные содержат общее количество поселений n и стоимость путешествия между любой +парой поселений в виде матрицы. Поселения нумеруются от 0 до n-1. + +Формат выхода. В качестве результата необходимо вывести самый короткий путь между поселениями, который удалось +найти Фаносу, а также его стоимость. Если пути не существует, вывести "Lost". + +Пример. + +Ввод: + +``` +4 +0 1 2 3 +4 0 5 6 +7 8 0 9 +10 11 12 0 +``` + +Вывод: + +``` +25 +0 1 2 3 0 +``` diff --git a/src/n_graphs/__init__.py b/src/n_graphs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/n_graphs/greed_stone.py b/src/n_graphs/greed_stone.py new file mode 100644 index 0000000..3d35d7d --- /dev/null +++ b/src/n_graphs/greed_stone.py @@ -0,0 +1,46 @@ +INF = 10**20 + + +# O(n^3) +def greed_stone(matrix: list[list[int]]) -> tuple[int, list[int]] | None: + n = len(matrix) + # Преобразуем матрицу стоимости так, чтобы стоимость перемещения из города в самого себя была равна + # бесконечности. Это помогает нам убедиться, что мы не останемся в том же городе при выборе следующего + # города для посещения. + matrix = [[(x if x != 0 else INF) for x in costs] for costs in matrix] + # Инициализируем минимальную стоимость и путь. + min_cost, min_path = INF, [] + # Для каждого города мы пытаемся найти путь, начинающийся в этом городе, который посещает все города + # и возвращает нас обратно в начальный город. + for start_city in range(n): + # Инициализируем список посещенных городов и путь. + visited = [False] * n + path, cost = [start_city], 0 + # Инициализируем текущий город. + city = start_city + for _ in range(n - 1): + visited[city] = True + # Выбираем следующий город, который еще не был посещен и имеет минимальную стоимость + # перемещения из текущего города. + next_city, score = min( + ((c, matrix[city][c]) for c in range(n) if not visited[c]), + key=lambda x: x[1], + ) + # Добавляем выбранный город в путь и увеличиваем общую стоимость на стоимость перемещения до + # выбранного города. + path.append(next_city) + cost += score + # Обновляем текущий город. + city = next_city + # После посещения всех городов мы возвращаемся в начальный город. + # Увеличиваем общую стоимость на стоимость перемещения обратно в начальный город. + cost += matrix[city][start_city] + path.append(start_city) + # Если общая стоимость пути меньше минимальной стоимости пути, который мы нашли до этого, + # обновляем минимальную стоимость и путь. + if cost < min_cost: + min_cost = cost + min_path = path + if min_cost == INF: + return None + return min_cost, min_path diff --git a/tests/test_graphs/test_dfs.py b/tests/test_graphs/test_dfs.py deleted file mode 100644 index f23f805..0000000 --- a/tests/test_graphs/test_dfs.py +++ /dev/null @@ -1,44 +0,0 @@ -from typing import Any - -import pytest - -from src.n_graphs.dfs import dfs_graph, dfs_iterative - -tree = { - "A": ["B", "C"], - "B": ["D", "E"], - "C": ["F", "G"], - "D": [], - "E": [], - "F": [], - "G": [], -} -""" - A - / | - B C - / | / | - D E F G -""" - - -@pytest.mark.parametrize("impl", [dfs_graph]) -def test_dfs(impl: Any) -> None: - assert impl(tree, "A") == ["A", "B", "D", "E", "C", "F", "G"] - assert impl(tree, "B") == ["B", "D", "E"] - assert impl(tree, "C") == ["C", "F", "G"] - assert impl(tree, "D") == ["D"] - assert impl(tree, "E") == ["E"] - assert impl(tree, "F") == ["F"] - assert impl(tree, "G") == ["G"] - - -@pytest.mark.parametrize("impl", [dfs_iterative]) -def test_dfs_iterative(impl: Any) -> None: - assert impl(tree, "A") == ["A", "C", "G", "F", "B", "E", "D"] - assert impl(tree, "B") == ["B", "E", "D"] - assert impl(tree, "C") == ["C", "G", "F"] - assert impl(tree, "D") == ["D"] - assert impl(tree, "E") == ["E"] - assert impl(tree, "F") == ["F"] - assert impl(tree, "G") == ["G"]