diff --git a/src/dnc/ABSTRACT.md b/src/dnc/ABSTRACT.md new file mode 100644 index 0000000..d86858b --- /dev/null +++ b/src/dnc/ABSTRACT.md @@ -0,0 +1,25 @@ +# Разделяй и властвуй + +## Описание + +**Разделяй и властвуй** (Divide and Conquer) - это метод решения задачи, который заключается в разбиении задачи +на более мелкие подзадачи, а затем объединении результатов решения подзадач в решение исходной +задачи. В общем случае алгоритм "разделяй и властвуй" состоит из трех шагов: + +1. **Разделение**. Задача разбивается на несколько подзадач, которые являются меньшими экземплярами исходной задачи. +2. **Властвование**. Подзадачи решаются рекурсивно. Если подзадачи достаточно малы, то они решаются непосредственно. +3. **Объединение**. Решения подзадач объединяются в решение исходной задачи. + +Важно, что подзадачи должны быть независимыми, то есть решение одной подзадачи не должно зависеть от решения другой. + +**Принцип уменьшай и властвуй** (Decrease and Conquer) - это вариация принципа разделяй и властвуй, в которой задача +переходит в более маленькую задачу. + +## Применение + +Мы уже рассмотрели несколько алгоритмов, которые основаны на принципе разделяй и властвуй: + +- Наибольший общий делитель +- Бинарный поиск +- Сортировка слиянием +- Поиск k-ой порядковой статистики diff --git a/src/dnc/README.md b/src/dnc/README.md index 0e84e2a..8c97a1e 100644 --- a/src/dnc/README.md +++ b/src/dnc/README.md @@ -1,33 +1,15 @@ -## Разделяй и властвуй +# Задачи по "разделяй и властвуй" -### Принцип разделяй и властвуй +## A. Возведение в степень -**Принцип разделяй и властвуй** (Divide and Conquer) - это метод решения задачи, который заключается в разбиении задачи на -более мелкие подзадачи, а затем объединении результатов решения подзадач в решение исходной -задачи. В общем случае алгоритм "разделяй и властвуй" состоит из трех шагов: +| Поле | Значение | +|-----------|-------------------------------------------------| +| Сложность | Средняя | +| Источник | https://leetcode.com/problems/powx-n/solutions/ | -1. **Разделение**. Задача разбивается на несколько подзадач, которые являются меньшими экземплярами исходной задачи. -2. **Властвование**. Подзадачи решаются рекурсивно. Если подзадачи достаточно малы, то они решаются непосредственно. -3. **Объединение**. Решения подзадач объединяются в решение исходной задачи. +## B. Умножение больших чисел -Важно, что подзадачи должны быть независимыми, то есть решение одной подзадачи не должно зависеть от решения другой. - -**Принцип уменьшай и властвуй** (Decrease and Conquer) - это вариация принципа разделяй и властвуй, в которой задача -переходит в более маленькую задачу. - -### Применение - -- Наибольший общий делитель -- Бинарный поиск -- Сортировка слиянием -- Поиск медианы -- Поиск k-ой порядковой статистики -- Быстрое возведение в степень -- Умножение больших чисел - -### Задачи - -| Задача | Код | -|-------------------------|------------| -| Возведение в степень | `power` | -| Умножение больших чисел | `multiply` | \ No newline at end of file +| Поле | Значение | +|-----------|-------------------------------------------------| +| Сложность | Средняя | +| Источник | https://leetcode.com/problems/multiply-strings/ | diff --git a/src/dnc/__init__.py b/src/dnc/__init__.py index 9c24f96..e69de29 100644 --- a/src/dnc/__init__.py +++ b/src/dnc/__init__.py @@ -1,10 +0,0 @@ -from .multiply import multiply_naive, multiply_dnc, karatsuba -from .power import fast_power, power - -__all__ = [ - "multiply_naive", - "multiply_dnc", - "karatsuba", - "fast_power", - "power", -] diff --git a/src/dnc/multiply.py b/src/dnc/multiply.py index 3f74473..ea77966 100644 --- a/src/dnc/multiply.py +++ b/src/dnc/multiply.py @@ -1,119 +1,107 @@ -"""Быстрое умножение целых чисел.""" +# O(n^2), где n - количество бит в максимальном числе +import math -def multiply_naive(x: int, y: int) -> int: - """ - Умножение двух больших чисел методом "в лоб". Сложность O(N^2), где N - количество бит в числе. +def multiply(x: str, y: str) -> str: + return str(mul_karatsuba(int(x), int(y))) + +# O(n^2), где n - количество бит в y +def mul_naive(x: int, y: int) -> int: + """ Принцип работы: y = { - 2 * (y // 2), если y четный - 1 + 2 * (y // 2), если y нечетный + 2 * (y // 2), если y - четное + 1 + 2 * (y // 2), если y - нечетное } x * y = { - 2 * (x * y // 2), если y четный - x + 2 * (x * y // 2), если y нечетный + 2 * (x * y // 2), если y - четное + x + 2 * (x * y // 2), если y - нечетное } """ if y == 0: return 0 - - # Всего будет сделано N = log2(y) рекурсивных вызовов. - z = multiply_naive(x, y >> 1) - - # На каждом вызове будет сделано O(1) операций, если мы имеем дело с небольшими числами, потому что - # используются побитовые операции. Однако, если мы имеем дело с большими числами, которые представляются - # строками, то на каждом вызове будет - # сделано O(N) операций. + # Всего будет сделано n = log2(y) рекурсивных вызовов. + # z = x * y // 2 + z = mul_naive(x, y >> 1) + # Если y - четное, то x * y = 2 * (x * y // 2) = 2 * z if y & 1 == 0: return z << 1 + # Если y - нечетное, то x * y = x + 2 * (x * y // 2) = x + 2 * z else: return x + (z << 1) -def length(x: int) -> int: - """Длина числа в двоичной системе счисления. Сложность O(log(X)).""" - if x <= 1: - return 1 - - result = 0 - while x > 0: - x >>= 1 - result += 1 - - return result - - -def two_halves(x: int, n: int) -> tuple[int, int]: - """Получение левой и правой частей числа. Сложность O(1).""" - half_n = n >> 1 - x_l = x >> half_n - x_r = x & ((1 << half_n) - 1) - - return x_l, x_r - - -def multiply_dnc(x: int, y: int) -> int: +# O(n^2), где n - количество бит в максимальном числе +def mul_dnc(x: int, y: int) -> int: """ - Умножение двух больших чисел методом разделяй и властвуй. Сложность всё ещё O(N^2), где N - количество бит - в числе. - Принцип работы: - Разобьём числа на их левую и правую части: + Разобьём числа на левую и правую части: x = [x_l][x_r] = x_l * 2^(n // 2) + x_r y = [y_l][y_r] = y_l * 2^(n // 2) + y_r x * y = (x_l * 2^(n // 2) + x_r) * (y_l * 2^(n // 2) + y_r) = = 2^n * (x_l * y_l) + 2^(n // 2) * ((x_l * y_r) + (x_r * y_l)) + (x_r * y_r) - Таким образом, задача разделяется на 4 подзадачи: + Задача разделяется на 4 подзадачи: 1) x_l * y_l 2) x_l * y_r 3) x_r * y_l 4) x_r * y_r """ - - n = max(length(x), length(y)) - + n = max(bit_length(x), bit_length(y)) if n <= 16: return x * y - half_n = n >> 1 x_l, x_r = two_halves(x, n) y_l, y_r = two_halves(y, n) - # 2^n * (x_l * y_l) + 2^(n // 2) * ((x_l * y_r) + (x_r * y_l)) + (x_r * y_r) - p1 = multiply_dnc(x_l, y_l) - p2 = multiply_dnc(x_l, y_r) - p3 = multiply_dnc(x_r, y_l) - p4 = multiply_dnc(x_r, y_r) - + p1 = mul_dnc(x_l, y_l) + p2 = mul_dnc(x_l, y_r) + p3 = mul_dnc(x_r, y_l) + p4 = mul_dnc(x_r, y_r) return (p1 << 2 * half_n) + ((p2 + p3) << half_n) + p4 -def karatsuba(x: int, y: int) -> int: +# O(n^log2(3)) = O(n^1.6), где n - количество бит в максимальном числе +def mul_karatsuba(x: int, y: int) -> int: """ - Умножение двух больших чисел методом Карацубы. Сложность O(N^log2(3)) = O(N^1.6), где N - количество - бит в числе. - Принцип работы: Вместо 4 рекурсивных вызовов будем делать 3 вызова: 1) (x_l * y_l) 2) (x_r * y_r) 3) (x_l + x_r) * (y_l + y_r) + Тогда слагаемое (x_l * y_r) + (x_r * y_l) можно выразить через слагаемые 1), 2) и 3): (x_l * y_r + x_r * y_l) = (x_l + x_r)*(y_l + y_r) - (x_l * y_l) - (x_r * y_r) """ - n = max(length(x), length(y)) - + n = max(bit_length(x), bit_length(y)) if n <= 16: return x * y - x_l, x_r = two_halves(x, n) y_l, y_r = two_halves(y, n) - p1 = karatsuba(x_l, y_l) - p2 = karatsuba(x_r, y_r) - p3 = karatsuba(x_l + x_r, y_l + y_r) - + p1 = mul_karatsuba(x_l, y_l) + p2 = mul_karatsuba(x_r, y_r) + p3 = mul_karatsuba(x_l + x_r, y_l + y_r) half_n = n >> 1 - return (p1 << 2 * half_n) + ((p3 - p1 - p2) << half_n) + p2 + + +# O(n), где n - количество бит в числе +def bit_length(x: int) -> int: + # Не делайте так. + # if x <= 1: + # return 1 + # result = 0 + # while x > 0: + # x >>= 1 + # result += 1 + # return result + return math.ceil(math.log2(x + 1)) + + +# O(n), где n - количество бит в числе +def two_halves(x: int, n: int) -> tuple[int, int]: + half_n = n >> 1 + x_l = x >> half_n + x_r = x & ((1 << half_n) - 1) + return x_l, x_r diff --git a/src/dnc/power.py b/src/dnc/power.py index 2878bfd..b4af6f5 100644 --- a/src/dnc/power.py +++ b/src/dnc/power.py @@ -1,21 +1,9 @@ -"""Быстрое возведение числа в степень""" - - -# https://leetcode.com/problems/powx-n/solutions/ -def power(a: int, n: int) -> int: - """Возведение числа в степень. Сложность O(N).""" - result = 1 - - for _ in range(n): - result *= a - - return result - - -def fast_power(a: int, n: int) -> int: - """Быстрое возведение числа в степень. Сложность O(log(N)).""" +# O(log n) +def fast_power(a: float, n: int) -> float: if n == 0: return 1 + if n < 0: + return fast_power(1 / a, -n) elif n % 2 == 1: return a * fast_power(a, n - 1) else: diff --git a/src/intro/README.md b/src/intro/README.md index 2ebc9bf..894e721 100644 --- a/src/intro/README.md +++ b/src/intro/README.md @@ -9,31 +9,6 @@ | Stepik | https://stepik.org/lesson/13228/step/7?unit=3414 | | Похожие | https://leetcode.com/problems/n-th-tribonacci-number/description/ | -Числами Фибоначчи называется последовательность чисел, в которой первое и второе числа равны 1, а каждое следующее число -является суммой двух предыдущих. - -Дано натуральное число N. Вычислите N-е число Фибоначчи. - -Формат ввода. -Вводится натуральное число N (1 <= N <= 100). - -Формат вывода. -Выведите ответ на задачу. - -Пример. - -Ввод: - -```text -10 -``` - -Вывод: - -```text -55 -``` - ## B. Последовательные символы | Поле | Значение | @@ -55,7 +30,6 @@ | Сложность | Легкая | | Источник | https://leetcode.com/problems/third-maximum-number/description/ | - ## E. Линия симметрии | Поле | Значение | diff --git a/tests/test_dnc/test_multiply.py b/tests/test_dnc/test_multiply.py index 27285f2..e0e457a 100644 --- a/tests/test_dnc/test_multiply.py +++ b/tests/test_dnc/test_multiply.py @@ -2,12 +2,12 @@ import pytest -from src.dnc import multiply_dnc, multiply_naive, karatsuba +from src.dnc.multiply import mul_dnc, mul_naive, mul_karatsuba @pytest.mark.parametrize( "func", - [multiply_naive, multiply_dnc, karatsuba], + [mul_naive, mul_dnc, mul_karatsuba], ) @pytest.mark.parametrize( "x, y", diff --git a/tests/test_dnc/test_power.py b/tests/test_dnc/test_power.py index 9e8f467..5c736cd 100644 --- a/tests/test_dnc/test_power.py +++ b/tests/test_dnc/test_power.py @@ -1,13 +1,13 @@ -from typing import Callable +from typing import Any import pytest -from src.dnc import power, fast_power +from src.dnc.power import fast_power @pytest.mark.parametrize( "func", - [power, fast_power], + [fast_power], ) @pytest.mark.parametrize( "x, y", @@ -25,7 +25,9 @@ (10, 100), (100, 10), (123, 987), + (2.1, 3), + (2, -2), ], ) -def test_power(func: Callable[[int, int], int], x: int, y: int) -> None: +def test_power(func: Any, x: float, y: int) -> None: assert func(x, y) == x**y