動態規劃學習筆記

Everybody's good at something. 
天生我材必有用。

引言

動態規劃應該算是算法裏比較難以掌握的了,常常是知其然不知其因此然。也是初學者學習算法中的‘攔路虎’,今天本武松將現場直播教學--如何打‘虎’,不對,考慮到老虎乃國家保護動物,本着‘保護動物,人人有責’的原則,換個說法,能用圖的就不用文字。java


最後但願各位看官老爺看完本文以後,都可以有所收穫,最起碼可以作到動態規劃入門。 本文主要分享了什麼是動態規劃,動態規劃的幾個經典案例。git

前言

什麼是動態規劃?

先整點官話 github

動態規劃是一種在數學、管理科學、計算機科學、經濟學和生物信息學中使用的,經過把原問題分解爲相對簡單的子問題的方式求解複雜問題的方法。算法

數學思想:分階段求解決策問題
數組

 動態規劃經常適用於有重疊子問題和最優子結構性質的問題,動態規劃方法所耗時間每每遠少於樸素解法。 (ps: 記住關鍵字,後面分析代碼時會用到)
bash

彆着急,聽我慢慢道來。
app

動態規劃整體思路在於解題的步驟,本文大部分代碼的實現思想都是基於這個解題步驟,更加的便於理解。學習

解題步驟

  1. 創建模型
  2. 尋找約束條件
  3. 尋找邊界值
  4. 判斷是否知足最優性原理
  5. 推出大問題和小問題的遞推公式
  6. 創建矩陣表並填寫
  7. 計算


話很少說,全部源碼均已上傳至github:測試

最長公共子序列

題目

給定兩個字符串,求解這兩個字符串的最長公共子序列。ui

好比字符串s1:abcbdab;字符串s2:bdcaba

則這兩個字符串的最長公共子序列是:dcba

最長公共子序列長度爲4

解析

先構建二階矩陣表,至關於一個二維數組,這裏用dp[i][j]表示,即

d[i][j]表示s1中前i個字符與s2中前j個字符分別組成的兩個前綴字符串的最長公共長度.


初始化邊界條件,這裏 s1長度 m,s2長度n

s1,s2表示佔位符,便於邊界條件的處理

dp[i][0] = 0; (0 < i < m)

dp[0][j] = 0; (0 < i < n)


注:在這裏,我經過不斷地對i和j這兩個數字量進行不斷地求解,直到最終獲得答案。這個數字量稱之爲狀態

當出現不匹配狀況的時候,則咱們取和它相鄰的兩個點的最大值,即

dp[i][j] = max{dp[i][j-1],dp[i-1][j]}; (s1[i-1] != s2[j-1])

同理的,若是匹配,則在原來基礎上加一。即:

dp[i][j] = dp[i-1][j-1] + 1; (s1[i] == s2[j])


ps: 常規 數組索引爲0是用來當佔位符,便於計算,這裏看錶可知,能夠把s1的 a和s2的b做爲邊界

呢如今的二階矩陣表構建完畢,轉換成代碼以下:

準備 (二維數組的打印)

private void print(int[][] dp) {
        for (int[] ds : dp) {
            System.out.println(Arrays.toString(ds));
        }
    }複製代碼

方法

private int solution(String s1, String s2) {
        char[] str1 = s1.toCharArray();
        char[] str2 = s2.toCharArray();
        int[][] dp = new int[str1.length][str2.length];
        for (int i = 0; i < str1.length; i++) {
            if (str1[i] == str2[0]){
                dp[i][0] = 1;
            }else{
                dp[i][0] = 0;
            }
        }
        for (int j = 0; j < str2.length; j++) {
            if (str2[j] == str1[0]){
                dp[0][j] = 1;
            }else{
                dp[0][j] = 0;
            }
        }
        for (int i = 1; i < str1.length; i++) {
            for (int j = 1; j < str2.length; j++) {
                if (str1[i] == str2[j]){
                    dp[i][j] = dp[i-1][j-1] +1;
                }else{
                    dp[i][j] = Math.max(dp[i][j-1],dp[i-1][j]);
                }
            }
        }
        print(dp);
        int length = 0;
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < str1.length; i++) {
            for (int j = 0; j < str2.length; j++) {
                if (dp[i][j] == length && stringBuilder.indexOf(String.valueOf(str1[i])) == -1) {
                    stringBuilder.append(str1[i]);
                }
                length = Math.max(length,dp[i][j]);
            }
        }
        System.out.println(stringBuilder.reverse().toString());
        return length;
    }複製代碼

