动态规划问题--算法讲解(每日更新 1/29)完全背包问题理论基础以及零钱兑换II

动态规划问题要点

  • dp数组以及下标的含义
  • 递推公式
  • DP数组初始化
  • DP数组遍历顺序
    • 从前往后/从后往前
    • 多层循环中先遍历谁,后遍历谁
  • 打印DP数组,用于测试

在思考的过程中,往往先确定递推公式,再考虑初始化。因为递推公式会影响初始化。

动态规划入门

一.斐波那契数列

1.确定dp[i]及其含义

​ dp[i]表示第i个斐波那契数的值

​ dp[0] = 0 dp[1] = 1 dp[2] = 1 dp[3] = 2

2.递推公式

​ 题目已经告诉我们递推公式为

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

3.dp数组如何初始化?

​ 题目已经告诉我们了

​ dp[0] = 0 dp[1] = 1 dp[2] = 1 dp[3] = 2

4.遍历顺序

​ 从前向后

5.打印遍历数组,用于debug

class Solution {
    public int fib(int n) {
        int[] dp = new int[31];
        dp[0] = 0;
        dp[1] = 1;
        for(int i = 2;i <= n;i++) {
            dp[i] = dp[i - 1] + dp[i - 2];
        }
        return dp[n];
    }
}

我们可以发现,其实不需要这么长的数组,可以通过3个变量来替代dp数组与递推公式,优化后

class Solution {
    public int fib(int n) {
        if(n < 2)  return n;
        int sum = 1,d = 0,p = 1;
        for(int i = 2;i < n;i++) {
            d = p;
            p = sum;
            sum = d + p;
        }
        return sum;
    }
}

二.爬楼梯

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

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

1.确定dp[i]及其含义

​ dp[i]:达到第i阶有dp[i]种方法

2.递推公式

​ 分析:最后一步可能跨了1级台阶,也可能跨2级台阶

​ 我们用f(x)表示爬到第x级台阶的方案数,那么
f ( x ) = f ( x − 1 ) ∗ 1 + f ( x − 2 ) ∗ 1 f(x) = f(x - 1) * 1 + f(x - 2)*1 f(x)=f(x1)1+f(x2)1
​ 即爬到第 x 级台阶的方案数是爬到第 x - 1 级台阶的方案数 * 从x-1级台阶 只用一步到第x级台阶的方法数(只有跨越1步 一 种) 和爬到第 x−2 级台阶的 方案数的和

​ 如此,我们便可以写出递推公式

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

3.dp数组如何初始化?

​ 可以发现

  • 到达第一阶有1种方法 dp[1] = 1
  • 到达第二阶有2种方法 dp[2] = 2
  • 到达第三阶有3种方法 dp[3] = 3
  • 而n为正整数,第0阶实际上也是没有任何含义的,我们可以不用管它,让dp[0]取默认值0

4.遍历顺序

从前往后

5.打印遍历数组,用于debug

class Solution {
    public int climbStairs(int n) {
        if(n < 4)   return n;
        int[] dp = new int[n + 1];
        dp[1] = 1;
        dp[2] = 2;
        for(int i = 3;i < n + 1;i++) {
            dp[i] = dp[i-1] + dp[i-2];
        }
        return dp[n];
    }
}

同斐波那契数列一样,优化后

class Solution {
    public int climbStairs(int n) {
        int p = 0, q = 0, r = 1;
        for (int i = 1; i <= n; ++i) {
            p = q; 
            q = r; 
            r = p + q;
        }
        return r;
    }
}

三.使用最小花费爬楼梯

题目:

给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。

你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。

请你计算并返回达到楼梯顶部的最低花费。

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/min-cost-climbing-stairs

对题意的解释与分析

设一共爬了 j 次,爬楼梯过程中踩到的台阶为i1、i2…ij,j表示顶楼,而踩到某台阶时,不会花费,只有当从该台阶往上跳时才花费。

因此
到达第 j 阶的总花费 = c o s t [ i 1 ] + c o s t [ i 2 ] + . . . + c o s t [ i ( j − 1 ) ] 到达第j阶的总花费=cost[i1] + cost[i2] + ... + cost[i(j-1)] 到达第j阶的总花费=cost[i1]+cost[i2]+...+cost[i(j1)]
1.确定dp[i]及其含义

