Files
hello-algo/ru/chapters/chapter_12.md
2026-03-25 16:54:42 +08:00

512 lines
44 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Разделяй и властвуй
![](ru/docs/assets/media/image717.jpeg){width="3.5416655730533684in" height="4.583333333333333in"}
#### Стратегия «разделяй и властвуй»
> «*Разделяй и властвуй*» -- это важная и распространенная стратегия в алгорит- мах. Обычно она реализуется с помощью рекурсии и включает два этапа: раз- деление и объединение.
- **Разделение (этап разбиения)**: рекурсивное разбиение исходной зада- чи на две или более подзадачи до тех пор, пока не будет достигнута наи- меньшая подзадача.
- **Объединение (этап слияния)**: начиная с решения наименьших подза- дач, снизу вверх объединяются решения всех других подзадач, чтобы по- строить решение исходной задачи.
> Сортировка слиянием является типичным примером применения страте- гии «разделяй и властвуй» (см. рис. 12.1).
- **Разделение**: рекурсивное разбиение исходного массива (исходной за- дачи) на два подмассива (подзадачи) до тех пор, пока в подмассивах не останется по одному элементу (наименьшая подзадача).
- **Объединение**: снизу вверх объединяются упорядоченные подмассивы (решения подзадач), чтобы получить упорядоченный исходный массив (решение исходной задачи).
![](ru/docs/assets/media/image719.jpeg)
> **Рис. 12.1.** Стратегия «разделяй и властвуй» в сортировке слиянием
1. **Определение задачи для метода**
> **«разделяй и властвуй»**
>
> Чтобы определить, подходит ли задача для решения методом «разделяй и вла- ствуй», можно использовать следующие критерии:
1) **задачу можно разбить**: исходную задачу можно разбить на более мел- кие, аналогичные подзадачи, которые можно рекурсивно разделить ана- логичным образом;
2) **подзадачи независимы**: подзадачи не пересекаются, не зависят друг от друга и могут быть решены независимо;
3) **решения подзадач можно объединить**: решение исходной задачи по- лучается путем объединения решений подзадач.
> Очевидно, что сортировка слиянием соответствует этим трем критериям.
- **Задачу можно разбить**: рекурсивное разбиение массива (исходной за- дачи) на два подмассива (подзадачи).
- **Подзадачи независимы**: каждый подмассив можно отсортировать не- зависимо (подзадачи можно решить независимо).
- **Решения подзадач можно объединить**: два упорядоченных подмасси- ва (решения подзадач) можно объединить в один упорядоченный массив (решение исходной задачи).
### Повышение эффективности с помощью стратегии «разделяй и властвуй»
> **Стратегия «разделяй и властвуй» позволяет не только эффективно ре- шать алгоритмические задачи**, **но и повышать эффективность алгорит- мов**. Алгоритмы быстрой сортировки, сортировки слиянием и пирамидальной сортировки быстрее, чем сортировка выбором, пузырьком и вставками, имен- но благодаря применению стратегии «разделяй и властвуй».
>
> Возникает вопрос: **почему метод «разделяй и властвуй» повышает эф- фективность алгоритма**, **в чем его основная логика**? Иными словами, поче- му разбиение большой задачи на несколько подзадач, решение этих подзадач и объединение их решений в решение исходной задачи оказывается более эф- фективным, чем непосредственное решение исходной задачи? Этот вопрос мож- но обсудить с точки зрения количества операций и параллельных вычислений.
##### Оптимизация количества операций
> Возьмем, к примеру, сортировку пузырьком, которая требует времени *O*(*n*2) для обработки массива длиной *n*. Предположим, что мы разделили массив на два подмассива, как показано на рис. 12.2. Тогда разбиение потребует времени *O*(*n*), сортировка каждого подмассива -- *O*((*n*/2)2), а объединение двух подмас- сивов -- *O*(*n*). Общая временная составит:
( ( *[n]{.underline}*  2   ( *n*^2^  
*O**n+*  ξ 2 + *n*   = *O* ∴ + 2*n*  .
 
 
>     2  
>
> Далее, решим следующее неравенство, в котором левая и правая части пред- ставляют общее количество операций до и после разбиения соответственно:
>
> *n*^2^ \> *n*2 + 2*n,*
2
> *n*^2^ *n*2 2*n \>* 0,
2
> *n*(*n* *4*) \> 0.
>
> ![](ru/docs/assets/media/image721.jpeg)
>
> **Рис. 12.2.** Сортировка пузырьком до и после разбиения массива
>
> **Это означает**, **что при** *n* \> 4 **количество операций после разбиения мень- ше**, **и эффективность сортировки должна быть выше**. Обратите внимание, что временная сложность после разбиения остается квадратичной *O*(*n*2), но по- стоянный коэффициент в сложности уменьшается.
>
> **Если продолжить разбиение подмассивов пополам**, **пока в них не оста- нется по одному элементу**, то получится сортировка слиянием, временная сложность которой составляет *O*(*n* log *n*).
>
> А что, **если мы установим несколько дополнительных точек разделе- ния** и равномерно разделим исходный массив на *k* подмассивов? Эта ситуация очень похожа на блочную сортировку, которая хорошо подходит для сортиров- ки очень больших объемов данных, и теоретически ее временная сложность может достигать *O*(*n* + *k*).
##### Оптимизация параллельных вычислений
> Известно, что подзадачи, созданные методом «разделяй и властвуй», незави- симы друг от друга, **поэтому их обычно можно решать параллельно**. Таким образом, этот метод не только снижает временную сложность алгоритма, **но и способствует параллельной оптимизации операционной системы**.
>
> Параллельная оптимизация особенно эффективна в многоядерной или многопроцессорной среде, поскольку система может одновременно обраба- тывать несколько подзадач, более полно используя вычислительные ресурсы, что значительно сокращает общее время выполнения.
>
> Например, в блочной сортировке, изображенной на рис. 12.3, огромный объем данных равномерно распределяется по блокам. Задачи сортировки всех блоков можно распределить по вычислительным единицам, а затем объеди- нить результаты.
>
> ![](ru/docs/assets/media/image723.jpeg)
>
> **Рис. 12.3.** Параллельные вычисления в блочной сортировке
### Типичные сценарии применения стратегии «разделяй и властвуй»
> С одной стороны, стратегию «разделяй и властвуй» можно использовать для решения многих классических алгоритмических задач.
- **Поиск ближайшей пары точек**: этот алгоритм сначала делит множе- ство точек на две части, затем находит ближайшую пару точек в каж- дой части, а затем находит ближайшую пару точек, охватывающую обе части.
- **Умножение больших чисел**: например, алгоритм Карацубы, который разлагает умножение больших чисел на несколько операций умножения и сложения меньших чисел.
- **Умножение матриц**: например, алгоритм Штрассена, который разлага- ет умножение больших матриц на несколько операций умножения и сло- жения матриц меньшего размера.
- **Задача о Ханойских башнях**: эту задачу можно решить с помощью рекурсии, что является типичным применением стратегии «разделяй и властвуй».
- **Задача о количестве инверсий**: если в последовательности предыду- щее число больше последующего, то эти два числа образуют инверсию. Задачу о количестве инверсий можно решить с помощью подхода «раз- деляй и властвуй» и сортировки слиянием.
> С другой стороны, стратегия «разделяй и властвуй» широко применяется в разработке алгоритмов и структур данных.
- **Двоичный поиск**: такой поиск делит отсортированный массив на две части по индексу среднего элемента. Затем, в зависимости от
> результата сравнения целевого значения со средним элементом, ре- шает, какую половину исключить, и выполняет ту же операцию на оставшейся части.
- **Сортировка слиянием**: уже была рассмотрена в начале этого раздела, не будем еще раз повторяться.
- **Быстрая сортировка**: эта сортировка выбирает опорное значение, затем делит массив на два подмассива, элементы одного из которых меньше опорного значения, а элементы другого -- больше. Затем выполняет ту же операцию с обеими частями, пока в подмассиве не останется один элемент.
- **Блочная сортировка**: основная идея этой сортировки заключается в распределении данных по нескольким блокам и сортировке элемен- тов в каждом из них. Затем происходит последовательное извлечение элементов из каждого блока для построения отсортированного массива.
- **Деревья**: например, двоичные деревья поиска, АВЛ-дерево, красно-чер- ное дерево, B-дерево, дерево B+ и т. д. Операции поиска, вставки и уда- ления в них можно рассматривать как применение стратегии «разделяй и властвуй».
- **Кучи**: куча -- это особый вид полного двоичного дерева, и такие опера- ции, как вставка, удаление и упорядочивание, фактически подразумева- ют использование метода «разделяй и властвуй».
- **Хеш-таблицы**: хотя хеш-таблицы напрямую не применяют подход
> «разделяй и властвуй», некоторые решения для разрешения коллизий в хешировании косвенно используют эту стратегию. Например, длинные цепочки в методе цепной адресации преобразуются в красно-черные де- ревья для повышения эффективности поиска.
>
> Можно сказать, **что стратегия «разделяй и властвуй»** -- **это своего рода**
>
> **«скрытая» алгоритмическая идея**, присутствующая в различных алгорит- мах и структурах данных.
#### применение стратегии «разделяй
> **и властвуй» для поиска**
>
> Мы уже знаем, что алгоритмы поиска делятся на две большие категории.
- **Полный перебор**: реализуется путем обхода структуры данных, вре- менная сложность составляет *O*(*n*).
- **Адаптивный поиск**: использует особую организацию данных или апри- орную информацию, временная сложность может достигать *O*(log *n*) или даже *O*(1).
> На практике **алгоритмы поиска с временной сложностью** *O*(log *n*) **обыч- но реализуются на основе стратегии «разделяй и властвуй»**, например двоичный поиск и деревья.
- **Двоичный поиск** на каждом шаге разбивает задачу (поиск целевого элемента в массиве) на более мелкую задачу (поиск целевого элемента в половине массива). Этот процесс продолжается до тех пор, пока массив не станет пустым или не будет найден целевой элемент.
<!-- -->
- **Деревья** являются представителями стратегии «разделяй и властвуй». В структурах данных, таких как двоичное дерево поиска, АВЛ-дерево, куча и др., временная сложность различных операций составляет *O*(log *n*).
> Стратегия «разделяй и властвуй» для двоичного поиска выглядит следую- щим образом.
- **Задачу можно разбить**: двоичный поиск рекурсивно разбивает ис- ходную задачу (поиск в массиве) на подзадачи (поиск в половине массива), что достигается сравнением среднего элемента с целевым элементом.
- **Подзадачи независимы**: в двоичном поиске на каждом этапе об- рабатывается только одна подзадача, которая не зависит от других подзадач.
- **Решения подзадач не требуют объединения**: двоичный поиск на- правлен на поиск конкретного элемента, поэтому объединять решения подзадач не требуется. Когда подзадача решена, исходная задача также считается решенной.
> Стратегия «разделяй и властвуй» повышает эффективность поиска, по- скольку при грубом поиске на каждом этапе можно исключить только один вариант, тогда как при поиске «разделяй и властвуй» на каждом этапе можно исключить половину вариантов.
##### Реализация двоичного поиска на основе стратегии «разделяй и властвуй»
> В предыдущих главах двоичный поиск был реализован на основе итераций. Теперь мы реализуем его на основе принципа «разделяй и властвуй» (ре- курсии).
>
> Для применения стратегии «разделяй и властвуй» обозначим подзадачу для поискового интервала \[*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
> или интервал не станет пустым.
>
> На рис. 12.4 иллюстрируется процесс применения стратегии «разделяй и властвуй» при двоичном поиске элемента 6 в массиве.
>
> ![](ru/docs/assets/media/image725.jpeg)
>
> **Рис. 12.4.** Стратегия «разделяй и властвуй» в двоичном поиске
>
> В коде реализации объявляется рекурсивная функция dfs() для решения за- дачи *f*(*i*, *j*).
>
> \# === File: binary_search_recur.py ===
>
> def dfs(nums: list\[int\], target: int, i: int, j: int) -\> int: \"\"\" Двоичный поиск: задача f(i, j).\"\"\"
>
> \# Если интервал пуст, значит целевой элемент отсутствует, возвращается -1. if i \> j:
>
> return -1
>
> \# Вычисление индекса средней точки m. m = (i + j) // 2
>
> if nums\[m\] \< target:
>
> \# Рекурсивная подзадача f(m+1, j). return dfs(nums, target, m + 1, j)
>
> elif nums\[m\] \> target:
>
> \# Рекурсивная подзадача f(i, m-1). return dfs(nums, target, i, m - 1)
>
> else:
>
> \# Найден целевой элемент, возвращается его индекс. return m
>
> def binary_search(nums: list\[int\], target: int) -\> int: \"\"\" Двоичный поиск.\"\"\"
>
> n = len(nums)
>
> \# Решение задачи f(0, n-1).
>
> return dfs(nums, target, 0, n - 1)
#### задача построения двоичного дерева
![](ru/docs/assets/media/image727.jpeg)
> **Рис. 12.5.** Пример данных для построения двоичного дерева
##### Проверка критериев стратегии «разделяй и властвуй»
> Исходная задача, заключающаяся в построении двоичного дерева из обходов
>
> preorder и inorder, является типичной задачей типа «разделяй и властвуй».
- **Задачу можно разбить**: с точки зрения стратегии «разделяй и вла- ствуй» исходную задачу можно разделить на две подзадачи. Построение левого поддерева и построение правого поддерева плюс один шаг: ини- циализация корневого узла. Для каждого поддерева (подзадачи) можно повторно использовать вышеуказанный метод разделения и разделить его на более мелкие поддеревья (подзадачи), пока не будет достигнута минимальная подзадача (пустое поддерево).
- **Подзадачи независимы**: левое и правое поддеревья независимы друг от друга, между ними нет пересечений. При построении левого подде- рева необходимо учитывать только части симметричного и прямого по- рядка обхода, соответствующие левому поддереву. Для правого подде- рева аналогично.
- **Решения подзадач можно объединить**: как только получены левое и правое поддеревья (решения подзадач), их можно связать с корневым узлом и получить решение исходной задачи.
##### Разделение поддеревьев
> Мы определили, что эту задачу можно решить с помощью стратегии «разделяй и властвуй». **Но как именно разделить левое и правое поддеревья с помо- щью прямого** (preorder) **и симметричного** (inorder) **порядков обхода**?
>
> Согласно определению preorder и inorder можно разделить на три части.
- **Прямой обход**: \[корневой узел \| левое поддерево \| правое поддерево\], например для дерева на рис. 12.5 это соответствует \[3 \| 9 \| 2 1 7\].
- **Симметричный обход**: \[левое поддерево \| корневой узел \| правое под- дерево\], например для дерева на рис. 12.5 это соответствует \[9 \| 3 \| 1 2 7\].
> На примере этих данных можно получить результат разделения, следуя ал- горитму на рис. 12.6.
1. Первый элемент прямого обхода 3 является значением корневого узла.
2. Найти индекс корневого узла 3 в inorder -- используя этот индекс, можно разделить inorder на \[9 \| 3 \| 1 2 7\].
3. На основании результата разделения inorder легко определить, что ко- личество узлов в левом и правом поддеревьях составляет 1 и 3 соответ- ственно. Таким образом, можно разделить preorder на \[3 \| 9 \| 2 1 7\].
> ![](ru/docs/assets/media/image729.jpeg)**Первый элемент является корневым узлом**
>
> Прямой обход
>
> preorder
>
> **Поиск индекса корневого узла**
>
> Симметричный обход inorder
>
> Левое поддерево имеет **1** узел
Прямой обход
> Правое поддерево имеет **3** узла
>
> **Рис. 12.6.** Разделение поддеревьев в прямом и симметричном обходах
##### Описание интервалов поддеревьев на основе переменных
> По вышеописанному методу разделения **мы получили интервалы индек- сов корневого узла**, **левого и правого поддеревьев в** preorder и inorder. Для описания этих интервалов индексов необходимо использовать несколько указателей.
- Индекс корневого узла текущего дерева в preorder обозначим как *i*.
- Индекс корневого узла текущего дерева в inorder обозначим как *m*.
- Интервал индексов текущего дерева в inorder обозначим как \[*l*, *r*\].
> С помощью этих переменных можно описать индекс корневого узла в preorder
>
> и интервал индексов поддеревьев в inorder, как показано в табл. 12.1.
>
> Обратите внимание, что значение (*m* -- l) в индексе корневого узла правого поддерева означает количество узлов левого поддерева, рекомендуется разо- брать эту таблицу вместе с рис. 12.7.
>
> **Таблица 12.1.** Индексы корневого узла и поддеревьев в прямом и симметричном обходах
+--------------------+----------------------------------------+-------------------------------------------------+
| | > **Индекс корневого узла в** preorder | > **Интервал индексов подде- ревьев в** inorder |
+====================+========================================+=================================================+
| > Текущее дерево | > i | > \[*l*, *r*\] |
+--------------------+----------------------------------------+-------------------------------------------------+
| > Левое поддерево | > *i* + 1 | > \[*l*, *m* -- 1\] |
+--------------------+----------------------------------------+-------------------------------------------------+
| > Правое поддерево | > *i* + 1 + (*m* -- l) | > \[*m* + 1, r\] |
+--------------------+----------------------------------------+-------------------------------------------------+
![](ru/docs/assets/media/image731.jpeg)
> **Рис. 12.7.** Представление интервалов индексов корневого узла, левого и правого поддеревьев
##### Код реализации
> Для повышения эффективности поиска средней точки *m* используется хеш- таблица hmap, в которой хранятся отображения элементов массива inorder в индексы.
>
> \# === File: build_tree.py === def dfs(
>
> preorder: list\[int\], inorder_map: dict\[int, int\], i: int,
>
> l: int,
>
> r: int,
>
> ) -\> TreeNode \| None:
>
> \"\"\" Построение двоичного дерева: \"разделяй и властвуй\".\"\"\" \# Завершение, если интервал поддерева пуст.
>
> if r - l \< 0:
>
> return None
>
> \# Инициализация корневого узла. root = TreeNode(preorder\[i\])
>
> \# Поиск m для разделения на левое и правое поддеревья. m = inorder_map\[preorder\[i\]\]
>
> \# Подзадача: построение левого поддерева.
>
> root.left = dfs(preorder, inorder_map, i + 1, l, m - 1) \# Подзадача: построение правого поддерева.
>
> root.right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r)
>
> \# Возврат корневого узла. return root
>
> def build_tree(preorder: list\[int\], inorder: list\[int\]) -\> TreeNode \| None: \"\"\" Построение двоичного дерева.\"\"\"
>
> \# Инициализация хеш-таблицы для хранения отображения элементов inorder
>
> [в]{.smallcaps} индексы.
>
> inorder_map = {val: i for i, val in enumerate(inorder)} root = dfs(preorder, inorder_map, 0, 0, len(inorder) - 1) return root
>
> На рис. 12.8 демонстрируется рекурсивный процесс построения двоичного дерева, в котором каждый узел создается в процессе спуска, а каждое ребро (ссылка) создается в процессе подъема.
![](ru/docs/assets/media/image733.jpeg)![](ru/docs/assets/media/image735.jpeg)
> **Рис. 12.8.** Рекурсивный процесс построения двоичного дерева. Шаги 1--2
>
> ![](ru/docs/assets/media/image737.jpeg)
![](ru/docs/assets/media/image739.jpeg)![](ru/docs/assets/media/image741.jpeg)
> **Рис. 12.8.** *Продолжение*. Шаги 3--5
>
> ![](ru/docs/assets/media/image743.jpeg)
![](ru/docs/assets/media/image745.jpeg)![](ru/docs/assets/media/image747.jpeg)
> **Рис. 12.8.** *Продолжение*. Шаги 6--8
>
> ![](ru/docs/assets/media/image749.jpeg)
>
> **Рис. 12.8.** *Окончание*. Шаг 9
>
> Результаты разделения прямого обхода preorder и симметричного обхода in- order в каждом рекурсивном вызове показаны на рис. 12.9.
![](ru/docs/assets/media/image751.jpeg)
> **Рис. 12.9.** Результаты разделения в каждом рекурсивном вызове
>
> Пусть количество узлов в дереве равно *n*, инициализация каждого узла (выполнение одного рекурсивного вызова dfs()) занимает время *O*(1). **Сле- довательно**, **общая временная сложность составляет** *O*(*n*).
>
> Хеш-таблица хранит отображение элементов inorder в индексы, про- странственная сложность составляет *O*(*n*). В худшем случае, когда двоичное дерево вырождается в список, глубина рекурсии достигает *n*, что требует *O*(*n*) пространства стека. **Поэтому общая пространственная сложность составляет** *O*(*n*).
#### задача о ханойских башнях
> В алгоритмах сортировки слиянием и построения двоичного дерева мы раз- бивали исходную задачу на две подзадачи, каждая из которых имела полови- ну размера исходной задачи. Однако для задачи о Ханойских башнях исполь- зуется другая стратегия разбиения.
![](ru/docs/assets/media/image753.jpeg)
> **Рис. 12.10.** Пример задачи о Ханойских башнях
>
> Обозначим задачу о Ханойских башнях с *i* дисками как *f*(*i*). Например, *f*(3) соответствует задаче о перемещении 3 дисков с A на C.
##### 1. Базовый случай
> Для случая *f*(1), когда имеется только один диск, можно просто переместить единственный диск с A на C, как показано на рис. 12.11.
![](ru/docs/assets/media/image755.png)
![](ru/docs/assets/media/image757.jpeg)
> **Рис. 12.11.** Решение задачи размера 1
>
> Для задачи *f*(2), когда имеется два диска, уже требуется соблюдать условие, что меньший диск находится на большем. **Поэтому для выполнения пере- мещения потребуется использовать стержень** B.
1. Сначала переместить верхний диск с A на B.
2. Затем переместить большой диск с A на C.
3. ![](ru/docs/assets/media/image759.jpeg)Переместить маленький диск с B на C.
> **Рис. 12.12.** Решение задачи размера 2. Шаг 1
>
> ![](ru/docs/assets/media/image761.jpeg)
![](ru/docs/assets/media/image763.jpeg)![](ru/docs/assets/media/image765.jpeg)
> **Рис. 12.12.** *Окончание*. Шаги 2--4
>
> Процесс решения задачи *f*(2) можно кратко описать следующим образом: **переместить два диска с** A **на** C **с помощью** B. Здесь C называется целевым стержнем, а B -- вспомогательным стержнем.
##### Разделение на подзадачи
> Для задачи *f*(3), когда имеется три диска, ситуация становится несколько сложнее. Поскольку решения *f*(1) и *f*(2) уже известны, можно рассмотреть задачу с точ- ки зрения метода «разделяй и властвуй». Можно считать два верхних диска на A единым целым и выполнить шаги, показанные на рис. 12.13. Таким образом,
>
> три диска успешно переместятся с A на C.
1. Пусть B будет целевым стержнем, а C -- вспомогательным. Переместить два диска с A на B.
2. Переместить оставшийся диск с A непосредственно на C.
3. Пусть C будет целевым стержнем, а A -- вспомогательным стержнем. Пе- реместить два диска с B на C.
![](ru/docs/assets/media/image767.jpeg)![](ru/docs/assets/media/image769.jpeg)
> **Рис. 12.13.** Решение задачи размера 3. Шаги 1-2
>
> ![](ru/docs/assets/media/image771.jpeg)
![](ru/docs/assets/media/image773.jpeg)
> **Рис. 12.13.** *Окончание*. Шаги 3--4
>
> По сути, **задача** *f*(3) **делится на две подзадачи** *f*(2) **и одну подзадачу** *f*(1). После последовательного решения этих трех подзадач исходная задача также решается. Это показывает, что подзадачи независимы, и их решения можно объединить.
>
> Таким образом, можно обобщить стратегию «разделяй и властвуй» для ре- шения задачи Ханойской башни, как показано на рис. 12.14: разделить исход- ную задачу *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) уже известно и требует только одного перемещения.
>
> ![](ru/docs/assets/media/image775.jpeg)
>
> **Рис. 12.14.** Стратегия «разделяй и властвуй» для решения задачи Ханойской башни
##### Код реализации
> В коде объявляется рекурсивная функция dfs(i, src, buf, tar), которая пере- мещает *i* дисков с вершины стержня src на целевой стержень tar с помощью вспомогательного стержня buf.
>
> \# === File: hanota.py ===
>
> def move(src: list\[int\], tar: list\[int\]): \"\"\" Перемещение одного диска.\"\"\"
>
> \# Извлечение диска с вершины src. pan = src.pop()
>
> \# Помещение диска на вершину tar. tar.append(pan)
>
> def dfs(i: int, src: list\[int\], buf: list\[int\], tar: list\[int\]): \"\"\" Решение задачи Ханойской башни f(i).\"\"\"
>
> \# Если в src остался только один диск, то переместить его на tar. if i == 1:
>
> move(src, tar) return
>
> \# Подзадача f(i-1): переместить i-1 дисков с вершины src на buf с помощью tar. dfs(i - 1, src, tar, buf)
>
> \# Подзадача f(1): переместить оставшийся диск с src на tar. move(src, tar)
>
> \# Подзадача f(i-1): переместить i-1 дисков с вершины buf на tar с помощью src. dfs(i - 1, buf, src, tar)
1. Резюме ❖ **363**
> def solve_hanota(A: list\[int\], B: list\[int\], C: list\[int\]): \"\"\" Решение задачи Ханойской башни.\"\"\"
>
> n = len(A)
>
> \# Переместить n дисков с вершины A на C с помощью B. dfs(n, A, B, C)
>
> Задача Ханойской башни формирует рекурсивное дерево высотой *n*, каж- дый узел которого представляет подзадачу, соответствующую вызову функции dfs(), как показано на рис. 12.15. **Поэтому временная сложность составляет** *O*(2*n*), **а пространственная сложность** -- *O*(*n*).
![](ru/docs/assets/media/image777.jpeg)
> **Рис. 12.15.** Рекурсивное дерево задачи Ханойской башни
>
> **12.5. резюме**
- «Разделяй и властвуй» -- это распространенная стратегия разработки ал- горитмов, включающая два этапа -- разделение (декомпозиция) и объ- единение (синтез) -- и обычно реализуемая с помощью рекурсии.
<!-- -->
- Критерии применимости этой стратегии к задаче включают: возмож- ность декомпозиции задачи, независимость подзадач и возможность их объединения.
- Сортировка слиянием -- это типичное применение стратегии «разделяй и властвуй». Эта сортировка рекурсивно разделяет массив на два под- массива равной длины, пока не останется массив из одного элемента. После чего начинается поэтапное объединение.
- Введение стратегии «разделяй и властвуй» часто позволяет повысить эффективность алгоритма. С одной стороны, стратегия уменьшает коли- чество операций. С другой стороны, после разделения она способствует оптимизации для параллельного выполнения.
- Принцип «разделяй и властвуй» не только позволяет решать множество алгоритмических задач, но и широко применяется в проектировании структур данных и алгоритмов, его можно встретить повсюду.
- Адаптивный поиск более эффективен по сравнению с полным перебо- ром. Алгоритмы поиска со сложностью *O*(log *n*) обычно реализуются на основе стратегии «разделяй и властвуй».
- Двоичный поиск -- это еще одно типичное применение стратегии «разде- ляй и властвуй», которое не содержит этап объединения решений подзадач. Двоичный поиск можно реализовать с помощью рекурсивного подхода.
- Задачу построения двоичного дерева можно разделить на построение левого и правого поддеревьев (подзадачи), что достигается путем раз- деления индексов в порядке предварительного и симметричного обхода.
- Задачу Ханойской башни размера *n* можно разделить на две подзадачи размера *n* -- 1 и одну подзадачу размера 1. После последовательного ре- шения этих трех подзадач исходная задача будет также решена.
> Глава 13