前兩個for循環構建邊界條件,第三個for循環填充二維數組。其實在這裏dp[i][j](i,j取max)就是咱們要求得長度。第四個for循環,是根據所得進行回溯,得到子序列,這個子序列也就是他的最長公共子串。

測試代碼

LongestCommonSequence longestCommonSubString = new LongestCommonSequence();
        String s1 = "abcbdab";
        String s2 = "bdcaba";
        int res = longestCommonSubString.solution(s1,s2);
        System.out.println(res);複製代碼

測試結果


最長公共子串

題目

給定兩個字符串,求解這兩個字符串的最長公共子串。

好比字符串str1:abcbdab;字符串str2:bdcaba

則這兩個字符串的最長公共子串是:ab 或者 bd

這裏的二階矩陣表大體思路和求最長公共子序列有點類似,可是注意:

由於最長公共子串要求必須在原串中是連續的,因此一但某處出現不匹配的狀況,此處的值就重置爲0。即:

dp[i][j] = 0; (str1[i] != str2[j])

完整公式

  1. dp[i][0] = 0; (0<=i<=m) 
  2. dp[0][j] = 0; (0<=j<=n) 
  3. dp[i][j] = dp[i-1][j-1] +1; (str1[i] == str2[j]) 
  4. dp[i][j] = 0; (str1[i] != str2[j])

完整二階矩陣表


呢如今的二階矩陣表構建完畢,轉換成代碼以下:

準備

最長公共子串結果拼接 (這裏的str 傳 str1或者str2任意一個便可)

private String resJoint(String str, int x, int y) {
        StringBuilder stringBuilder = new StringBuilder();
        while (x >= 0 && y >= 0) {
            stringBuilder.append(str.charAt(y--));
            --x;
        }
        return stringBuilder.reverse().toString();
    }複製代碼

二維數組打印

private void print(int[][] dp) {
        for (int[] ds : dp) {
            System.out.println(Arrays.toString(ds));
        }
    }複製代碼

方法一

private String solution(String str1, String str2) {
        int[][] dp = new int[str1.length()][str2.length()];
        char[] str1Chars = str1.toCharArray();
        char[] str2Chars = str2.toCharArray();

        for (int i = 0; i < str1Chars.length; i++) {
            if (str1Chars[i] == str2Chars[0]) {
                dp[i][0] = 1;
            } else {
                dp[i][0] = 0;
            }
        }
        for (int j = 0; j < str2Chars.length; j++) {
            if (str2Chars[j] == str1Chars[0]) {
                dp[0][j] = 1;
            } else {
                dp[0][j] = 0;
            }
        }
        for (int i = 1; i < str1Chars.length; i++) {
            for (int j = 1; j < str2Chars.length; j++) {
                if (str1Chars[i] == str2Chars[j]) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = 0;
                }
            }
        }
        print(dp);
        int max = dp[0][0];
        //--> j
        int x = 0;
        // --> i
        int y = 0;
        for (int i = 0; i < str1Chars.length; i++) {
            for (int j = 0; j < str2Chars.length; j++) {
                if (dp[i][j] > max) {
                    max = dp[i][j];
                    y = i;
                    x = j;
                }
            }
        }
        System.out.println(max + "," + x + "," + y);

        return resJoint(str1, x, y);
	}複製代碼

這裏只分析第四個for循環,根據結果,求二階矩陣中的最大值,由表可知,最長公共子串不是惟一的。這裏只須要求出任意一組便可。若是須要所有羅列,只須要將x,y當數組存儲,而後根據索引就能夠拿出全部的最長公共子串了。

方法二

其實二階矩陣表咱們能夠找出規律,將其轉換爲求二階矩陣的最大地址對角線問題

private String solution2(String str1, String str2) {
        char[] str1Chars = str1.toCharArray();
        char[] str2Chars = str2.toCharArray();
        int x = 0;
        int y = 0;
        int index = 0;
        int max = 0;
        //列
        int row = 0;
        //行
        int col = str2Chars.length - 1;
        //計算矩陣中的每一條斜對角線上的值
        while (row < str1Chars.length) {
            int i = row;
            int j = col;
            while (i < str1Chars.length && j < str2Chars.length) {
                if (str1Chars[i] == str2Chars[j]) {
                    if (++index > max) {
                        max = index;
                        x = j;
                        y = i;
                    }
                } else {
                    index = 0;
                }
                i++;
                j++;
            }
            if (col > 0) {
                --col;
            } else {
                ++row;
            }
        }
        System.out.println(max + "," + x + "," + y);
        return resJoint(str1, x, y);
    }複製代碼

