動態規劃快速入門

更多內容,歡迎關注微信公衆號:全菜工程師小輝。公衆號回覆關鍵詞,領取免費學習資料。程序員

動態規劃算法一直是面試手撕算法中比較有挑戰的一種類型。不少的分配問題或者調度問題實際上均可能用動態規劃進行解決。(固然,若是問題的規模較大,有時候會抽象模型使用動歸來解決,有時候則能夠經過不斷迭代的機率算法解決查找次優解)面試

因此,動歸很重要,至少算法思想很重要。算法

什麼是動態規劃?

經過把原問題分解爲相對簡單的子問題的方式求解複雜問題的方法。動態規劃經常適用於有重疊子問題和最優子結構性質的問題。 > 最優子結構:當問題的最優解包含了其子問題的最優解時,稱該問題具備最優子結構性質。微信

> 重疊子問題:在用遞歸算法自頂向下解問題時,每次產生的子問題並不老是新問題,有些子問題被反覆計算屢次。動態規劃算法正是利用了這種子問題的重疊性質,對每個子問題只解一次,然後將其解保存在一個表格中,在之後儘量多地利用這些子問題的解。學習

不理解不用怕,結合後面題目來理解這些概念。這些概念徹底是已經會動歸的人來總結出來的,因此先理解動歸,而後再來看這些文縐縐的歸納。code

分治與動態規劃

共同點:
兩者都要求原問題具備最優子結構性質,都是將原問題分而治之,分解成若干個規模較小(小到很容易解決)的子問題。而後將子問題的解合併,造成原問題的解。遞歸

不一樣點:ip

  • 分治法將分解後的子問題當作相互獨立的,經過用遞歸來作。
  • 動態規劃將分解後的子問題理解爲相互間有聯繫,有重疊部分,須要記憶,一般用迭代來作。

動態規劃的步驟

問題建模

  1. 根據問題,找到【最優子結構】。把原問題從大化小的第一步,找到比當前問題要小一號的最好的結果,而通常狀況下當前問題能夠由最優子結構進行表示。
  2. 肯定問題的【邊界】。根據上述的最優子結構,一步一步從大化小,最終能夠獲得最小的,能夠一眼看出答案的最優子結構,也就是邊界。
  3. 經過上述兩步,經過分析最優子結構與最終問題之間的關係,咱們能夠獲得【狀態轉移方程】。

問題求解的各個方法

暴力枚舉:

全部的動態規劃問題均可以經過多層嵌套循環遍歷全部的可能,將符合條件的個數統計起來。只是時間複雜度是指數級的,因此不推薦。leetcode

遞歸:

  1. 遞歸的時間複雜度是由遞歸層數和最優子結構的個數決定的。
  2. 在爬階梯問題,最少找零錢問題中,遞歸的時間複雜度和空間複雜度都比動歸方法的差,可是在國王與金礦的問題中,不一樣的數據規模,動歸方法的時間複雜度和空間複雜度不必定比遞歸的要好。因此具體問題具體分析。

> 上面提到的三個問題是動態規劃裏很常見的題目,題目內容能夠百度查看一下。篇幅緣由,本文後邊只講解前兩道題get

備忘錄算法:

  1. 在階梯數N比較多的時候,遞歸算法的缺點就顯露出來了:時間複雜度很高。若是畫出遞歸圖(像二叉樹同樣),會發現有不少不少重複的節點。然而傳統的遞歸算法並不能識別節點是否是重複的,只要不到終止條件,它就會一直遞歸下去。
  2. 爲了不上述狀況,使遞歸算法可以不重複遞歸,就把已經獲得的節點都存起來,下次再遇到的時候,直接用存起來的結果就好了。這就是備忘錄算法。
  3. 備忘錄算法的時間複雜度和空間複雜度都獲得了簡化。

動態規劃算法:

  1. 上述的備忘錄算法,儘管已經不錯了,可是依然仍是從原問題,遍歷獲得全部的最小子問題,空間複雜度是O(N)。
  2. 爲了再次縮小空間複雜度,咱們能夠自底向上的構造遞歸問題,經過分析最優子結構與最終問題之間的關係,咱們能夠獲得【狀態轉移方程】。 而後從最小的問題不斷往上迭代,即便一直迭代到最大的原問題,也是隻依賴於前面的幾個最優子結構。這樣,空間複雜度就大大簡化。也就獲得了動歸算法算法。

例題

例1: Climbing Stairs(爬樓梯問題)
leetcode原題:你正在爬一個有n個臺階的樓梯,每次只能上1個或者2個臺階,那麼到達頂端共有多少種不一樣的方法?

  1. 創建模型:
  • 最終問題F(N):假設從0到達第N個臺階的方法共有F(N)個。
  • 最優子結構F(N-1),F(N-2):到達N個臺階,有兩種可能,第一種多是從第 N-1 個臺階上1個臺階到達終點,第二種多是從第 N-2 個臺階上2個臺階到達終點。
  • 最優子結構與最終問題之間的關係:按照上述表達,那麼能夠概括出F(N) = F(N-1) + F(N-2) (n>=3)
  • 邊界:F(1) = 1,F(2) = 2
  1. 問題求解:
  • 遞歸:
