动态规划思想是算法设计中很重要的一个思想,所谓动态规划就是“边走边看”,前面的知道了,后面的根据前面的也就可以推出来了。和分治算法相似又不同,相同的是都需要去寻找最优子结构,重复子问题,边界条件。不同的是动态规划算法存储前面算得的每一个结果,后面的结果由前面的结果推倒得出。而分治则是分而治之,把问题分开解决,再合并。不存在前后两个状态之间的转换关系(想想快速排序和LCS即可想到),快速排序法就是分治的一个典型应用。通俗来说,动态规划本质上来说还是规划,是不断进行决策的问题,一般用于求解最(优)值;而分治是一种处理复杂问题的方法,不仅仅只用于解决最值问题。
先来看动态规划,这是五大算法思想中最重要也是很常用的一个,在以后的blog中,我会慢慢地更新其他四种算法思想的分析。
动态规划有以下几个重要方面的组成:
1.最优子结构:如果问题的最优解包含的子问题的解也是最优的,就称该问题具有最优子结构
2.重叠子问题:你找到的最优子结构我们要将其转化为重叠子问题,用相同的方法循环求解
3.状态转换方程:你找到的每一个子问题都可以认为是一个状态下的规划问题,而状态与状态之间你需要找到关联方程,这样我们才能从最简单的状态循环求解出所需要求解的较复杂的状态
4.边界条件:即子问题的初值,你必须给出规模较小的一些子问题的初值来开启循环,否则无法循环求解
4.子问题独立:独立问题,主要是子问题各对象的独立,不要发生一个子问题在操作时修改了另一个子问题中的变量的情况,具体可见我的递归模式的思考一文
5.无后效性:某个阶段状态一旦确定就不再受后续状态决策的影响。即某状态之后的过程不会影响以前的状态,只与当前状态有关
其中问题具有最优子结构,可以分解为重复子问题,无后效性这是想使用DP的问题所必须具备的性质
———————————-我是分割线~———————————
本文先介绍一类动态规划的常见问题——串与序列(串必须连续,序列可以不连续)
1. 最长公共子序列(LCS):
重叠子问题(以后称为状态):设数组dp[m][n]来表示长度为m的str1和长度为n的str2的最长公共子序列长度
状态转换方程:if(str1[m]==str2[n]) dp[m][n]=dp[m-1][n-1]+1;
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));
}
}
如有不足,还望大家指出~