diff --git a/.gitignore b/.gitignore index 2d0653a..226bbf0 100644 --- a/.gitignore +++ b/.gitignore @@ -159,4 +159,4 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. .idea/ - +assets/extra/ diff --git a/README.md b/README.md index 155f368..93d0326 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ## Ключевые особенности -- **14** разобранных тем, к каждой теме легкий конспект +- **15+** разобранных тем, к каждой теме легкий конспект - **70+** практических задач с LeetCode, тренировок от Яндекса и реальных собеседований - Решение **к каждой задаче** на лаконичном Python с комментариями - **800+** автоматизированных тестов для проверки решений @@ -26,7 +26,8 @@ | 11 | Теория чисел | Продвинутые подходы | `k_number_theory` | | 12 | 2D Динамическое программирование | Продвинутые подходы | `l_dp2` | | 13 | Деревья | Продвинутые структуры данных | `m_trees` | -| 14 | Графы | Продвинутые структуры данных | `n_trees` | +| 14 | Кучи | Продвинутые структуры данных | `n_heaps` | +| 15 | Графы | Продвинутые структуры данных | `o_trees` | Каждая тема содержит: diff --git a/src/n_heaps/README.md b/src/n_heaps/README.md new file mode 100644 index 0000000..284dcc0 --- /dev/null +++ b/src/n_heaps/README.md @@ -0,0 +1,188 @@ +# Задачи на кучи + +## A. Построение кучи + +| Поле | Значение | +|-----------|---------------------------------------------------| +| Сложность | Легкая | +| Источник | https://stepik.org/lesson/41560/step/1?unit=20013 | + +Переставить элементы заданного массива чисел так, чтобы он удовлетворял свойствам мин-кучи. + +Формат входа. В первой строке задано число N (1 ≤ N ≤ 10^5) — количество элементов в массиве. Во второй +строке задано N целых чисел, разделенных пробелами — элементы массива. Все числа по модулю не превосходят 10^9. + +Формат выхода. В первой строке выведите число S — количество перестановок, которые необходимо сделать, чтобы +превратить массив в мин-кучу. В следующих S строках выведите по одной перестановке в формате "i j", где i и j — индексы +элементов, которые нужно поменять местами. Индексация элементов массива начинается с нуля. + +Пример. +Ввод: + +```text +5 +5 4 3 2 1 +``` + +Вывод: + +```text +3 +1 4 +0 1 +1 3 +``` + +## B. Очередь с приоритетом + +| Поле | Значение | +|-----------|--------------------------------------------------| +| Сложность | Легкая | +| Источник | https://stepik.org/lesson/13240/step/8?unit=3426 | + +Реализуйте структуру данных "очередь с приоритетом" на основе кучи. Ваша очередь должна поддерживать следующие +операции: + +- insert(x) - вставить элемент x в очередь с приоритетом +- extract_max() - извлечь элемент с максимальным приоритетом из очереди с приоритетом и вывести его. Гарантируется, что + очередь с приоритетом не будет пустой в момент вызова операции extract_max(). + +Формат входа. В первой строке число N (1 ≤ N ≤ 10^5) — количество операций. В каждой из следующих N строк записано +описание операции. Операция insert(x) задается строкой "Insert x", где x — целое число, по модулю не превосходящее 10^9. +Операция extract_max() задается строкой "ExtractMax". + +Формат выхода. Для каждой операции extract_max() выведите в отдельной строке число, извлеченное из очереди с +приоритетом. + +Пример. + +Ввод: + +```text +6 +Insert 200 +Insert 10 +ExtractMax +Insert 5 +Insert 500 +ExtractMax +``` + +Вывод: + +```text +200 +500 +``` + +## С. Сортировка массива + +| Поле | Значение | +|-----------|----------------------------------------------------------| +| Сложность | Средняя | +| Источник | https://leetcode.com/problems/sort-an-array/description/ | + +## D. Кодирование строки + +| Поле | Значение | +|-----------|----------------------------------------| +| Сложность | Средняя | +| Источник | https://stepik.org/lesson/13239/step/5 | + +Дана строка из строчных букв латинского алфавита. Закодируйте строку с помощью алгоритма Хаффмана. + +Формат входа. Строка, которую нужно закодировать. Длина строки не превосходит 10^4. + +Формат выхода. В первой строке выведите количество различных букв K, встречающихся в строке, и размер получившейся +закодированной +строки. В следующих K строках запишите коды букв в формате "letter: code". В последней строке выведите закодированную +строку. + +Пример. + +Ввод: + +```text +abacabad +``` + +Вывод: + +```text +4 14 +a: 0 +b: 10 +c: 110 +d: 111 +01001100100111 +``` + +## E. Параллельная обработка + +| Поле | Значение | +|-----------|---------------------------------------------------| +| Сложность | Средняя | +| Источник | https://stepik.org/lesson/41560/step/2?unit=20013 | + +По данным n процессорам и m задачам определите, для каждой из задач, +каким процессором она будет обработана. + +Формат входа. В первой строке заданы два целых числа n и m (1 ≤ n ≤ 10^5, 1 ≤ m ≤ 10^5) — количество процессоров и +задач. Во второй строке задано m целых чисел t_i (0 ≤ t_i ≤ 10^9) — время, необходимое для обработки i-й задачи. + +Формат выхода. Выведите m строк. В i-й строке должно быть указано, каким процессором будет обработана i-я задача и в +какое время она начнет обрабатываться. Процессоры нумеруются от 0 до n-1. Если несколько процессоров свободны в момент +начала обработки задачи, выберите процессор с наименьшим номером. + +Пример 1. + +Ввод: + +```text +2 5 +1 2 3 4 5 +``` + +Вывод: + +```text +0 0 +1 0 +0 1 +1 2 +0 4 +``` + +Пример 2. + +Ввод: + +```text +4 20 +1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 +``` + +Вывод: + +```text +0 0 +1 0 +2 0 +3 0 +0 1 +1 1 +2 1 +3 1 +0 2 +1 2 +2 2 +3 2 +0 3 +1 3 +2 3 +3 3 +0 4 +1 4 +2 4 +3 4 +``` diff --git a/src/n_graphs/__init__.py b/src/n_heaps/__init__.py similarity index 100% rename from src/n_graphs/__init__.py rename to src/n_heaps/__init__.py diff --git a/src/n_heaps/heap_sort.py b/src/n_heaps/heap_sort.py new file mode 100644 index 0000000..139b91b --- /dev/null +++ b/src/n_heaps/heap_sort.py @@ -0,0 +1,45 @@ +# сортировка кучей - O(NlogN) +import heapq + + +def heap_sort(a: list[int]) -> list[int]: + n = len(a) + h: list[int] = [] + for i in range(n): + heapq.heappush(h, a[i]) + return [heapq.heappop(h) for _ in range(n)] + + +# просеивание вниз - O(logN) +def sift_down(a: list[int], size: int, i: int) -> None: + while 2 * i + 1 < size: + # потомки вершины i + left = 2 * i + 1 + right = 2 * i + 2 + # берём максимум из потомков + j = left + if right < size and a[right] > a[left]: + j = right + # если текущий элемент больше или равен потомка, + # значит он располагается правильно - выходим + if a[i] >= a[j]: + break + a[i], a[j] = a[j], a[i] + i = j + + +# построение кучи - O(N) +def build_heap(a: list[int], size: int) -> None: + for i in range(size // 2, -1, -1): + sift_down(a, size, i) + + +# сортировка кучей на месте - O(NlogN) +def heap_sort_inplace(a: list[int]) -> None: + n = len(a) + build_heap(a, n) + size = n + for _i in range(n, 1, -1): + a[size - 1], a[0] = a[0], a[size - 1] + size -= 1 + sift_down(a, size, 0) diff --git a/src/n_heaps/heapify.py b/src/n_heaps/heapify.py new file mode 100644 index 0000000..5ad57e4 --- /dev/null +++ b/src/n_heaps/heapify.py @@ -0,0 +1,21 @@ +def sift_down(a: list[int], size: int, i: int) -> list[tuple[int, int]]: + swaps: list[tuple[int, int]] = [] + while 2 * i + 1 < size: + left = 2 * i + 1 + right = 2 * i + 2 + j = left + if right < size and a[right] < a[left]: + j = right + if a[i] <= a[j]: + break + a[i], a[j] = a[j], a[i] + swaps.append((i, j)) + i = j + return swaps + + +def heapify(size: int, a: list[int]) -> list[tuple[int, int]]: + swaps: list[tuple[int, int]] = [] + for i in range(size // 2, -1, -1): + swaps += sift_down(a, size, i) + return swaps diff --git a/src/n_heaps/huffman.py b/src/n_heaps/huffman.py new file mode 100644 index 0000000..dacf265 --- /dev/null +++ b/src/n_heaps/huffman.py @@ -0,0 +1,66 @@ +import heapq + + +# O(n log n) +def huffman_encode(s: str) -> tuple[str, dict[str, str]]: + result = "" + tree = huffman_tree(s) + for c in s: + result += tree[c] + return result, tree + + +# O(n) +def huffman_decode(s: str, tree: dict[str, str]) -> str: + result = "" + code = "" + for c in s: + code += c + if code in tree: + result += tree[code] + code = "" + return result + + +# O(n log n) +def huffman_tree(s: str) -> dict[str, str]: + # Считаем частоту встречаемости каждого символа в строке. + freq = count_chars(s) + # Создаем кучу: каждый элемент очереди представляет собой кортеж (частота, символ). + queue = [(v, k) for k, v in freq.items()] + heapq.heapify(queue) + # Создаем словарь для хранения кодов Хаффмана. + tree = {c: "" for c in freq} + # Если в строке есть только один символ, то кодируем его как "0". + if len(freq) == 1: + tree[s[0]] = "0" + # Пока в очереди есть хотя бы два узла. + while len(queue) >= 2: + # Извлекаем два узла с минимальной частотой - их нужно закодировать в первую очередь. + left = extract_min(queue) + right = extract_min(queue) + # Добавляем новый узел в очередь: + # его название равно конкатенации названий двух извлеченных узлов (a + b = ab), + # а его частота равна сумме частот двух извлеченных узлов (1 + 2 = 3). + queue.append((left[0] + right[0], left[1] + right[1])) + # Обновляем предков: добавляем "0" к коду левого узла и "1" к коду правого узла. + for ancestor in left[1]: + tree[ancestor] = "0" + tree[ancestor] + for ancestor in right[1]: + tree[ancestor] = "1" + tree[ancestor] + return tree + + +# O(n) +def count_chars(s: str) -> dict[str, int]: + freq = {} + for c in s: + if c not in freq: + freq[c] = 0 + freq[c] += 1 + return freq + + +# O(log n) +def extract_min(queue: list[tuple[int, str]]) -> tuple[int, str]: + return heapq.heappop(queue) diff --git a/src/n_heaps/parallel.py b/src/n_heaps/parallel.py new file mode 100644 index 0000000..73e3c78 --- /dev/null +++ b/src/n_heaps/parallel.py @@ -0,0 +1,14 @@ +import heapq + + +def parallel(n: int, m: int, time: list[int]) -> list[tuple[int, int]]: + result = [] + h: list[tuple[int, int]] = [] + for i in range(n): + heapq.heappush(h, (0, i)) + for i in range(m): + t = time[i] + x = heapq.heappop(h) + result.append((x[1], x[0])) + heapq.heappush(h, (x[0] + t, x[1])) + return result diff --git a/src/n_heaps/priority_queue.py b/src/n_heaps/priority_queue.py new file mode 100644 index 0000000..51eb75c --- /dev/null +++ b/src/n_heaps/priority_queue.py @@ -0,0 +1,59 @@ +""" +Макс-куча +""" + + +class PriorityQueue: + def __init__(self, arr: list[int] | None = None) -> None: + self.arr: list[int] = [] if arr is None else arr + self.size = len(self.arr) + self.heapify() + + def swap(self, i: int, j: int) -> None: + tmp = self.arr[i] + self.arr[i] = self.arr[j] + self.arr[j] = tmp + + # просеивание вверх + def sift_up(self, i: int) -> None: + # (i - 1) // 2 - индекс родителя i-й вершины + while i > 0 and self.arr[i] > self.arr[(i - 1) // 2]: + self.swap(i, (i - 1) // 2) + i = (i - 1) // 2 + + # просеивание вниз + def sift_down(self, i: int) -> None: + while 2 * i + 1 < self.size: + # потомки вершины i + left = 2 * i + 1 + right = 2 * i + 2 + # берём максимум из потомков + j = left + if right < self.size and self.arr[right] > self.arr[left]: + j = right + # если текущий элемент больше или равен потомка, + # значит он располагается правильно - выходим + if self.arr[i] >= self.arr[j]: + break + self.swap(i, j) + i = j + + # превращение массива в кучу + def heapify(self) -> None: + for i in range(self.size // 2 - 1, -1, -1): + self.sift_down(i) + + # вставка + def insert(self, x: int) -> None: + self.arr.append(x) + self.sift_up(self.size) + self.size += 1 + + # извлечение максимума + def extract_max(self) -> int: + mx = self.arr[0] + self.swap(0, self.size - 1) + self.arr.pop() + self.size -= 1 + self.sift_down(0) + return mx diff --git a/src/n_graphs/ABSTRACT.md b/src/o_graphs/ABSTRACT.md similarity index 100% rename from src/n_graphs/ABSTRACT.md rename to src/o_graphs/ABSTRACT.md diff --git a/src/n_graphs/README.md b/src/o_graphs/README.md similarity index 100% rename from src/n_graphs/README.md rename to src/o_graphs/README.md diff --git a/src/o_graphs/__init__.py b/src/o_graphs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/n_graphs/greed_stone.py b/src/o_graphs/greed_stone.py similarity index 100% rename from src/n_graphs/greed_stone.py rename to src/o_graphs/greed_stone.py diff --git a/src/n_graphs/journey_path.py b/src/o_graphs/journey_path.py similarity index 100% rename from src/n_graphs/journey_path.py rename to src/o_graphs/journey_path.py diff --git a/tasks.xlsx b/tasks.xlsx new file mode 100644 index 0000000..1a8df4c Binary files /dev/null and b/tasks.xlsx differ diff --git a/tests/test_graphs/test_greed_stone.py b/tests/test_graphs/test_greed_stone.py index 2019b54..e69a43a 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.n_graphs.greed_stone import greed_stone +from src.o_graphs.greed_stone import greed_stone @pytest.mark.parametrize( diff --git a/tests/test_graphs/test_journey_path.py b/tests/test_graphs/test_journey_path.py index 84401e4..b325e15 100644 --- a/tests/test_graphs/test_journey_path.py +++ b/tests/test_graphs/test_journey_path.py @@ -3,7 +3,7 @@ import pytest -from src.n_graphs.journey_path import Flight, get_route, get_route_enhanced +from src.o_graphs.journey_path import Flight, get_route, get_route_enhanced @pytest.mark.parametrize("func", [get_route, get_route_enhanced]) diff --git a/tests/test_heaps/__init__.py b/tests/test_heaps/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_heaps/test_heap_sort.py b/tests/test_heaps/test_heap_sort.py new file mode 100644 index 0000000..b0fee6f --- /dev/null +++ b/tests/test_heaps/test_heap_sort.py @@ -0,0 +1,35 @@ +from collections.abc import Callable + +import pytest + +from src.n_heaps.heap_sort import heap_sort, heap_sort_inplace + + +@pytest.mark.parametrize( + "func", + [ + heap_sort, + heap_sort_inplace, + ], +) +@pytest.mark.parametrize( + "a", + [ + [1, 2, 3, 4, 5], + [5, 4, 3, 2, 1], + [1, 3, 2, 5, 4], + [1, 1, 1, 1, 1], + [1, 1, 2, 2, 3, 3, 4, 4, 5, 5], + [5, 5, 4, 4, 3, 3, 2, 2, 1, 1], + [1], + ], +) +def test_heap_sort(func: Callable[[list[int]], list[int] | None], a: list[int]) -> None: + a = a.copy() + result = func(a) + expected = sorted(a) + + if result is None: + assert a == expected + else: + assert result == expected diff --git a/tests/test_heaps/test_heapify.py b/tests/test_heaps/test_heapify.py new file mode 100644 index 0000000..855ab10 --- /dev/null +++ b/tests/test_heaps/test_heapify.py @@ -0,0 +1,129 @@ +import pytest + +from src.n_heaps.heapify import heapify + + +def is_min_heap(a: list[int]) -> bool: + return all(a[i] <= a[2 * i + 1] and (2 * i + 2 >= len(a) or a[i] <= a[2 * i + 2]) for i in range(len(a) // 2)) + + +def replay_swaps(original: list[int], swaps: list[tuple[int, int]]) -> list[int]: + a = original[:] + for i, j in swaps: + a[i], a[j] = a[j], a[i] + return a + + +@pytest.mark.parametrize( + ("a", "expected_swaps", "expected_heap"), + [ + ([], [], []), # пустой массив → никаких свапов, результат тот же + ([1], [], [1]), # один элемент → куча уже валидна + ([1, 2], [], [1, 2]), # уже min-heap → изменений нет + ([2, 1], [(0, 1)], [1, 2]), # два элемента в обратном порядке → один свап + ([1, 2, 3], [], [1, 2, 3]), # уже корректная куча + ([3, 2, 1], [(0, 2)], [1, 2, 3]), # выбирается правый ребёнок (он меньше) + ([3, 1, 2], [(0, 1)], [1, 3, 2]), # выбирается левый ребёнок + ([5, 4, 3, 2, 1], [(1, 4), (0, 1), (1, 3)], [1, 2, 3, 5, 4]), # несколько sift_down + ([5, 5, 5], [], [5, 5, 5]), # все элементы равны → свапов нет + ([2, 2, 1], [(0, 2)], [1, 2, 2]), # равные + меньший правый ребёнок + ([0, -1, -2, -3], [(1, 3), (0, 1), (1, 3)], [-3, -1, -2, 0]), # отрицательные числа + ], +) +def test_heapify_exact_cases(a: list[int], expected_swaps: list[tuple[int, int]], expected_heap: list[int]) -> None: + # проверяем точные сценарии: последовательность свапов и финальное состояние + original = a[:] + + swaps = heapify(len(a), a) + + assert swaps == expected_swaps + assert a == expected_heap + assert is_min_heap(a) + assert replay_swaps(original, swaps) == a + + +def test_heapify_ignores_elements_after_size() -> None: + # heapify должен работать только на первых size элементах + a = [5, 4, 3, 2, 1, -100, -200] + + swaps = heapify(5, a) + + assert swaps == [(1, 4), (0, 1), (1, 3)] + assert a == [1, 2, 3, 5, 4, -100, -200] + assert is_min_heap(a[:5]) + assert a[5:] == [-100, -200] + + +def test_heapify_size_smaller_than_zero_swaps_nothing() -> None: + # если size = 0 → функция не должна ничего менять + a = [3, 2, 1] + + swaps = heapify(0, a) + + assert swaps == [] + assert a == [3, 2, 1] + + +def test_heapify_returns_empty_for_already_valid_heap() -> None: + # если массив уже min-heap → никаких свапов не происходит + a = [1, 3, 2, 7, 6, 4, 5] + + swaps = heapify(len(a), a) + + assert swaps == [] + assert a == [1, 3, 2, 7, 6, 4, 5] + assert is_min_heap(a) + + +@pytest.mark.parametrize( + "a", + [ + [7, 6, 5, 4, 3, 2, 1], + [10, 9, 8, 7, 6, 5, 4, 3], + [4, 1, 3, 2, 16, 9, 10, 14, 8, 7], + [3, 1, 1, 0, 2, 2, -1], + ], +) +def test_heapify_general_properties(a) -> None: + # проверяем общие свойства: + # - результат является min-heap + # - элементы не теряются + # - свапы корректно воспроизводят результат + original = a[:] + + swaps = heapify(len(a), a) + + assert is_min_heap(a) + assert sorted(a) == sorted(original) + assert replay_swaps(original, swaps) == a + assert all(0 <= i < len(a) and 0 <= j < len(a) for i, j in swaps) + + +def test_heapify_prefers_right_child_when_right_is_smaller() -> None: + # если правый ребёнок меньше левого → выбирается правый + a = [3, 2, 1] + + swaps = heapify(len(a), a) + + assert swaps == [(0, 2)] + assert a == [1, 2, 3] + + +def test_heapify_prefers_left_child_when_children_are_equal() -> None: + # если дети равны → выбирается левый (по коду) + a = [3, 1, 1] + + swaps = heapify(len(a), a) + + assert swaps == [(0, 1)] + assert a == [1, 3, 1] + + +def test_heapify_stops_when_parent_equals_child() -> None: + # если родитель <= ребёнка → sift_down останавливается + a = [1, 1, 2] + + swaps = heapify(len(a), a) + + assert swaps == [] + assert a == [1, 1, 2] diff --git a/tests/test_heaps/test_huffman.py b/tests/test_heaps/test_huffman.py new file mode 100644 index 0000000..0db8380 --- /dev/null +++ b/tests/test_heaps/test_huffman.py @@ -0,0 +1,53 @@ +import pytest + +from src.n_heaps.huffman import huffman_decode, huffman_encode, huffman_tree + + +# O(n) +def is_prefix_code(tree: dict[str, str]) -> bool: + codes = list(tree.values()) + for i in range(len(codes)): + for j in range(i + 1, len(codes)): + # Если один код является префиксом другого, то это не префиксный код. + if codes[i].startswith(codes[j]) or codes[j].startswith(codes[i]): + return False + return True + + +@pytest.mark.parametrize( + ("tree", "expected"), + [ + ({"a": "0"}, True), + ({"a": "0", "b": "1"}, True), + ({"a": "00", "b": "01", "c": "1"}, True), + ({"a": "00", "b": "01", "c": "10", "d": "11"}, True), + ({"a": "000", "b": "001", "c": "01", "d": "10", "e": "11"}, True), + ({"a": "00", "b": "00"}, False), # 'a' и 'b' имеют одинаковые коды. + ({"a": "0", "b": "01"}, False), # 'a' является префиксом 'b'. + ], +) +def test_is_prefix_code(tree: dict[str, str], expected: bool) -> None: + assert is_prefix_code(tree) == expected + + +@pytest.mark.parametrize( + "string", + ["a", "ab", "abc", "aabbcc", "aabbccdd", "aabbccddeeff", "hello world", "aaaaaabbcccddddeeeee"], +) +def test_huffman_tree(string: str) -> None: + tree = huffman_tree(string) + assert is_prefix_code(tree) + + +@pytest.mark.parametrize( + "string", + ["a", "ab", "abc", "aabbcc", "aabbccdd", "aabbccddeeff", "hello world", "aaaaaabbcccddddeeeee"], +) +def test_huffman_encode_decode(string: str) -> None: + encoded, tree = huffman_encode(string) + + assert len(encoded) <= 8 * len(string) + assert all(char in "01" for char in encoded) + + decode_tree = {v: k for k, v in tree.items()} + assert huffman_decode(encoded, decode_tree) == string diff --git a/tests/test_heaps/test_parallel.py b/tests/test_heaps/test_parallel.py new file mode 100644 index 0000000..39f1476 --- /dev/null +++ b/tests/test_heaps/test_parallel.py @@ -0,0 +1,216 @@ +import pytest + +from src.n_heaps.parallel import parallel + + +def test_parallel_sample_1() -> None: + # пример 1 из условия: разные времена задач, 2 процессора + assert parallel(2, 5, [1, 2, 3, 4, 5]) == [ + (0, 0), + (1, 0), + (0, 1), + (1, 2), + (0, 4), + ] + + +def test_parallel_sample_2() -> None: + # пример 2 из условия: 4 процессора, все задачи длятся 1 + assert parallel(4, 20, [1] * 20) == [ + (0, 0), + (1, 0), + (2, 0), + (3, 0), + (0, 1), + (1, 1), + (2, 1), + (3, 1), + (0, 2), + (1, 2), + (2, 2), + (3, 2), + (0, 3), + (1, 3), + (2, 3), + (3, 3), + (0, 4), + (1, 4), + (2, 4), + (3, 4), + ] + + +def test_parallel_one_processor() -> None: + # один процессор: все задачи выполняются строго последовательно + assert parallel(1, 5, [3, 2, 4, 1, 5]) == [ + (0, 0), + (0, 3), + (0, 5), + (0, 9), + (0, 10), + ] + + +def test_parallel_more_processors_than_tasks() -> None: + # процессоров больше, чем задач: каждая задача стартует в 0 + assert parallel(5, 3, [10, 20, 30]) == [ + (0, 0), + (1, 0), + (2, 0), + ] + + +def test_parallel_processors_equal_tasks() -> None: + # процессоров столько же, сколько задач: все задачи стартуют в 0 + assert parallel(3, 3, [5, 1, 10]) == [ + (0, 0), + (1, 0), + (2, 0), + ] + + +def test_parallel_zero_duration_tasks() -> None: + # задачи длительностью 0 не увеличивают время освобождения процессора + assert parallel(2, 5, [0, 0, 0, 0, 0]) == [ + (0, 0), + (0, 0), + (0, 0), + (0, 0), + (0, 0), + ] + + +def test_parallel_mixed_zero_and_positive_tasks() -> None: + # нулевые задачи сразу освобождают процессор и влияют на выбор по номеру + assert parallel(2, 6, [0, 5, 0, 2, 1, 0]) == [ + (0, 0), + (0, 0), + (1, 0), + (1, 0), + (1, 2), + (1, 3), + ] + + +def test_parallel_tie_choose_smallest_processor_id() -> None: + # если несколько процессоров свободны одновременно, выбирается меньший номер + assert parallel(3, 6, [2, 2, 2, 1, 1, 1]) == [ + (0, 0), + (1, 0), + (2, 0), + (0, 2), + (1, 2), + (2, 2), + ] + + +def test_parallel_tie_after_different_durations() -> None: + # проверяем tie-breaker после задач разной длины + assert parallel(2, 5, [5, 1, 4, 1, 1]) == [ + (0, 0), + (1, 0), + (1, 1), + (0, 5), + (1, 5), + ] + + +def test_parallel_large_values() -> None: + # большие значения времени не должны ломать вычисления + assert parallel(2, 4, [10**9, 10**9, 10**9, 10**9]) == [ + (0, 0), + (1, 0), + (0, 10**9), + (1, 10**9), + ] + + +def test_parallel_empty_time_but_m_zero() -> None: + # формально по условию m >= 1, но функция корректно возвращает пустой ответ при m = 0 + assert parallel(3, 0, []) == [] + + +def test_parallel_uses_only_first_m_tasks() -> None: + # функция обрабатывает только первые m элементов списка time + assert parallel(2, 3, [1, 2, 3, 100, 200]) == [ + (0, 0), + (1, 0), + (0, 1), + ] + + +def test_parallel_raises_if_m_greater_than_time_length() -> None: + # если m больше длины time, текущая реализация падает с IndexError + with pytest.raises(IndexError): + parallel(2, 3, [1, 2]) + + +def test_parallel_raises_if_no_processors() -> None: + # если n = 0, куча пустая, heappop падает с IndexError + with pytest.raises(IndexError): + parallel(0, 1, [10]) + + +@pytest.mark.parametrize( + ("n", "time", "expected"), + [ + ( + 2, + [1, 1, 1, 1], + [(0, 0), (1, 0), (0, 1), (1, 1)], + ), + ( + 3, + [3, 1, 2, 1, 1], + [(0, 0), (1, 0), (2, 0), (1, 1), (1, 2)], + ), + ( + 4, + [2, 4, 1, 3, 5, 2], + [(0, 0), (1, 0), (2, 0), (3, 0), (2, 1), (0, 2)], + ), + ], +) +def test_parallel_exact_cases(n, time, expected) -> None: + # несколько точных сценариев для разных n и длительностей задач + assert parallel(n, len(time), time) == expected + + +@pytest.mark.parametrize( + ("n", "time"), + [ + (1, [1, 2, 3, 4]), + (2, [5, 0, 2, 0, 3]), + (3, [1, 1, 1, 1, 1, 1, 1]), + (4, [10, 1, 7, 3, 0, 2, 8]), + (5, [0, 0, 5, 5, 1, 1, 10]), + ], +) +def test_parallel_general_properties(n, time) -> None: + # общие инварианты: + # - ответ имеет длину m + # - номера процессоров в допустимом диапазоне + # - время старта неотрицательное + # - каждый процессор получает задачи в неубывающем порядке времени старта + result = parallel(n, len(time), time) + + assert len(result) == len(time) + assert all(0 <= processor < n for processor, _ in result) + assert all(start_time >= 0 for _, start_time in result) + + starts_by_processor: dict[int, list[int]] = {} + for processor, start_time in result: + starts_by_processor.setdefault(processor, []).append(start_time) + + for starts in starts_by_processor.values(): + assert starts == sorted(starts) + + +def test_parallel_input_list_is_not_modified() -> None: + # функция не должна изменять список длительностей задач + time = [3, 1, 2, 4] + original = time[:] + + parallel(2, len(time), time) + + assert time == original diff --git a/tests/test_heaps/test_priority_queue.py b/tests/test_heaps/test_priority_queue.py new file mode 100644 index 0000000..a59b65d --- /dev/null +++ b/tests/test_heaps/test_priority_queue.py @@ -0,0 +1,294 @@ +import pytest + +from src.n_heaps.priority_queue import PriorityQueue + + +def is_max_heap(a: list[int]) -> bool: + return all(a[i] >= a[2 * i + 1] and (2 * i + 2 >= len(a) or a[i] >= a[2 * i + 2]) for i in range(len(a) // 2)) + + +def test_init_empty_queue() -> None: + # пустая очередь создаётся без ошибок + pq = PriorityQueue() + + assert pq.arr == [] + assert pq.size == 0 + + +def test_init_heapifies_array() -> None: + # при создании из массива он превращается в max-heap + pq = PriorityQueue([1, 5, 3, 2, 4]) + + assert pq.size == 5 + assert is_max_heap(pq.arr) + assert sorted(pq.arr) == [1, 2, 3, 4, 5] + + +def test_init_keeps_already_valid_heap() -> None: + # уже корректная max-heap остаётся валидной + pq = PriorityQueue([10, 7, 9, 1, 3, 4]) + + assert pq.arr == [10, 7, 9, 1, 3, 4] + assert pq.size == 6 + assert is_max_heap(pq.arr) + + +def test_swap_changes_two_elements() -> None: + # swap меняет местами два элемента + pq = PriorityQueue([3, 2, 1]) + + pq.swap(0, 2) + + assert pq.arr == [1, 2, 3] + + +def test_sift_up_moves_element_to_root() -> None: + # sift_up поднимает большой элемент до корня + pq = PriorityQueue([10, 7, 9]) + pq.arr.append(15) + pq.size += 1 + + pq.sift_up(3) + + assert pq.arr[0] == 15 + assert is_max_heap(pq.arr) + + +def test_sift_up_does_nothing_when_parent_is_larger() -> None: + # если родитель больше, sift_up ничего не меняет + pq = PriorityQueue([10, 7, 9]) + before = pq.arr[:] + + pq.sift_up(1) + + assert pq.arr == before + assert is_max_heap(pq.arr) + + +def test_sift_up_does_nothing_for_root() -> None: + # для корня sift_up ничего не делает + pq = PriorityQueue([10, 7, 9]) + before = pq.arr[:] + + pq.sift_up(0) + + assert pq.arr == before + + +def test_sift_down_moves_small_root_down() -> None: + # sift_down опускает слишком маленький корень вниз + pq = PriorityQueue([10, 9, 8, 7, 6]) + pq.arr[0] = 1 + + pq.sift_down(0) + + assert is_max_heap(pq.arr) + assert pq.arr[0] == 9 + + +def test_sift_down_prefers_right_child_when_right_is_larger() -> None: + # если правый потомок больше левого, выбирается правый + pq = PriorityQueue([10, 9, 8]) + pq.arr = [1, 2, 3] + pq.size = 3 + + pq.sift_down(0) + + assert pq.arr == [3, 2, 1] + assert is_max_heap(pq.arr) + + +def test_sift_down_prefers_left_child_when_children_are_equal() -> None: + # если потомки равны, выбирается левый + pq = PriorityQueue([10, 9, 8]) + pq.arr = [1, 3, 3] + pq.size = 3 + + pq.sift_down(0) + + assert pq.arr == [3, 1, 3] + assert is_max_heap(pq.arr) + + +def test_sift_down_stops_when_parent_is_equal_to_child() -> None: + # если родитель равен максимальному потомку, sift_down останавливается + pq = PriorityQueue([3, 3, 2]) + before = pq.arr[:] + + pq.sift_down(0) + + assert pq.arr == before + assert is_max_heap(pq.arr) + + +def test_insert_into_empty_queue() -> None: + # вставка в пустую очередь + pq = PriorityQueue() + + pq.insert(10) + + assert pq.arr == [10] + assert pq.size == 1 + assert is_max_heap(pq.arr) + + +def test_insert_small_element_stays_leaf() -> None: + # маленький элемент остаётся внизу + pq = PriorityQueue([10, 7, 9]) + + pq.insert(1) + + assert pq.size == 4 + assert sorted(pq.arr) == [1, 7, 9, 10] + assert is_max_heap(pq.arr) + + +def test_insert_large_element_becomes_root() -> None: + # большой элемент поднимается в корень + pq = PriorityQueue([10, 7, 9]) + + pq.insert(100) + + assert pq.arr[0] == 100 + assert pq.size == 4 + assert sorted(pq.arr) == [7, 9, 10, 100] + assert is_max_heap(pq.arr) + + +def test_insert_duplicates() -> None: + # дубликаты корректно хранятся в куче + pq = PriorityQueue([5, 5, 5]) + + pq.insert(5) + + assert pq.size == 4 + assert pq.arr == [5, 5, 5, 5] + assert is_max_heap(pq.arr) + + +def test_insert_negative_numbers() -> None: + # отрицательные числа тоже поддерживаются + pq = PriorityQueue([-10, -20, -30]) + + pq.insert(-5) + + assert pq.arr[0] == -5 + assert pq.size == 4 + assert sorted(pq.arr) == [-30, -20, -10, -5] + assert is_max_heap(pq.arr) + + +def test_extract_max_from_one_element_queue() -> None: + # извлечение единственного элемента + pq = PriorityQueue([42]) + + result = pq.extract_max() + + assert result == 42 + assert pq.arr == [] + assert pq.size == 0 + + +def test_extract_max_returns_largest_element() -> None: + # extract_max возвращает максимум + pq = PriorityQueue([1, 5, 3, 2, 4]) + + result = pq.extract_max() + + assert result == 5 + assert pq.size == 4 + assert sorted(pq.arr) == [1, 2, 3, 4] + assert is_max_heap(pq.arr) + + +def test_extract_max_multiple_times_returns_descending_order() -> None: + # последовательные извлечения возвращают элементы по убыванию + values = [3, 1, 10, 7, 2, 10, -1] + pq = PriorityQueue(values) + + extracted = [pq.extract_max() for _ in range(len(values))] + + assert extracted == sorted(values, reverse=True) + assert pq.arr == [] + assert pq.size == 0 + + +def test_extract_max_with_duplicates() -> None: + # одинаковые максимумы извлекаются корректно + pq = PriorityQueue([5, 1, 5, 3]) + + assert pq.extract_max() == 5 + assert pq.extract_max() == 5 + assert pq.extract_max() == 3 + assert pq.extract_max() == 1 + assert pq.arr == [] + assert pq.size == 0 + + +def test_extract_max_with_negative_numbers() -> None: + # максимум среди отрицательных чисел — число ближе к нулю + pq = PriorityQueue([-10, -1, -5, -20]) + + result = pq.extract_max() + + assert result == -1 + assert pq.size == 3 + assert sorted(pq.arr) == [-20, -10, -5] + assert is_max_heap(pq.arr) + + +def test_extract_max_from_empty_queue_raises_index_error() -> None: + # текущая реализация на пустой очереди падает с IndexError + pq = PriorityQueue() + + with pytest.raises(IndexError): + pq.extract_max() + + +def test_heapify_empty_array() -> None: + # heapify для пустого массива ничего не делает + pq = PriorityQueue() + + pq.heapify() + + assert pq.arr == [] + assert pq.size == 0 + + +def test_heapify_single_element() -> None: + # heapify для одного элемента ничего не меняет + pq = PriorityQueue([1]) + + pq.heapify() + + assert pq.arr == [1] + assert pq.size == 1 + assert is_max_heap(pq.arr) + + +@pytest.mark.parametrize( + "values", + [ + [1, 2, 3, 4, 5], # возрастающий массив + [5, 4, 3, 2, 1], # убывающий массив + [4, 1, 3, 2, 16, 9, 10], # смешанный массив + [0, -1, -2, -3, -4], # отрицательные числа + [2, 2, 2, 2], # все элементы равны + [7, 1, 7, 3, 7, 2], # дубликаты максимумов + ], +) +def test_priority_queue_general_properties(values: list[int]) -> None: + # общие свойства очереди: + # - после heapify структура является max-heap + # - все элементы сохранены + # - extract_max возвращает элементы по убыванию + pq = PriorityQueue(values[:]) + + assert is_max_heap(pq.arr) + assert sorted(pq.arr) == sorted(values) + + extracted = [pq.extract_max() for _ in range(len(values))] + + assert extracted == sorted(values, reverse=True) + assert pq.arr == [] + assert pq.size == 0