feat: Traditional Chinese version (#1163)
* First commit * Update mkdocs.yml * Translate all the docs to traditional Chinese * Translate the code files. * Translate the docker file * Fix mkdocs.yml * Translate all the figures from SC to TC * 二叉搜尋樹 -> 二元搜尋樹 * Update terminology. * Update terminology * 构造函数/构造方法 -> 建構子 异或 -> 互斥或 * 擴充套件 -> 擴展 * constant - 常量 - 常數 * 類 -> 類別 * AVL -> AVL 樹 * 數組 -> 陣列 * 係統 -> 系統 斐波那契數列 -> 費波那契數列 運算元量 -> 運算量 引數 -> 參數 * 聯絡 -> 關聯 * 麵試 -> 面試 * 面向物件 -> 物件導向 歸併排序 -> 合併排序 范式 -> 範式 * Fix 算法 -> 演算法 * 錶示 -> 表示 反碼 -> 一補數 補碼 -> 二補數 列列尾部 -> 佇列尾部 區域性性 -> 區域性 一摞 -> 一疊 * Synchronize with main branch * 賬號 -> 帳號 推匯 -> 推導 * Sync with main branch * First commit * Update mkdocs.yml * Translate all the docs to traditional Chinese * Translate the code files. * Translate the docker file * Fix mkdocs.yml * Translate all the figures from SC to TC * 二叉搜尋樹 -> 二元搜尋樹 * Update terminology * 构造函数/构造方法 -> 建構子 异或 -> 互斥或 * 擴充套件 -> 擴展 * constant - 常量 - 常數 * 類 -> 類別 * AVL -> AVL 樹 * 數組 -> 陣列 * 係統 -> 系統 斐波那契數列 -> 費波那契數列 運算元量 -> 運算量 引數 -> 參數 * 聯絡 -> 關聯 * 麵試 -> 面試 * 面向物件 -> 物件導向 歸併排序 -> 合併排序 范式 -> 範式 * Fix 算法 -> 演算法 * 錶示 -> 表示 反碼 -> 一補數 補碼 -> 二補數 列列尾部 -> 佇列尾部 區域性性 -> 區域性 一摞 -> 一疊 * Synchronize with main branch * 賬號 -> 帳號 推匯 -> 推導 * Sync with main branch * Update terminology.md * 操作数量(num. of operations)-> 操作數量 * 字首和->前綴和 * Update figures * 歸 -> 迴 記憶體洩漏 -> 記憶體流失 * Fix the bug of the file filter * 支援 -> 支持 Add zh-Hant/README.md * Add the zh-Hant chapter covers. Bug fixes. * 外掛 -> 擴充功能 * Add the landing page for zh-Hant version * Unify the font of the chapter covers for the zh, en, and zh-Hant version * Move zh-Hant/ to zh-hant/ * Translate terminology.md to traditional Chinese
|
After Width: | Height: | Size: 21 KiB |
@@ -0,0 +1,45 @@
|
||||
# 分治搜尋策略
|
||||
|
||||
我們已經學過,搜尋演算法分為兩大類。
|
||||
|
||||
- **暴力搜尋**:它透過走訪資料結構實現,時間複雜度為 $O(n)$ 。
|
||||
- **自適應搜尋**:它利用特有的資料組織形式或先驗資訊,時間複雜度可達到 $O(\log n)$ 甚至 $O(1)$ 。
|
||||
|
||||
實際上,**時間複雜度為 $O(\log n)$ 的搜尋演算法通常是基於分治策略實現的**,例如二分搜尋和樹。
|
||||
|
||||
- 二分搜尋的每一步都將問題(在陣列中搜索目標元素)分解為一個小問題(在陣列的一半中搜索目標元素),這個過程一直持續到陣列為空或找到目標元素為止。
|
||||
- 樹是分治思想的代表,在二元搜尋樹、AVL 樹、堆積等資料結構中,各種操作的時間複雜度皆為 $O(\log n)$ 。
|
||||
|
||||
二分搜尋的分治策略如下所示。
|
||||
|
||||
- **問題可以分解**:二分搜尋遞迴地將原問題(在陣列中進行查詢)分解為子問題(在陣列的一半中進行查詢),這是透過比較中間元素和目標元素來實現的。
|
||||
- **子問題是獨立的**:在二分搜尋中,每輪只處理一個子問題,它不受其他子問題的影響。
|
||||
- **子問題的解無須合併**:二分搜尋旨在查詢一個特定元素,因此不需要將子問題的解進行合併。當子問題得到解決時,原問題也會同時得到解決。
|
||||
|
||||
分治能夠提升搜尋效率,本質上是因為暴力搜尋每輪只能排除一個選項,**而分治搜尋每輪可以排除一半選項**。
|
||||
|
||||
### 基於分治實現二分搜尋
|
||||
|
||||
在之前的章節中,二分搜尋是基於遞推(迭代)實現的。現在我們基於分治(遞迴)來實現它。
|
||||
|
||||
!!! question
|
||||
|
||||
給定一個長度為 $n$ 的有序陣列 `nums` ,其中所有元素都是唯一的,請查詢元素 `target` 。
|
||||
|
||||
從分治角度,我們將搜尋區間 $[i, j]$ 對應的子問題記為 $f(i, j)$ 。
|
||||
|
||||
以原問題 $f(0, n-1)$ 為起始點,透過以下步驟進行二分搜尋。
|
||||
|
||||
1. 計算搜尋區間 $[i, j]$ 的中點 $m$ ,根據它排除一半搜尋區間。
|
||||
2. 遞迴求解規模減小一半的子問題,可能為 $f(i, m-1)$ 或 $f(m+1, j)$ 。
|
||||
3. 迴圈第 `1.` 步和第 `2.` 步,直至找到 `target` 或區間為空時返回。
|
||||
|
||||
下圖展示了在陣列中二分搜尋元素 $6$ 的分治過程。
|
||||
|
||||

|
||||
|
||||
在實現程式碼中,我們宣告一個遞迴函式 `dfs()` 來求解問題 $f(i, j)$ :
|
||||
|
||||
```src
|
||||
[file]{binary_search_recur}-[class]{}-[func]{binary_search}
|
||||
```
|
||||
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,99 @@
|
||||
# 構建二元樹問題
|
||||
|
||||
!!! question
|
||||
|
||||
給定一棵二元樹的前序走訪 `preorder` 和中序走訪 `inorder` ,請從中構建二元樹,返回二元樹的根節點。假設二元樹中沒有值重複的節點(如下圖所示)。
|
||||
|
||||

|
||||
|
||||
### 判斷是否為分治問題
|
||||
|
||||
原問題定義為從 `preorder` 和 `inorder` 構建二元樹,是一個典型的分治問題。
|
||||
|
||||
- **問題可以分解**:從分治的角度切入,我們可以將原問題劃分為兩個子問題:構建左子樹、構建右子樹,加上一步操作:初始化根節點。而對於每棵子樹(子問題),我們仍然可以複用以上劃分方法,將其劃分為更小的子樹(子問題),直至達到最小子問題(空子樹)時終止。
|
||||
- **子問題是獨立的**:左子樹和右子樹是相互獨立的,它們之間沒有交集。在構建左子樹時,我們只需關注中序走訪和前序走訪中與左子樹對應的部分。右子樹同理。
|
||||
- **子問題的解可以合併**:一旦得到了左子樹和右子樹(子問題的解),我們就可以將它們連結到根節點上,得到原問題的解。
|
||||
|
||||
### 如何劃分子樹
|
||||
|
||||
根據以上分析,這道題可以使用分治來求解,**但如何透過前序走訪 `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: 34 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 19 KiB |
@@ -0,0 +1,91 @@
|
||||
# 分治演算法
|
||||
|
||||
<u>分治(divide and conquer)</u>,全稱分而治之,是一種非常重要且常見的演算法策略。分治通常基於遞迴實現,包括“分”和“治”兩個步驟。
|
||||
|
||||
1. **分(劃分階段)**:遞迴地將原問題分解為兩個或多個子問題,直至到達最小子問題時終止。
|
||||
2. **治(合併階段)**:從已知解的最小子問題開始,從底至頂地將子問題的解進行合併,從而構建出原問題的解。
|
||||
|
||||
如下圖所示,“合併排序”是分治策略的典型應用之一。
|
||||
|
||||
1. **分**:遞迴地將原陣列(原問題)劃分為兩個子陣列(子問題),直到子陣列只剩一個元素(最小子問題)。
|
||||
2. **治**:從底至頂地將有序的子陣列(子問題的解)進行合併,從而得到有序的原陣列(原問題的解)。
|
||||
|
||||

|
||||
|
||||
## 如何判斷分治問題
|
||||
|
||||
一個問題是否適合使用分治解決,通常可以參考以下幾個判斷依據。
|
||||
|
||||
1. **問題可以分解**:原問題可以分解成規模更小、類似的子問題,以及能夠以相同方式遞迴地進行劃分。
|
||||
2. **子問題是獨立的**:子問題之間沒有重疊,互不依賴,可以獨立解決。
|
||||
3. **子問題的解可以合併**:原問題的解透過合併子問題的解得來。
|
||||
|
||||
顯然,合併排序滿足以上三個判斷依據。
|
||||
|
||||
1. **問題可以分解**:遞迴地將陣列(原問題)劃分為兩個子陣列(子問題)。
|
||||
2. **子問題是獨立的**:每個子陣列都可以獨立地進行排序(子問題可以獨立進行求解)。
|
||||
3. **子問題的解可以合併**:兩個有序子陣列(子問題的解)可以合併為一個有序陣列(原問題的解)。
|
||||
|
||||
## 透過分治提升效率
|
||||
|
||||
**分治不僅可以有效地解決演算法問題,往往還可以提升演算法效率**。在排序演算法中,快速排序、合併排序、堆積排序相較於選擇、冒泡、插入排序更快,就是因為它們應用了分治策略。
|
||||
|
||||
那麼,我們不禁發問:**為什麼分治可以提升演算法效率,其底層邏輯是什麼**?換句話說,將大問題分解為多個子問題、解決子問題、將子問題的解合併為原問題的解,這幾步的效率為什麼比直接解決原問題的效率更高?這個問題可以從操作數量和平行計算兩方面來討論。
|
||||
|
||||
### 操作數量最佳化
|
||||
|
||||
以“泡沫排序”為例,其處理一個長度為 $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)$ 。
|
||||
|
||||
### 平行計算最佳化
|
||||
|
||||
我們知道,分治生成的子問題是相互獨立的,**因此通常可以並行解決**。也就是說,分治不僅可以降低演算法的時間複雜度,**還有利於作業系統的並行最佳化**。
|
||||
|
||||
並行最佳化在多核或多處理器的環境中尤其有效,因為系統可以同時處理多個子問題,更加充分地利用計算資源,從而顯著減少總體的執行時間。
|
||||
|
||||
比如在下圖所示的“桶排序”中,我們將海量的資料平均分配到各個桶中,則可所有桶的排序任務分散到各個計算單元,完成後再合併結果。
|
||||
|
||||

|
||||
|
||||
## 分治常見應用
|
||||
|
||||
一方面,分治可以用來解決許多經典演算法問題。
|
||||
|
||||
- **尋找最近點對**:該演算法首先將點集分成兩部分,然後分別找出兩部分中的最近點對,最後找出跨越兩部分的最近點對。
|
||||
- **大整數乘法**:例如 Karatsuba 演算法,它將大整數乘法分解為幾個較小的整數的乘法和加法。
|
||||
- **矩陣乘法**:例如 Strassen 演算法,它將大矩陣乘法分解為多個小矩陣的乘法和加法。
|
||||
- **河內塔問題**:河內塔問題可以透過遞迴解決,這是典型的分治策略應用。
|
||||
- **求解逆序對**:在一個序列中,如果前面的數字大於後面的數字,那麼這兩個數字構成一個逆序對。求解逆序對問題可以利用分治的思想,藉助合併排序進行求解。
|
||||
|
||||
另一方面,分治在演算法和資料結構的設計中應用得非常廣泛。
|
||||
|
||||
- **二分搜尋**:二分搜尋是將有序陣列從中點索引處分為兩部分,然後根據目標值與中間元素值比較結果,決定排除哪一半區間,並在剩餘區間執行相同的二分操作。
|
||||
- **合併排序**:本節開頭已介紹,不再贅述。
|
||||
- **快速排序**:快速排序是選取一個基準值,然後把陣列分為兩個子陣列,一個子陣列的元素比基準值小,另一子陣列的元素比基準值大,再對這兩部分進行相同的劃分操作,直至子陣列只剩下一個元素。
|
||||
- **桶排序**:桶排序的基本思想是將資料分散到多個桶,然後對每個桶內的元素進行排序,最後將各個桶的元素依次取出,從而得到一個有序陣列。
|
||||
- **樹**:例如二元搜尋樹、AVL 樹、紅黑樹、B 樹、B+ 樹等,它們的查詢、插入和刪除等操作都可以視為分治策略的應用。
|
||||
- **堆積**:堆積是一種特殊的完全二元樹,其各種操作,如插入、刪除和堆積化,實際上都隱含了分治的思想。
|
||||
- **雜湊表**:雖然雜湊表並不直接應用分治,但某些雜湊衝突解決方案間接應用了分治策略,例如,鏈式位址中的長鏈結串列會被轉化為紅黑樹,以提升查詢效率。
|
||||
|
||||
可以看出,**分治是一種“潤物細無聲”的演算法思想**,隱含在各種演算法與資料結構之中。
|
||||
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 23 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)$ 的過程可總結為:**將兩個圓盤藉助 `B` 從 `A` 移至 `C`** 。其中,`C` 稱為目標柱、`B` 稱為緩衝柱。
|
||||
|
||||
### 子問題分解
|
||||
|
||||
對於問題 $f(3)$ ,即當有三個圓盤時,情況變得稍微複雜了一些。
|
||||
|
||||
因為已知 $f(1)$ 和 $f(2)$ 的解,所以我們可從分治角度思考,**將 `A` 頂部的兩個圓盤看作一個整體**,執行下圖所示的步驟。這樣三個圓盤就被順利地從 `A` 移至 `C` 了。
|
||||
|
||||
1. 令 `B` 為目標柱、`C` 為緩衝柱,將兩個圓盤從 `A` 移至 `B` 。
|
||||
2. 將 `A` 中剩餘的一個圓盤從 `A` 直接移動至 `C` 。
|
||||
3. 令 `C` 為目標柱、`A` 為緩衝柱,將兩個圓盤從 `B` 移至 `C` 。
|
||||
|
||||
=== "<1>"
|
||||

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

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

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

|
||||
|
||||
從本質上看,**我們將問題 $f(3)$ 劃分為兩個子問題 $f(2)$ 和一個子問題 $f(1)$** 。按順序解決這三個子問題之後,原問題隨之得到解決。這說明子問題是獨立的,而且解可以合併。
|
||||
|
||||
至此,我們可總結出下圖所示的解決河內塔問題的分治策略:將原問題 $f(n)$ 劃分為兩個子問題 $f(n-1)$ 和一個子問題 $f(1)$ ,並按照以下順序解決這三個子問題。
|
||||
|
||||
1. 將 $n-1$ 個圓盤藉助 `C` 從 `A` 移至 `B` 。
|
||||
2. 將剩餘 $1$ 個圓盤從 `A` 直接移至 `C` 。
|
||||
3. 將 $n-1$ 個圓盤藉助 `A` 從 `B` 移至 `C` 。
|
||||
|
||||
對於這兩個子問題 $f(n-1)$ ,**可以透過相同的方式進行遞迴劃分**,直至達到最小子問題 $f(1)$ 。而 $f(1)$ 的解是已知的,只需一次移動操作即可。
|
||||
|
||||

|
||||
|
||||
### 程式碼實現
|
||||
|
||||
在程式碼中,我們宣告一個遞迴函式 `dfs(i, src, buf, tar)` ,它的作用是將柱 `src` 頂部的 $i$ 個圓盤藉助緩衝柱 `buf` 移動至目標柱 `tar` :
|
||||
|
||||
```src
|
||||
[file]{hanota}-[class]{}-[func]{solve_hanota}
|
||||
```
|
||||
|
||||
如下圖所示,河內塔問題形成一棵高度為 $n$ 的遞迴樹,每個節點代表一個子問題,對應一個開啟的 `dfs()` 函式,**因此時間複雜度為 $O(2^n)$ ,空間複雜度為 $O(n)$** 。
|
||||
|
||||

|
||||
|
||||
!!! quote
|
||||
|
||||
河內塔問題源自一個古老的傳說。在古印度的一個寺廟裡,僧侶們有三根高大的鑽石柱子,以及 $64$ 個大小不一的金圓盤。僧侶們不斷地移動圓盤,他們相信在最後一個圓盤被正確放置的那一刻,這個世界就會結束。
|
||||
|
||||
然而,即使僧侶們每秒鐘移動一次,總共需要大約 $2^{64} \approx 1.84×10^{19}$ 秒,合約 $5850$ 億年,遠遠超過了現在對宇宙年齡的估計。所以,倘若這個傳說是真的,我們應該不需要擔心世界末日的到來。
|
||||
@@ -0,0 +1,9 @@
|
||||
# 分治
|
||||
|
||||

|
||||
|
||||
!!! abstract
|
||||
|
||||
難題被逐層拆解,每一次的拆解都使它變得更為簡單。
|
||||
|
||||
分而治之揭示了一個重要的事實:從簡單做起,一切都不再複雜。
|
||||
@@ -0,0 +1,11 @@
|
||||
# 小結
|
||||
|
||||
- 分治是一種常見的演算法設計策略,包括分(劃分)和治(合併)兩個階段,通常基於遞迴實現。
|
||||
- 判斷是否是分治演算法問題的依據包括:問題能否分解、子問題是否獨立、子問題能否合併。
|
||||
- 合併排序是分治策略的典型應用,其遞迴地將陣列劃分為等長的兩個子陣列,直到只剩一個元素時開始逐層合併,從而完成排序。
|
||||
- 引入分治策略往往可以提升演算法效率。一方面,分治策略減少了操作數量;另一方面,分治後有利於系統的並行最佳化。
|
||||
- 分治既可以解決許多演算法問題,也廣泛應用於資料結構與演算法設計中,處處可見其身影。
|
||||
- 相較於暴力搜尋,自適應搜尋效率更高。時間複雜度為 $O(\log n)$ 的搜尋演算法通常是基於分治策略實現的。
|
||||
- 二分搜尋是分治策略的另一個典型應用,它不包含將子問題的解進行合併的步驟。我們可以透過遞迴分治實現二分搜尋。
|
||||
- 在構建二元樹的問題中,構建樹(原問題)可以劃分為構建左子樹和右子樹(子問題),這可以透過劃分前序走訪和中序走訪的索引區間來實現。
|
||||
- 在河內塔問題中,一個規模為 $n$ 的問題可以劃分為兩個規模為 $n-1$ 的子問題和一個規模為 $1$ 的子問題。按順序解決這三個子問題後,原問題隨之得到解決。
|
||||