class Solution {
    int climbStairs(int n) {
        if (n <= 2) {
            return n;
        } else {
            return climbStairs(n - 1) + climbStairs(n - 2);
        }
    }
}

> 遞歸的時間複雜度是由遞歸層數和最優子結構的個數決定的。這裏的階梯數是 N ,最優子結構個數是2。若是想象成一個二叉樹,那麼就能夠認爲是一個高度爲N-1,節點個數接近2的N-1次方的樹,所以此方法的時間複雜度能夠近似的看做是O(2<sup>N</sup>) 。

  • 備忘錄算法:
    這裏咱們想到了把重複的參數存儲起來,下次遞歸遇到時就直接返回該參數的結果,也就是備忘錄算法了,最簡單的備忘錄就是哈希表。
class Solution {
    private Map<integer, integer> map = new HashMap&lt;&gt;();

    int climbStairs(int n) {
        if (n &lt;= 2) {
            return n;
        } else if (map.containsKey(n)) {
            return map.get(n);
        } else {
            int value = climbStairs(n - 1) + climbStairs(n - 2);
            map.put(n, value);
            return value;
        }
    }
}
  • 動態規劃:
    以前都是自頂向下的求解,考慮一下自底向上的求解過程。從F(1)和F(2)邊界條件求,可知F(3) = F(1)+F(2)。不斷向上,可知F(N)只依賴於前兩個狀態F(N-1)和F(N-2)。因而咱們只須要保留前兩個狀態,就能夠求得F(N)。相比於備忘錄算法,咱們再一次簡化了空間複雜度。
class Solution {
    int climbStairs(int n) {
        if (n &lt;= 2) {
            return n;
        }
        // 邊界條件
        int a = 1;
        int b = 2;
        int result = 0;
        // 最優子結構與最終問題之間的關係
        for (int i = 3; i &lt;= n; i++) {
            result = a + b;
            a = b;
            b = result;
        }
        return result;
    }
}

> 空間複雜度O(1), 時間複雜度O(N)

例2: Making change using the fewest coins(最少找零錢問題)
Google面試題:假設你是一家自動售貨機制造商的程序員。你的公司正設法在每一筆交易 找零時都能提供最少數目的硬幣以便工做能更加簡單。已知硬幣有四種(1美分,5美分,10美分,25美分)。假設一個顧客投了1美圓來購買37美分的物品 ,你用來找零的硬幣的最小數量是多少?

  1. 創建模型:
  • 最優子結構:回想找到最優子結構的方法,就是日後退一步,可以獲得的最好的結果。這裏有四個選擇,1 + mincoins(63-1),1 + mincoins(63-5),1 + mincoins(63-10) 或者 1 + mincoins(63-25),這四個選擇能夠認爲是63的最優子結構。
  • 狀態轉移方程:按照上述的最優子結構,mincoins(63)也就等於上述四個最優子結構的最小值。
  • 邊界: 當須要找零的面額正好等於手中單枚硬幣的金額時,返回1便可。
  1. 問題求解:
  • 遞歸:
class Solution {
    Set<integer> coinSet = new HashSet<integer>() {
        {
            add(1);
            add(5);
            add(10);
            add(25);
        }
    };

    int getFewestCoins(int n) {
        if (n &lt; 1) {
            return 0;
        }
        if (coinSet.contains(n)) {
            return 1;
        }
        int minCoins = n;
        int numCoins = Integer.MAX_VALUE;

        for (int coin : coinSet) {
            if (n &gt;= coin) {
                // 若是要計算的n小於單個硬幣金額,則不能出如今狀態轉移方程中
                numCoins = 1 + getFewestCoins(n - coin);
            }
            // 更新最小值
            if (numCoins &lt; minCoins) {
                minCoins = numCoins;
            }
        }
        return minCoins;
    }
}
  • 備忘錄算法:
    就是將遞歸裏計算的中間變量都保存在一個哈希表,代碼略。

  • 動態規劃:
    自底向上,從找零數等於1開始往上迭代,參考最優子結構,記錄下來最少硬幣數。一直迭代到實際要求。

class Solution {
    Set<integer> coinSet = new HashSet<integer>() {
        {
            add(1);
            add(5);
            add(10);
            add(25);
        }
    };

    int getFewestCoins(int n) {
        int[] list = new int[n + 1];
        List<integer> subCal = new ArrayList&lt;&gt;();
        for (int i = 0; i &lt;= n; i++) {
            // 邊界
            if (i &lt;= 1) {
                list[i] = i;
                continue;
            }
            for (int cent : coinSet) {
                if (i &gt;= cent) {
                    subCal.add(list[i - cent] + 1);
                }
            }
            list[i] = Collections.min(subCal);
            subCal.clear();
        }
        return list[n];
    }
}

更多內容,歡迎關注微信公衆號:全菜工程師小輝。公衆號回覆關鍵詞,領取免費學習資料。

哎呀,若是個人名片丟了。微信搜索「全菜工程師小輝」,依然能夠找到我

</integer></integer></integer></integer></integer></integer,>

相關文章
相關標籤/搜索