Skip to content

Latest commit

 

History

History
556 lines (437 loc) · 17.3 KB

动态规划.md

File metadata and controls

556 lines (437 loc) · 17.3 KB

动态规划思想

image-20210720164647513

写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项(即 F(N))。斐波那契数列的定义如下:

F(0) = 0, F(1) = 1 F(N) = F(N - 1) + F(N - 2), 其中 N > 1. 斐波那契数列由 0 和 1 开始,之后的斐波那契数就是由之前的两数相加而得出。

答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。

  public void fib(){
        int n = 45;
        int l = 1, r = 0, tar = 0;
      //l代表F(N - 1),r代表F(N - 2)
        for (int i = 2; i <= n; i++) { //前两个已知无需计算
            tar = (l + r)%1000000007;
            r = l;
            l = tar;
            System.out.println(tar);
        }
    }

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

public void lengthOfLIS() {
    int[] nums = new int[]{10,9,2,5,3,7,101,18};
    int[] dp = new int[nums.length];
     //dp代表以i为结尾的最长子序列长度
    int max = 1;
    //备忘录 记录最大值
    Arrays.fill(dp,1);
   //初始化为1,每个最长子序列最小为1
    for (int i = 1; i < nums.length; i++) {
        for (int j = 0; j < i ; j++) {
            if (nums[i] > nums[j]){
                dp[i] = Math.max(dp[j] + 1, dp[i]);
            }
            max = Math.max(max, dp[i]);
        }
    }
    System.out.println(max);
}		

给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。 两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

思路:其实很简单,就是dp表示的是以i和j为结尾的序列的最大公共子序列的长度

知识点

1、思路问题:动态规划也是有套路的:单个数组或者字符串要用动态规划时,可以把动态规划 dp[i] 定义为 nums[0:i] 中想要求的结果;当两个数组或者字符串要用动态规划时,可以把动态规划定义成两维的 dp[i][j] ,其含义是在 A[0:i] 与 B[0:j] 之间匹配得到的想要的结果。

2、边界问题:动态规划dp经常需要定义为n+1和m+1 这个题浪费的全部时间都在于这个边界不明确

3、base问题:这个的base 是当text1和text2为空指针的时候的最大公共子序列的长度,那么一定为0

  int[][] dp = new int[text1.length() + 1][text2.length() + 1];
        //dp记录的是以i为结尾和以j为结尾的序列的最大公共子序列的长度

//        for (int i = 0; i < text1.length(); i++) {
//            System.out.println(i);
//            Arrays.fill(dp[i], 0);
//        }  
//完全没有必要,因为数组的默认值为0

        int max = 0;
		//记录最大值

//下面全部是在浪费时间,只需要把数组定义为+1就行了
//        for (int i = 0; i < text1.length(); i++) {
//            if (text1.charAt(i) == text2.charAt(0)){
//                if (i > 0 && dp[i-1][0] == 1){
//                    dp[i][0] = 1;
//
//                }
//                else {
//                    dp[i][0] = 1;
//                }
//            }
//            else {
//                if (i > 0) dp[i][0] = dp[i-1][0];
//            }
//            max = Math.max(max,  dp[i][0]);
//        }
//        for (int j = 0; j < text2.length(); j++) {
//            if (text1.charAt(0) == text2.charAt(j)){
//                if(j > 0 && dp[0][j-1] == 1){
//                    dp[0][j] = 1;
//                }
//                else {
//                    dp[0][j] = 1;
//                }
//            }
//            else {
//                if (j > 0) dp[0][j] = dp[0][j-1];
//            }
//            max = Math.max(dp[0][j], max);
//        }
        
        for (int i = 1; i <= text1.length(); i++) {
            for (int j = 1; j <= text2.length(); j++) {
                if (text1.charAt(i - 1) == text2.charAt(j - 1)){
                    dp[i][j] = dp[i-1][j-1] + 1;
                    //这里不需要取[i-1][j] 和 [i][j-1],因为在不相同的时候已经比较过了,所以直接全部往前一个指针就行了
                }
                else {
                    dp[i][j] =  Math.max(dp[i-1][j], dp[i][j-1]);
                }
                System.out.println("i:"+i+",j:"+j+",dp:"+dp[i][j]);
                max = Math.max(dp[i][j], max);
            }
        }

        System.out.println(max);

    }

背包九讲:https://blog.csdn.net/yandaoqiusheng/article/details/84782655/

正序逆序问题 https://blog.csdn.net/yandaoqiusheng/article/details/84929357

01背包问题

https://www.acwing.com/problem/content/2/

二维数组

