數據結構與算法——分治算法


介紹

分治法 是一種很重要的算法。字面上的解釋是 分而治之,把一個復雜的問題 分成兩個或更多的相同或相似的子問題,再把子問題分成更小的子問題.... 直到最后子問題可以簡單的直接求解,原問題的解即 子問題的解的合並

這個技巧是很多高效算法的基礎,比如 排序算法:快速排序歸並排序,傅里葉變換、快速傅里葉變換

分治算法可以 求解的一些經典問題 如:

  • 二分搜索
  • 大整數乘法
  • 棋盤覆蓋
  • 快速排序
  • 歸並排序
  • 線性時間選擇
  • 最接近點對問題
  • 循環賽日程表
  • 漢諾塔

回過頭看了下之前的快速排序和歸並排序,他們也是難點在於如何把一個大問題 分解 成一個小問題進行 解決,然后再 合並 小問題的結果。

基本步驟

分治法在每一層遞歸上都有三個步驟:

  • 分解:將原問題分解為若干個規模較小、相互獨立、與原問題形式相同的子問題
  • 解決:若子問題規模較小而容易被解決則直接解決,否則遞歸的解各個子問題
  • 合並:將各個子問題的解合並為原問題的解

分治算法的設計模式

分治(Divide-and-Conquer(P))的算法設計模式如下:

if |P| ≤ n0
  then return (ADHOC(P))
// 將 P 分解為較小的子問題 P1,P2...Pk
for i ← to k
do yi ← Divide-and-Conquer(Pi) // 遞歸解決 pi
T ← MERGE(y1,y2,..yk)          // 合並子問題
return(T)
  • |P|:表示問題 P 的規模

  • n0:為閥值,表示當問題 P 的規模不超過 n0 時,問題已容易直接解出,不必再繼續分解

  • ADHOC(P) :該分治法中的基本子算法,用於直接解小規模的問題 P

    因此,當 P 的規模不超過 n0 時,直接用 ADHOC(P) 求解。

  • MERGE(y1,y2,..yk):該分治法中的合並子算法,用於將 P 的子問題 P1、P2...Pk 的相應的解 y1、y2...yk 合並為 P 的解

實踐-漢諾塔

漢諾塔的傳說

漢諾塔(又稱河內塔)問題是源於印度一個古老傳說的益智玩具。大梵天創造世界的時候做了三根金剛石柱子,在一根柱子上從下往上按照大小順序摞着 64 片黃金圓盤。大梵天命令婆羅門把圓盤從下面開始按大小順序重新擺放在另一根柱子上。並且規定,在小圓盤上不能放大圓盤,在三根柱子之間一次只能移動一個圓盤。

假如每秒鍾一次,共需多長時間呢?移完這些金片需要 5845.54 億年以上,太陽系的預期壽命據說也就是數百億年。真的過了 5845.54 億年,地球上的一切生命,連同梵塔、廟宇等,都早已經灰飛煙滅。

如上圖所示:

  • 只有一個盤的時候:直接從 A → C
  • 有兩個盤的時候:
    1. A → B
    2. A → C
    3. B → C
  • 有三個盤的時候:
    1. A → C
    2. A → B
    3. C → B
    4. A → C
    5. B → A
    6. B → C
    7. A → C

從以上步驟來看:

  1. 當只有一個盤時,需要走 1 步
  2. 當有兩個盤時,需要走 3 步
  3. 當有三個盤時,需要走 7 步

當有 3 個盤的時候,就很復雜了:

  1. 我們就將最小問題規模限定為 1,只有一個盤時:A → C

  2. 當有 n ≥ 2 的情況,我們總是可以看做是兩個盤:

    1. 最下邊的盤
    2. 最上邊的盤

    那么執行兩個盤的操作步驟:

    1. A → B:最上面的盤移動
    2. A → C:最下面的盤移動
    3. B → C:最上面的盤移動
/**
 * 漢諾塔算法
 */
