【演算法】分治四步走

Nemo&發表於2021-03-26

分治法在每一層遞迴上都有三個步驟:
1 ) 分解:將原問題分解為若干個規模較小,相互獨立,與原問題形式相同的子問題
2 ) 解決:若子問題規模較小而容易被解決則直接解,否則遞迴地解各個子問題
3 ) 合併:將各個子問題的解合併為原問題的解。

分治四步走

  1. 明確分解策略:明確大問題通過怎樣的分解策略一步步分解為最終的小問題,之後我們需要根據分解策略明確函式的功能
    比如說,我們的分解策略如果是折半分解,那麼我們的函式就需要有範圍域來確定分解範圍;如果是遞減分解,那麼我們的函式需要有計數,來記錄遞減分解結果。

    比如說,快速排序的大的問題可以分解為就是將n個元素擺到正確位置,漢諾塔的大的問題就是將n個圓盤由下而上擺到正確位置。

  2. 尋找最小問題:最小問題也就是大問題的最簡化版本,問題的起始狀態,最小子問題即是出口。

  3. 解決次小問題:使用分解策略將大問題分解為次小的問題。次小問題也就是介於最小問題與大問題之間的問題,比最小問題稍稍大那麼一點,這使得次小問題具有解決大問題的通用性,即 可以通過次小問題找到大問題的通解。由次小問題得到解決方法。

    比如說,快速排序的次小問題就是將一個元素擺到正確位置,漢諾塔的次小問題就是將一個最下面的圓盤擺到正確位置。

  4. 合併次小問題:這個按照問題需要進行新增。

【演算法】分治四步走

明確分解策略

第一步,明確怎麼把大的問題一步步分解為最終的小問題;並明確這個函式的功能是什麼,它要完成什麼樣的一件事。
分解策略:大問題 = n * 小問題。如果大問題是一個陣列,那麼小問題就是陣列中的一個元素。

比如說,快速排序演算法的大問題就是將陣列中的n個元素進行排序擺放到正確的位置,那麼分解而成的小問題就是將陣列中的一個元素擺放到正確的位置。
而漢諾塔的大問題就是將A柱子上的n個盤子借用B柱子由大到小放在C柱子上,那麼分解而成的小問題就是將A柱子上的最底下的最大盤子借用B放在C柱子上。

而這個功能,是完全由你自己來定義的。也就是說,我們先不管函式裡面的程式碼是什麼、怎麼寫,而首先要明白,你這個函式是要用來幹什麼的。

例如:找出一個陣列中的最大值
要做出這個題,
第一步,要明確我們的分解策略,這裡我的分解策略是折半分解;
既然分解策略是折半分解,那麼我們即將要寫出的這個函式必須指明分解範圍,不然沒有辦法進行折半分解。

明確分解策略:大問題=從n個元素中找到最大的數字,折半分解,小問題=從2個元素比較大小找到最大數字。

//找出一個陣列中的最大值
// 明確分解策略:大問題=從n個元素中找到最大的數字並返回,折半分解,小問題=從2個元素比較大小找到最大數字並返回。
int f(int[] nums, int l, int r) {
    
}

尋找最小問題(初始條件)

分治就是在函式實現的內部程式碼中,將大問題不斷的分解為次小問題,再將小問題進一步分解為更小的問題。所以,我們必須要找出分治的結束條件,即 給定一個分解的閾值,不然的話,它會一直分解自己,無窮無盡。

【演算法】分治四步走

  • 必須有一個明確的結束條件。因為分治就是有“分”“並”,所以必須又有一個明確的點,到了這個點,就不用“分解下去”,而是開始“合併”。

第二步,我們需要找出當引數為何值、分解到何種程度時,分治結束,之後直接把結果返回。
一般為初始條件,然後從初始條件一步一步擴充到最終結果

注意:這個時候我們必須能根據這個引數的值,能夠直接知道函式的結果是什麼。

讓我們繼續完善上面那個最大函式。
第二步,尋找最小問題:
當l>=r時,即 分解到只剩一個元素了,我們能夠直接知道f(l)就是最大值
那麼遞迴出口就是l>=r時函式返回f(l)。
如下:

// 明確分解策略:大問題=從n個元素中找到最大的數字並返回,折半分解,小問題=從2個元素比較大小找到最大數字並返回。
int f(int[] nums, int l, int r) {

      // 尋找最小問題:最小問題即是隻有一個元素的時候
      if (l >= r) {
            return nums[l];
      }
}

當然,當l>=r時,我們也是知道f(r)等於多少的,f(r)也可以作為遞迴出口。分治出口可能並不唯一的。

解決次小問題

第三步,之前我們明確了分解策略,現在正是使用的時候了,我們需要使用這個分解策略,將大問題分解為次小問題。這樣就能一步步分解到最小問題,然後作為函式出口。

  • 最小問題:分解至只剩一個元素,l>=r,f(l)即為最大值
  • 分解策略:折半分解,f(nums, l, r) → f(nums, l, (l+r)/2) , f(nums, (l+r)/2+1, r)

