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
|
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$ в массиве.
|
||||
|
||||

|
||||
|
||||
В реализации кода мы объявляем рекурсивную функцию `dfs()` для решения задачи $f(i, j)$ :
|
||||
|
||||
```src
|
||||
[file]{binary_search_recur}-[class]{}-[func]{binary_search}
|
||||
```
|
||||
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,99 @@
|
||||
# Задача построения двоичного дерева
|
||||
|
||||
!!! question
|
||||
|
||||
Даны прямой обход `preorder` и симметричный обход `inorder` некоторого двоичного дерева. Постройте по ним двоичное дерево и верните его корневой узел. Предполагается, что в дереве нет узлов с одинаковыми значениями (как показано на рисунке ниже).
|
||||
|
||||

|
||||
|
||||
### Проверка, является ли это задачей 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 ]` .
|
||||
|
||||

|
||||
|
||||
### Описание интервалов поддеревьев через переменные
|
||||
|
||||
Согласно описанному выше способу разбиения, **мы уже получили интервалы индексов корневого узла, левого и правого поддеревьев в `preorder` и `inorder`**. Чтобы описывать эти интервалы, нам понадобится несколько указателей-переменных.
|
||||
|
||||
- Обозначим индекс корневого узла текущего дерева в `preorder` через $i$ .
|
||||
- Обозначим индекс корневого узла текущего дерева в `inorder` через $m$ .
|
||||
- Обозначим интервал индексов текущего дерева в `inorder` через $[l, r]$ .
|
||||
|
||||
Как показано в таблице ниже, этих переменных достаточно для описания индекса корневого узла в `preorder` и интервалов поддеревьев в `inorder` .
|
||||
|
||||
<p align="center"> Таблица <id> Индексы корневого узла и поддеревьев в прямом и симметричном обходах </p>
|
||||
|
||||
| | Индекс корневого узла в `preorder` | Интервал индексов поддерева в `inorder` |
|
||||
| ---------------- | ---------------------------------- | ---------------------------------------- |
|
||||
| Текущее дерево | $i$ | $[l, r]$ |
|
||||
| Левое поддерево | $i + 1$ | $[l, m-1]$ |
|
||||
| Правое поддерево | $i + 1 + (m - l)$ | $[m+1, r]$ |
|
||||
|
||||
Обратите внимание, что $(m-l)$ в индексе корневого узла правого поддерева означает "число узлов в левом поддереве"; лучше всего понимать это выражение вместе с рисунком ниже.
|
||||
|
||||

|
||||
|
||||
### Реализация кода
|
||||
|
||||
Чтобы ускорить поиск $m$ , мы используем хеш-таблицу `hmap` для хранения отображения значений массива `inorder` в индексы:
|
||||
|
||||
```src
|
||||
[file]{build_tree}-[class]{}-[func]{build_tree}
|
||||
```
|
||||
|
||||
На рисунке ниже показан рекурсивный процесс построения двоичного дерева: каждый узел создается в фазе "спуска", а каждое ребро (ссылка) формируется в фазе "подъема".
|
||||
|
||||
=== "<1>"
|
||||

|
||||
|
||||
=== "<2>"
|
||||

|
||||
|
||||
=== "<3>"
|
||||

|
||||
|
||||
=== "<4>"
|
||||

|
||||
|
||||
=== "<5>"
|
||||

|
||||
|
||||
=== "<6>"
|
||||

|
||||
|
||||
=== "<7>"
|
||||

|
||||
|
||||
=== "<8>"
|
||||

|
||||
|
||||
=== "<9>"
|
||||

|
||||
|
||||
Результаты разбиения `preorder` и `inorder` внутри каждого рекурсивного вызова показаны на рисунке ниже.
|
||||
|
||||

|
||||
|
||||
Пусть число узлов дерева равно $n$ ; инициализация каждого узла (то есть выполнение одного рекурсивного вызова `dfs()` ) занимает $O(1)$ времени. **Следовательно, общая временная сложность равна $O(n)$** .
|
||||
|
||||
Хеш-таблица хранит отображение значений `inorder` в индексы, поэтому ее пространственная сложность равна $O(n)$ . В худшем случае, когда двоичное дерево вырождается в связный список, глубина рекурсии достигает $n$ и требует $O(n)$ памяти стека. **Следовательно, общая пространственная сложность также равна $O(n)$** .
|
||||
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
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, обычно можно ориентироваться на следующие критерии.
|
||||
|
||||
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)
|
||||
$$
|
||||
|
||||

