动态规划(DP问题)

目录

1. 概念

 适用于原问题可以分解为相对简单的子问题方式,子问题非常相似,而且会有重叠部分,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量:一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。 这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。
 类似将递归算法重新写成非递归算法,让后者把那些子问题的答案系统地记录在一个表内。

2. 分治与动态规划

共同点:二者都要求原问题具有最优子结构性质,都是将原问题分而治之,分解成若干个规模较小(小到很容易解决的程序)的子问题.然后将子问题的解合并,形成原问题的解.
不同点:分治法将分解后的子问题看成相互独立的,通过用递归来做。
    动态规划将分解后的子问题理解为相互间有联系,有重叠部分,需要记忆,通常用迭代来做。

3. 求解问题的特点

(1)最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。
(2) 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。
(3)有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)

4. 步骤

1、创建一个一维数组或者二维数组,保存每一个子问题的结果,具体创建一维数组还是二维数组看题目而定,基本上如果题目中给出的是一个一维数组进行操作,就可以只创建一个一维数组,如果题目中给出了两个一维数组进行操作或者两种不同类型的变量值,比如背包问题中的不同物体的体积与总体积,找零钱问题中的不同面值零钱与总钱数,这样就需要创建一个二维数组。
注:需要创建二维数组的解法,都可以创建一个一维数组运用滚动数组的方式来解决,即一位数组中的值不停的变化,后面会详细徐叙述
2、设置数组边界值,一维数组就是设置第一个数字,二维数组就是设置第一行跟第一列的值,特别的滚动一维数组是要设置整个数组的值,然后根据后面不同的数据加进来变幻成不同的值。
3、找出状态转换方程,也就是说找到每个状态跟他上一个状态的关系,根据状态转化方程写出代码。
4、返回值,一般是数组的最后一个或者二维数组的最右下角。

5.斐波那契数列

数列变形问题:
跳台阶问题:每次只能跳一个或者两个台阶,跳到n层台阶上有几种方法。
填充长方体问题:将一个2x1的长方体填充到2xN的长方体中,有多少种方法。