​ dp[i]表示 到达第j阶(楼顶)的最小花费

2.递推公式

​ 参考爬楼梯的思想,爬楼梯时只能爬1个或2个。考虑dp[i - 1]、dp[i - 2]对dp[i]的关系

​ dp[i] = min{dp[i - 1] + cost[i - 1] , dp[i - 2] + cost[i - 2]}

3.dp数组如何初始化?

​ 以第0阶和第1阶为楼顶,由题目"你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯",而经过分析,到达楼顶不需要花费,易得

​ dp[0] = 0

​ dp[1] = 0

4.遍历顺序

​ 从前向后

5.打印遍历数组,用于debug

class Solution {
    public int minCostClimbingStairs(int[] cost) {
        int[] dp = new int[cost.length + 1];
        
        dp[0] = 0;
        dp[1] = 0;
        
        for(int i = 2;i < cost.length + 1;i++) {
            dp[i] = Math.min(dp[i-1] + cost[i-1],dp[i-2]+cost[i-2]);
        }
        return dp[cost.length];
    }
}

四.不同路径

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

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

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

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/unique-paths

1.确定dp[i][j]及其含义

​ 由m*n网格,我们可以很容易想到dp数组为一个二维数组,题目要求求出共有多少条不同路径,dp[i][j]表示到达第(i,j)位置有多少条不同的路径

2.递推公式

​ 机器人只能向下或者向右移动一步,那么上一步一定是在上面一格或者左边一格,考虑dp[i - 1][j]dp[i][j-1]dp[i][j]的关系
d p [ i ] [ j ] = d p [ i − 1 ] [ j ] + d p [ i ] [ j − 1 ] dp[i][j] =dp[i - 1][j]+dp[i][j-1] dp[i][j]=dp[i1][j]+dp[i][j1]
3.dp数组如何初始化?

​ 由递推公式,dp[i][j]dp[i - 1][j]dp[i][j-1]得到,那么我们必须初始化第0行和第0列才能顺利得到dp[i][j]

dp[0][0]无意义,为0

dp[0][j]=1

dp[i][0]=1

4.遍历顺序

​ 推导的基础在最上面一行和最左边一列,

​ 从左往右,从上到下

5.打印遍历数组,用于debug

class Solution {
    public int uniquePaths(int m, int n) {
        int[][] dp = new int[m][n];
        for(int i = 0;i < m;i++) {
            dp[i][0] = 1;
        }
        for(int j = 0;j < n;j++) {
            dp[0][j] = 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”)。

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

网格中的障碍物和空位置分别用 1 和 0 来表示。

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/unique-paths-ii

在这里插入图片描述

与上题区别在于多了障碍物,思路与上题大体相似。

1.确定dp[i][j]及其含义

​ 由m*n网格,我们可以很容易想到dp数组为一个二维数组,题目要求求出共有多少条不同路径,dp[i][j]表示到达第(i,j)位置有多少条不同的路径

2.递推公式

​ 机器人只能向下或者向右移动一步,那么上一步一定是在上面一格或者左边一格,考虑dp[i - 1][j]dp[i][j-1]dp[i][j]的关系

障碍物和空位置由二维数组obstaclesGrid[i][j]来表示,当obstaclesGrid[i][j]==1时,有障碍物。

只有当obstaclesGrid[i][j]==0时,有以下递推公式
d p [ i ] [ j ] = d p [ i − 1 ] [ j ] + d p [ i ] [ j − 1 ] dp[i][j] =dp[i - 1][j]+dp[i][j-1] dp[i][j]=dp[i1][j]+dp[i][j1]

3.dp数组如何初始化?

​ 由递推公式,dp[i][j]dp[i - 1][j]dp[i][j-1]得到,那么我们必须初始化第0行和第0列才能顺利得到dp[i][j]

dp[0][0]无意义,为0

当障碍物出现在第一行时,障碍物右侧的网格是机器人无法走到的;同理,当障碍物出现在第一列时,障碍物下侧的网格是机器人无法走到的

dp[0][j]=1(在障碍物左侧)

dp[i][0]=1(在障碍物上方)

4.遍历顺序

​ 推导的基础在最上面一行和最左边一列,

​ 从左往右,从上到下

5.打印遍历数组,用于debug

class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        
        int m = obstacleGrid.length;
        int n = obstacleGrid[0].length;
        int[][] dp = new int[m][n];
        //初始化dp数组
        for(int i = 0;i < m && obstacleGrid[i][0] == 0;i++) {
            dp[i][0] = 1;
        }
        for(int j = 0;j < n && obstacleGrid[0][j] == 0;j++) {
            dp[0][j] = 1;
        }
		//遍历dp数组
        for(int i = 1;i < m;i++){
            for(int j = 1;j <n;j++) {
                //递推公式
                if(obstacleGrid[i][j] == 0) {
                    dp[i][j] = dp[i - 1][j] + dp[i][j-1];
                }
            }
        }

        return dp[m - 1][n - 1];

    }
}

