Bootstrap

算法Day7:动态规划——背包、序列专题

蓝桥杯算法合集: 蓝桥杯算法合集(终极完结版)

动态规划

动态规划一般是用来解决最优子结构或者是子问题最优性,这类问题常见有重叠子问题。比如像递归解决斐波那契数列、爬楼梯方案数等。会发现递归调用过程中存在重复计算。对调用过程进行记忆化也是dp的一种体现。

思路:
我自己写的话一般是用画图工具(也可以用excel),列出表格,从小范围根据递推基一步一步去推,进而发现状态转移方程。
用一个表(常见数组)来记录所有已解子问题的答案。不管该子问题以后是否被用到,只要他被计算过就将结果填入表中,也叫打表法。

一般步骤:

  1. 确定状态(一是般先确定dp数组所表示的意义)
  2. 找到状态转移方程
    要找状态方程可以这样思考,当前面临一个状态,它有可能是从哪儿来的?dp问题通常要逆向思考达到当前这个状态的最后一步,即压死骆驼的最后一根稻草。

本阶段状态=(上一个阶段状态+上一个阶段决策)
状态转移方程要保证当前状态可以接到所有该转移状态的转移。

解法形式:
自顶向上:递归 如果有重叠子问题,带备忘录。比如递归求斐波那契数列还有爬楼梯方案数(这也是dp解法的一种形式,会发现使用带备忘录的递归通常可以ac)。
自底向下:递推
应用:

  • 01背包
  • 完全背包问题
  • 最长上升子序列问题
  • 最长公共子序列

总结有

  • 序列专题:
  • 背包专题:

刷题过程中的小知识点:
(1)子矩阵不一定要取自原矩阵的连续行和列
有些初学者可能会认为,子矩阵一定是如下图这种取自原矩阵连续行列的矩阵。
注意边界处理
原矩阵:
1 2 3
4 5 6
7 8 9

子矩阵:
1 2 3
4 5 6

其实子矩阵不一定取自原矩阵的连续行列,也可以是不连续行或者列:
1 2
4 5

1 3
4 6
7 9
(2)、子序列不一要求连续但是子串要连续

背包问题专题

背包问题常见以下几种

01背包
模型特点:每种物品只有一个,且要么选要么不选,有01两种状态因而以01得名。有限制范围,常见体积或者重量限制,一般用二维数组dp[i][j]表示状态,其中第一个维度表示可选范围,第二个维度表示容量等限制范围。

完全背包
模型特点:与01背包相似,唯一不同的是,完全背包的物品是由无限个。

01背包问题

/*
题目描述
给定N个物品,每个物品有一个重量W和一个价值V.你有一个能装M重量的背包.问怎么装使得所装价值最大.每个物品只有一个.
输入
输入的第一行包含两个整数n, m,分别表示物品的个数和背包能装重量。
以后N行每行两个数Wi和Vi,表示物品的重量和价值
输出
输出1行,包含一个整数,表示最大价值。
样例输入
3 5
2 3
3 5
4 7
样例输出
8
 
4 5
2 3
1 2
3 4
2 2
得到的dp矩阵:
0 0 3 3 3 3
0 2 3 5 5 5
0 2 3 5 6 7
0 2 3 5 6 7
 *
 */

在这里插入图片描述

 package 动态规划;