import java.util.Scanner;
public class Pack01 {
    public static void main(String[] args) {
        int N = 1001;
        int n, m;
        //总个数和总体积
        int[] v = new int[N];//每个的体积
        int[] w = new int[N];//每个的价值
        int[][] dp = new int[N][N];//代表的是前i件物品放入剩余容量为j的背包中的最大价值
        Scanner scanner = new Scanner(System.in);
        n = scanner.nextInt();
        m = scanner.nextInt();
        for (int i = 1; i <= n; i++) {
            v[i] = scanner.nextInt();
            w[i] = scanner.nextInt();
        }

        int best = 0;
        for (int i = 1; i <= n; i++) {
            for (int j = 0; j <= m; j++) {
                if (j < v[i]){ //第一个选择,如果容量不够vi
                    dp[i][j] = dp[i-1][j];
                }else {
                    dp[i][j] = Math.max(dp[i-1][j-v[i]]+w[i],dp[i-1][j]);//第二个选择,如果够vi,vi放不放
                }
//                System.out.println(dp[i][j]);
                best = Math.max(best, dp[i][j]);
            }
        }
        System.out.println(best);
    }
}

一维数组

主要思路要转过来弯

https://blog.csdn.net/weixin_41061962/article/details/80319436 讲的非常好。

数组中保存的是外循环中i-1次的值,还未更新,所以max中的两个值都代表的dp[i-1]

public static void main(String[] args) {
    int N = 1001;
    int n, m;
    //总个数和总体积

    int[] v = new int[N];//每个的体积
    int[] w = new int[N];//每个的价值
    int[] dp = new int[N];//代表的是前i件物品放入剩余容量为j的背包中的最大价值

    Scanner scanner = new Scanner(System.in);
    n = scanner.nextInt();
    m = scanner.nextInt();

    for (int i = 1; i <= n; i++) {
        v[i] = scanner.nextInt();
        w[i] = scanner.nextInt();
    }

    int best = 0;
    for (int i = 1; i <= n; i++) {
        for (int j = m; j >= v[i]; j--) { //这里是个优化,j必须要在大于V[i]的前提下进行循环

            dp[j] = Math.max(dp[j-v[i]]+w[i],dp[j]);//第二个选择,如果够vi,vi放不放
//          代表的是dp[i][j]                  代表的是dp[i-1][j]
//          原因:现在数组中保存的是外循环中i-1次的值,还未更新,所以max中的两个值都代表的dp[i-1]
            best = Math.max(best, dp[j]);
        }
    }
    System.out.println(best);
}
}
完全背包问题

方法1:当做0,1背包问题进行理解,区别在于是否能够增加k个i

//换成01背包问题
    public static void main(String[] args) {
        int N = 1001;
        int n, m;
        //总个数和总体积

        int[] v = new int[N];//每个的体积
        int[] w = new int[N];//每个的价值
        int[] dp = new int[N];//代表的是剩余容量为j的背包中的最大价值

        Scanner scanner = new Scanner(System.in);
        n = scanner.nextInt();
        m = scanner.nextInt();

        for (int i = 1; i <= n; i++) {
            v[i] = scanner.nextInt();
            w[i] = scanner.nextInt();
        }

        int best = 0;

        for (int i = 1; i <= n ; i++) {
            for (int j = m; j >= v[i] ; j--) {
                for (int k = 0; k*v[i] <= j; k++) { // 可以增加多个i
                    dp[j] = Math.max(dp[j - k* v[i]] + k * w[i], dp[j]);
                    best = Math.max(best, dp[j]);
                }

            }
        }
        System.out.println(best);
    }

方法2:正统做法

img

理解:当外循环为i时,内循环正序可以多次添加当前i的物品

public class WanquanPack {
    public static void main(String[] args) {
        int N = 1001;
        int n, m;
        //总个数和总体积

        int[] v = new int[N];//每个的体积
        int[] w = new int[N];//每个的价值
        int[] dp = new int[N];//代表的是剩余容量为j的背包中的最大价值

        Scanner scanner = new Scanner(System.in);
        n = scanner.nextInt();
        m = scanner.nextInt();

        for (int i = 1; i <= n; i++) {
            v[i] = scanner.nextInt();
            w[i] = scanner.nextInt();
        }

        int best = 0;

        for (int i = 1; i <= n ; i++) {
            for (int j = v[i]; j <= m ; j++) {
                dp[j] = Math.max(dp[j - v[i]] + w[i], dp[j]);
                best = Math.max(best, dp[j]);
            }
        }
        System.out.println(best);
    }

}
多重背包问题

方法1:转化成为完全背包中的01背包问题即可。