六.整数拆分

给定一个正整数 n ,将其拆分为 k正整数 的和( k >= 2 ),并使这些整数的乘积最大化。

返回 你可以获得的最大乘积

  • 2 <= n <= 58

https://leetcode.cn/problems/integer-break/

1.确定dp[i]及其含义

​ dp[i] 表示 正整数 i 经过拆分之后得到的最大化乘积。

2.递推公式

​ 假设正整数 i 被拆分为 j 和 i - j,此时的乘积是
j × ( i − j ) j × (i-j) j×(ij)
假定 j 是一个确定值,当我们继续对 i-j 拆分,那么要想获得 i 的最大乘积,就要得到 i - j 对应的最大乘积。
j × d p [ i − j ] j × dp[i-j] j×dp[ij]
得到递推公式,0 < j < i
d p [ i ] = m a x ( j × ( i − j ) , j × d p [ i − j ] ) dp[i] = max(j × (i-j),j × dp[i-j]) dp[i]=max(j×(ij),j×dp[ij])
3.dp数组如何初始化?

​ 由题目得2 <= n,dp[0]和dp[1]无意义,不用管,按默认值来。

​ dp[2] = 1 2 = 1 + 1, 1 × 1 = 1

4.遍历顺序

​ 对于 i 的遍历,从小到大

​ 对于 j 的遍历,随意

5.打印遍历数组,用于debug

class Solution {
    public int integerBreak(int n) {
        if(n == 2){
            return 1;
        }
        int[] dp = new int[n + 1];
        dp[2] = 1;
        for(int i = 3;i <= n;i++) {
            for(int j = 1;j < i;j++) {
                dp[i] = Math.max(Math.max(j * (i - j),j * dp[i-j]),dp[i]);
            }
        }

        return dp[n];
    }
}

七.不同的二叉搜索树

给你一个整数 n ,求恰由 n 个节点组成且节点值从 1n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。

  • 1 <= n <= 19

.https://leetcode.cn/problems/unique-binary-search-trees/


我们先来康康什么是二叉搜索树

它或者是一棵空树,或者是具有下列性质的二叉树

  • 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  • 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
  • 它的左、右子树也分别为二叉排序树

1.确定dp[i]及其含义

dp[i] 表示由i个节点组成且节点值从 1i 互不相同的 二叉搜索树 有多少种。

2.递推公式

先举例子来观察动态的过程

空树也是二叉搜索树,dp[0] = 1

dp[1] = 1

dp[2] = 2

dp[3] = 5

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xigp9Ru0-1674309318647)(Dynamic Programming.assets/image-20230120155553679.png)]

dp[3] = dp[0] * dp[2] + dp[1] * dp[1] + dp[2] * dp[0]

j作为根结点,对应的二叉搜索树有 dp[j - 1] * dp[i - j]

那么递推公式就是 dp[i] += dp[j - 1] * dp[i - j] (1 <= j <= i)

3.初始化

空树对应的二叉搜索树种类 dp[0] = 1

4.遍历顺序

i 从小到大

j 随意

5.打印遍历数组,用于debug