|
||||
|
||||
Теперь рассмотрим следующее неравенство, в котором левая и правая части обозначают общее число операций до разбиения и после него:
|
||||
|
||||
$$
|
||||
\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
|
||||
|
||||
С одной стороны, 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 - это "тихая" алгоритмическая идея**, скрыто присутствующая внутри самых разных алгоритмов и структур данных.
|
||||
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,97 @@
|
||||
# Задача о Ханойской башне
|
||||
|
||||
В задачах сортировки слиянием и построения двоичного дерева мы делили исходную задачу на две подзадачи, каждая из которых имела размер, равный примерно половине исходной задачи. Однако для задачи о Ханойской башне используется другая стратегия разбиения.
|
||||
|
||||
!!! question
|
||||
|
||||
Даны три стержня, обозначенные как `A` , `B` и `C` . В начальном состоянии на стержне `A` находятся $n$ дисков, расположенных сверху вниз в порядке от меньшего к большему. Нужно переместить эти $n$ дисков на стержень `C` , сохранив их исходный порядок (как показано на рисунке ниже). Во время перемещения дисков необходимо соблюдать следующие правила.
|
||||
|
||||
1. Диск можно снять только с вершины одного стержня и положить только на вершину другого стержня.
|
||||
2. За один раз можно перемещать только один диск.
|
||||
3. Меньший диск всегда должен лежать на большем.
|
||||
|
||||

|
||||
|
||||
**Обозначим задачу о Ханойской башне размера $i$ как $f(i)$** . Например, $f(3)$ означает задачу перемещения 3 дисков со стержня `A` на стержень `C` .
|
||||
|
||||
### Рассмотрим базовые случаи
|
||||
|
||||
Как показано на рисунке ниже, для задачи $f(1)$ , то есть когда имеется только один диск, достаточно просто переместить его напрямую со стержня `A` на стержень `C` .
|
||||
|
||||
=== "<1>"
|
||||

|
||||
|
||||
=== "<2>"
|
||||

|
||||
|
||||
Как показано на рисунке ниже, для задачи $f(2)$ , то есть когда есть два диска, **поскольку меньший диск все время должен лежать на большем, приходится использовать `B` как вспомогательный стержень**.
|
||||
|
||||
1. Сначала переместить верхний маленький диск с `A` на `B` .
|
||||
2. Затем переместить большой диск с `A` на `C` .
|
||||
3. Наконец, переместить маленький диск с `B` на `C` .
|
||||
|
||||
=== "<1>"
|
||||

|
||||
|
||||
=== "<2>"
|
||||

|
||||
|
||||
=== "<3>"
|
||||

|
||||
|
||||
=== "<4>"
|
||||

|
||||
|
||||
Процесс решения задачи $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>"
|
||||

|
||||
|
||||
=== "<2>"
|
||||

|
||||
|
||||
=== "<3>"
|
||||

|
||||
|
||||
=== "<4>"
|
||||

|
||||
|
||||
По своей сути **мы разбиваем задачу $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)$ уже известно и требует всего одного перемещения.
|
||||
|
||||

|
||||
|
||||
### Реализация кода
|
||||
|
||||
В коде мы объявляем рекурсивную функцию `dfs(i, src, buf, tar)` , которая перемещает $i$ верхних дисков со стержня `src` на целевой стержень `tar` с помощью буферного стержня `buf` :
|
||||
|
||||
```src
|
||||
[file]{hanota}-[class]{}-[func]{solve_hanota}
|
||||
```
|
||||
|
||||
Как показано на рисунке ниже, задача о Ханойской башне формирует дерево рекурсии высоты $n$ , в котором каждый узел представляет подзадачу и соответствует одному открытому вызову `dfs()` ; **поэтому временная сложность равна $O(2^n)$ , а пространственная сложность равна $O(n)$** .
|
||||
|
||||

|
||||
|
||||
!!! quote
|
||||
|
||||
Задача о Ханойской башне происходит из древней легенды. В одном из храмов древней Индии монахи имели три высоких алмазных стержня и $64$ золотых диска разного размера. Монахи непрерывно перекладывали диски и верили, что в тот момент, когда последний диск будет правильно перенесен, мир подойдет к концу.
|
||||
|
||||
Однако даже если бы монахи перемещали по одному диску в секунду, им понадобилось бы примерно $2^{64} \approx 1.84×10^{19}$ секунд, то есть около $585$ миллиардов лет, что намного превышает текущую оценку возраста Вселенной. Поэтому, если легенда и верна, нам, вероятно, пока не о чем беспокоиться.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Разделяй и властвуй
|
||||
|
||||

|
||||
|
||||
!!! 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$ . После последовательного решения этих трех подзадач исходная задача также оказывается решенной.
|
||||