測試代碼

LongestCommonSubString longestCommonSubString = new LongestCommonSubString();
        String s1 = "abcbdab";
        String s2 = "bdcaba";
        String res = longestCommonSubString.solution(s1, s2);
        System.out.println("最長公共子串爲:" + res);
       String res2 = longestCommonSubString.solution2(s1, s2);
       System.out.println("最長公共子串爲:" + res2);
複製代碼

測試結果

方法一


方法二


最長遞增子序列

題目

 最長遞增子序列是指找到一個給定序列的最長子序列的長度,使得子序列中的全部元素單調遞增。

 例如:{ 3,5,7,1,2,8 } 的 LIS 是 { 3,5,7,8 },長度爲 4。

解析

這個問題和前兩個問題又不太同樣,須要本身和本身比較。

當 {3}的時候 LIS {3},長度 1

當 {3,5}的時候 LIS {3,5},長度 2

當 {3,5,7}的時候 LIS {3,5,7},長度 3

當 {3,5,7,1}的時候 LIS {3,5,7},長度 3

...

這裏咱們能夠把原問題分解成 子問題來解決。(文章開頭提過)

先考慮邊界狀況. F(1) = 1; i = 1;

根據上面可總結出公式 

i表示 當前數組的索引,j表示當前數組的子數組的索引

F[i] = max{1,F[j]+1} ( F[j]<F[i] && j<i)

公式總結出來了,根據公式將其轉換成代碼,具體實現以下:

private int solution(int[] nums){
        //推公式
        //F[i] = max{1,F[j]+1|aj<ai && j<i}
        int[] F = new int[nums.length];
        for (int i = 0; i < nums.length; i++) {
            F[i] = 1;
        }
        for (int i = 0; i < nums.length; i++) {
            for (int j = 0; j < i; j++) {
                if(nums[j] < nums[i] && F[i] < F[j] + 1) {
                    F[i] = F[j] + 1;
                }
            }
        }
        int max = 0;
        for (int i = 0; i < nums.length; i++) {
            if(F[i] > max) {
                max = F[i];
            }
            System.out.println("F[" + i + "] = " + F[i]);
        }
        System.out.println();
        return max;
    }複製代碼

第一個for循環初始化 公式數組,將其所有置爲1.

第二個for循環根據所得的公式將數組填滿

第三個for循環獲取知足條件的索引及最大值

測試代碼

int[] nums = { 3,5,7,1,2,8 };
        int res = new LongestIncreasingSubSequence().solution(nums);
        System.out.println(res);複製代碼

測試結果

根據打印log能夠看出,1 - 2 -3 -4是連貫的,這說明 其對應的索引就是該問題的LIS

即{3,5,7,8}


固然 最長遞減子序列的實現也就很容易了。

最大子序列和

題目

給定一個整數數組 nums ,找到一個具備最大和的連續子數組(子數組最少包含一個元素),返回其最大和。 

例如 [-2,1,-3,4,-1,2,1,-5,4], 

連續子數組 [4,-1,2,1] 的和最大,爲 6。

解析

 這道題的求解思路和求最長遞增子序列。這裏直接羅列出狀態轉移方程式了:

dp[0] = nums[i]; (i = 0) 

dp[i] = dp[i-1]>=0 ? dp[i-1]+nums[i] : nums[i] (i > 1)

具體實現以下:

private int solution(int[] nums){
	int[] dp = new int[nums.length];
	dp[0] = nums[0];
	for (int i = 1; i < nums.length; i++) {
		if(dp[i - 1] >= 0) {
			dp[i] = dp[i - 1] + nums[i];
		}else {
			dp[i] = nums[i];
		}
		System.out.println("dp[" + i  + "] = " + dp[i]);
	}
	int res = dp[0];
	for (int i = 1; i < dp.length; i++) {
		if(dp[i] > res) {
			res = dp[i];
		}
	}
	return res;
}複製代碼

固然還有一種更簡單的解法只須要一個for循環就能夠解決了,我只須要假設一個理想最大子序列res,而後遍歷數組,當遍歷值是正的,說明是增益的,加上,而後每次取max就能夠了。