import java.util.Scanner;
public class _01背包问题_动态规划 {
    public static void main(String[] args) {
        Scanner reader=new Scanner(System.in);
        int n=reader.nextInt();//物品个数
        int m=reader.nextInt();//背包承重量
        int[] w=new int[n];//存放每个物品的重量
        int[] v=new int[n];//存放每个物品的价值
        for(int i=0;i<n;i++) {
            w[i]=reader.nextInt();
            v[i]=reader.nextInt();
//dp[i][j]中第一个维度表示可选范围 第二个维度表示容量
        }
        int [][]dp=new int[n][m+1];
        //初始化dp数组第一行
        for(int i=0;i<=m;i++) {//w[0]<i 中的i刚好可以表示背包容量
            if(w[0]<=i) {//放得下0号物品
                dp[0][i]=v[0];
            }
        }
        for(int i=1;i<n;i++) {
            for(int j=0;j<=m;j++) {
                if(w[i]<=j) {
                    int v1=dp[i-1][j];//不要当前这个物品
                    int v2=dp[i-1][j-w[i]]+v[i];//要当前这个物品
                    dp[i][j]=Math.max(v1, v2);
                }else {
                    dp[i][j]=dp[i-1][j];
                }
            }
        }
        System.out.println(dp[n-1][m]);
//      for(int i=0;i<n;i++) {
//          for(int j=0;j<=m;j++) {
//              System.out.print(dp[i][j]+" ");
//              }
//          System.out.println();
//      }

完全背包问题

/*
有 N 种物品和一个容量是 V
的背包,每种物品都有无限件可用。
第 i种物品的体积是 vi,价值是 wi
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行两个整数,N,V
,用空格隔开,分别表示物品种数和背包容积。
接下来有 N
行,每行两个整数 vi,wi,用空格隔开,分别表示第 i
种物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤1000
0<vi,wi≤1000
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
10
链接:
https://www.acwing.com/problem/content/description/3/
 */
package 动态规划;
import java.util.Scanner;
public class 完全背包问题 {
    public static void main(String[] args) {
        Scanner reader=new Scanner(System.in);
        int n=reader.nextInt();//物品个数
        int m=reader.nextInt();//背包承重量
        int[] w=new int[n];//存放每个物品的重量
        int[] v=new int[n];//存放每个物品的价值
        for(int i=0;i<n;i++) {
            w[i]=reader.nextInt();
            v[i]=reader.nextInt();
        }
        int[][] dp=new int[n][m+1];
        //初始化dp数组第一行
        for(int i=0;i<m;i++) {
            if(w[0]<=i) {
                dp[0][i]=v[0]+dp[0][i-w[0] ];
            }
            
        }
        for(int i=1;i<n;i++) {//i表示行号 同时也是物品号
            for(int j=1;j<=m;j++) {//j表示列好 同时也是背包容量
                if(w[i]<=j) {
                    int v1=v[i]+dp[i][j-w[i]];//选当前物品
                    int v2=dp[i-1][j];
                    dp[i][j]=Math.max(v1,v2);
                }else {
                    dp[i][j]=dp[i-1][j];
                }
                
            }
        }
//      for(int i=0;i<n;i++) {
//          for(int j=1;j<=m;j++) {
//              System.out.print(dp[i][j]+" ");
//          }
//          System.out.println();
//      }
        System.out.println(dp[n-1][m]);
        
    }
}
    }
}

序列专题

动态规划常见序列模型专题有以下几种:

最大子段问题

最长上升子序列
dp[i]表示以i号元素为结尾的最长上升子序列长度

最大公共子序列
dp[i][j]表示s1前i个元素和s2前j元素的公共子列长度

最大子段和问题

最大子段和问题
最大子段和就是所有子段中和最大的
例如:
-2 11 -4 13 - 5 -2
最大子段和就是:11+(-4)+13=20
在这里插入图片描述

package 常见动态规划模型;
/*
测试数据:
6
-2 11 -4 13 -5 -2
答案:
20
*/


import java.util.Scanner;
public class 最大子段和 {
    static int INF=0x7fffffff;//16进制表示的最大整数
    public static void main(String[] args) {
        Scanner reader=new Scanner(System.in);
        int n=reader.nextInt();
        int []num=new int[n+1];
        for(int i=0;i<n;i++) {
            num[i]=reader.nextInt();
        }
        int ans=-INF;
        for(int i=0;i<n;i++) {//如果序列全为非正数 最大子段和为最大值
            ans=Math.max(ans, num[i]);
        }
        if(ans<=0) {//特判掉全为非正数的情况
            System.out.println(ans);
        }else{//序列有正也有负
            int sum=0;
            for(int i=0;i<n;i++) {
                if(sum+num[i]<=0) {
                    sum=0;
                }else {//以i号元素为结尾的序列有正向作用
                    sum=sum+num[i];
                }
                ans=Math.max(ans, sum);//看看是否需要更新ans
            }
            System.out.println(ans);
        }
    }
}

