動態規劃從入門到入土——入門篇

一、什麼是動態規劃?

要先理解動態規劃的話,我們要先知道什麼叫“分治”思想。
這就要提一下分治的祖師爺了——大禹
當面對波濤洶涌的洪水,人力顯得多麼的蒼白無力,然鵝,大佬站出來了,提出:我們把洪水不斷的分流,然後對每一條支流進行處理就可以處理好洪水了。

那麼分治思想就是:大事化小,小事化無
與此同時,動態規劃算是分治思想的延伸,它主要是把原問題分治爲各個大問題,把大問題分解成各個小問題,然後保存各個小問題的解用於處理上層大問題的解。

那麼動態規劃的特點也就出現了:

  1. 把原來的問題分解成幾個相似的子問題
  2. 所有子問題都只解決一次
  3. 存儲子問題的解

二、如何使用動態規劃

我們先要認知動態規劃的本質:
是對問題狀態的定義和狀態轉移方程的定義(狀態以及狀態之間的遞推關係)

我們一般從四個角度來考慮問題:

  1. 狀態的定義
  2. 狀態間的轉移方程定義
  3. 狀態初始化
  4. 返回結果

適用場景:最大值/最小值, 可不可行, 是不是,方案個數

三、具體案例

不談案例的算法都是耍流氓   		 --沃茲基碩德

下面從簡單到難的案例,來學會動態規劃。

1.Fibonacci

大家都知道斐波那契數列,現在要求輸入一個整數n,請你輸出斐波那契數列的第n項(從0開始,第0項爲 0)。 n<=39 int
Fibonacci(int n)

 public int Fibonacci(int n) {
        if(n == 0){
            return 0;
        }
        if(n == 1 || n == 2){
            return 1;
        }
        return Fibonacci(n-1)+Fibonacci(n-2);
    }

這是遞歸版本的斐波那契數列,我們都知道它的缺陷,太費棧空間,重複運算太多次,那麼我們用動態規劃試一下。

1.狀態的定義
這裏很明顯,我們求F(n)的值,F(n)爲第n項的值
2.轉移方程
就是通項公式 F(N) = F(N-1)+F(N-2);
3.初始化
F(0)=0
F(1) = 1;
4.返回結果
F(n)

到此,對於這道題已經完成了85%以上了,代碼只佔15%,那麼我們寫一下代碼吧。

		if(n == 0){
            return 0;
        }
        if(n == 1 || n == 2){
            return 1;
        }
        int F1 = 1;
        int F2 = 1;
        int result = 0;
        for (int i=3; i<=n; i++){
            result = F1 + F2;
            F1 = F2;
            F2 = result;
        }
        return F2;

這道題只是初步感受一下,下面我們感受第二題。

2.變態青蛙跳臺階(Climbing Stairs)

一隻青蛙一次可以跳上1級臺階,也可以跳上2級……它也可以跳上n級。求該青蛙跳上一個n級的臺階總共有多少種跳法。

我們來分析一下。

1.狀態的定義
這和上一題差不多,我們定義F(n),表示到n階臺階的走法。

2.轉移方程
這裏就需要思考了,我們到N階臺階的話,那麼我們前一步應該在哪?
由於這個青蛙比較牛批,它可以任意跳,也就是說,它在n-1個臺階裏面的任何一個臺階都可以一步跳到最後一格,那麼,也就是說,F(N)的值,應該是它前面每一個臺階值的累計和,換而言之,就是到第1個臺階的路徑個數+到第二個臺階的路徑個數+…+n-1個臺階路徑個數。

F(N) = F(N-1) + F(N-2) + F(N-3) + … + F(0);

那麼這個東西還是不好整啊,這是什麼鬼?我們不妨再推一下,

F(N-1) = F(N-2)+F(N-3)+…F(0);

用這個算式替代一下第一個算式的部分,那麼就有

狀態方程: F(N) = 2*F(N-1);

3.狀態初始化:
F(0) = 0; // 輔助狀態,表明沒有臺階
F(1) = 1; // 一個臺階的話,只有一種走法

4.返回結果
F(N)

至此,這道題結束了,上代碼。

public int JumpFloorII(int target) {
        if(target <= 0){
            return 0;
        }
        if(target == 1){
            return 1;
        }
        int F1 = 1;
        for (int i=2;i<=target;i++){
            F1 *= 2;
        }
        return F1;
    }

是不是很簡單了,下面上老生常談的問題。

3.最大連續子數組和(Maximum Subarray)

HZ偶爾會拿些專業問題來忽悠那些非計算機專業的同學。今天測試組開完會後,他又發話了:在古老的一維模式識別中,常常需要計算連續子向量的最大和,當向量全爲正數的時候,問題很好解決。但是,如果向量中包含負數,是否應該包含某個負數,並期望旁邊的正數會彌補它呢?
例如:{6,-3,-2,7,-15,1,2,2},連續子向量的最大和爲8(從第0個開始,到第3個爲止)。給一個數組,返回它的最大連續子序列的和,你會不會被他忽悠住?(子向量的長度至少是1)

這道題就是在求最大連續子數組和。繼續分析

1.狀態的定義
我們先定義一下:F(i),表明第i個元素連續最大子數組和。
這樣開始分析,我們要找到第i個的最大連續子數組和,那麼我們勢必要找i-1,但是一直遞推下去,我們根本不知道前面的元素該不該用。那麼這樣定義肯定是不行的。我們轉念一想,如果這樣定義呢:F(i)表示,以第i個元素元素結尾的最大連續子數組和。因爲我們知道,當前的元素值,那麼根據前面的最大連續和來判斷這個當前該不該加。不該加就是從當前開始

2.轉移方程
F(i) = Max(A[i],F(i-1)+A[i]);
3.初始化
建立一個矩陣用於保存到i的最大值
F[0] = Array[0];
4.結果
返回這個矩陣裏面最大的那個值。

public int FindGreatestSumOfSubArray(int[] array) {
       int[] dp = new int[array.length];
        dp[0] = array[0];
        int result = array[0];
        for (int i=1; i<array.length; i++){
            dp[i] = Math.max(array[i], dp[i - 1] + array[i]);
            if(dp[i] > result){
                result = dp[i];
            }
        }
        return result;
    }

這樣還是浪費空間,可以改進一下,如果題對原數組不做要求,直接改原數組就可以了

public class Solution {
    public int FindGreatestSumOfSubArray(int[] array) {
         int result = array[0];
         for (int i=1; i<array.length; i++){
            array[i] = Math.max(array[i], array[i - 1] + array[i]);
            if(array[i] > result){
                result = array[i];
            }
        }
        return result;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章