class Solution {
    public int numTrees(int n) {
        int[] dp = new int[n + 1];

        dp[0] = 1;
        
        for(int i = 1;i < n+1;i++) {
            for(int j = 1;j <= i;j++) {
                dp[i] += dp[i - j] * dp[j - 1];
            }
        }
        return dp[n];
    }
}

01背包问题

在这里插入图片描述

二维数组求解

标准的背包问题:有n件物品和一个最多能背重量为w的背包。第i件物品的重量是weight[i],得到的价值是value[i]每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

举个例子:背包最大重量为4。物品为:

重量价值
物品0115
物品1320
物品2430

问背包能背的物品最大价值是多少?

1.确定dp数组及其含义

dp[i][j]表示从编号为[0,i]的物品里任意取,放进容量为j的背包的最大价值总和

2.递推公式

动态过程:往容量为j的背包中放编号为i的物品

那么可以有两个方向推出来dp[i][j],

  • 不放物品i:由dp[i - 1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以背包内的价值依然和前面相同。)
  • 放物品i:由dp[i - 1][j - weight[i]]推出,dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值

所以递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

3.dp数组初始化

如果背包容量j=0,背包无法放入物品,背包价值总和一定为0。

dp[i][0]=0

由递推公式,dp[i][j]需要由dp[i - 1][j]推出,那么我们也要初始化i = 0的时候

很明显当 j < weight[0]的时候,dp[0][j] = 0

当j >= weight[0]时,dp[0][j]=value[0]

4.遍历顺序

有两个遍历的维度:物品与背包重量

那么问题来了,先遍历 物品还是先遍历背包重量呢?

其实都可以!! 但是先遍历物品更好理解

public class BagProblem {
    public static void main(String[] args) {
        int[] weight = {1,3,4};
        int[] value = {15,20,30};
        int bagSize = 4;
        testWeightBagProblem(weight,value,bagSize);
    }

    /**
     * 动态规划获得结果
     * @param weight  物品的重量
     * @param value   物品的价值
     * @param bagSize 背包的容量
     */
    public static void testWeightBagProblem(int[] weight, int[] value, int bagSize){

        // 创建dp数组
        int goods = weight.length;  // 获取物品的数量
        int[][] dp = new int[goods][bagSize + 1];

        // 初始化dp数组
        // 创建数组后,其中默认的值就是0
        for (int j = weight[0]; j <= bagSize; j++) {
            dp[0][j] = value[0];
        }

        // 填充dp数组
        for (int i = 1; i < weight.length; i++) {
            for (int j = 1; j <= bagSize; j++) {
                if (j < weight[i]) {
                    /**
                     * 当前背包的容量都没有当前物品i大的时候,是不放物品i的
                     * 那么前i-1个物品能放下的最大价值就是当前情况的最大价值
                     */
                    dp[i][j] = dp[i-1][j];
                } else {
                    /**
                     * 当前背包的容量可以放下物品i
                     * 那么此时分两种情况:
                     *    1、不放物品i
                     *    2、放物品i
                     * 比较这两种情况下,哪种背包中物品的最大价值最大
                     */
                    dp[i][j] = Math.max(dp[i-1][j] , dp[i-1][j-weight[i]] + value[i]);
                }
            }
        }

        // 打印dp数组
        for (int i = 0; i < goods; i++) {
            for (int j = 0; j <= bagSize; j++) {
                System.out.print(dp[i][j] + "\t");
            }
            System.out.println("\n");
        }
    }
}

这个二维数组是可以一维化的

dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

八.零一和(Ones and Zeroes)

You are given an array of binary strings strs and two integers m and n.

Return the size of the largest subset of strs such that there are at most m 0’s and n 1’s in the subset.

A set x is a subset of a set y if all elements of x are also elements of y.

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/ones-and-zeroes

Since now,we think and write in English.

