# Разделяй и властвуй ![](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