Add ru version (#1865)

* Add Russian docs site baseline

* Add Russian localized codebase

* Polish Russian code wording

* Update ru code translation.

* Update code translation and chapter covers.

* Fix pythontutor extraction.

* Add README and landing page.

* placeholder of profiles

* Use figures of English version

* Remove chapter paperbook
This commit is contained in:
Yudong Jin
2026-03-28 04:24:07 +08:00
committed by GitHub
parent 2ca570cc33
commit 772183705e
1958 changed files with 108186 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

@@ -0,0 +1,45 @@
# Поисковая стратегия divide and conquer
Мы уже знаем, что алгоритмы поиска делятся на две большие категории.
- **Полный перебор**: реализуется через обход структуры данных, временная сложность равна $O(n)$ .
- **Адаптивный поиск**: использует особую организацию данных или априорную информацию, временная сложность может достигать $O(\log n)$ и даже $O(1)$ .
На практике **алгоритмы поиска с временной сложностью $O(\log n)$ обычно реализуются на основе стратегии divide and conquer**, например двоичный поиск и деревья.
- На каждом шаге двоичный поиск раскладывает задачу (поиск целевого элемента в массиве) на более мелкую задачу (поиск целевого элемента в одной половине массива), и этот процесс продолжается, пока массив не станет пустым или пока не будет найден целевой элемент.
- Деревья являются типичными представителями идей divide and conquer; в таких структурах данных, как двоичное дерево поиска, AVL-дерево и куча, временная сложность различных операций равна $O(\log n)$ .
Стратегия divide and conquer для двоичного поиска выглядит следующим образом.
- **Задача раскладывается на части**: двоичный поиск рекурсивно разбивает исходную задачу (поиск в массиве) на подзадачу (поиск в одной половине массива), и это достигается сравнением среднего элемента с целевым значением.
- **Подзадачи независимы**: в двоичном поиске на каждом шаге обрабатывается только одна подзадача, и она не зависит от других подзадач.
- **Решения подзадач не нужно объединять**: двоичный поиск нацелен на поиск конкретного элемента, поэтому объединять решения подзадач не требуется. Как только подзадача решена, одновременно считается решенной и исходная задача.
По сути divide and conquer повышает эффективность поиска потому, что при полном переборе за один шаг удается исключить только один вариант, **тогда как при поиске на основе divide and conquer за один шаг можно исключить половину вариантов**.
### Реализация двоичного поиска на основе divide and conquer
В предыдущих главах двоичный поиск реализовывался через итерацию. Теперь реализуем его с помощью divide and conquer, то есть через рекурсию.
!!! question
Дан отсортированный массив `nums` длины $n$ , в котором все элементы уникальны. Найдите элемент `target` .
С точки зрения divide and conquer обозначим подзадачу, соответствующую интервалу поиска $[i, j]$ , через $f(i, j)$ .
Начиная с исходной задачи $f(0, n-1)$ , выполняем двоичный поиск по следующим шагам.
1. Вычислить середину $m$ интервала поиска $[i, j]$ и с ее помощью исключить половину интервала.
2. Рекурсивно решить подзадачу вдвое меньшего размера; это может быть либо $f(i, m-1)$ , либо $f(m+1, j)$ .
3. Повторять шаг `1.` и шаг `2.` , пока не будет найден `target` или пока интервал не станет пустым.
На рисунке ниже показан процесс применения divide and conquer для поиска элемента $6$ в массиве.
![Процесс двоичного поиска в стиле divide and conquer](binary_search_recur.assets/binary_search_recur.png)
В реализации кода мы объявляем рекурсивную функцию `dfs()` для решения задачи $f(i, j)$ :
```src
[file]{binary_search_recur}-[class]{}-[func]{binary_search}
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

@@ -0,0 +1,99 @@
# Задача построения двоичного дерева
!!! question
Даны прямой обход `preorder` и симметричный обход `inorder` некоторого двоичного дерева. Постройте по ним двоичное дерево и верните его корневой узел. Предполагается, что в дереве нет узлов с одинаковыми значениями (как показано на рисунке ниже).
![Пример данных для построения двоичного дерева](build_binary_tree_problem.assets/build_tree_example.png)
### Проверка, является ли это задачей divide and conquer
Исходная задача - построить двоичное дерево по `preorder` и `inorder` - является типичной задачей divide and conquer.
- **Задача раскладывается на части**: если смотреть с точки зрения divide and conquer, исходную задачу можно разбить на две подзадачи: построение левого поддерева и построение правого поддерева, плюс одно действие: инициализация корневого узла. Для каждого поддерева (подзадачи) можно использовать тот же способ разбиения, пока не будет достигнута наименьшая подзадача (пустое поддерево).
- **Подзадачи независимы**: левое и правое поддеревья независимы друг от друга и не пересекаются. При построении левого поддерева нам нужно смотреть только на ту часть прямого и симметричного обходов, которая соответствует левому поддереву. Для правого поддерева рассуждение аналогично.
- **Решения подзадач можно объединить**: когда левое и правое поддеревья (решения подзадач) уже построены, их можно присоединить к корневому узлу и тем самым получить решение исходной задачи.
### Как разделить поддеревья
Из анализа выше видно, что эта задача действительно решается через divide and conquer, **но как именно, имея прямой обход `preorder` и симметричный обход `inorder`, разделить левое и правое поддеревья**?
По определению и `preorder` , и `inorder` можно разбить на три части.
- Прямой обход: `[ корневой узел | левое поддерево | правое поддерево ]` , например для дерева на рисунке выше это `[ 3 | 9 | 2 1 7 ]` .
- Симметричный обход: `[ левое поддерево | корневой узел | правое поддерево ]` , например для дерева на рисунке выше это `[ 9 | 3 | 1 2 7 ]` .
На примере данных с рисунка можно получить результат разбиения по следующим шагам.
1. Первый элемент прямого обхода, равный 3, является значением корневого узла.
2. Найти индекс корневого узла 3 в `inorder` ; используя этот индекс, можно разбить `inorder` на `[ 9 | 3 | 1 2 7 ]` .
3. По результату разбиения `inorder` нетрудно определить, что число узлов в левом и правом поддеревьях равно 1 и 3 соответственно, а значит, `preorder` можно разбить как `[ 3 | 9 | 2 1 7 ]` .
![Разбиение поддеревьев в прямом и симметричном обходах](build_binary_tree_problem.assets/build_tree_preorder_inorder_division.png)
### Описание интервалов поддеревьев через переменные
Согласно описанному выше способу разбиения, **мы уже получили интервалы индексов корневого узла, левого и правого поддеревьев в `preorder` и `inorder`**. Чтобы описывать эти интервалы, нам понадобится несколько указателей-переменных.
- Обозначим индекс корневого узла текущего дерева в `preorder` через $i$ .
- Обозначим индекс корневого узла текущего дерева в `inorder` через $m$ .
- Обозначим интервал индексов текущего дерева в `inorder` через $[l, r]$ .
Как показано в таблице ниже, этих переменных достаточно для описания индекса корневого узла в `preorder` и интервалов поддеревьев в `inorder` .
<p align="center"> Таблица <id> &nbsp; Индексы корневого узла и поддеревьев в прямом и симметричном обходах </p>
| | Индекс корневого узла в `preorder` | Интервал индексов поддерева в `inorder` |
| ---------------- | ---------------------------------- | ---------------------------------------- |
| Текущее дерево | $i$ | $[l, r]$ |
| Левое поддерево | $i + 1$ | $[l, m-1]$ |
| Правое поддерево | $i + 1 + (m - l)$ | $[m+1, r]$ |
Обратите внимание, что $(m-l)$ в индексе корневого узла правого поддерева означает "число узлов в левом поддереве"; лучше всего понимать это выражение вместе с рисунком ниже.
![Представление индексных интервалов корня и поддеревьев](build_binary_tree_problem.assets/build_tree_division_pointers.png)
### Реализация кода
Чтобы ускорить поиск $m$ , мы используем хеш-таблицу `hmap` для хранения отображения значений массива `inorder` в индексы:
```src
[file]{build_tree}-[class]{}-[func]{build_tree}
```
На рисунке ниже показан рекурсивный процесс построения двоичного дерева: каждый узел создается в фазе "спуска", а каждое ребро (ссылка) формируется в фазе "подъема".
=== "<1>"
![Рекурсивный процесс построения двоичного дерева](build_binary_tree_problem.assets/built_tree_step1.png)
=== "<2>"
![built_tree_step2](build_binary_tree_problem.assets/built_tree_step2.png)
=== "<3>"
![built_tree_step3](build_binary_tree_problem.assets/built_tree_step3.png)
=== "<4>"
![built_tree_step4](build_binary_tree_problem.assets/built_tree_step4.png)
=== "<5>"
![built_tree_step5](build_binary_tree_problem.assets/built_tree_step5.png)
=== "<6>"
![built_tree_step6](build_binary_tree_problem.assets/built_tree_step6.png)
=== "<7>"
![built_tree_step7](build_binary_tree_problem.assets/built_tree_step7.png)
=== "<8>"
![built_tree_step8](build_binary_tree_problem.assets/built_tree_step8.png)
=== "<9>"
![built_tree_step9](build_binary_tree_problem.assets/built_tree_step9.png)
Результаты разбиения `preorder` и `inorder` внутри каждого рекурсивного вызова показаны на рисунке ниже.
![Результаты разбиения в каждом рекурсивном вызове](build_binary_tree_problem.assets/built_tree_overall.png)
Пусть число узлов дерева равно $n$ ; инициализация каждого узла (то есть выполнение одного рекурсивного вызова `dfs()` ) занимает $O(1)$ времени. **Следовательно, общая временная сложность равна $O(n)$** .
Хеш-таблица хранит отображение значений `inorder` в индексы, поэтому ее пространственная сложность равна $O(n)$ . В худшем случае, когда двоичное дерево вырождается в связный список, глубина рекурсии достигает $n$ и требует $O(n)$ памяти стека. **Следовательно, общая пространственная сложность также равна $O(n)$** .
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

@@ -0,0 +1,91 @@
# Алгоритмы "разделяй и властвуй"
<u>Разделяй и властвуй (divide and conquer)</u> - это очень важная и широко используемая стратегия построения алгоритмов. Обычно она реализуется через рекурсию и включает два этапа: "разделение" и "решение".
1. **Разделение (этап декомпозиции)**: рекурсивно разбить исходную задачу на две или более подзадачи, пока не будет достигнута наименьшая подзадача.
2. **Решение (этап объединения)**: начиная с уже известных решений наименьших подзадач, снизу вверх объединять решения подзадач и тем самым получать решение исходной задачи.
Как показано на рисунке ниже, "сортировка слиянием" является одним из типичных примеров применения стратегии "разделяй и властвуй".
1. **Разделение**: рекурсивно разделить исходный массив (исходную задачу) на два подмассива (подзадачи), пока в подмассиве не останется только один элемент (наименьшая подзадача).
2. **Решение**: снизу вверх объединять упорядоченные подмассивы (решения подзадач), чтобы получить упорядоченный исходный массив (решение исходной задачи).
![Стратегия divide and conquer в сортировке слиянием](divide_and_conquer.assets/divide_and_conquer_merge_sort.png)
## Как определить задачу divide and conquer
Чтобы понять, подходит ли задача для решения методом divide and conquer, обычно можно ориентироваться на следующие критерии.
1. **Задача раскладывается на части**: исходную задачу можно разбить на более мелкие и похожие подзадачи, причем такое разбиение можно применять рекурсивно.
2. **Подзадачи независимы**: подзадачи не пересекаются, не зависят друг от друга и могут решаться независимо.
3. **Решения подзадач можно объединить**: решение исходной задачи получается объединением решений подзадач.
Очевидно, что сортировка слиянием удовлетворяет всем трем критериям.
1. **Задача раскладывается на части**: массив (исходная задача) рекурсивно делится на два подмассива (подзадачи).
2. **Подзадачи независимы**: каждый подмассив можно сортировать отдельно (то есть каждую подзадачу можно решать независимо).
3. **Решения подзадач можно объединить**: два упорядоченных подмассива (решения подзадач) можно объединить в один упорядоченный массив (решение исходной задачи).
## Повышение эффективности с помощью divide and conquer
**Стратегия divide and conquer не только позволяет эффективно решать алгоритмические задачи, но и часто повышает эффективность самих алгоритмов**. Именно поэтому быстрая сортировка, сортировка слиянием и пирамидальная сортировка обычно работают быстрее, чем сортировка выбором, пузырьком и вставками.
Тогда возникает естественный вопрос: **почему divide and conquer повышает эффективность алгоритма и какова логика этого на более глубоком уровне**? Иными словами, почему разбиение большой задачи на несколько подзадач, решение этих подзадач и последующее объединение их решений оказывается эффективнее, чем прямое решение исходной задачи? Этот вопрос можно рассмотреть с двух сторон: через число операций и через параллельные вычисления.
### Оптимизация числа операций
Рассмотрим "сортировку пузырьком": для массива длины $n$ ей требуется $O(n^2)$ времени. Предположим, что мы разделим массив на два подмассива в середине, как показано на рисунке ниже. Тогда само разбиение потребует $O(n)$ времени, сортировка каждого подмассива займет $O((n / 2)^2)$ времени, а объединение двух подмассивов потребует еще $O(n)$ времени. Общая временная сложность будет равна:
$$
O(n + (\frac{n}{2})^2 \times 2 + n) = O(\frac{n^2}{2} + 2n)
$$
![Сортировка пузырьком до и после разбиения массива](divide_and_conquer.assets/divide_and_conquer_bubble_sort.png)
Теперь рассмотрим следующее неравенство, в котором левая и правая части обозначают общее число операций до разбиения и после него:
$$
\begin{aligned}
n^2 & > \frac{n^2}{2} + 2n \newline
n^2 - \frac{n^2}{2} - 2n & > 0 \newline
n(n - 4) & > 0
\end{aligned}
$$
**Это означает, что при $n > 4$ число операций после разбиения становится меньше, а значит, сортировка должна работать быстрее**. При этом важно заметить, что временная сложность после разбиения все еще остается квадратичной, то есть $O(n^2)$ ; уменьшается лишь константный множитель.
Если пойти дальше и **продолжать делить каждый подмассив пополам**, пока в нем не останется только один элемент, то мы фактически получим "сортировку слиянием", чья временная сложность равна $O(n \log n)$ .
Можно пойти еще дальше и спросить: **что если задать несколько точек разделения** и равномерно разбить исходный массив на $k$ подмассивов? Такая ситуация очень похожа на "блочную сортировку", которая особенно хорошо подходит для сортировки очень больших объемов данных и теоретически может достигать временной сложности $O(n + k)$ .
### Оптимизация параллельных вычислений
Мы знаем, что подзадачи, порождаемые divide and conquer, являются независимыми, **а значит, их обычно можно решать параллельно**. Иначе говоря, divide and conquer не только может уменьшить временную сложность алгоритма, **но и хорошо сочетается с параллельной оптимизацией на уровне системы**.
Параллельная оптимизация особенно эффективна в среде с несколькими ядрами или несколькими процессорами, потому что система может одновременно обрабатывать разные подзадачи, лучше загружая вычислительные ресурсы и тем самым заметно сокращая общее время работы.
Например, в показанной ниже "блочной сортировке" большой объем данных равномерно распределяется по блокам. Тогда сортировку каждого блока можно поручить отдельным вычислительным единицам, а после завершения просто объединить результаты.
![Параллельные вычисления в блочной сортировке](divide_and_conquer.assets/divide_and_conquer_parallel_computing.png)
## Типичные применения divide and conquer
С одной стороны, divide and conquer можно использовать для решения многих классических алгоритмических задач.
- **Поиск ближайшей пары точек**: сначала множество точек делится на две части, затем ищется ближайшая пара в каждой части, а затем ближайшая пара, пересекающая границу между двумя частями.
- **Умножение больших чисел**: например, алгоритм Карацубы, который раскладывает умножение больших чисел на несколько умножений и сложений меньших чисел.
- **Умножение матриц**: например, алгоритм Штрассена, который раскладывает умножение больших матриц на несколько умножений и сложений матриц меньшего размера.
- **Задача о Ханойской башне**: задача о Ханойской башне решается рекурсивно и является типичным примером применения divide and conquer.
- **Подсчет инверсий**: если в последовательности предыдущее число больше следующего, то такая пара образует инверсию. Эту задачу можно решить с помощью идей divide and conquer, опираясь на сортировку слиянием.
С другой стороны, divide and conquer очень широко применяется при проектировании алгоритмов и структур данных.
- **Двоичный поиск**: двоичный поиск делит отсортированный массив на две части по индексу середины, а затем, в зависимости от результата сравнения целевого значения со средним элементом, исключает одну из половин и повторяет ту же операцию на оставшемся интервале.
- **Сортировка слиянием**: она уже была рассмотрена в начале этого раздела, поэтому не будем повторяться.
- **Быстрая сортировка**: в ней выбирается опорное значение, после чего массив делится на два подмассива: один содержит элементы меньше опорного, а другой - больше. Затем такая же операция повторяется для обеих частей, пока в подмассиве не останется один элемент.
- **Блочная сортировка**: ее основная идея заключается в распределении данных по нескольким блокам, сортировке элементов внутри каждого блока и последующем последовательном извлечении элементов из блоков для построения отсортированного массива.
- **Деревья**: например, двоичные деревья поиска, AVL-деревья, красно-черные деревья, B-деревья, B+ деревья и т.д. Их операции поиска, вставки и удаления можно рассматривать как применение divide and conquer.
- **Кучи**: куча является особым видом полного бинарного дерева, а такие операции, как вставка, удаление и упорядочивание, по сути содержат идеи divide and conquer.
- **Хеш-таблицы**: хотя хеш-таблицы напрямую не используют divide and conquer, некоторые способы разрешения коллизий косвенно опираются на эту стратегию. Например, длинные цепочки в методе цепочек могут преобразовываться в красно-черные деревья для повышения эффективности поиска.
Нетрудно заметить, что **divide and conquer - это "тихая" алгоритмическая идея**, скрыто присутствующая внутри самых разных алгоритмов и структур данных.
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

@@ -0,0 +1,97 @@
# Задача о Ханойской башне
В задачах сортировки слиянием и построения двоичного дерева мы делили исходную задачу на две подзадачи, каждая из которых имела размер, равный примерно половине исходной задачи. Однако для задачи о Ханойской башне используется другая стратегия разбиения.
!!! question
Даны три стержня, обозначенные как `A` , `B` и `C` . В начальном состоянии на стержне `A` находятся $n$ дисков, расположенных сверху вниз в порядке от меньшего к большему. Нужно переместить эти $n$ дисков на стержень `C` , сохранив их исходный порядок (как показано на рисунке ниже). Во время перемещения дисков необходимо соблюдать следующие правила.
1. Диск можно снять только с вершины одного стержня и положить только на вершину другого стержня.
2. За один раз можно перемещать только один диск.
3. Меньший диск всегда должен лежать на большем.
![Пример задачи о Ханойской башне](hanota_problem.assets/hanota_example.png)
**Обозначим задачу о Ханойской башне размера $i$ как $f(i)$** . Например, $f(3)$ означает задачу перемещения 3 дисков со стержня `A` на стержень `C` .
### Рассмотрим базовые случаи
Как показано на рисунке ниже, для задачи $f(1)$ , то есть когда имеется только один диск, достаточно просто переместить его напрямую со стержня `A` на стержень `C` .
=== "<1>"
![Решение задачи размера 1](hanota_problem.assets/hanota_f1_step1.png)
=== "<2>"
![hanota_f1_step2](hanota_problem.assets/hanota_f1_step2.png)
Как показано на рисунке ниже, для задачи $f(2)$ , то есть когда есть два диска, **поскольку меньший диск все время должен лежать на большем, приходится использовать `B` как вспомогательный стержень**.
1. Сначала переместить верхний маленький диск с `A` на `B` .
2. Затем переместить большой диск с `A` на `C` .
3. Наконец, переместить маленький диск с `B` на `C` .
=== "<1>"
![Решение задачи размера 2](hanota_problem.assets/hanota_f2_step1.png)
=== "<2>"
![hanota_f2_step2](hanota_problem.assets/hanota_f2_step2.png)
=== "<3>"
![hanota_f2_step3](hanota_problem.assets/hanota_f2_step3.png)
=== "<4>"
![hanota_f2_step4](hanota_problem.assets/hanota_f2_step4.png)
Процесс решения задачи $f(2)$ можно кратко описать так: **переместить два диска с `A` на `C` с помощью `B`** . Здесь `C` называется целевым стержнем, а `B` - буферным стержнем.
### Разбиение на подзадачи
Для задачи $f(3)$ , то есть когда имеется три диска, ситуация становится немного сложнее.
Поскольку решения $f(1)$ и $f(2)$ уже известны, можно подойти к задаче с точки зрения divide and conquer и **рассматривать два верхних диска на `A` как единое целое**, выполняя шаги, показанные на рисунке ниже. Так три диска успешно перемещаются с `A` на `C` .
1. Сделать `B` целевым стержнем, а `C` буферным, и переместить два диска с `A` на `B` .
2. Переместить оставшийся один диск с `A` напрямую на `C` .
3. Сделать `C` целевым стержнем, а `A` буферным, и переместить два диска с `B` на `C` .
=== "<1>"
![Решение задачи размера 3](hanota_problem.assets/hanota_f3_step1.png)
=== "<2>"
![hanota_f3_step2](hanota_problem.assets/hanota_f3_step2.png)
=== "<3>"
![hanota_f3_step3](hanota_problem.assets/hanota_f3_step3.png)
=== "<4>"
![hanota_f3_step4](hanota_problem.assets/hanota_f3_step4.png)
По своей сути **мы разбиваем задачу $f(3)$ на две подзадачи $f(2)$ и одну подзадачу $f(1)$** . Если последовательно решить эти три подзадачи, исходная задача тоже будет решена. Это показывает, что подзадачи независимы и что их решения можно объединить.
Таким образом, можно сформулировать показанную на рисунке ниже стратегию divide and conquer для задачи о Ханойской башне: исходная задача $f(n)$ разбивается на две подзадачи $f(n-1)$ и одну подзадачу $f(1)$ , которые затем решаются в следующем порядке.
1. Переместить $n-1$ дисков с `A` на `B` с помощью `C` .
2. Переместить оставшийся $1$ диск напрямую с `A` на `C` .
3. Переместить $n-1$ дисков с `B` на `C` с помощью `A` .
Для двух подзадач $f(n-1)$ **можно применять тот же способ рекурсивного разбиения**, пока не будет достигнута наименьшая подзадача $f(1)$ . А решение для $f(1)$ уже известно и требует всего одного перемещения.
![Стратегия divide and conquer для решения задачи о Ханойской башне](hanota_problem.assets/hanota_divide_and_conquer.png)
### Реализация кода
В коде мы объявляем рекурсивную функцию `dfs(i, src, buf, tar)` , которая перемещает $i$ верхних дисков со стержня `src` на целевой стержень `tar` с помощью буферного стержня `buf` :
```src
[file]{hanota}-[class]{}-[func]{solve_hanota}
```
Как показано на рисунке ниже, задача о Ханойской башне формирует дерево рекурсии высоты $n$ , в котором каждый узел представляет подзадачу и соответствует одному открытому вызову `dfs()` ; **поэтому временная сложность равна $O(2^n)$ , а пространственная сложность равна $O(n)$** .
![Дерево рекурсии задачи о Ханойской башне](hanota_problem.assets/hanota_recursive_tree.png)
!!! quote
Задача о Ханойской башне происходит из древней легенды. В одном из храмов древней Индии монахи имели три высоких алмазных стержня и $64$ золотых диска разного размера. Монахи непрерывно перекладывали диски и верили, что в тот момент, когда последний диск будет правильно перенесен, мир подойдет к концу.
Однако даже если бы монахи перемещали по одному диску в секунду, им понадобилось бы примерно $2^{64} \approx 1.84×10^{19}$ секунд, то есть около $585$ миллиардов лет, что намного превышает текущую оценку возраста Вселенной. Поэтому, если легенда и верна, нам, вероятно, пока не о чем беспокоиться.
@@ -0,0 +1,9 @@
# Разделяй и властвуй
![Разделяй и властвуй](../assets/covers/chapter_divide_and_conquer.jpg)
!!! abstract
Сложная задача раскладывается слой за слоем, и каждое новое разбиение делает ее проще.
Принцип "разделяй и властвуй" показывает важный факт: если начать с простого, многое перестает быть сложным.
@@ -0,0 +1,13 @@
# Резюме
### Ключевые выводы
- Divide and conquer - это распространенная стратегия проектирования алгоритмов, которая включает два этапа: разделение (декомпозицию) и решение (объединение), и обычно реализуется с помощью рекурсии.
- Критерии применимости этой стратегии к задаче включают: возможность разложения задачи, независимость подзадач и возможность объединения их решений.
- Сортировка слиянием является типичным применением divide and conquer: она рекурсивно делит массив на два равных по длине подмассива, пока не останется массив из одного элемента, после чего начинает поэтапное объединение.
- Введение стратегии divide and conquer часто позволяет повысить эффективность алгоритма. С одной стороны, стратегия уменьшает число операций; с другой - после разбиения она способствует параллельной оптимизации на уровне системы.
- Divide and conquer не только помогает решать многие алгоритмические задачи, но и широко используется при проектировании структур данных и алгоритмов, поэтому его можно встретить буквально повсюду.
- По сравнению с полным перебором адаптивный поиск работает эффективнее. Алгоритмы поиска со сложностью $O(\log n)$ обычно реализуются на основе стратегии divide and conquer.
- Двоичный поиск - еще одно типичное применение divide and conquer, в котором отсутствует шаг объединения решений подзадач. Мы можем реализовать двоичный поиск рекурсивно, через divide and conquer.
- В задаче построения двоичного дерева исходная задача построения дерева может быть разбита на две подзадачи: построение левого и правого поддеревьев, а реализуется это через разбиение индексных интервалов прямого и симметричного обходов.
- В задаче о Ханойской башне задача размера $n$ разбивается на две подзадачи размера $n-1$ и одну подзадачу размера $1$ . После последовательного решения этих трех подзадач исходная задача также оказывается решенной.