public static int solutionFibonacci(int n){
        if(n==0){
            return 0;
        }else if(n == 1){
            return 1;
        }else{
            int result[] = new int[n+1];
            result[0] = 0;
            result[1] = 1;
            for(int i=2;i<=n;i++){
                result[i] = result[i-1] + result[i-2];
            }
            return result[n];
        }

6. 最长公共子序列(LCS)与最长公共子串(DP)

子序列和子串的区别:

 子串是要求更严格的一种子序列,要求在母串中连续地出现。子序列中的元素只要在母串中出现并且顺序一致,不要求连续。

LCS:

基本思想:
 设 X=(x1,x2,.....xn)和 Y={y1,y2,.....ym} 是两个序列,将 X 和 Y 的最长公共子序列记为LCS(X,Y)。
xn=ym:即X的最后一个元素与Y的最后一个元素相同,这说明该元素一定位于公共子序列中。现在只需要找到LCS(Xn-1,Ym-1)就好,LCS(X,Y)=LCS(Xn-1,Ym-1)+1
xn != ym:产生两个子问题:LCS(Xn-1,Ym) 和 LCS(Xn,Ym-1)

用二维数组c[i][j]记录串x1x2⋯xi与y1y2⋯yj的LCS长度,则可得到状态转移方程


状态转移方程

设所给的两个序列为X=<A,B,C,B,D,A,B>和Y=<B,D,C,A,B,A>。由算法LCS_LENGTH和LCS计算出的结果如下图所示

image.png

长度

public static int lcs(String str1, String str2) {
    int len1 = str1.length();
    int len2 = str2.length();
    int c[][] = new int[len1+1][len2+1];
    for (int i = 0; i <= len1; i++) {
        for( int j = 0; j <= len2; j++) {
            if(i == 0 || j == 0) {
                c[i][j] = 0;
            } else if (str1.charAt(i-1) == str2.charAt(j-1)) {
                c[i][j] = c[i-1][j-1] + 1;
            } else {
                c[i][j] = max(c[i - 1][j], c[i][j - 1]);
            }
        }
    }
    return c[len1][len2];
}

字符串


public static void lcs_put(String str1, String str2) {
    int len1 = str1.length();
    int len2 = str2.length();
    int c[][] = new int[len1+1][len2+1];
    for (int i = 0; i <= len1; i++) {
        for( int j = 0; j <= len2; j++) {
            if(i == 0 || j == 0) {
                c[i][j] = 0;
            } else if (str1.charAt(i-1) == str2.charAt(j-1)) {
                c[i][j] = c[i-1][j-1] + 1;
            } else {
                c[i][j] = max(c[i - 1][j], c[i][j - 1]);
            }
        }
    }
    Stack stack = new Stack();
    while((len1-1 >= 0) && (len2-1 >= 0)){
            if(s1[len1-1] == s2[len2-1]){//字符串从后开始遍历,如若相等,则存入栈中
                stack.push(s1[len1-1]);
                len1--;
                len2--;
            }else{
                if(array[len1+1][len2] > array[len1][len2+1]){
//如果字符串的字符不同,则在数组中找相同的字符
//数组的行列要比字符串中字符的个数大1,因此i和j要各加1
                    len2--;
                }else{
                    len1--;
                }
            }
        }
     while(!stack.isEmpty()){//打印输出栈正好是正向输出最大的公共子序列
            System.out.print(stack.pop());
        }   
}    
    /**或者使用递归方法**/
   // 输出LCS序列
public static void print(int[][] c, char[] str1, char[] str2, int len1, int len2) {
        if(len1 == 0 || len2 == 0)
            return;
        if(X[len1-1] == Y[len2-1]) {
            System.out.print("element " + str1[len1-1] + " ");
            // 寻找的
            print(c, str1, str2, len1-1, len2-1);
        }else if(c[len1-1][len2] >= c[len1][len2-1]) {
            print(c, str1, str2, len1-1, len2);
        }else{
            print(c, str1, str2, len1, len2-1);
        }
    }
DP:
状态转移方程

例子:

image.png

长度

public static int lcs(String str1, String str2) {
    int len1 = str1.length();
    int len2 = str2.length();
    int result = 0;     //记录最长公共子串长度
    int c[][] = new int[len1+1][len2+1];
    for (int i = 0; i <= len1; i++) {
        for( int j = 0; j <= len2; j++) {
            if(i == 0 || j == 0) {
                c[i][j] = 0;
            } else if (str1.charAt(i-1) == str2.charAt(j-1)) {
                c[i][j] = c[i-1][j-1] + 1;
                result = max(c[i][j], result);
            } else {
                c[i][j] = 0;
            } 
      }
   }
     return result;
}

字符串

public static String lcs_put(String str1, String str2) {
    String res = " ";
    int len1 = str1.length();
    int len2 = str2.length();
    int result = 0;     //记录最长公共子串长度
    int index = 0;     
    int c[][] = new int[len1+1][len2+1];
    for (int i = 0; i <= len1; i++) {
        for( int j = 0; j <= len2; j++) {
            if(i == 0 || j == 0) {
                c[i][j] = 0;
            } else if (str1.charAt(i-1) == str2.charAt(j-1)) {
                c[i][j] = c[i-1][j-1] + 1;
                if(c[i][j]>result){
                    result = c[i][j];
                    index = i;  
                }
            } else {
                c[i][j] = 0; 
             }
         }
    }
    res = s1.substring(index-result+1,index+1); 
    return res;
}

7.背包问题

0-1背包问题:每件物品或被带走,或被留下,(需要做出0-1选择)。不能只带走某个物品的一部分或带走两次以上同一个物品。
部分背包问题:可以只带走某个物品的一部分,不必做出0-1选择。
//(0-1背包问题的一件物品可以想象成是一个金锭;而部分背包问题中的一件物品可以想象成是金粉)
关于0-1背包问题,采用的是动态规划的解决方法;而部分背包问题采用的是贪心法。
 0-1背包问题:
 在选择是否要把一个物品加到背包中,必须把该物品加进去的子问题的解与不取该物品的子问题的解进行比较。这种方式形成的问题导致了许多重叠子问题,满足动态规划的特征。
 部分背包问题:
 总是选择每一磅价值 (Vi / Wi) 最大的物品添加进背包中。那么其解决过程是:对每磅价值进行排序,依次从大到小选择添加进背包中。


0-1背包基本思路
void FindMax()//动态规划
{
    int i,j;
    //填表
    for(i=1;i<=number;i++)
    {
        for(j=1;j<=capacity;j++)
        {
            if(j<w[i])//包装不进
            {
                V[i][j]=V[i-1][j];
            }
            else//能装
            {
                if(V[i-1][j]>V[i-1][j-w[i]]+v[i])//不装价值大
                {
                    V[i][j]=V[i-1][j];
                }
                else//前i-1个物品的最优解与第i个物品的价值之和更大
                {
                    V[i][j]=V[i-1][j-w[i]]+v[i];
                }
            }
        }
    }
}

回朔法寻找组成的物品:


image.png
void FindWhat(int i,int j)//寻找解的组成方式
{
    if(i>=0)
    {
        if(V[i][j]==V[i-1][j])//相等说明没装
        {
            item[i]=0;//全局变量,标记未被选中
            FindWhat(i-1,j);
        }
        else if( j-w[i]>=0 && V[i][j]==V[i-1][j-w[i]]+v[i] )
        {
            item[i]=1;//标记已被选中
            FindWhat(i-1,j-w[i]);//回到装包之前的位置
        }
    }
}

空间优化(二维转一维俗称滚动数组)
(1)每一次V(i)(j)改变的值只与V(i-1)(x) {x:1...j}有关,V(i-1)(x)是前一次i循环保存下来的值;
(2)V缩减成一维数组,从而达到优化空间的目的,状态转移方程转换为 B(j)= max{B(j), B(j-w(i))+v(i)}

并且,状态转移方程,每一次推导V(i)(j)是通过V(i-1)(j-w(i))来推导的,所以一维数组中j的扫描顺序应该从大到小(capacity到0),否者前一次循环保存下来的值将会被修改,从而造成错误。

void FindMaxBetter()//优化空间后的动态规划
{
    int i,j;
    for(i=1;i<=number;i++)
    {
        for(j=capacity;j>=0"(或者j>=w[i])";j--)
        {
            if(B[j]<=B[j-w[i]]+v[i] && j-w[i]>=0 )//二维变一维
            {
                B[j]=B[j-w[i]]+v[i];
            }else{
                B[j] = B[j]; 
           }
        }
    }
}

若扫描顺序从小到大,它却是另一个重要的背包问题P02最简捷的解决方案。
背包问题P02--完全背包问题
有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的体积是w[i],价值是c[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

这个算法使用一维数组,先看伪代码:
for i=1..N
    for v=0..V
        f[v]=max{f[v],f[v-cost]+weight}

public class test{
      public static void main(String[] args){
           int[] weight = {3,4,6,2,5};
           int[] val = {6,8,7,5,9};
           int maxw = 10;
           int[] f = new int[maxw+1];
           for(int i=0;i<f.length;i++){
               f[i] = 0;
           }
           for(int i=0;i<val.length;i++){
               for(int j=weight[i]"(或者j=0)";j<f.length;j++){
                   f[j] = Math.max(f[j], f[j-weight[i]]+val[i]);
               }
           }
           System.out.println(f[maxw]);
      }
}

小技巧,初始化
如果要求恰好装满背包,那么在初始化时除了f[0]为0其它f[1..V]均设为-∞,这样就可以保证最终得到的f[N]是一种恰好装满背包的最优解。
如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将f[0..V]全部设为0。

8.找零钱问题

9.数组最大不连续递增子序列与数组最大连续子序列和

数组最大不连续递增子序列
arr[] = {3,1,4,1,5,9,2,6,5}的最长递增子序列长度为4。即为:1,4,5,9

public static int MaxChildArrayOrder(int a[]) {
        int n = a.length;
        int temp[] = new int[n];//temp[i]代表0...i上最长递增子序列
        for(int i=0;i<n;i++){
            temp[i] = 1;//初始值都为1
        }
        for(int i=1;i<n;i++){
            for(int j=0;j<i;j++){
                if(a[i]>a[j]&&temp[j]+1>temp[i]){
                    //如果有a[i]比它前面所有的数都大,则temp[i]为它前面的比它小的数的那一个temp+1取得的最大值
                    temp[i] = temp[j]+1;
                }
            }
        }
        int max = temp[0];
        //从temp数组里取出最大的值
        for(int i=1;i<n;i++){
            if(temp[i]>max){
                max = temp[i];
            }
        }
        return max;
    }

数组最大连续子序列和
如arr[] = {6,-1,3,-4,-6,9,2,-2,5}的最大连续子序列和为14。即为:9,2,-2,5

public static int MaxContinueArraySum(int a[]) {
        int n = a.length;
        int max = a[0];
        int sum = a[0];
        for(int i=1;i<n;i++){
            sum = Math.max(sum+a[i], a[i]);
            if(sum>=max){
                max = sum;
            }
        }
        return max;
    }

10.数字塔从上到下所有路径中和最大的路径

数字塔是第i行有i个数字组成,从上往下每个数字只能走到他正下方数字或者正右方数字,求数字塔从上到下所有路径中和最大的路径,如有下数字塔

3

1 5

8 4 3

2 6 7 9

6 2 3 5 1

最大路径是3-5-3-9-5,和为25。我们可以分别从从上往下看跟从下往上看两种动态规划的方式去解这个题

从上往下看:当从上往下看时,每进来新的一行,新的一行每个元素只能选择他正上方或者左上方的元素,也就是说,第一个元素只能连他上方的元素,最后一个元素只能连他左上方的元素,其他元素可以有两种选择,所以需要选择加起来更大的那一个数字,并把这个位置上的数字改成相应的路径值,具体过程如下图所示

image.png

所以最大值就是最底层的最大值也就是25。

具体运算过程就是,建立一个n*n的二维数组dp[][],n是数字塔最后一行的数字个数,二维数组每一行数字跟数字塔每一行数字个数一样,保存的值是从上方到这一个位置最大路径的值,填入边界值dp[0][0]=3,每一行除了第一个值跟最后一个值,其他的值选择上方或者左上方更大的值与这个位置上的值相加得来的值,即dp[i][j]=Math.max(dp[i-1][j-1], dp[i-1][j]) + n[i][j]

public static int minNumberInRotateArray(int n[][]) {
        int max = 0;
        int dp[][] = new int[n.length][n.length];
        dp[0][0] = n[0][0];
        for(int i=1;i<n.length;i++){
            for(int j=0;j<=i;j++){
                if(j==0){
                    //如果是第一列,直接跟他上面数字相加
                    dp[i][j] = dp[i-1][j] + n[i][j];
                }else{
                    //如果不是第一列,比较他上面跟上面左面数字谁大,谁大就跟谁相加,放到这个位置
                    dp[i][j] = Math.max(dp[i-1][j-1], dp[i-1][j]) + n[i][j];
                }
                max = Math.max(dp[i][j], max);
            }
        }
        return max;
    }

优化:动态规划中每一个需要创建一个二维数组的解法,都可以换成只创建一个一维数组的滚动数组解法,依据的规则是一般二维数组中存放的是所有的结果,但是一般我们需要的结果实在二维数组的最后一行的某个值,前面几行的值都是为了得到最后一行的值而需要的,所以可以开始就创建跟二维数组最后一行一样大的一维数组,每次存放某一行的值,下一次根据这一行的值算出下一行的值,在存入这个数组,也就是把这个数组滚动了,最后数组存储的结果就是原二维数组中最后一行的值。

拿到本题来说,开始创建一个一维数组dp[n],初始值只有dp[0]=3,新进来一行时,仍然遵循
dp[i][j]=Math.max(dp[i-1][j-1], dp[i-1][j]) + n[i][j],现在为求dp[j],所以现在dp[i-1][j]其实就是数组中这个位置本来的元素即dp[j],而dp[i-1][j-1]其实就是数组中上一个元素dp[j-1],
也就是说dp[j]=Math.max(dp[j], dp[j-1])+n[i][j]

public static int minNumberInRotateArray2(int n[][]) {
        int[] temp = new int[n.length];
        temp[0] = n[0][0];
        for(int i=1;i<n.length;i++){
            for(int j=i;j>=0;j--){
                if(j==i){
                    temp[i]=temp[i-1]+n[i][j];
                }else if(j==0){
                    temp[0]+=n[i][0];
                }else{
                    temp[j]=Math.max(temp[j], temp[j-1])+n[i][j];
                }
            }
        }
        int max = temp[0];
        //从temp数组里取出最大的值
        for(int i=1;i<temp.length;i++){
            if(temp[i]>max){
                max = temp[i];
            }
        }
        return max;
    }

这样空间复杂度就大幅度下降了。

从下往上看时:从下往上看时大体思路跟从上往下看一样,但是要简单一些,因为不用考虑边界数据,从下往上看时,每进来上面一行,上面一行每个数字有两条路径到达下面一行,所以选一条最大的就可以

所以最大值就是最上面数字就是25.

具体方法也是建立一个二维数组,最下面一行数据添到二维数组最后一行,从下往上填数字,所以状态转化方程是dp[i][j]=Math.max(dp[i+1][j+1], dp[i+1][j]) + n[i][j],具体解决方法跟从上往下看一样,就不写具体代码了。

优化:滚动数组,只创建一个一维数组,数组初始值是数字塔最下面一行的值,每次新加一行值,将数组中的值改变,最后数组中第一个数字就是最大路径的值。状态转化方程就是
temp[j] = Math.max(temp[j], temp[j+1])+n[i][j]。具体代码如下

public static int minNumberInRotateArray3(int n[][]) {
        int[] temp = new int[n.length];
        for(int i=0;i<n.length;i++){
            temp[i] = n[n.length-1][i];
        }
        for(int i=n.length-2;i>=0;i--){
            for(int j=0;j<=i;j++){
                temp[j] = Math.max(temp[j], temp[j+1])+n[i][j];
            }
        }
        return temp[0];
    }

从下往上看跟从上往下看相比,虽然逻辑较为简单,但是从下往上看时需要得到完整的数字塔之后才能开始计算,而从上往下看时可以随着数字塔的深入来计算,也可以返回任意一层的结果,是最好的方法。

  • 找出矩阵乘法的最优顺序
  • 最优二叉查找树
  • 所有点的最短路径
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335