public class Hanoitower {
    /**
     * 漢諾塔算法
     *
     * @param num 有幾個盤子
     * @param a   a 柱子
     * @param b   b 柱子
     * @param c   c 柱子
     */
    public void hanoiTower(int num, char a, char b, char c) {
        // 當只有一個盤時:直接從 a -> c
        if (num == 1) {
//            System.out.printf("第 1 個盤從 %s → %s \n", a, c);
            System.out.printf("第 %d 個盤從 %s → %s \n", num, a, c);
        } else {
            // 否則,始終看成只有兩個盤
            // 1. 最上面的盤:a -> b, 中間會用到 c
            // 因為最小規模是只有一個盤的時候,直接移動到 c
            hanoiTower(num - 1, a, c, b);
            // 2. 最下面的盤:a -> c
            System.out.printf("第 %d 個盤從 %s → %s \n", num, a, c);
            // 3. 把 B 塔所有的盤,移動到 c:b -> c, 移動過程中使用到 a
            hanoiTower(num - 1, b, a, c);
        }
    }

    @Test
    public void han1() {
        hanoiTower(1, 'A', 'B', 'C');
    }

    @Test
    public void han2() {
        hanoiTower(2, 'A', 'B', 'C');
    }

    @Test
    public void han3() {
        hanoiTower(3, 'A', 'B', 'C');
    }

    @Test
    public void han4() {
        hanoiTower(4, 'A', 'B', 'C');
    }

    @Test
    public void han5() {
        hanoiTower(5, 'A', 'B', 'C');
    }
}

測試輸出:

1 個盤

第 1 個盤從 A → C  

2 個盤

第 1 個盤從 A → B 
第 2 個盤從 A → C 
第 1 個盤從 B → C 

3 個盤

第 1 個盤從 A → C 
第 2 個盤從 A → B 
第 1 個盤從 C → B 
第 3 個盤從 A → C 
第 1 個盤從 B → A 
第 2 個盤從 B → C 
第 1 個盤從 A → C 

4 個盤

第 1 個盤從 A → B 
第 2 個盤從 A → C 
第 1 個盤從 B → C 
第 3 個盤從 A → B 
第 1 個盤從 C → A 
第 2 個盤從 C → B 
第 1 個盤從 A → B 
第 4 個盤從 A → C 
第 1 個盤從 B → C 
第 2 個盤從 B → A 
第 1 個盤從 C → A 
第 3 個盤從 B → C 
第 1 個盤從 A → B 
第 2 個盤從 A → C 
第 1 個盤從 B → C 

5 個盤

第 1 個盤從 A → C 
第 2 個盤從 A → B 
第 1 個盤從 C → B 
第 3 個盤從 A → C 
第 1 個盤從 B → A 
第 2 個盤從 B → C 
第 1 個盤從 A → C 
第 4 個盤從 A → B 
第 1 個盤從 C → B 
第 2 個盤從 C → A 
第 1 個盤從 B → A 
第 3 個盤從 C → B 
第 1 個盤從 A → C 
第 2 個盤從 A → B 
第 1 個盤從 C → B 
第 5 個盤從 A → C 
第 1 個盤從 B → A 
第 2 個盤從 B → C 
第 1 個盤從 A → C 
第 3 個盤從 B → A 
第 1 個盤從 C → B 
第 2 個盤從 C → A 
第 1 個盤從 B → A 
第 4 個盤從 B → C 
第 1 個盤從 A → C 
第 2 個盤從 A → B 
第 1 個盤從 C → B 
第 3 個盤從 A → C 
第 1 個盤從 B → A 
第 2 個盤從 B → C 
第 1 個盤從 A → C 

盤子越大編號越小,上面的第 n 個盤是和盤子大小對應的。

代碼簡單,但是這個為什么會實現這種效果,筆者還是看得很懵逼。最主要的是如何分解,學算法是要理解它的思想。

這里主要的難點在於:

  1. 分解到只有 1 個盤子時,才會移動第一步
  2. 每次分解,那個 a、b、c 代表的柱子,完全不一樣

靠腦袋去分析這個流程很難,只能當問題規模足夠小的時候,比如只有 1 個盤子,只有 2 個盤子時,你還很容易分清楚,多了遞歸下去,你就搞不懂了


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM