Everybody's good at something.
天生我材必有用。
動態規劃應該算是算法裏比較難以掌握的了,常常是知其然不知其因此然。也是初學者學習算法中的‘攔路虎’,今天本武松將現場直播教學--如何打‘虎’,不對,考慮到老虎乃國家保護動物,本着‘保護動物,人人有責’的原則,換個說法,能用圖的就不用文字。java
最後但願各位看官老爺看完本文以後,都可以有所收穫,最起碼可以作到動態規劃入門。 本文主要分享了什麼是動態規劃,動態規劃的幾個經典案例。git
先整點官話 github
動態規劃是一種在數學、管理科學、計算機科學、經濟學和生物信息學中使用的,經過把原問題分解爲相對簡單的子問題的方式求解複雜問題的方法。算法
數學思想:分階段求解決策問題
數組
動態規劃經常適用於有重疊子問題和最優子結構性質的問題,動態規劃方法所耗時間每每遠少於樸素解法。 (ps: 記住關鍵字,後面分析代碼時會用到)
bash
彆着急,聽我慢慢道來。
app
動態規劃整體思路在於解題的步驟,本文大部分代碼的實現思想都是基於這個解題步驟,更加的便於理解。學習
話很少說,全部源碼均已上傳至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])
呢如今的二階矩陣表構建完畢,轉換成代碼以下:
最長公共子串結果拼接 (這裏的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}
固然 最長遞減子序列的實現也就很容易了。
例如 [-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揹包 問題能夠說是再經典不過了。
假設有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);複製代碼
您的點贊和關注是對我最大的支持,謝謝!