mirror of
https://github.com/krahets/hello-algo.git
synced 2026-05-05 03:52:41 +08:00
512 lines
44 KiB
Markdown
512 lines
44 KiB
Markdown
# Разделяй и властвуй
|
||
|
||
{width="3.5416655730533684in" height="4.583333333333333in"}
|
||
|
||
#### Стратегия «разделяй и властвуй»
|
||
|
||
> «*Разделяй и властвуй*» -- это важная и распространенная стратегия в алгорит- мах. Обычно она реализуется с помощью рекурсии и включает два этапа: раз- деление и объединение.
|
||
|
||
- **Разделение (этап разбиения)**: рекурсивное разбиение исходной зада- чи на две или более подзадачи до тех пор, пока не будет достигнута наи- меньшая подзадача.
|
||
|
||
- **Объединение (этап слияния)**: начиная с решения наименьших подза- дач, снизу вверх объединяются решения всех других подзадач, чтобы по- строить решение исходной задачи.
|
||
|
||
> Сортировка слиянием является типичным примером применения страте- гии «разделяй и властвуй» (см. рис. 12.1).
|
||
|
||
- **Разделение**: рекурсивное разбиение исходного массива (исходной за- дачи) на два подмассива (подзадачи) до тех пор, пока в подмассивах не останется по одному элементу (наименьшая подзадача).
|
||
|
||
- **Объединение**: снизу вверх объединяются упорядоченные подмассивы (решения подзадач), чтобы получить упорядоченный исходный массив (решение исходной задачи).
|
||
|
||

|
||
|
||
> **Рис. 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.
|
||
>
|
||
> 
|
||
>
|
||
> **Рис. 12.2.** Сортировка пузырьком до и после разбиения массива
|
||
>
|
||
> **Это означает**, **что при** *n* \> 4 **количество операций после разбиения мень- ше**, **и эффективность сортировки должна быть выше**. Обратите внимание, что временная сложность после разбиения остается квадратичной *O*(*n*2), но по- стоянный коэффициент в сложности уменьшается.
|
||
>
|
||
> **Если продолжить разбиение подмассивов пополам**, **пока в них не оста- нется по одному элементу**, то получится сортировка слиянием, временная сложность которой составляет *O*(*n* log *n*).
|
||
>
|
||
> А что, **если мы установим несколько дополнительных точек разделе- ния** и равномерно разделим исходный массив на *k* подмассивов? Эта ситуация очень похожа на блочную сортировку, которая хорошо подходит для сортиров- ки очень больших объемов данных, и теоретически ее временная сложность может достигать *O*(*n* + *k*).
|
||
|
||
##### Оптимизация параллельных вычислений
|
||
|
||
> Известно, что подзадачи, созданные методом «разделяй и властвуй», незави- симы друг от друга, **поэтому их обычно можно решать параллельно**. Таким образом, этот метод не только снижает временную сложность алгоритма, **но и способствует параллельной оптимизации операционной системы**.
|
||
>
|
||
> Параллельная оптимизация особенно эффективна в многоядерной или многопроцессорной среде, поскольку система может одновременно обраба- тывать несколько подзадач, более полно используя вычислительные ресурсы, что значительно сокращает общее время выполнения.
|
||
>
|
||
> Например, в блочной сортировке, изображенной на рис. 12.3, огромный объем данных равномерно распределяется по блокам. Задачи сортировки всех блоков можно распределить по вычислительным единицам, а затем объеди- нить результаты.
|
||
>
|
||
> 
|
||
>
|
||
> **Рис. 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 в массиве.
|
||
>
|
||
> 
|
||
>
|
||
> **Рис. 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)
|
||
|
||
#### задача построения двоичного дерева
|
||
|
||

|
||
|
||
> **Рис. 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\].
|
||
|
||
> **Первый элемент является корневым узлом**
|
||
>
|
||
> Прямой обход
|
||
>
|
||
> 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\] |
|
||
+--------------------+----------------------------------------+-------------------------------------------------+
|
||
|
||

|
||
|
||
> **Рис. 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 демонстрируется рекурсивный процесс построения двоичного дерева, в котором каждый узел создается в процессе спуска, а каждое ребро (ссылка) создается в процессе подъема.
|
||
|
||

|
||
|
||
> **Рис. 12.8.** Рекурсивный процесс построения двоичного дерева. Шаги 1--2
|
||
>
|
||
> 
|
||
|
||

|
||
|
||
> **Рис. 12.8.** *Продолжение*. Шаги 3--5
|
||
>
|
||
> 
|
||
|
||

|
||
|
||
> **Рис. 12.8.** *Продолжение*. Шаги 6--8
|
||
>
|
||
> 
|
||
>
|
||
> **Рис. 12.8.** *Окончание*. Шаг 9
|
||
>
|
||
> Результаты разделения прямого обхода preorder и симметричного обхода in- order в каждом рекурсивном вызове показаны на рис. 12.9.
|
||
|
||