public int findMaxForm(String[] strs, int m, int n) {
        //it's a 0/1 knapsack problem.

        //the dp array means the size of the largest subset of strs such that there are at most i 0's and j 1's in the subset.
        int[][] dp = new int[m + 1][n + 1];
        dp[0][0] = 0;
        //to solve the problem,we should clearify the volum,weight and value of the problem.
        /**
         volum   i and j
         weight  x 0's and y 1's in one str of the strs
         value   1
         */
        //then we can identify the equation of state.
        //dp[i][j] = max(dp[i][j],dp[i - x][j - y] + 1)
        for(String str : strs) {
            //get x and y
            int x = 0,y = 0;
            for(int k = 0;k < str.length();k++) {
                if(str.charAt(k) == '0') {
                    x++;
                }else{
                    y++;
                }
            }
            //get dp[i][j]
            for(int i = m;i >= x;i--) {
                for(int j = n;j >= y;j--) {
                    dp[i][j] = Math.max(dp[i][j],dp[i - x][j - y] + 1);
                }
            }
        }
        return dp[m][n];
    }

最长回文字符字串Longest Palindromic Substring

Given a string s, return the longest palindromic substring in s.
Constraints:

1 <= s.length <= 1000
s consist of only digits and English letters.

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/longest-palindromic-substring

class Solution {
    public String longestPalindrome(String s) {
        if(s.length() < 2) {
            return s;
        }

        //we can use dynamic programming to solve the problem.
        //dp[i][j] refers to whether the substring[i..j] is palindromic or not.
        int len = s.length();
        boolean[][] dp = new boolean[len][len];

        //Assume that substring[i..j] is palindromic.
        //If we add the same character to both sides of it,
        //the substring is still palindromic.
        //So we can identify the equation of state.
        //dp[i][j] = dp[i + 1][j - 1] & (CHi == CHj)

        //From the equation of state,the longer substring is derived from the shorter one.
        //the initial substring's length is one,that is,
        //dp[i][i] = true
        for(int i = 0;i < len;i++) {
            dp[i][i] = true;
        }
        
        //to sign the start and end indexes of the result substring.
        int start = 0;
        int end = 1;

        char[] str = s.toCharArray();
        for(int i = len - 1;i >= 0;i--) {
            for(int j = i;j < len;j++) {
                //and we should think of edge case.
                //The length of str[i+1..j-1] should less than two,that is,
                //(j - 1) - (i + 1) + 1 < 2,equal to j - i < 3.
                //At this time, whether str [i.. j] is palindrome
                // only depends on whether str [i] and str [j] are equal.

                //dp[i][j] = (str[i] == str[j]) && (j - i < 3 || dp[i + 1][j - 1]);
                if(str[i] != str[j]) {
                    dp[i][j] = false;
                } else {
                    if(j - i < 3) {
                        dp[i][j] = true;
                    } else {
                        dp[i][j] = dp[i + 1][j - 1];
                    }
                }
                if(dp[i][j] && j - i + 1 > end - start) {
                    start = i;
                    end = j + 1;
                }
            }
        }

        return s.substring(start,end);
    }
}

完全背包理论基础

unbounded knapsack problem (UKP)

1.完全背包和01背包的区别在于:01背包问题,每个物品只能放一次;完全背包问题,每个物品可以放多次(unbounded 不限制次数)

2.完全背包问题的递推公式

dp[j] = max(dp[j],dp[j - weight[i]] + value[i])

3.完全背包问题的遍历顺序

与0/1背包不同的是,既可以先遍历物品,也可以先遍历背包。

但是遍历背包时应该从小到大遍历[weight[i]至bagWeight] j++。这个过程体现了完全背包的每个物品可以放多次

1.零钱兑换IICoin Change II

You are given an integer array coins representing coins of different denominations and an integer amount representing a total amount of money.

Return the number of combinations that make up that amount. If that amount of money cannot be made up by any combination of the coins, return 0.

You may assume that you have an infinite number of each kind of coin.

The answer is guaranteed to fit into a signed 32-bit integer.

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/coin-change-ii

class Solution {
    public int change(int amount, int[] coins) {
        //dp[j] represents the number of combinations that make up j.
        int[] dp = new int[amount + 1];
        //when j equals to zero,there is 1 combination to make up 0,that is,dp[0] = 1
        dp[0] = 1;

        //dp[j] += dp[j - coins[i]]

        for(int i = 0; i < coins.length;i++) {
            for(int j = coins[i]; j <= amount;j++) {
                dp[j] += dp[j - coins[i]];
            }
        }
        return dp[amount];
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Fantasy`

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值