分治:

  • 分:將f(nums, l, r) → f(nums, l, (l+r)/2) , f(nums, (l+r)/2+1, r)了。這樣,問題就由n縮小為了n/2,我們只需要找到這n/2個元素的最大值即可。就這樣慢慢從f(n),f(n/2)“分”到f(1)。
  • 並:這樣就可以從1,一步一步“並”到n/2,n...
// 明確分解策略:大問題=從n個元素中找到最大的數字並返回,折半分解,小問題=從2個元素比較大小找到最大數字並返回。
int f(int[] nums, int l, int r) {

      // 尋找最小問題:最小問題即是隻有一個元素的時候
      if (l >= r) {
            return nums[l];
      }

      // 使用分解策略
      int lMax = f(nums, l, (l+r)/2);
      int rMax = f(nums, (l+r)/2+1, r);
}

第四步:解決次小問題。
上一步我們將大問題分解為了次小問題,那麼這個次小問題怎麼解決呢,解決這個次小問題,也是為我們接下來的分解提供問題的解決方案,所以不能大意。
解決次小問題的方法,就是比較兩個次小問題的大小,得出最大的一個值,問題即可解決。

// 明確分解策略:大問題=從n個元素中找到最大的數字並返回,折半分解,小問題=從2個元素比較大小找到最大數字並返回。
int f(int[] nums, int l, int r) {

      // 尋找最小問題:最小問題即是隻有一個元素的時候
      if (l >= r) {
            return nums[l];
      }

      // 使用分解策略
      int lMax = f(nums, l, (l+r)/2);
      int rMax = f(nums, (l+r)/2+1, r);

      // 解決次小問題:比較兩個元素得到最大的數字
      return lMax > rMax ? lMax : rMax;
}

合併次小問題

這裡的合併就是解決次小問題,即 比較兩個元素得到最大的數字。

到這裡,分治五步走就完成了,那麼這個分治函式的功能我們也就實現了。
可能初學的讀者會感覺很奇妙,這就能得到最大值了?
那麼,我們來一步一步推一下。
假設n為陣列中元素的個數,f(n)則為n個元素的最大值
f(1)只有一個元素,可以得到一個確定的值
f(2)比較f(1)的值,也能確定了
f(4)比較f(2)的值,也能確定下來了
...
f(n/2)
f(n)比較f(n/2)也能確定下來了
你看看是不是解決了,n都能分治得到結果!

例項

最大數

在一個陣列中找最大的數字。

分解策略:對半分

從l到r中找到最大的一個元素。

// 明確分解策略:大問題=從n個元素中找到最大的數字並返回,折半分解,小問題=從2個元素比較大小找到最大數字並返回。
int f(int[] nums, int l, int r) {

      // 尋找最小問題:最小問題即是隻有一個元素的時候
      if (l >= r) {
            return nums[l];
      }

      // 使用分解策略
      int lMax = f(nums, l, (l+r)/2);
      int rMax = f(nums, (l+r)/2+1, r);

      // 解決次小問題:比較兩個元素得到最大的數字
      return lMax > rMax ? lMax : rMax;
}

漢諾塔

漢諾塔的傳說

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

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

漢諾塔遊戲的演示和思路分析:

1 ) 如果是有一個盤,A->C

如果我們有 n>= 2 情況,我們總是可以看做是兩個盤 1 .最下邊的盤 2. 上面的盤
2 ) 先把 最上面的盤A->B
3 ) 把最下邊的盤A->C
4 ) 把B塔的所有盤 從 B->C

漢諾塔遊戲的程式碼實現:

看老師程式碼演示:

package com.atguigu.dac;

public class Hanoitower {

    public static void main(String[] args) {
        hanoiTower(10, 'A', 'B', 'C');
    }
    
    //漢諾塔的移動的方法
    //使用分治演算法
    // 明確分解策略:我們的問題是有n個盤子,可是如果是n個盤子的話我們不會分,不知道結果;如果盤子數量為1、2、3就好了,所以我們按盤子數依次減一分解
    public static void hanoiTower(int num, char a, char b, char c) {
        // 尋找最小問題:只有一個盤
        //如果只有一個盤
        if(num == 1) {
            System.out.println("第1個盤從 " + a + "->" + c);
        } else {
            // 解決次小問題:由於我們是按盤子數-1來進行分解的,所以次小問題是一個盤子和n-1個盤子的漢諾塔,將一個最下面的盤子擺放到正確的位置
            //如果我們有 n >= 2 情況,我們總是可以看做是兩個盤 1.最下邊的一個盤 2. 上面的所有盤
            //1. 先把 最上面的所有盤 A->B, 移動過程會使用到 c
            hanoiTower(num - 1, a, c, b);
            //2. 把最下邊的盤 A->C
            System.out.println("第" + num + "個盤從 " + a + "->" + c);
            //3. 把B塔的所有盤 從 B->C , 移動過程使用到 a塔  
            hanoiTower(num - 1, b, a, c);
        }
    }
}

相關文章