|
||
|
||
> **Рис. 12.9.** Результаты разделения в каждом рекурсивном вызове
|
||
>
|
||
> Пусть количество узлов в дереве равно *n*, инициализация каждого узла (выполнение одного рекурсивного вызова dfs()) занимает время *O*(1). **Сле- довательно**, **общая временная сложность составляет** *O*(*n*).
|
||
>
|
||
> Хеш-таблица хранит отображение элементов inorder в индексы, про- странственная сложность составляет *O*(*n*). В худшем случае, когда двоичное дерево вырождается в список, глубина рекурсии достигает *n*, что требует *O*(*n*) пространства стека. **Поэтому общая пространственная сложность составляет** *O*(*n*).
|
||
|
||
#### задача о ханойских башнях
|
||
|
||
> В алгоритмах сортировки слиянием и построения двоичного дерева мы раз- бивали исходную задачу на две подзадачи, каждая из которых имела полови- ну размера исходной задачи. Однако для задачи о Ханойских башнях исполь- зуется другая стратегия разбиения.
|
||
|
||

|
||
|
||
> **Рис. 12.10.** Пример задачи о Ханойских башнях
|
||
>
|
||
> Обозначим задачу о Ханойских башнях с *i* дисками как *f*(*i*). Например, *f*(3) соответствует задаче о перемещении 3 дисков с A на C.
|
||
|
||
##### 1. Базовый случай
|
||
|
||
> Для случая *f*(1), когда имеется только один диск, можно просто переместить единственный диск с A на C, как показано на рис. 12.11.
|
||
|
||

|
||
|
||

|
||
|
||
> **Рис. 12.11.** Решение задачи размера 1
|
||
>
|
||
> Для задачи *f*(2), когда имеется два диска, уже требуется соблюдать условие, что меньший диск находится на большем. **Поэтому для выполнения пере- мещения потребуется использовать стержень** B.
|
||
|
||
1. Сначала переместить верхний диск с A на B.
|
||
|
||
2. Затем переместить большой диск с A на C.
|
||
|
||
3. Переместить маленький диск с B на C.
|
||
|
||
> **Рис. 12.12.** Решение задачи размера 2. Шаг 1
|
||
>
|
||
> 
|
||
|
||

|
||
|
||
> **Рис. 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.
|
||
|
||

|
||
|
||
> **Рис. 12.13.** Решение задачи размера 3. Шаги 1-2
|
||
>
|
||
> 
|
||
|
||

|
||
|
||
> **Рис. 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) уже известно и требует только одного перемещения.
|
||
>
|
||
> 
|
||
>
|
||
> **Рис. 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*).
|
||
|
||

|
||
|
||
> **Рис. 12.15.** Рекурсивное дерево задачи Ханойской башни
|
||
>
|
||
> **12.5. резюме**
|
||
|
||
- «Разделяй и властвуй» -- это распространенная стратегия разработки ал- горитмов, включающая два этапа -- разделение (декомпозиция) и объ- единение (синтез) -- и обычно реализуемая с помощью рекурсии.
|
||
|
||
<!-- -->
|
||
|
||
- Критерии применимости этой стратегии к задаче включают: возмож- ность декомпозиции задачи, независимость подзадач и возможность их объединения.
|
||
|
||
- Сортировка слиянием -- это типичное применение стратегии «разделяй и властвуй». Эта сортировка рекурсивно разделяет массив на два под- массива равной длины, пока не останется массив из одного элемента. После чего начинается поэтапное объединение.
|
||
|
||
- Введение стратегии «разделяй и властвуй» часто позволяет повысить эффективность алгоритма. С одной стороны, стратегия уменьшает коли- чество операций. С другой стороны, после разделения она способствует оптимизации для параллельного выполнения.
|
||
|
||
- Принцип «разделяй и властвуй» не только позволяет решать множество алгоритмических задач, но и широко применяется в проектировании структур данных и алгоритмов, его можно встретить повсюду.
|
||
|
||
- Адаптивный поиск более эффективен по сравнению с полным перебо- ром. Алгоритмы поиска со сложностью *O*(log *n*) обычно реализуются на основе стратегии «разделяй и властвуй».
|
||
|
||
- Двоичный поиск -- это еще одно типичное применение стратегии «разде- ляй и властвуй», которое не содержит этап объединения решений подзадач. Двоичный поиск можно реализовать с помощью рекурсивного подхода.
|
||
|
||
- Задачу построения двоичного дерева можно разделить на построение левого и правого поддеревьев (подзадачи), что достигается путем раз- деления индексов в порядке предварительного и симметричного обхода.
|
||
|
||
- Задачу Ханойской башни размера *n* можно разделить на две подзадачи размера *n* -- 1 и одну подзадачу размера 1. После последовательного ре- шения этих трех подзадач исходная задача будет также решена.
|
||
|
||
> Глава 13
|