動態規劃思想分析——經典題目
動態規劃思想是演算法設計中很重要的一個思想,所謂動態規劃就是“邊走邊看”,前面的知道了,後面的根據前面的也就可以推出來了。和分治演算法相似又不同,相同的是都需要去尋找最優子結構,重複子問題,邊界條件。不同的是動態規劃演算法儲存前面算得的每一個結果,後面的結果由前面的結果推倒得出。而分治則是分而治之,把問題分開解決,再合併。不存在前後兩個狀態之間的轉換關係(想想快速排序和LCS即可想到),快速排序法就是分治的一個典型應用。通俗來說,動態規劃本質上來說還是規劃,是不斷進行決策的問題,一般用於求解最(優)值;而分治是一種處理複雜問題的方法,不僅僅只用於解決最值問題。
先來看動態規劃,這是五大演算法思想中最重要也是很常用的一個,在以後的blog中,我會慢慢地更新其他四種演算法思想的分析。
動態規劃有以下幾個重要方面的組成:
1.最優子結構:如果問題的最優解包含的子問題的解也是最優的,就稱該問題具有最優子結構
2.重疊子問題:你找到的最優子結構我們要將其轉化為重疊子問題,用相同的方法迴圈求解
3.狀態轉換方程:你找到的每一個子問題都可以認為是一個狀態下的規劃問題,而狀態與狀態之間你需要找到關聯方程,這樣我們才能從最簡單的狀態迴圈求解出所需要求解的較複雜的狀態
4.邊界條件:即子問題的初值,你必須給出規模較小的一些子問題的初值來開啟迴圈,否則無法迴圈求解
4.子問題獨立:獨立問題,主要是子問題各物件的獨立,不要發生一個子問題在操作時修改了另一個子問題中的變數的情況,具體可見我的遞迴模式的思考一文
5.無後效性:某個階段狀態一旦確定就不再受後續狀態決策的影響。即某狀態之後的過程不會影響以前的狀態,只與當前狀態有關
其中問題具有最優子結構,可以分解為重複子問題,無後效性這是想使用DP的問題所必須具備的性質
----------------------------------我是分割線~---------------------------------
本文先介紹一類動態規劃的常見問題——串與序列(串必須連續,序列可以不連續)
1. 最長公共子序列(LCS):
重疊子問題(以後稱為狀態):設陣列dp[m][n]來表示長度為m的str1和長度為n的str2的最長公共子序列長度
狀態轉換方程:if
if(str1[m]!=str2[n]) dp[m][n]=max(dp[m-1][n],dp[m][n-1]);
邊界條件(初始條件):dp[i][0]=0 dp[0][j]=0 0<=i<=m 0<=j<=n
滿足 最優子結構 以及 無後效性
2.最長公共子字串(LCS):
狀態:設陣列dp[m][n]來表示以str1[m]結尾的str1和以str2[n]結尾的str2的最長公共子字串長度,最長的長度為dp[m][n]中最大的元素值
狀態轉換方程:if(str1[m]==str2[n]) dp[m][n]=dp[m-1][n-1]+1;
if(str1[m]!=str2[n]) dp[m][n]=0;
初始條件:dp[i][0]=0 dp[0][j]=0 0<=i<=m 0<=j<=n
滿足 最優子結構 以及 無後效性
3.最長遞增子序列(LIS):
LIS問題可以使用排序+LCS問題完成,還可以用動態規劃,這些時間複雜度為n2
狀態:設陣列dp[m]為以a[m]結尾的序列中的最長遞增序列的長度,最終的LIS長度為dp陣列中的最大值
狀態轉換方程:dp[i]: for(int j=1;j<i;j++){ if(a[j-1]<=a[i-1]&&dp[j]>max) max=dp[j]; } dp[i]=max+1;
初始條件:dp[0]=0;
滿足 最優子結構 以及 無後效性
4.編輯距離問題:
狀態:設edit[m][n]為長度為m的str1和長度為n的str2的編輯距離
狀態轉換方程:if(j>i) edit[i][j]=edit[i][i]+j-i; else if(j<i) edit[i][j]=edit[j][j]+i-j; else{ if(str1.charAt(i-1)==str2.charAt(j-1)) edit[i][j]=edit[i-1][j-1]; else edit[i][j]=edit[i-1][j-1]+1;}
初始條件:for(int i=0;i<=m;i++) edit[i][0]=i; for(int j=0;j<=n;j++) edit[0][j]=j;
-------------------------------------------------下面兩個問題並不是DP問題,但也是串/序列問題------------------------------------------
5.判斷是否為子序列:boolean isSubSeq(String str1,String str2)
6.判斷是否為子字串:contains(String str)方法:這個問題沒有想象的那麼簡單,涉及到KMP演算法,有關於KMP演算法我會在之後講解
-----------------------------------------------------------------------下面為程式---------------------------------------------------------------------
上面給出的六個問題,下面的均用java來實現之,其中問題3給出了兩種方法,因為DP有點慢為n2複雜度,給出的新方法為nlogn複雜度!
import java.util.logging.Logger;
public class Soulution {
//1. 最長公共子序列(LCS):
public static int LCS(String str1,String str2){
int m=str1.length();
int n=str2.length();
int[][] dp=new int[m+1][n+1];
//if(str1.charAt(i)==str2.charAt(j)) dp[i][j]==dp[i-1][j-1]+1;
//else dp[i][j]=max(dp[i-1][j],dp[i][j-1])
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
if(str1.charAt(i-1)==str2.charAt(j-1))
dp[i][j]=dp[i-1][j-1]+1;
else
dp[i][j]=Math.max(dp[i-1][j],dp[i][j-1]);
}
}
return dp[m][n];
}
//2.最長公共子字串(LCS):
public static int LCString(String str1,String str2){
int m=str1.length();
int n=str2.length();
int longest=0;
int[][] dp=new int[m+1][n+1]; // 表示長度為mn的兩個子串的lcs
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
if(str1.charAt(i-1)==str2.charAt(j-1))
dp[i][j]=dp[i-1][j-1]+1;
else
dp[i][j]=0;
longest=Math.max(longest, dp[i][j]);
}
}
return longest;
}
//3.最長遞增子序列(LIS):動態規劃n2解法
public static int LISDP(int[] a){
int[] dp=new int[a.length+1];
int res=0;
for(int i=1;i<a.length+1;i++){
int max=0;
for(int j=1;j<i;j++){
if(a[j-1]<=a[i-1]&&dp[j]>max)
max=dp[j];
}
dp[i]=max+1;
if(res<dp[i])
res=dp[i];
}
return res;
}
//3.最長遞增子序列(LIS)nlogn解法:
//LIS有動態規劃和LCS轉LIS(排序+LCS)兩種 n2方法,還有一個nlogn方法如下:
//構造一個LIS(本身不是原本的LIS),保證最末位。
static int len=0;
public static int LIS(int[] arr){
int[] lis=new int[arr.length];
lis[0]=arr[0]; //目前LIS序列長度為1,末尾為arr[0]
len=1;
for(int i=1;i<arr.length;i++){
if(arr[i]>=lis[len-1]){
lis[len]=arr[i];
len++;
}
else{
lis[binarysearch(lis, arr[i])]=arr[i];
}
}
return len;
}
private static int binarysearch(int[] a,int key){
int left=0;
int right=len-1;
int mid=(left+right)/2;
while(left<right){
mid=left+(right-left)/2;
if(a[mid]<key)
left=mid+1;
else
right=mid;
}
return left;//right is also ok
}
//4.編輯距離問題:
public static int editdis(String str1,String str2){
int m=str1.length();
int n=str2.length();
int[][] edit=new int[m+1][n+1]; // 防止低端越界以及特殊情況i
for(int i=0;i<=m;i++)
edit[i][0]=i;
for(int i=0;i<=n;i++)
edit[0][i]=i;
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){ // 把這裡的j寫成i是很常見的一個錯誤
if(j>i) edit[i][j]=edit[i][i]+j-i;
else if(j<i) edit[i][j]=edit[j][j]+i-j;
else {
if(str1.charAt(i-1)==str2.charAt(j-1))
edit[i][j]=edit[i-1][j-1];
else
edit[i][j]=edit[i-1][j-1]+1;
}
}
}
return edit[m][n];
}
//5.判斷是否為子序列
public static boolean isSubSeq(String str1,String str2){
int pointer1=0;
int pointer2=0;
while(pointer1<str1.length()&&pointer2<str2.length()){
if(str1.charAt(pointer1)==str2.charAt(pointer2))
pointer2++;
if(pointer2==str2.length()) return true;
pointer1++;
}
return false;
}
public static void main(String[] args){
//測試程式:判斷是否為子串!
System.out.println("abcsdf".contains("abcd"));
//測試程式:判斷是否為子序列!
System.out.println(isSubSeq("abscdljjkjkefg", "abcdefg"));
//測試程式:輸出最長公共子序列的長度
System.out.println(LCS("abscljjkjkefg", "abcdefg"));
//測試程式:輸出最長遞增子序列LIS nlogn
int[] a={1,4,6,2,3,5,7};
System.out.println(LIS(a));
//測試程式:輸出最長遞增子序列LISDP n2
System.out.println(LISDP(a));
//測試程式:輸出最長公共子字串長度
System.out.println(LCString("abscljjkjkdefg", "abcdefg")); //3
//測試程式:編輯距離
System.out.println(editdis("kitten", "sitting")); //3
//int[] b={1,2,6};
//System.out.println(binarysearch(b,3));
}
}
如有不足,還望大家指出~