最长上升子序列

在这里插入图片描述
在这里插入图片描述

package 常见动态规划模型;
/*
测试数据:
6
3 2 6 1 4 5
答案:3
6
4 10 4 3 8 9
答案:3
*/
import java.util.Scanner;
public class 最长上升子序列 {
    static int n;
    static int []a;
    static int []dp;
    //dp[i]表示以i号元素结尾的最长上升子序列个数
    //状态方程:dp[i]=max(dp[i],dp[j]+1) (1<=j<i  && a[j]<a[i])
    //时间复杂度:O(n^2)
    static int LIS() {
        int ans=0;
        for(int i=1;i<=n;i++){
            
            dp[i]=1;//初值
            for(int j=1;j<i;j++) {
                if(a[j]<a[i]) {//更新为较大者
                    dp[i]=Math.max(dp[i],  dp[j]+1);
                }
            }
            ans=Math.max(ans, dp[i]);
        }
        return ans;
    }
    public static void main(String[] args) {
        Scanner reader=new Scanner(System.in);
        n=reader.nextInt();
        dp=new int[n+1];
        a=new int[n+1];
        for(int i=1;i<=n;i++) {
            a[i]=reader.nextInt();
        }
        System.out.println(LIS());
    }
}

最长公共子序列

dp表
在这里插入图片描述
再举个例子
在这里插入图片描述

package 动态规划;
public class 最长公共子序列 {
    
    /**递归
     * @param nums1
     * @param i 表示nums1数组的前i个
     * @param nums2
     * @param j 表示nums2数组的前j个
     * @return 最长公共子序列的长度
     * 空间复杂度:O(Math.min(m,n) 递归深度*辅助空间
     * 时间复杂度:O(2^n)
     */
    int LongestCommonSubsequence0(String s1,int  i,String s2,int j) {
        if(i==0||j==0||s1==null||s2==null)return  0;
        if(s1.charAt(i-1)==s2.charAt(j-1)) {
            return  LongestCommonSubsequence0(s1,i-1,s2,j-1)+1;
        }
        //返回nums1前i个与nums2前j-1个 和 nums1前i-1个与nums2前j个最长公共子序列的较大者
        return  Math.max(LongestCommonSubsequence0(s1,i,s2,j-1  ),
                LongestCommonSubsequence0(  s1,i-1,s2,j ) );
    }
    
