Skip to content

Commit

Permalink
Refactor dnc
Browse files Browse the repository at this point in the history
  • Loading branch information
everysoftware committed Jan 20, 2025
1 parent 4093d3c commit 83db148
Show file tree
Hide file tree
Showing 8 changed files with 102 additions and 153 deletions.
25 changes: 25 additions & 0 deletions src/dnc/ABSTRACT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Разделяй и властвуй

## Описание

**Разделяй и властвуй** (Divide and Conquer) - это метод решения задачи, который заключается в разбиении задачи
на более мелкие подзадачи, а затем объединении результатов решения подзадач в решение исходной
задачи. В общем случае алгоритм "разделяй и властвуй" состоит из трех шагов:

1. **Разделение**. Задача разбивается на несколько подзадач, которые являются меньшими экземплярами исходной задачи.
2. **Властвование**. Подзадачи решаются рекурсивно. Если подзадачи достаточно малы, то они решаются непосредственно.
3. **Объединение**. Решения подзадач объединяются в решение исходной задачи.

Важно, что подзадачи должны быть независимыми, то есть решение одной подзадачи не должно зависеть от решения другой.

**Принцип уменьшай и властвуй** (Decrease and Conquer) - это вариация принципа разделяй и властвуй, в которой задача
переходит в более маленькую задачу.

## Применение

Мы уже рассмотрели несколько алгоритмов, которые основаны на принципе разделяй и властвуй:

- Наибольший общий делитель
- Бинарный поиск
- Сортировка слиянием
- Поиск k-ой порядковой статистики
40 changes: 11 additions & 29 deletions src/dnc/README.md
Original file line number Diff line number Diff line change
@@ -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` |
| Поле | Значение |
|-----------|-------------------------------------------------|
| Сложность | Средняя |
| Источник | https://leetcode.com/problems/multiply-strings/ |
10 changes: 0 additions & 10 deletions src/dnc/__init__.py
Original file line number Diff line number Diff line change
@@ -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",
]
120 changes: 54 additions & 66 deletions src/dnc/multiply.py
Original file line number Diff line number Diff line change
@@ -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
20 changes: 4 additions & 16 deletions src/dnc/power.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
26 changes: 0 additions & 26 deletions src/intro/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. Последовательные символы

| Поле | Значение |
Expand All @@ -55,7 +30,6 @@
| Сложность | Легкая |
| Источник | https://leetcode.com/problems/third-maximum-number/description/ |


## E. Линия симметрии

| Поле | Значение |
Expand Down
4 changes: 2 additions & 2 deletions tests/test_dnc/test_multiply.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 6 additions & 4 deletions tests/test_dnc/test_power.py
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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

0 comments on commit 83db148

Please sign in to comment.