public class Main {
    //换成01背包问题
    public static void main(String[] args) {
        int N = 1001;
        int n, m;
        //总个数和总体积
        int[] v = new int[N];//每个的体积
        int[] w = new int[N];//每个的价值
        int[] s = new int[N];//每个的个数
        int[] dp = new int[N];//代表的是剩余容量为j的背包中的最大价值
   Scanner scanner = new Scanner(System.in);
    n = scanner.nextInt();
    m = scanner.nextInt();

    for (int i = 1; i <= n; i++) {
        v[i] = scanner.nextInt();
        w[i] = scanner.nextInt();
        s[i] = scanner.nextInt();
    }

    int best = 0;

    for (int i = 1; i <= n ; i++) {
        for (int j = m; j >= v[i] ; j--) {
            for (int k = 0; k <= s[i] && k * v[i] <= j; k++) { //这里就s
                dp[j] = Math.max(dp[j - k* v[i]] + k * w[i], dp[j]);
                best = Math.max(best, dp[j]);
            }

        }
    }
    System.out.println(best);
}

方法2:对于整数s,k = ceil(log2(s)), 2^0 2^1....2^k 可以表示小于等于s的任何数。

所以可以将物品分为2^0 2^1....2^k个,重量也分别为w*2^0 w*2^1... w*2^k

public class DuochongBeibao {   
	public static void main(String[] args) {
        int N = 1001;
        int n, m;
        int[] dp = new int[N];//代表的是剩余容量为j的背包中的最大价值
        Scanner scanner = new Scanner(System.in);
        n = scanner.nextInt();
        m = scanner.nextInt();
        ArrayList<Good> goods = new ArrayList<>(); //数组存储分类之后的物品
        for (int i = 1; i <= n; i++) {
            int v = scanner.nextInt();
            int w = scanner.nextInt();
            int s = scanner.nextInt(); //代表物品原个数

            for (int k = 1; k <= s ; k *= 2) { //k代表物品分类的个数 每次*2
                s -= k;//s减去已经分类之后的物品数量
                goods.add(new Good(v * k, w * k));
            }
            if (s > 0) goods.add(new Good(v * s, w * s));//如果还有剩余,继续添加到数组中
        }

        int best = 0;
		//此时就完全转化为0,1背包问题
        for (Good good: goods) {
            for (int j = m; j >= good.v ; j--){
                dp[j] = Math.max(dp[j - good.v] + good.w, dp[j]);
                best = Math.max(best, dp[j]);
            }
        }
        System.out.println(best);
    }
}

class Good{
    int v = 0;
    int w = 0;
    public Good(int v, int w){
        this.v = v;
        this.w = w;
    }
}

对于动态规划问题,我将拆解为如下五步曲,这五步都搞清楚了,才能说把动态规划真的掌握了!

  1. 确定dp数组(dp table)以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

**注意:**给定 n 是一个正整数。

示例 1:

输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1.  1 阶 + 1 阶
2.  2 阶

思路:

dp代表的是走到第i层的方法数

dp[i] = dp[i-1] + dp[i-2]

class Solution {
    int res;
    public int climbStairs(int n) {
        if(n==1) return 1;
       int[] dp = new int[n + 1];//代表的是走到第i层的方法数
       dp[1] = 1;
       dp[2] = 2;
       for(int i = 3; i <= n; i++){
           dp[i] = dp[i-1] + dp[i-2];
       }
       return dp[n];
    }
}

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?

输入:m = 3, n = 7
输出:28

思路:初始化不仅仅是第一个,还需要将上、左两个边上的点进行初始化

class Solution {
    public int uniquePaths(int m, int n) {
        int[][] dp = new int[m][n];//i,j坐标有多少方法
         //初始化应该上面一排和最左边一排都为1
        for(int i = 0; i < m; i++){
            dp[i][0] = 1;
        }
        for(int i = 0; i < n; i++){
            dp[0][i] = 1;
        }
        
        for(int i = 1; i < m; i++){
            for(int j = 1; j < n; j++){
                dp[i][j] = dp[i-1][j] + dp[i][j-1];
            }           
        }
        return dp[m-1][n-1];
    }
}

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。

现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?

输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:
3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:

1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右

思路:有了障碍物,

会影响初始化和动态规划的过程

初始化的时候,左、上的要在障碍物之后都标记为0

过程中,遇到障碍物,该点定为0

其他的不变

class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int m = obstacleGrid.length, n = obstacleGrid[0].length;
        int[][] dp = new int[m][n];
        int tempi = m, tempj = n;; //记录上方和左面的障碍物的点
        for(int i = 0; i < m; i++){
            if(obstacleGrid[i][0] == 1) {
                dp[i][0] = 0;
                tempi = i;
            }
            else if(i > tempi) { //障碍物之后的点初始化全为0
                dp[i][0] = 0;       
            }
            else dp[i][0] = 1;
        }
         for(int i = 0; i < n; i++){
            if(obstacleGrid[0][i] == 1){
                dp[0][i] = 0;
                tempj = i;
            } 
             else if(i > tempj) { 
                dp[0][i] = 0;       
            }
            else dp[0][i] = 1;           
        }
        
        for(int i = 1; i < m; i++){
            for(int j = 1; j < n; j++){
                if(obstacleGrid[i][j] == 1){
                    dp[i][j] = 0;
                }
                else dp[i][j] = dp[i-1][j] + dp[i][j-1];
            }
        }
        return dp[m-1][n-1];
    }
}