    //动态规划解法
    /**确定状态 dp[i][j] 表示s1的前i个字符与s2的前j个字符的公共子序列长度
     * 递推基 dp[0][j] 和 dp[i][0]都为0
     * @param s1
     * @param s2
     * @return dp[len1][len2]即为s1和s2的最大公共子序列长度
     * 空间复杂度:O(m*n)
     * 时间复杂度:O(m*n)
     *
     */
    int LongestCommonSubsequence1(String  s1,String s2) {
        if(s1==null||s2==null)return 0;
        int m=s1.length();
        int n=s2.length();
        int [][]dp=new int [m+1][n+1];
        for(int i=1;i<=m;i++) {//枚举每一行
            for(int j=1;j<=n;j++) {
                if(s1.charAt(i-1)==s2.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];
    }
    /**
     * 优化空间复杂度为O(2*(m或n))
     * 使用两行的滚动数组存储状态
     * 字符串为空即 "" 存在对象但length为0
     * 与字符串等于null不一样 为null表示该对象不存在 不能调用字符串的方法
     * @param s1
     * @param s2
     * @return
     */
    int LongestCommonSubsequence2(String  s1,String s2) {
        if(s1==null||s2==null)return 0;
        int m=s1.length();
        int n=s2.length();
        if(m==0||n==0) return 0;
        int [][]dp=new int [2][n+1];
        for(int i=1;i<=m;i++) {
            for(int j=1;j<=n;j++) {
                if(s1.charAt(i-1)==s2.charAt(j-1)) {
                    dp[i%2][j]=dp[(i-1)%2][j-1]+1;
                }else {
                    dp[i%2][j]=Math.max(dp[(i-1)%2][j],  dp[i%2][j-1]);
                }
            }
        }
        return dp[m%2][n];
    }
    int LongestCommonSubsequence3(String  s1,String s2) {
        if(s1==null||s2==null)return 0;
        int m=s1.length();
        int n=s2.length();
        if(m==0||n==0) return 0;
        int []dp=new int[n+1];
        for(int i=1;i<=m;i++) {
            int cur=0;
            for(int j=1;j<=n;j++) {
                int lefttop=cur;
                cur=dp[j];
                if(s1.charAt(i-1)==s2.charAt(j-1)) {
                    dp[j]=lefttop+1;
                }else {
                    dp[j]=Math.max(dp[j-1],  dp[j]);
                }
            }
            
        }
        return dp[n];
    }
    /**
     * 使用一维数组 包含上一行部分dp值和本行部分dp值
     * @param s1
     * @param s2
     * @return
     */
    int LongestCommonSubsequence4(String  s1,String s2) {
        if(s1==null||s2==null)return 0;
        int m=s1.length();
        int n=s2.length();
        if(m==0||n==0) return 0;
        int []dp=new int[n+1];
        for(int i=1;i<=m;i++) {
            int lefttop=0;//换到下一行之后要清零
            for(int j=1;j<=n;j++) {
                if(s1.charAt(i-1)==s2.charAt(j-1)) {
                    //覆盖前先保存dp[j] 该值为下一个的左上
                    int now=dp[j];
                    dp[j]=lefttop+1;
                    lefttop=now;
                }else {
                    lefttop=dp[j];
                    dp[j]=Math.max(dp[j-1],  dp[j]);
                }
            }
            
        }
        return dp[n];
    }
    
    /**
     * 以长度较小者作为dp的长度
     * @param s1
     * @param s2
     * @return
     */
    int LongestCommonSubsequence5(String  s1,String s2) {
        if(s1==null||s2==null)return 0;
        if(s1.length()==0||s2.length()==0)  return 0;
        String rowNums,colNums;
        //s1长度比s2大
        if(s1.length()>s2.length()) {
            rowNums=s1;
            colNums=s2;
        
        }else {
            rowNums=s2;
            colNums=s1;     
        }
        
        int []dp=new int[colNums.length()+1];
        for(int i=1;i<=rowNums.length();i++) {
            int cur=0;
            for(int j=1;j<=colNums.length();j++)  {
                int lefttop=cur;
                cur=dp[j];
                if(rowNums.charAt(i-1)==colNums.charAt(j-1)) {
                    dp[j]=lefttop+1;
                }else {
                    dp[j]=Math.max(dp[j-1],  dp[j]);
                }
            }
            
        }
        return dp[colNums.length()];
    }
    public static void main(String[] args) {
        String s1="BA34C";
        String s2="A1BC2";
        int len1=s1.length();
        int len2=s2.length();
        最长公共子序列 a=new 最长公共子序列();
        System.out.println(a.LongestCommonSubsequence0(s1,len1,s2,len2));
        System.out.println(a.LongestCommonSubsequence1(s1,s2));
        System.out.println(a.LongestCommonSubsequence2(s1,s2));
        System.out.println(a.LongestCommonSubsequence3(s1,s2));
        System.out.println(a.LongestCommonSubsequence4(s1,s2));
        System.out.println(a.LongestCommonSubsequence5(s1,s2));
    }
}
;