private int solution2(int[] nums){
        int res = nums[0];
        int sum = 0;
        for(int num: nums) {
            if(sum > 0) {
                sum += num;
            } else {
                sum = num;
            }
            res = Math.max(res, sum);
        }
        return res;
    }複製代碼

測試代碼

int[] nums = {-2,1,-3,4,-1,2,1,-5,4};
        int res = new MaxSubsequenceSum().solution(nums);
        System.out.println("最大子序列和爲:" + res);複製代碼

測試結果


根據輸出的結果能夠看到子問題的每一種狀況

進階--01揹包

題目

01揹包 問題能夠說是再經典不過了。

 假設有N件物品和一個容量爲V的揹包。第i件物品的體積是v[i],價值是p[i],將哪些物品裝入揹包可以使價值總和最大?

例題

N = 4, V = 8


解析

每一種物品都有兩種可能即放入揹包或者不放入揹包。 

能夠用dp[i][j]表示第i件物品放入容量爲j的揹包所得的最大價值,則狀態轉移方程能夠推出。

邊界條件

dp[i][0] = dp[0][j] = 0 (i > 0,j > 0) 

當揹包容量小於物品重量時 即j < v[i],則裝不進去,保持原狀便可

dp[i][j] = dp[i-1][j] (j < v[i]) 

當能裝下的時候,則須要考慮裝入以前是什麼狀態,確定是v[i-1][j-v[i]],當前物品的價值是p[i]

dp[i][j] = max{dp[i-1][j], dp[i-1][j-v[i]]+p[i]} (j >= v[i]) 

初始化二階矩陣


根據公式填表


則dp[4][8] = 10,也就是裝入物品的最大價值,可是裝進去的是哪一些呢,則須要進行回溯了。

dp(i,j)=dp(i-1,j)   說明沒有選擇第i 個商品,則回到dp(i-1,j); 

dp(i,j)=dp(i-1,j-v(i))+p(i) 說明裝了第i個商品,該商品是最優解組成的一部分,隨後咱們得回到裝該商品以前,即回到dp(i-1,j-v(i)); 一直遍歷到i=0結束爲止.

略low的表示一下

dp[4][8] != dp[3][8] 而且 dp[4][8] =dp[3][8 - 5] + 6 ===> d[3][3]

說明第四個商品選中;

dp[3][3] = dp[2][3] 第三個商品沒有被選中;

dp[2][3]!=dp[1][3]而且 dp[2][3] = dp[1][4-4] + 4 ====> dp[1][0]

說明第二件商品被選中;

dp[1][0] == dp[0][0]

說明第一件商品沒有被選中。

具體實現代碼以下

private int solution(int[] v, int[] p, int c) {
        int[][] dp = new int[v.length][c + 1];
        for (int i = 1; i < v.length; i++) {
            for (int j = 1; j < c + 1; j++) {
                if (j < v[i]) {
                    dp[i][j] = dp[i - 1][j];
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - v[i]] + p[i]);
                }
            }
        }
        print(dp);
        int[] items = new int[v.length];
        situation(items, v, p, dp, v.length - 1, c);
        System.out.println("回溯選中的物品:(1表示選中)");
        System.out.println("體積:" + Arrays.toString(v));
        System.out.println("價格:" + Arrays.toString(p));
        System.out.println("選中:" + Arrays.toString(items));
        return dp[v.length - 1][c];
    }複製代碼

回溯方法

private void situation(int[] items, int[] v, int[] p, int[][] dp, int i, int j) {
        if (i > 0) {
            if (dp[i][j] == dp[i - 1][j]) {
                situation(items, v, p, dp, i - 1, j);
            } else if (j - v[i] >= 0 && dp[i][j] == dp[i - 1][j - v[i]] + p[i]) {
                items[i] = 1;
                situation(items, v, p, dp, i - 1, j -v[i]);
            }
        }
    }複製代碼

打印數組 print (參考上面)

測試代碼

// 0 佔位
        int[] v = {0, 2, 3, 4, 5};
        int[] p = {0, 3, 4, 5, 6};
        int c = 8;
        BackPack01 backPack01 = new BackPack01();
        int max = backPack01.solution(v, p, c);
        System.out.println("當體積爲" + c + "時,最大價值爲" + max);複製代碼

測試結果


end


您的點贊和關注是對我最大的支持,謝謝!
相關文章
相關標籤/搜索