First version.

This commit is contained in:
krahets
2026-01-20 15:08:42 +08:00
parent 2213a59ff6
commit 8071daddaa
106 changed files with 11790 additions and 0 deletions
@@ -0,0 +1,97 @@
# Задача о Ханойских башнях
В сортировке слиянием и построении двоичного дерева мы разбиваем исходную задачу на две подзадачи, размер которых составляет половину исходной задачи. Однако для задачи о Ханойских башнях мы используем другую стратегию разбиения.
!!! question
Даны три стержня, обозначенные как `A`, `B` и `C`. В начальном состоянии на стержне `A` находится $n$ дисков, расположенных сверху вниз в порядке возрастания размера. Наша задача -- переместить эти $n$ дисков на стержень `C`, сохранив их исходный порядок (как показано на рисунке ниже). При перемещении дисков необходимо соблюдать следующие правила.
1. Диск можно снять только с верхушки одного стержня и поместить на верхушку другого стержня.
2. За один раз можно переместить только один диск.
3. Меньший диск всегда должен находиться над большим диском.
![Пример задачи о Ханойских башнях](../assets/hanota_example.png)
**Обозначим задачу о Ханойских башнях размера $i$ как $f(i)$**. Например, $f(3)$ представляет задачу о Ханойских башнях по перемещению $3$ дисков с `A` на `C`.
### Рассмотрение базового случая
Как показано на рисунке ниже, для задачи $f(1)$, то есть когда имеется только один диск, мы можем просто переместить его с `A` на `C`.
=== "<1>"
![Решение задачи размера 1](../assets/hanota_f1_step1.png)
=== "<2>"
![hanota_f1_step2](../assets/hanota_f1_step2.png)
Как показано на рисунке ниже, для задачи $f(2)$, то есть когда имеется два диска, **поскольку необходимо всегда соблюдать условие, что меньший диск находится над большим, нужно использовать `B` для завершения перемещения**.
1. Сначала переместить верхний меньший диск с `A` на `B`.
2. Затем переместить больший диск с `A` на `C`.
3. Наконец, переместить меньший диск с `B` на `C`.
=== "<1>"
![Решение задачи размера 2](../assets/hanota_f2_step1.png)
=== "<2>"
![hanota_f2_step2](../assets/hanota_f2_step2.png)
=== "<3>"
![hanota_f2_step3](../assets/hanota_f2_step3.png)
=== "<4>"
![hanota_f2_step4](../assets/hanota_f2_step4.png)
Процесс решения задачи $f(2)$ можно обобщить следующим образом: **переместить два диска с `A` на `C`, используя `B`**. При этом `C` называется целевым стержнем, а `B` -- буферным стержнем.
### Разбиение на подзадачи
Для задачи $f(3)$, то есть когда имеется три диска, ситуация становится немного сложнее.
Поскольку решения $f(1)$ и $f(2)$ уже известны, мы можем мыслить с точки зрения стратегии «разделяй и властвуй», **рассматривая два верхних диска на `A` как единое целое**, и выполнить шаги, показанные на рисунке ниже. Таким образом, три диска успешно перемещаются с `A` на `C`.
1. Сделать `B` целевым стержнем, а `C` -- буферным стержнем, и переместить два диска с `A` на `B`.
2. Переместить оставшийся один диск с `A` непосредственно на `C`.
3. Сделать `C` целевым стержнем, а `A` -- буферным стержнем, и переместить два диска с `B` на `C`.
=== "<1>"
![Решение задачи размера 3](../assets/hanota_f3_step1.png)
=== "<2>"
![hanota_f3_step2](../assets/hanota_f3_step2.png)
=== "<3>"
![hanota_f3_step3](../assets/hanota_f3_step3.png)
=== "<4>"
![hanota_f3_step4](../assets/hanota_f3_step4.png)
По сути, **мы разбиваем задачу $f(3)$ на две подзадачи $f(2)$ и одну подзадачу $f(1)$**. После последовательного решения этих трех подзадач исходная задача также решается. Это показывает, что подзадачи независимы, и их решения можно объединить.
Таким образом, мы можем обобщить стратегию «разделяй и властвуй» для решения задачи о Ханойских башнях, показанную на рисунке ниже: разбить исходную задачу $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)$ известно -- требуется только одна операция перемещения.
![Стратегия «разделяй и властвуй» для решения задачи о Ханойских башнях](../assets/hanota_divide_and_conquer.png)
### Реализация кода
В коде мы объявляем рекурсивную функцию `dfs(i, src, buf, tar)`, которая перемещает $i$ дисков с верхушки стержня `src` на целевой стержень `tar`, используя буферный стержень `buf`:
```src
[file]{hanota}-[class]{}-[func]{solve_hanota}
```
Как показано на рисунке ниже, задача о Ханойских башнях формирует рекурсивное дерево высотой $n$, где каждый узел представляет подзадачу, соответствующую вызову функции `dfs()`, **поэтому временная сложность составляет $O(2^n)$, а пространственная сложность -- $O(n)$**.
![Рекурсивное дерево задачи о Ханойских башнях](../assets/hanota_recursive_tree.png)
!!! quote
Задача о Ханойских башнях происходит из древней легенды. В храме в древней Индии у монахов было три высоких алмазных стержня и $64$ золотых диска разного размера. Монахи постоянно перемещали диски, веря, что в тот момент, когда последний диск будет правильно размещен, этот мир закончится.
Однако, даже если монахи будут перемещать диски каждую секунду, потребуется примерно $2^{64} \approx 1.84×10^{19}$ секунд, что составляет около $5850$ миллиардов лет, что намного превышает текущую оценку возраста Вселенной. Поэтому, если эта легенда правдива, нам не стоит беспокоиться о конце света.