【代码随想录】第九章-动态规划(上)

【代码随想录】第九章-动态规划


第九章 动态规划-上

1 斐波那契数列

509.斐波那契数列

斐波那契数,通常用F(n)表示,形成的序列称为斐波那契数列。该数列由0和1开始,后面的每一项数字都是前面两项数字的和。也就是:F(0)=0,F(1)=1F(n)=F(n-1)+F(n-2),其中n>1给你n,请计算F(n)。

输入:2 输出:1

解释:F(2) = F(1) + F(0) = 1 + 0 = 1

思路:

Method1:递归

正常递归。

java 复制代码
class Solution {
    public int fib(int n) {
        if(n==0)    return 0;
        if(n==1)    return 1;
        return fib(n-1)+fib(n-2);
    }
}
Method2:动态规划

创建dp数组,存取之前已经计算的结果降低时间复杂度。状态转移方程为: dp [ i ] = dp [ i − 1 ] + dp [ i − 2 ] \text{dp}\left[ i \right]=\text{dp}\left[ i-1 \right]+\text{dp}\left[ i-2 \right]\ dp[i]=dp[i−1]+dp[i−2]

java 复制代码
class Solution {
    public int fib(int n) {
        if(n<2) return n;
        int [] dp=new int[n+1];
        dp[0]=0;
        dp[1]=1;
        for(int i=2;i<=n;i++){
            dp[i]=dp[i-1]+dp[i-2];
        }
        return dp[n];
    }
}

70.爬楼梯

假设你正在爬楼梯。需要n阶你才能到达楼顶。每次你可以爬1或2个台阶。你有多少种不同的方法可以爬到楼顶呢?

输入:3 输出:3

解释:有三种方法可以爬到楼顶。

思路:

动态规划,和斐波那契类似,只不过初始化有一些不同。

java 复制代码
class Solution {
    public int climbStairs(int n) {
        if(n<2)return n;
        int[] dp=new int[n+1];
        dp[0]=0;dp[1]=1;dp[2]=2;
        for(int i=3;i<=n;i++){
            dp[i]=dp[i-1]+dp[i-2];
        }
        return dp[n];
    }
}

更加正规的写法:

本质上是个完全背包求排列数的问题,完全背包求排列数方法:

  • 外层循环是背包容量
  • 内层循环是物品种类,这样首先符合排列数,组合数则是相反。更加正规的写法应该是两层循环
  • 状态转移方程为: dp [ j ] = dp [ j − nums [ i ] ] + dp [ j ] \text{dp}\left[ j \right]=\text{dp}\left[ j-\text{nums}[i] \right]+\text{dp}\left[ j \right]\ dp[j]=dp[j−nums[i]]+dp[j]
cpp 复制代码
class Solution {
    public int climbStairs(int n) {
        int []dp=new int[n+1];
        dp[0]=1;
        for(int i=0;i<=n;i++){
            for(int j=0;j<2;j++){
                if(i>=nums[j]){
                    dp[i]+=dp[i-nums[j]];
                }
            }
        }
        return dp[n];
    }
}

746.使用最小花费爬楼梯

给你一个整数数组cost,其中cost[i]是从楼梯第i个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。你可以选择从下标为0或下标为1的台阶开始爬楼梯。请你计算并返回达到楼梯顶部的最低花费。

输入:cost = [1,100,1,1,1,100,1,1,100,1] 输出:6

解释:你将从下标为 0 的台阶开始。支付 1 ,向上爬两个台阶,到达下标为 2 的台阶。支付 1 ,向上爬两个台阶,到达下标为 4 的台阶。支付 1 ,向上爬两个台阶,到达下标为 6 的台阶。支付 1 ,向上爬一个台阶,到达下标为 7 的台阶。支付 1 ,向上爬两个台阶,到达下标为 9 的台阶。支付 1 ,向上爬一个台阶,到达楼梯顶部。

思路:

动态规划,和斐波那契类似,只不过初始化有一些不同。

我们要思考一下为何初始化不同?因为我们最初可以选择的起始点不同导致的我们的初始化不同,斐波那契数列我们可以从0,1开始。爬楼梯是从我们可以一步到1层或者一步到2层,所以初始化是0,1,2。本题中我们可以从0或者1开始。

接下来不同的应该是状态转移方程了,我们是要找代价最小的,自然应该是看1层或者2层的代价,哪个代价小我们选择哪一层,所以状态转移方程为:
dp [ i ] = { dp [ 0 ] = 0 , dp [ 1 ] = 0 min { dp [ i − 1 ] + cost [ i − 1 ] , dp [ i − 2 ] + cost [ i − 2 ] } } \text{dp}\left[ i \right]=\left\{ \begin{align} & \text{dp}\left[ 0 \right]=0,\text{dp}\left[ 1 \right]=0 \\ & \text{min}\left\{ \text{dp}\left[ i-1 \right]+\text{cost}\left[ i-1 \right],\text{dp}\left[ i-2 \right]+\text{cost}\left[ i-2 \right] \right\} \\ \end{align} \right\}\ dp[i]={dp[0]=0,dp[1]=0min{dp[i−1]+cost[i−1],dp[i−2]+cost[i−2]}}

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

2 不同路径

62.不同路径

一个机器人位于一个mxn网格的左上角(起始点在下图中标记为"Start")。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为"Finish")。问总共有多少条不同的路径?

输入:m = 3, n = 2 输出:3

解释:从左上角开始,总共有 3 条路径可以到达右下角。1. 向右 -> 向下 -> 向下、2. 向下 -> 向下 -> 向右、3. 向下 -> 向右 -> 向下。

思路:

这里开始就是二维的动态规划了,因为涉及到两个维度的规划,自然也要对两个维度进行初始化,在本题中,我们思考因为只能往右或者往下移一步,那么最左边的边界只能通过一直往下才能到达,因此最左边的边界初始化都是1,也就只有一条路可以选。最上面的边界同理。这样我们的初始化就结束了。

接下来是状态转移,我们思考一下到达除边界点的位置我们很自然有两条路可以选,从上面来或者从左边来,所以当前点的状态转移也是从这两个点来的。

java 复制代码
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 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];  
    }
}

63.不同路径II

给定一个mxn的整数数组grid。一个机器人初始位于左上角(即grid[0][0])。机器人尝试移动到右下角(即grid[m-1][n-1])。机器人每次只能向下或者向右移动一步。网格中的障碍物和空位置分别用1和0来表示。机器人的移动路径中不能包含任何有障碍物的方格。返回机器人能够到达右下角的不同路径数量。测试用例保证答案小于等于2*109。

输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]] 输出:2

解释:3x3 网格的正中间有一个障碍物。从左上角到右下角一共有 2 条不同的路径:1. 向右 -> 向右 -> 向下 -> 向下、2. 向下 -> 向下 -> 向右 -> 向右。

思路:

这里与62不同点在出现了障碍物,那么我们自然也要针对障碍物做特殊的处理,如果边界出现障碍物,边界中障碍物后的位置自然是不可达的,就相当于是0了。

接下来是状态转移,62我们是从上面和左边进行的状态转移,但是在63题,有了障碍物,我们自然要先检查我们这个点是不是障碍物,如果是障碍物直接就是0,也就是不可达了,自然不需要状态转移,只有非障碍物的点我们才进行状态转移。

java 复制代码
class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int m=obstacleGrid.length;
        int n=obstacleGrid[0].length;
        int[][] dp=new int[m][n];
        if(obstacleGrid[0][0]==1||obstacleGrid[m-1][n-1]==1)    return 0;
        for(int i=0;i<m&&obstacleGrid[i][0]==0;i++) dp[i][0]=1;
        for(int i=0;i<n&&obstacleGrid[0][i]==0;i++) dp[0][i]=1;
        for(int i=1;i<m;i++){
            for(int j=1;j<n;j++){
                dp[i][j]=(obstacleGrid[i][j]==0)?dp[i-1][j]+dp[i][j-1]:0;
            }
        }
        return dp[m-1][n-1];      
    }
}

3 整数拆分

343.整数拆分

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

输入:n = 10 输出:36

解释:10 = 3 + 3 + 4, 3 × 3 × 4 = 36。

思路:

首先我们思考初始化怎么做,1,2的初始化自然是自己,2也只能拆分成1和1,所以我们的初始化也是针对1和2,之后就涉及状态转移了。

状态转移怎么得到的呢?针对数字i,相当于有两个阶段可以进行转移:

如果是两数字的话,那么就是从(i-j)*j进行转移的,比如4,拆成两个数字的话,3和1,那么4的一种转移就是从1和3的两个数字转移而来,其乘积就是(4-1)*1,如果是2和2,就是(4-2)*2同理。

如果是多个数字的话,那么转移应该从这个字还能拆的上一个状态量转移过来的,什么意思呢?比如数字6,6可以拆成两个数字(6-2)*2,也就是4*2,但是4还可以继续进行拆分,拆成4的状态转移量。所以6的另外一种多数字的拆法的状态转移来自上一个待拆的数字的状态量。所以状态转移方程应该是:
dp [ i ] = max { ( i − j ) ∗ j , dp [ i − j ] ∗ j } \text{dp}\left[ i \right]=\text{max}\left\{ \left( i-j \right)*j,\text{dp}\left[ i-j \right]*j \right\}\ dp[i]=max{(i−j)∗j,dp[i−j]∗j}

但是我们仔细思考一下,我们这里的i只能说找了其中的一种为i-j和j的拆法,我们应该找出i的所有拆法中最大的,所以我们最终的状态转移方程为:
dp [ i ] = { dp [ 1 ] = 1 , dp [ 2 ] = 1 max ⁡ { dp [ i ] , max { ( i − j ) ∗ j , dp [ i − j ] ∗ j } } } \text{dp}\left[ i \right]=\left\{ \begin{align} & \text{dp}\left[ 1 \right]=1,\text{dp}\left[ 2 \right]=1 \\ & \max \left\{ \text{dp}\left[ i \right],\text{max}\left\{ \left( i-j \right)*j,\text{dp}\left[ i-j \right]*j \right\} \right\} \\ \end{align} \right\}\ dp[i]={dp[1]=1,dp[2]=1max{dp[i],max{(i−j)∗j,dp[i−j]∗j}}}

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

96.不同的二叉搜索树

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

输入:n = 3 输出:5

解释:

思路:

先思考初始化,很自然发现如果节点是0个或者1个的二叉搜索树肯定是1,如果是两个节点的话是2个,这样初始化很自然完成了。

接下来思考状态转移方程,我们观察一下n=3的情况:

如果根节点的左右子树其中一个为空的话,那后面的情况自然演变成只有两个节点的情况了,也就是说图中最左边的两种情况,dp[3]=dp[0]*dp[2],dp[0]代表左右子树其中有个为空,dp[2]代表除根节点外的剩下的情况。最右边的两种图同理,只不过变成了dp[3]=dp[2]*dp[0]。

接下来剩中间的这种情况,左右子树都存在,那么也就是左右子树都是一个节点的情况下,也就是dp[3]=dp[1]*dp[1],这每种情况都需要进行累加,所以我们对于n=3情况下的状态转移方程为:
dp [ 3 ] = dp [ 0 ] ∗ dp [ 2 ] + dp [ 1 ] ∗ dp [ 1 ] + dp [ 2 ] ∗ dp [ 0 ] \text{dp}\left[ 3 \right]=\text{dp}\left[ 0 \right]*\text{dp}\left[ 2 \right]+\text{dp}\left[ 1 \right]*\text{dp}\left[ 1 \right]+\text{dp}\left[ 2 \right]*\text{dp}\left[ 0 \right]\ dp[3]=dp[0]∗dp[2]+dp[1]∗dp[1]+dp[2]∗dp[0]

所以类推到我们最终的状态转移方程为:
dp [ i ] = { dp [ 0 ] = 1 , dp [ 1 ] = 1 dp [ i ] + dp [ i − j ] ∗ dp [ j − 1 ] } \text{dp}\left[ i \right]\text{=}\left\{ \begin{align} & \text{dp}\left[ 0 \right]=1,\text{dp}\left[ 1 \right]=1 \\ & \text{dp}\left[ i \right]+\text{dp}\left[ i-j \right]*\text{dp}\left[ j-1 \right] \\ \end{align} \right\}\ dp[i]={dp[0]=1,dp[1]=1dp[i]+dp[i−j]∗dp[j−1]}

java 复制代码
class Solution {
    public int numTrees(int n) {
        if(n<2) return n;
        int[] dp=new int[n+1];
        dp[0]=1;dp[1]=1;dp[2]=2;
        for(int i=3;i<=n;i++){
            for(int j=1;j<=i;j++){
                dp[i]+=dp[i-j]*dp[j-1];
            }
        }
        return dp[n];
    }
}

4 0-1背包问题

Kama.46.携带研究材料

小明是一位科学家,他需要参加一场重要的国际科学大会,以展示自己的最新研究成果。他需要带一些研究材料,但是他的行李箱空间有限。这些研究材料包括实验设备、文献资料和实验样本等等,它们各自占据不同的空间,并且具有不同的价值。

小明的行李空间为N,问小明应该如何抉择,才能携带最大价值的研究材料,每种研究材料只能选择一次,并且只有选与不选两种选择,不能进行切割。

第一行包含两个正整数,第一个整数 M 代表研究材料的种类,第二个正整数 N,代表小明的行李空间。第二行包含 M 个正整数,代表每种研究材料的所占空间。 第三行包含 M 个正整数,代表每种研究材料的价值。

输入:goodsType:3 capcity:4 输出:35

Bag: 1 3 4

Value: 15 20 30

思路:

因为每种研究材料只能选择一种,所以是标准的背包问题。首先针对于动态规划,我们还是先思考如何初始化。首先i表示0-i号的物品,j表示背包的容量,我们要在足量的背包容量中找出最大的价值。

如果背包容量为0的话,也就是dp[i][0],所以无论选择哪个物品,都是为0。

拿输入进行举例,我们的种类有3种,背包容量为4,第二行是物品的空间,按照背包容量先进行放置:

背包容量 0 1 2 3 4
物品0 0 15 15 15 15
物品1 0 15 15 20 35
物品2 0 15 15 20 35

背包容量为0,放不下物品0,此时背包里的价值为0。背包容量为1,可以放下物品0,此时背包里的价值为15.背包容量为2,依然可以放下物品0 (注意 01背包里一个物品只能放一种),此时背包里的价值为15,其他的依次类推。

接下来就要思考如何推出状态转移方程了,针对背包里的物品,无非放与不放两种情况,比如dp[1][4]表示0-1物品可以放进背包,容量为4的最大价值。

  • 那么如果不放物品1,那么dp[1][4]就应该等于dp[0][4]也就是没放进去物品1。
  • 如果放物品1,那么就应该先预留出物品1的空间,那么dp[1][4]应由dp[0][1]+物品1的价值而转移过来的。
  • 故dp[1][4]的状态转移方程为
    dp [ 1 ] [ 4 ] = max ⁡ { dp [ 0 ] [ 4 ] , dp [ 0 ] [ 1 ] + value [ 1 ] } \text{dp}\left[ 1 \right]\left[ 4 \right]=\max \left\{ \text{dp}\left[ 0 \right]\left[ 4 \right],\text{dp}\left[ 0 \right]\left[ 1 \right]+\text{value}\left[ 1 \right] \right\}\ dp[1][4]=max{dp[0][4],dp[0][1]+value[1]}

那么就可以类推到所有的物品可以这样,分为不放和放两种情况,那么对应的状态转移方程为:
dp [ i ] [ j ] = max ⁡ { dp [ i − 1 ] [ j ] , dp [ i − 1 ] [ j − weight [ i ] ] + value [ i ] } \text{dp}\left[ i \right]\left[ j \right]=\max \left\{ \text{dp}\left[ i-1 \right]\left[ j \right],\text{dp}\left[ i-1 \right]\left[ j-\text{weight}\left[ i \right] \right]+\text{value}\left[ i \right] \right\}\ dp[i][j]=max{dp[i−1][j],dp[i−1][j−weight[i]]+value[i]}

因为我们的递推公式是i一直向前减的,所以dp的第一维一定要进行初始化,我们刚刚只是考虑了第二维为0的情况。那么思考一下dp[0][j]是什么意思,代表着i为0,存放物品0的时候,各个容量的背包所能存放的最大价值。那么很自然的我们的最大价值是受到我们第二位j,也就是背包容量的限制。也就是说当背包容量j<weight[0]时,dp[0][j]应该是0,因为背包不足以放下物品0的空间。

这样我们的初始化就有两步,第一步针对背包空间为0的初始化,也就是dp数组的左边界,第二步针对物品0进行初始化,也就是dp数组的上边界。

接下来就是正常遍历了,我们先遍历物品,再遍历背包容量(先遍历谁其实都一样),如果当前背包容量小于物品i的容量,也就是说物品i放不进去,那么直接继承上一个物品的价值即可。如果当前背包容量足够放下物品i,那么状态转移方程就是我们刚刚分析的结果。代码也就很容易的写出来了:

Method1:二维dp
java 复制代码
import java.util.*;
public class Main{
    public static int findBag(int[] weight, int[] value, int goodsType, int capacity){
        int[][] dp=new int[goodsType][capacity+1];
        for(int i = 0; i < goodsType; ++i) dp[i][0]=0;
        for(int i = weight[0]; i <= capacity; ++i) dp[0][i]=value[0];
        for(int i = 1; i < goodsType; ++i){
            for(int j = 1; j <= capacity; ++j){
                if(j<weight[i]) dp[i][j]=dp[i-1][j];
                else dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);
            }
        }
        return dp[goodsType-1][capacity];
    }
    public static void main(String[] args){
        Scanner sc=new Scanner(System.in);
        int goodsType=sc.nextInt();
        int capacity=sc.nextInt();
        int[] weight=new int[goodsType];
        int[] value=new int[goodsType];
        for(int i = 0; i < goodsType; ++i) weight[i]=sc.nextInt();
        for(int i = 0; i < goodsType; ++i) value[i]=sc.nextInt();
        int res=findBag(weight,value,goodsType,capacity);
        System.out.println(res);
    }
}
Method2:二维dp压缩为一维滚动数组

我们观察一下我们的递推关系式:
dp [ i ] [ j ] = max ⁡ { dp [ i − 1 ] [ j ] , dp [ i − 1 ] [ j − weight [ i ] ] + value [ i ] } \text{dp}\left[ i \right]\left[ j \right]=\max \left\{ \text{dp}\left[ i-1 \right]\left[ j \right],\text{dp}\left[ i-1 \right]\left[ j-\text{weight}\left[ i \right] \right]+\text{value}\left[ i \right] \right\}\ dp[i][j]=max{dp[i−1][j],dp[i−1][j−weight[i]]+value[i]}

我们发现除去第一维,我们发现第i层的数据实际上全是由第i-1层数据转换而来的,那么我们就没必要用二维矩阵保存i-1的结果,可以直接将i-1的结果在第i层进行运算,这样就相当于只对第二维进行运算,也就是对二维矩阵进行了压缩,那么我们的递推关系式也就压缩为:
dp [ j ] = max ⁡ { dp [ j ] , dp [ j − weight [ i ] ] + value [ i ] } \text{dp}\left[ j \right]=\max \left\{ \text{dp}\left[ j \right],\text{dp}\left[ j-\text{weight}\left[ i \right] \right]+\text{value}\left[ i \right] \right\}\ dp[j]=max{dp[j],dp[j−weight[i]]+value[i]}

接下来思考一下初始化,因为我们这个不会存上一步的结果,所以怎么初始化呢?dp[j]表示容量维j的背包所背的物品价值为dp[j],那么dp[0]应该是0,因为背包容量为0所背的背包的物品价值就是0。我们观察一下递推公式,如果我们的物品价值都是大于0的,那么非0下标的初始化为0也没什么问题。

接下里就是遍历顺序了,我们先遍历物品,再倒序遍历背包容量,为什么要倒序遍历背包容量呢?因为倒序遍历是为了保证物品i只被放入1次,为什么倒序遍历就能保证物品i只被放入一次呢?还是我们刚刚的例子,如果是按照背包容量从小往大进行遍历

物品i 物品0 物品1 物品2
Weight 1 3 4
Value 15 20 30

针对物品0:
dp [ 1 ] = max ⁡ { dp [ 1 ] , dp [ 1 − weight [ 0 ] ] + value [ 0 ] } = 15 dp [ 2 ] = max ⁡ { dp [ 2 ] , dp [ 2 − weight [ 0 ] ] + value [ 0 ] } = 30 \begin{align} & \text{dp}\left[ 1 \right]=\max \left\{ \text{dp}\left[ 1 \right],\text{dp}\left[ 1-\text{weight}\left[ 0 \right] \right]+\text{value}\left[ 0 \right] \right\}=15 \\ & \text{dp}\left[ 2 \right]=\max \left\{ \text{dp}\left[ 2 \right],\text{dp}\left[ 2-\text{weight}\left[ 0 \right] \right]+\text{value}\left[ 0 \right] \right\}=30 \\ \end{align}\ dp[1]=max{dp[1],dp[1−weight[0]]+value[0]}=15dp[2]=max{dp[2],dp[2−weight[0]]+value[0]}=30

这明显是不符合题意的,在背包容量为2的情况下,物品0应该只被放置一次,而如果背包容量从小到大进行遍历,物品0在背包容量为2的情况下被放置了两次。但如果背包容量从大到小进行遍历的话

针对物品0:

dp [ 4 ] = max ⁡ { dp [ 4 ] , dp [ 4 − weight [ 0 ] ] + value [ 0 ] } = 15 dp [ 3 ] = max ⁡ { dp [ 3 ] , dp [ 3 − weight [ 0 ] ] + value [ 0 ] } = 15 dp [ 2 ] = max ⁡ { dp [ 2 ] , dp [ 2 − weight [ 0 ] ] + value [ 0 ] } = 15 dp [ 1 ] = max ⁡ { dp [ 1 ] , dp [ 1 − weight [ 0 ] ] + value [ 0 ] } = 15 \begin{align} & \text{dp}\left[ 4 \right]=\max \left\{ \text{dp}\left[ 4 \right],\text{dp}\left[ 4-\text{weight}\left[ 0 \right] \right]+\text{value}\left[ 0 \right] \right\}=15 \\ & \text{dp}\left[ 3 \right]=\max \left\{ \text{dp}\left[ 3 \right],\text{dp}\left[ 3-\text{weight}\left[ 0 \right] \right]+\text{value}\left[ 0 \right] \right\}=15 \\ & \text{dp}\left[ 2 \right]=\max \left\{ \text{dp}\left[ 2 \right],\text{dp}\left[ 2-\text{weight}\left[ 0 \right] \right]+\text{value}\left[ 0 \right] \right\}=15 \\ & \text{dp}\left[ 1 \right]=\max \left\{ \text{dp}\left[ 1 \right],\text{dp}\left[ 1-\text{weight}\left[ 0 \right] \right]+\text{value}\left[ 0 \right] \right\}=15 \\ \end{align}\ dp[4]=max{dp[4],dp[4−weight[0]]+value[0]}=15dp[3]=max{dp[3],dp[3−weight[0]]+value[0]}=15dp[2]=max{dp[2],dp[2−weight[0]]+value[0]}=15dp[1]=max{dp[1],dp[1−weight[0]]+value[0]}=15

显然,背包容量从大往小遍历是不会受之前背包容量的影响的 ,也就是不会和之前的状态重合,是符合题意的。

所以我们的代码就在初始化和遍历顺序有些不同:

java 复制代码
public static int findBag(int[] weight, int[] value, int goodsType, int capacity){
    int[] dp=new int[capacity+1];
    dp[0]=0;
    for(int i = weight[0]; i <= capacity; ++i) dp[i]=value[0];
    for(int i = 1; i < goodsType; ++i){
        for(int j = capacity; j >= weight[i]; --j){
            dp[j]=Math.max(dp[j],dp[j-weight[i]]+value[i]);
        }
    }
    return dp[capacity];
}

416.分割等和子集 0-1背包可行性

给你一个只包含正整数的非空数组nums。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。。

输入:nums = [1,5,11,5] 输出:true

思路:

因为每个数字只用一次,就是经典的0-1背包问题,只不过这里的背包容量是数字总和的一半。其他与正常的背包问题类似。二维状态转移方程为:
dp [ i ] [ j ] = max ⁡ { dp [ i − 1 ] [ j ] , dp [ i − 1 ] [ j − weight [ i ] ] + value [ i ] } \text{dp}\left[ i \right]\left[ j \right]=\max \left\{ \text{dp}\left[ i-1 \right]\left[ j \right],\text{dp}\left[ i-1 \right]\left[ j-\text{weight}\left[ i \right] \right]+\text{value}\left[ i \right] \right\}\ dp[i][j]=max{dp[i−1][j],dp[i−1][j−weight[i]]+value[i]}

一维压缩的状态转移方程为:
dp [ j ] = max ⁡ { dp [ j ] , dp [ j − weight [ i ] ] + value [ i ] } \text{dp}\left[ j \right]=\max \left\{ \text{dp}\left[ j \right],\text{dp}\left[ j-\text{weight}\left[ i \right] \right]+\text{value}\left[ i \right] \right\}\ dp[j]=max{dp[j],dp[j−weight[i]]+value[i]}

  • 注意一下在01背包中二维的遍历顺序也就是先遍历数字还是先遍历背包容量是无所谓的
  • 但是一维遍历因为没有初始化并且会受到前一层的影响,所以应该先遍历数字再遍历背包容量,并且背包容量应该是从大往小进行遍历,以免后面的受到前面的影响。
Method1:二维dp
java 复制代码
class Solution {
    public boolean canPartition(int[] nums) {
        int len=nums.length,sum=0;
        if(len<2)return false;
        for(int i=0;i<len;i++) sum+=nums[i];
        if(sum%2!=0) return false;
        int target=sum/2;
        int [][] dp=new int [len][target+1];
        for(int i=nums[0];i<=target;i++){
            dp[0][i]=nums[0];
        }
        int i=0,j=0;
        for(i=1;i<len;i++){
            for(j=0;j<=target;j++){
                if(j<nums[i]){
                    dp[i][j]=dp[i-1][j];
                }
                else{
                    dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-nums[i]]+nums[i]);
                }
                if(dp[i][j]==target) return true;
            }
        }
        return dp[i-1][j-1]==target;
    }
}
Method2:一维dp
java 复制代码
class Solution {
    public boolean canPartition(int[] nums) {
        int len = nums.length, sum = 0;
        if (len < 2) return false;
        for (int i = 0; i < len; i++) sum += nums[i];
        if (sum % 2 != 0) return false;
        int target = sum / 2;
        int i = 0, j = 0;
        int[] dp = new int[target + 1];
        for (i = 0; i < len; i++) {
            for (j = target; j >= nums[i]; j--) {
                dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
            }
            if (dp[target] == target) return true;
        }
        return dp[target] == target;
    }
}
Method3:回溯+记忆搜索

暴力回溯超时,使用记忆搜索快速返回,两种结果,要么加入,要么跳过

java 复制代码
class Solution {
    private Boolean[][] memo;
    public boolean canPartition(int[] nums) {
        int sum = Arrays.stream(nums).sum();
        if (sum % 2 != 0) return false;
        int target = sum / 2;
        Arrays.sort(nums);
        for (int i = 0, j = nums.length - 1; i < j; i++, j--) {
            int temp = nums[i];
            nums[i] = nums[j];
            nums[j] = temp;
        }
        memo = new Boolean[nums.length][target + 1];
        return dfs(nums, 0, 0, target);
    }
    private boolean dfs(int[] nums, int index, int curSum, int target) {
        if (curSum == target) return true;
        if (curSum > target || index == nums.length) return false;
        if (memo[index][curSum] != null) return memo[index][curSum];
        boolean found = dfs(nums, index + 1, curSum + nums[index], target) ||
                        dfs(nums, index + 1, curSum, target);

        memo[index][curSum] = found;
        return found;
    }
}

1049.最后一块石头的重量II 0-1背包最小值

给你一个只包含正整数的非空数组nums。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等

有一堆石头,用整数数组stones表示。其中stones[i]表示第i块石头的重量。每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为x和y,且x<=y。那么粉碎的可能结果如下:

如果x==y,那么两块石头都会被完全粉碎;如果x!=y,那么重量为x的石头将会完全粉碎,而重量为y的石头新重量为y-x。

最后,最多只会剩下一块石头。返回此石头最小的可能重量。如果没有石头剩下,就返回0。

输入:stones = [2,7,4,1,8,1] 输出:1

解释:

组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],

组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],

组合 2 和 1,得到 1,所以数组转化为 [1,1,1],

组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。

思路:

与416题非常相像,这个是不一定非得一样的两个数组,如果不一样就取差值,还是一道0-1背包问题。

java 复制代码
class Solution {
    public int lastStoneWeightII(int[] stones) {
        int len=stones.length;
        if(len<2) return stones[0];
        int sum=0;
        for(int i=0;i<len;i++) sum+=stones[i];
        int target=sum/2;
        int [] dp=new int[target+1];
        for(int i=0;i<len;i++){
            for(int j=target;j>=stones[i];j--){
                dp[j]=Math.max(dp[j],dp[j-stones[i]]+stones[i]);
            }
        }
        return sum-(dp[target]*2);
    }
}

494.目标和 0-1背包方案数

给你一个非负整数数组nums和一个整数target。

向数组中的每个整数前添加'+'或'-',然后串联起所有整数,可以构造一个表达式:例如,nums=[2,1],可以在2之前添加'+',在1之前添加'-',然后串联起来得到表达式"+2-1"。

返回可以通过上述方法构造的、运算结果等于target的不同表达式的数目。

输入:nums = [1,1,1,1,1], target = 3 输出:5

解释:

-1 + 1 + 1 + 1 + 1 = 3

+1 - 1 + 1 + 1 + 1 = 3

+1 + 1 - 1 + 1 + 1 = 3

+1 + 1 + 1 - 1 + 1 = 3

+1 + 1 + 1 + 1 - 1 = 3

思路:

Method1:暴力回溯
java 复制代码
class Solution {
    int count=0;
    public int findTargetSumWays(int[] nums, int target) {
        dfs(nums,target,0,0);
        return count;
    }
    public void dfs(int[] nums, int target,int sum,int index){
        if(index==nums.length){
            if(sum==target) count++;
        }
        else{
            dfs(nums,target,sum+nums[index],index+1);
            dfs(nums,target,sum-nums[index],index+1);
        }
    }
}
Method2:记忆化回溯

因为我们回溯的范围要么全为负数,要么全为正数,所以记忆化搜索的范围就是2 * totalSum + 1,如果之前计算过就快速返回。

剪枝操作:

1、如果当前和加上后面所有的都小于target,说明永远加到不了,立刻剪枝。2、如果当前和减去后面所有的都大于target,说明永远减不到,立刻剪枝。memo[index][sum + totalSum] = add + subtract;存放当前索引满足的加法和减法的方案数。

java 复制代码
class Solution {
    private int count = 0;
    private Integer[][] memo;
    private int totalSum;
    public int findTargetSumWays(int[] nums, int target) {
        totalSum = 0;
        for (int num : nums) totalSum += num;
        if (Math.abs(target) > totalSum) return 0;
        memo = new Integer[nums.length][2 * totalSum + 1];
        return dfs(nums, target, 0, 0);
    }
    private int dfs(int[] nums, int target, int sum, int index) {
        if (index == nums.length) {
            return sum == target ? 1 : 0;
        }
        if (memo[index][sum + totalSum] != null) {
            return memo[index][sum + totalSum];
        }
        int remaining = 0;
        for (int i = index; i < nums.length; i++) {
            remaining += nums[i];
        }
        if (sum + remaining < target || sum - remaining > target) {
            return 0;
        }
        int add = dfs(nums, target, sum + nums[index], index + 1);
        int subtract = dfs(nums, target, sum - nums[index], index + 1);
        memo[index][sum + totalSum] = add + subtract;
        return memo[index][sum + totalSum];
    }
}
Method3:动态规划二维dp

这个问题相当于是01背包中有多手中不同的填满背包最大容量的方法。怎么理解这个问题呢?我们发现可以将最后转化的数组分为两类,一类正数数组add,一类负数数组subtract,这里最后add+subtract==target,但是substract=sum-add,所以add=(sum+target)>>1,问题就转化为在nums中找出和为add的组合的有几种的问题。

所以dp[i][j]就表示遍历到第i个数时,add为j时能填满背包的方法总数,所以我们初始化的时候,针对最上行dp[0][j]表示,遍历第一个数时,背包内的方法,只要nums[0]是小于add的,那么填满dp[0][nums[0]]就只有一种方法就是把第0个物品放进去,其余情况均为0,因为都填不满,针对dp[0][0]放满背包容量为0的方法个数是1,也就是不放物品。这是针对于最上行的初始化,最左边的初始化怎么做呢?最左边的为dp[i][0]表示当取到索引0到i的部分有n个0时(n>0)装满背包容量为0的方法也只有一种,就是放0减物品。但是如果物品数值本身就是0,如果有两个物品,物品0为0,物品1也为0,那么装满背包容量为0的情况就有4中,1、不放,2、放物品0,3、放物品1,4、放物品0和1,所以有四种情况。I个物品就有2i种情况。这样我们的初始化就推导完毕了,依次初始化了最上和最左。

动态规划问题怎么推出来递推公式呢?

我们可以抽象思考一下,例如dp[2][2],意思是索引0-2放满背包容量为2的方法数量,很轻易的得到是3种:1、放物品0和1,2、放物品0和2,3、放物品1和2。是不是可以分为放物品2和不放物品2两种情况。

先思考放物品2,刨去物品2的大小1,是不是只剩下一个格子了,那问题是不是转化成了dp[1][1],在索引0-1之间选一个放进1个格子的方法,有两种。接下来思考不放物品2,那问题就转化成了dp[1][2],在索引0-1之件选择两个填满背包容量2。这样我们就能很轻易地写出dp[2][2]的递推关系式了:
dp [ 2 ] [ 2 ] = dp [ 1 ] [ 1 ] + dp [ 1 ] [ 2 ] \text{dp}\left[ 2 \right]\left[ 2 \right]=\text{dp}\left[ 1 \right]\left[ 1 \right]+\text{dp}\left[ 1 \right]\left[ 2 \right]\ dp[2][2]=dp[1][1]+dp[1][2]

我们类推到一般情况:

dp [ i ] [ j ] = dp [ i − 1 ] [ j − nums [ i ] ] + dp [ i − 1 ] [ j ] \text{dp}\left[ i \right]\left[ j \right]=\text{dp}\left[ i-1 \right]\left[ j-\text{nums}[i] \right]+\text{dp}\left[ i-1 \right]\left[ j \right]\ dp[i][j]=dp[i−1][j−nums[i]]+dp[i−1][j]

如果nums[i]>背包容量,就继承上一个的大小即可。

java 复制代码
class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int len=nums.length;
        int sum=Arrays.stream(nums).sum();
        if(Math.abs(target)>sum)return 0;
        if((target+sum)%2!=0)return 0;
        int add=(sum+target)>>1;
        int[][]dp=new int[len][add+1];
        dp[0][0]=1;
        for(int i=0;i<=add;i++){
            if(nums[0]==i)dp[0][nums[0]]=1;
        }
        int count0=0;
        for(int i=0;i<len;i++){
            if(nums[i]==0) count0++;
            dp[i][0]=(int)Math.pow(2,count0);
        }
        for(int i=1;i<len;i++){
            for(int j=0;j<=add;j++){
                if(nums[i]>j) dp[i][j]=dp[i-1][j];
                else{
                    dp[i][j]=dp[i-1][j-nums[i]]+dp[i-1][j];
                }
            }
        }
        return dp[len-1][add];
    }
}
Method4:动态规划一维dp

还是观察我们刚刚二维动态规划的状态转移方程,i层的状态也还是由i-1层状态转移过来,我们依旧可以使用滚动数组来优化我们的空间复杂度,背包空间还是从大往小遍历。

用i层来推导i-1层这样就能实现压缩矩阵了,一维递推公式:
dp [ j ] = dp [ j − nums [ i ] ] + dp [ j ] \text{dp}\left[ j \right]=\text{dp}\left[ j-\text{nums}[i] \right]+\text{dp}\left[ j \right]\ dp[j]=dp[j−nums[i]]+dp[j]

java 复制代码
class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int len=nums.length;
        int sum=Arrays.stream(nums).sum();
        if(Math.abs(target)>sum)return 0;
        if((target+sum)%2!=0)return 0;
        int add=(sum+target)>>1;
        int[]dp=new int[add+1];dp[0]=1;
        for(int i=0;i<len;i++){
            for(int j=add;j>=nums[i];j--){
                dp[j]+=dp[j-nums[i]];
            }
        }
        return dp[add];
    }
}

474.一和零 0-1装满背包最多多少物品

给你一个二进制字符串数组strs和两个整数m和n。请你找出并返回strs的最大子集的长度,该子集中最多有m个0和n个1。如果x的所有元素也是y的元素,集合x是集合y的子集。

输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3 输出:4

解释:

最多有5个0和3个1的最大子集是{"10","0001","1","0"},因此答案是4。

其他满足题意但较小的子集包括{"0001","1"}和{"10","1","0"}。{"111001"}不满足题意,因为它含4个1,大于n的值3。

思路:

与之前相比,相当于有了一个拥有两个维度的背包,一个维度装0,一个维度装1,这样我们dp[i][j]就相当于是原先的一维滚动数组的背包问题,这里就没有以前的i维表示物品的那个维度了。相当于每次遍历字符串的时候找到0和1的背包容量,然后进行状态转移。

之前0-1背包问题的一维的状态转移方程:
dp [ j ] = max ⁡ { dp [ j ] , dp [ j − weight [ i ] ] + value [ i ] } \text{dp}\left[ j \right]=\max \left\{ \text{dp}\left[ j \right],\text{dp}\left[ j-\text{weight}\left[ i \right] \right]+\text{value}\left[ i \right] \right\}\ dp[j]=max{dp[j],dp[j−weight[i]]+value[i]}

那么我们现在的方程一维二维都表示背包容量,虽然是一个背包,但是有两维度。所以本质上还是一个一维的状态转移方程:
dp [ i ] [ j ] = max ⁡ { dp [ i ] [ j ] , dp [ i − str.num 0 ] [ j − str.num1 ] + 1 } \text{dp}\left[ i \right]\left[ j \right]=\max \left\{ \text{dp}\left[ i \right]\left[ j \right],\text{dp}\left[ i-\text{str}\text{.num}0 \right]\left[ j-\text{str}\text{.num1} \right]+1 \right\}\ dp[i][j]=max{dp[i][j],dp[i−str.num0][j−str.num1]+1}

Str.num0和str.num1则是str对应的0和1的体积,价值就是自己的数组的个数1。

java 复制代码
class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        int len=strs.length;
        int[][]dp=new int[m+1][n+1];
        for(String str:strs){
            int num0=0;int num1=0;
            for(char c:str.toCharArray()){
                if(c=='0') num0++;
                else num1++;
            }
            for(int i=m;i>=num0;i--){
                for(int j=n;j>=num1;j--){
                    dp[i][j]=Math.max(dp[i][j],dp[i-num0][j-num1]+1);
                }
            }
        }
        return dp[m][n];
    }
}

5 完全背包问题

Kama.52.携带研究材料

小明是一位科学家,他需要参加一场重要的国际科学大会,以展示自己的最新研究成果。他需要带一些研究材料,但是他的行李箱空间有限。这些研究材料包括实验设备、文献资料和实验样本等等,它们各自占据不同的重量,并且具有不同的价值。

小明的行李箱所能承担的总重量是有限的,问小明应该如何抉择,才能携带最大价值的研究材料,每种研究材料可以选择无数次,并且可以重复选择。

第一行包含两个整数,n,v,分别表示研究材料的种类和行李所能承担的总重量

接下来包含 n 行,每行两个整数 wi 和 vi,代表第 i 种研究材料的重量和价值

输入:goodsType:4 capcity:5 输出:10

Bag: 1 2 3 4

Value: 2 4 4 5

思路:

Method1:动态规划二维dp

相比于01背包问题,现在属于完全背包问题,完全背包问题中,一个物品可以使用多次,那么为了满足背包的情况,我们还是来分析一下dp[1][3]的情况,dp[1][3]表明要从索引0-1要装满背包容量为3的情况,所以针对物品1,还是两种情况,1、放物品1,2、不放物品1。

如果不放物品1,那么背包的价值应该是dp[0][3],也就是背包容量为3的情况下只放物品0的情况。如果放物品1,那么就要留出物品1的容量,目前容量为3,物品1的容量是2,此时背包剩下容量为1。

在我们0-1背包中,背包每个物品只有一个,但在完全背包问题中,物品是可以放置无限个的,即使空出物品1的空间重量,那背包中也可能还有物品1,所以我们此时考虑物品0和物品1的最大价值是dp[1][1]而不是dp[0][1]。所以放物品1的情况=dp[1][1]+物品1的价值。

所以,针对物品1的两种情况得到的状态转移方程为:
dp [ 1 ] [ 3 ] = max ⁡ { dp [ 0 ] [ 3 ] , dp [ 1 ] [ 3 − weight [ 1 ] ] + value [ 1 ] } \text{dp}\left[ 1 \right]\left[ 3 \right]=\max \left\{ \text{dp}\left[ 0 \right]\left[ 3 \right],\text{dp}\left[ 1 \right]\left[ 3-\text{weight}\left[ 1 \right] \right]+\text{value}\left[ 1 \right] \right\}\ dp[1][3]=max{dp[0][3],dp[1][3−weight[1]]+value[1]}

所以我们可以类推到一般情况:当背包容量为j的情况下,如果不放物品i,里面的最大价值是dp[i-1][j];如果放物品i,那么背包需要先空出物品i的容量,此时背包容量剩下j-weight[i],那么dp[i][ j-weight[i]]就是背包容量为j-weight[i]且不放物品i的最大价值,那么dp[i][ j-weight[i]]+value[i]则是背包放物品i所得到的最大价值。一般情况下的递推关系式为:

dp [ i ] [ j ] = max ⁡ { dp [ i − 1 ] [ j ] , dp [ i ] [ j − weight [ i ] ] + value [ i ] } \text{dp}\left[ i \right]\left[ j \right]=\max \left\{ \text{dp}\left[ i-1 \right]\left[ j \right],\text{dp}\left[ i \right]\left[ j-\text{weight}\left[ i \right] \right]+\text{value}\left[ i \right] \right\}\ dp[i][j]=max{dp[i−1][j],dp[i][j−weight[i]]+value[i]}

注意完全背包问题和01背包问题二维递推公式的区别,01背包是放了不能i再放i,所以要i-1,而完全背包不用管。01背包中是dp[i-1][ j-weight[i]]+value[i]。

递推关系式找出来了,我们应该思考一下初始化,如果背包容量j为0的话,那么无论选取那些物品,背包的价值总和一定是为0的,也就初始化了二维dp数组的最左边,相当于初始化了dp[i][0]。接下来思考dp[0][i],也就是在各个背包容量下能存放物品0的最大价值,这个很简单,直接用背包容量除以物品0的容量再乘价值,就是当前背包容量所能存放物品0的最大价值了。

java 复制代码
public class Main{
    public static int findBag(int[] weight,int[] value,int goodsType,int capacity){
        int[][]dp=new int[goodsType][capacity+1];
        for(int i=weight[0];i<=capacity;i++) {
            dp[0][i] = (i / weight[0]) * value[0];
        }
        for(int i=1;i<goodsType;i++){
            for(int j=0;j<=capacity;j++){
                if(j<weight[i]){
                    dp[i][j]=dp[i-1][j];
                }
                else{
                    dp[i][j]=Math.max(dp[i-1][j],dp[i][j-weight[i]]+value[i]);
                }
            }
        }
        return dp[goodsType-1][capacity];
    }
    public static void main(String[] args){
        Scanner sc=new Scanner(System.in);
        int goodsType=sc.nextInt();
        int capacity=sc.nextInt();
        int[] weight=new int[goodsType];
        int[] value=new int[goodsType];
        for (int i = 0; i < goodsType; i++) {
            weight[i] = sc.nextInt();
            value[i] = sc.nextInt();
        }
        int res=findBag(weight,value,goodsType,capacity);
        System.out.println(res);
    }
}
Method2:动态规划一维dp

我们来观察刚刚推导出来的完全背包的状态转移方程:
dp [ i ] [ j ] = max ⁡ { dp [ i − 1 ] [ j ] , dp [ i ] [ j − weight [ i ] ] + value [ i ] } \text{dp}\left[ i \right]\left[ j \right]=\max \left\{ \text{dp}\left[ i-1 \right]\left[ j \right],\text{dp}\left[ i \right]\left[ j-\text{weight}\left[ i \right] \right]+\text{value}\left[ i \right] \right\}\ dp[i][j]=max{dp[i−1][j],dp[i][j−weight[i]]+value[i]}

如果想要将他压缩成一维DP数组,类比一下我们之前01背包的一维遍历,因为我们针对背包容量的遍历是从后往前的,所以dp[i-1][j]并不会影响dp[i][j],因为dp[i][j]的值是空的,所以我们同样也可以去除掉第一维的维度,转为一维dp,状态转移方程变为:
dp [ j ] = max ⁡ { dp [ j ] , dp [ j − weight [ i ] ] + value [ i ] } \text{dp}\left[ j \right]=\max \left\{ \text{dp}\left[ j \right],\text{dp}\left[ j-\text{weight}\left[ i \right] \right]+\text{value}\left[ i \right] \right\}\ dp[j]=max{dp[j],dp[j−weight[i]]+value[i]}

推完了状态转移方程,接下来我们思考一下遍历的顺序,在01背包中一维的遍历是必须先物品再容量,且容量从大到小。而完全背包问题是不需要的,因为在完全背包问题中,物品是可以重复放置的,dp[j]是根据下表j之前对应的dp[j]所计算出来的,只要保证j之前的dp[j]都是经过计算的就可以了,所以我们需要正向遍历。只要当前背包容量大于物品的容量,我们就计算一次,实际意义就是只要背包容量够,我就一直放物品。

java 复制代码
public static int findBag(int[] weight,int[] value,int goodsType,int capacity){
    int[]dp=new int[capacity+1];
    for(int i=0;i<goodsType;i++){
        for(int j=0;j<=capacity;j++){
            if(j>=weight[i]){
               dp[j]=Math.max(dp[j],dp[j-weight[i]]+value[i]);
            }
        }
     }
     return dp[capacity];
 }

518.零钱兑换II 完全背包求组合数

给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。

输入:amount = 5, coins = [1, 2, 5] 输出:4

思路:

Method1:动态规划二维dp

这个题和494目标和非常像,也是相当于求方案数,那么我们回顾一下494,也就是01背包问题求方案数的二维递推公式:

dp [ i ] [ j ] = dp [ i − 1 ] [ j − nums [ i ] ] + dp [ i − 1 ] [ j ] \text{dp}\left[ i \right]\left[ j \right]=\text{dp}\left[ i-1 \right]\left[ j-\text{nums}[i] \right]+\text{dp}\left[ i-1 \right]\left[ j \right]\ dp[i][j]=dp[i−1][j−nums[i]]+dp[i−1][j]

结合01背包和完全背包之间的区别,我们也能推导出完全背包求方案数的二维递推公式:
dp [ i ] [ j ] = dp [ i ] [ j − nums [ i ] ] + dp [ i − 1 ] [ j ] \text{dp}\left[ i \right]\left[ j \right]=\text{dp}\left[ i \right]\left[ j-\text{nums}[i] \right]+\text{dp}\left[ i-1 \right]\left[ j \right]\ dp[i][j]=dp[i][j−nums[i]]+dp[i−1][j]

接下来就是初始化了,二维dp数组的最上行和最左行一定要初始化。思考一下dp[0][0],也就是背包容量为0,装满物品0的组合数是多少,是1,也就是不装。这样我们就把dp[i][0]都初始化完了,接下来思考dp[0][i],如果当前背包容量正好整除coins[0]就说明只有一种方法,就是放coins[0],也就是当前dp[0][i]=1。

java 复制代码
class Solution {
    public int change(int amount, int[] coins) {
        int[][] dp=new int[coins.length][amount+1];
        for(int i=0;i<coins.length;i++) dp[i][0]=1;
        for(int i=coins[0];i<=amount;i++){
            if(i%coins[0]==0)dp[0][i]=1;
        } 
        for(int i=1;i<coins.length;i++){
            for(int j=0;j<=amount;j++){
                if(j<coins[i]) dp[i][j]=dp[i-1][j];
                else{
                    dp[i][j]=dp[i-1][j]+dp[i][j-coins[i]];
                }
            }
        }
        return dp[coins.length-1][amount];
    }
}
Method2:动态规划一维dp

这个题和494目标和非常像,也是相当于求方案数,那么我们回顾一下494,也就是01背包问题求方案数的一维递推公式:

dp [ j ] = dp [ j − nums [ i ] ] + dp [ j ] \text{dp}\left[ j \right]=\text{dp}\left[ j-\text{nums}[i] \right]+\text{dp}\left[ j \right]\ dp[j]=dp[j−nums[i]]+dp[j]

这里没有变化,唯一需要变化的是遍历的开始,01背包中我们的一维dp遍历从背包的从大到小逆序遍历,是因为我们01问题的物品只能用一次,从大到小遍历避免一个物品放多次。而完全背包问题是允许一个物品放多次的,所以我们的背包容量就可以正向遍历。

java 复制代码
class Solution {
    public int change(int amount, int[] coins) {
        int[] dp=new int[amount+1];
        dp[0]=1;
        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];
    }
}

377.组合总和IV 完全背包求排列数

给你一个由不同整数组成的数组nums,和一个目标整数target。请你从nums中找出并返回总和为target的元素组合的个数。题目数据保证答案符合32位整数范围。

输入:nums = [1,2,3], target = 4 输出:7

思路:

注意这个题和518题区别,虽然都是求完全背包问题的方案数,但是无论是01背包方案数的494题,还是完全背包方案数的518题,都是要求方案的组合的方案数,而377题是要求的是方案的排列的方案数,这里就要分清排列和组合的区别。这个区别是由哪里来的呢?是因为遍历顺序的不同,虽然我们的完全背包问题并不在意先遍历背包容量还是先遍历物品,但是如果要区分最终方案数是排列数还是组合数就需要注意一下遍历的顺序。

**如果求组合数,那么外层for循环遍历物品,内层for遍历背包,如果求排列数,就是外层for遍历背包,内层for遍历物品。**我们思考一下为什么会有这样的差别?

如果把遍历物品放到外循环,遍历target作为内循环的话,dp[i][j]表示索引0-i的物品装满j背包容量的最大价值,举个例子,如果是dp[3][4]的话,表示索引0-3的物品装满背包容量为4的背包,那么在计算dp[3][4]的时候,结果集只会出现{0,1,2,3}这样递增的物品的序列,并不会出现{3,2,1,0}这样的序列,因为我们物品是在外层遍历一直递增的,而我们的排列数是需要{1,2,3}和{3,2,1}这样的排列的,所以我们需要把物品放在内层遍历,这样每次针对背包容量为j时,物品都会从头开始遍历,这样就能够实现排列数。其他与求组合数没什么区别,这里我们也就只用一维dp进行实现。

无论是求组合数,还是求排列数,我们的完全背包的一维状态转移方程还是:
dp [ j ] = max ⁡ { dp [ j ] , dp [ j − weight [ i ] ] + value [ i ] } \text{dp}\left[ j \right]=\max \left\{ \text{dp}\left[ j \right],\text{dp}\left[ j-\text{weight}\left[ i \right] \right]+\text{value}\left[ i \right] \right\}\ dp[j]=max{dp[j],dp[j−weight[i]]+value[i]}

遍历顺序我们刚刚已经解释过了,所以代码就很容易写出来:

java 复制代码
class Solution {
    public int combinationSum4(int[] nums, int target) {
        int n = nums.length;
        int []dp=new int[target+1];
        dp[0]=1;
        for(int i=0;i<=target;i++){
            for(int j=0;j<n;j++){
                if(i>=nums[j]){
                    dp[i]+=dp[i-nums[j]];
                }
            }
        }
        return dp[target];
    }
}

Kama.57.爬楼梯进阶版 完全背包求排列数

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

每次你可以爬至多m(1<=m<n)个台阶。你有多少种不同的方法可以爬到楼顶呢?

注意:给定n是一个正整数。输入共一行,包含两个正整数,分别表示n, m

输入:3 2 输出:3

思路:

Method1:回溯法

每次从第一步重新看,回溯即可

java 复制代码
import java.util.*;
public class Main {
    static int count=0;
    public static int findTargetSumWays(int target, int steps) {
        dfs(target,steps,0);
        return count;
    }
    private static void dfs(int target, int steps, int sum) {
        if(sum==target){
            count++;
            return;
        }
        else if(sum>target){
            return;
        }
        for(int i=1;i<=steps;i++) {
            dfs(target, steps, sum + i);
        }
    }
    public static void main(String[] args) {
        Scanner sc=new Scanner(System.in);
        int target=sc.nextInt();
        int steps=sc.nextInt();
        System.out.println(findTargetSumWays(target, steps));
    }
}
Method2:动态规划

这个和70题非常像,只不过70题是每次只能上一层或上两层,但是现在是能上n层,并且是有顺序的,什么是顺序呢,就是1,2,1层和1,1,2层是不同的,所以这个问题就转换成完全背包下的求排列数的问题。

那么完全背包中求排列数的一维状态转移方程是:
dp [ j ] = dp [ j − nums [ i ] ] + dp [ j ] \text{dp}\left[ j \right]=\text{dp}\left[ j-\text{nums}[i] \right]+\text{dp}\left[ j \right]\ dp[j]=dp[j−nums[i]]+dp[j]

但是本题中nums[i]就是步数,然后求排列数就是外层循环背包容量,内层循环物品。

java 复制代码
import java.util.Scanner;
public class Main {
    public static int findTargetSumWays(int target, int steps) {
        int[] dp=new int[target+1];
        dp[0]=1;
        for(int i=1;i<=target;i++){
            for(int j=1;j<=steps;j++){
                if(i>=j) dp[i]=dp[i]+dp[i-j];
            }
        }
        return dp[target];
    }
    public static void main(String[] args) {
        Scanner sc=new Scanner(System.in);
        int target=sc.nextInt();
        int steps=sc.nextInt();
        System.out.println(findTargetSumWays(target, steps));
    }
}

322.零钱兑换 完全背包求组合数个数的最小

给你一个整数数组coins,表示不同面额的硬币;以及一个整数amount,表示总金额。计算并返回可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回-1。你可以认为每种硬币的数量是无限的。

输入:coins = [1, 2, 5], amount = 11 输出:3

思路:11 = 5 + 5 + 1

Method1:记忆回溯法

将当前的枚数记录到记忆矩阵中,初始化记忆矩阵的值为max,每次碰到一样的取出记忆矩阵中的值,如果map中没存储,就将他设为当前最新,如果有存储,则进行更新。

java 复制代码
class Solution {
    Map<Integer, Integer> memo = new HashMap<>();
    public int coinChange(int[] coins, int amount) {
        if (amount == 0) return 0;
        Arrays.sort(coins);
        for (int i = 0, j = coins.length - 1; i < j; i++, j--) {
            int temp = coins[i];
            coins[i] = coins[j];
            coins[j] = temp;
        }
        int res = dfs(coins, amount);
        return res == Integer.MAX_VALUE ? -1 : res;
    }
    private int dfs(int[] coins, int amount) {
        if (amount == 0) return 0;
        if (amount < 0) return Integer.MAX_VALUE;
        if (memo.containsKey(amount)) return memo.get(amount);
        int minCount = Integer.MAX_VALUE;
        for (int coin : coins) {
            int res = dfs(coins, amount - coin);
            if (res != Integer.MAX_VALUE) {
                minCount = Math.min(minCount, res + 1);
            }
        }
        memo.put(amount, minCount);
        return minCount;
    }
}
Method2:动态规划

很明显这个问题是完全背包,并且是组合数问题,只不过需要返回的是组合数的数最小的情况。唯一不同的地方就是需要初始化为max,如果出现了组合数小于max就更新。最开始dp[0]为0。

java 复制代码
class Solution {
    public int coinChange(int[] coins, int amount) {
        if(amount==0)return 0;
        int [] dp=new int [amount+1];
        for(int i=0;i<dp.length;i++){
            dp[i]=Integer.MAX_VALUE;
        }
        dp[0]=0;
        for(int i=0;i<coins.length;i++){
            for(int j=coins[i];j<=amount;j++){
                if(dp[j-coins[i]]!=Integer.MAX_VALUE){
                    dp[j]=Math.min(dp[j],dp[j-coins[i]]+1);
                }
            }
        }
        return dp[amount]==Integer.MAX_VALUE?-1:dp[amount];
    }
}

279.完全平方数 完全背包求组合数个数的最小

给你一个整数n,返回和为n的完全平方数的最少数量。

完全平方数是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9和16都是完全平方数,而3和11不是。

输入:n = 12 输出:3
思路:12=4+4+4

很明显是一个完全背包问题,完全平方数就是可以无限使用的物品,凑成正整数n就是背包,问凑满这个背包最少需要多少个物品。和322题完全一样,只不过322题的coins[i]变成了完全平方数,也就是i*i,其他不变。

java 复制代码
class Solution {
    public int numSquares(int n) {
        int max=Integer.MAX_VALUE;
        int []dp=new int [n+1];
        for(int i=0;i<=n;i++) dp[i]=max;
        dp[0]=0;
        for(int i=1;i*i<=n;i++){
            for(int j=i*i;j<=n;j++){
                if(dp[j-i*i]!=max){
                    dp[j]=Math.min(dp[j],dp[j-i*i]+1);
                }
            }
        }
        return dp[n];
    }
}

139.单词拆分

给你一个字符串s和一个字符串列表wordDict作为字典。如果可以利用字典中出现的一个或多个单词拼接出s则返回true。

输入:s = "leetcode", wordDict = ["leet", "code"] 输出:true

思路:

Method1:记忆回溯法

先将字典存入set,然后开始分割字符串,如果当前分割出来index到i+1(前闭后开)的字符串不在set中,i就继续往下走,如果当前字符串在set中,就将当前位置i+1作为递归遍历的开始继续进行递归,检查i+1之后的字符串是否满足。如果最后满足返回true,如果遍历到最后都没有返回true就将当前位置i+1存入记忆map中标记为false,如果index到最后说明全部遍历完没有问题,就返回true。

java 复制代码
class Solution {
    HashMap<Integer,Boolean> memo=new HashMap<>();
    HashSet<String>set=new HashSet<>();
    public boolean wordBreak(String s, List<String> wordDict) {
        for(String ss:wordDict) set.add(ss);
        return dfs(s,wordDict,0);
    }
    public boolean dfs(String s, List<String> wordDict,int index){
        if(index>=s.length())    return true;
        if (memo.containsKey(index)) return memo.get(index);
        for(int i=index;i<s.length();i++){
            String str=s.substring(index,i+1);
            if(set.contains(str)&&dfs(s,wordDict,i+1)) return true;
        }
        memo.put(index,false);
        return false;
    }
}
Method2:动态规划

明显是一个完全背包问题,求是否能装满背包的问题,wordDict是可以使用无限次的物品,而s是一个背包,是求排列数 ,并不是求组合数。

所以外层遍历背包,内层遍历物品,遍历物品时,只有当前的索引大于word的长度,并且是从上一个dp[i]==1开始,并且word等于截取的字符串的时候才将当前dp设为1。dp[i]==1保证是从0开始,len保证是连续的。

java 复制代码
class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        int[]dp=new int[s.length()+1];
        dp[0]=1;
        for(int i=1;i<=s.length();i++){
            for(String str:wordDict){
                int len=str.length();
                if(i>=len){
                    String sub=s.substring(i-len,i);
                    if(sub.equals(str)&&dp[i-len]==1){
                        dp[i]=1;break;
                    }
                }
            }
        }
        return dp[s.length()]==1;
    }
}

6 多重背包

Kama.56.携带矿石资源

你是一名宇航员,即将前往一个遥远的行星。在这个行星上,有许多不同类型的矿石资源,每种矿石都有不同的重要性和价值。你需要选择哪些矿石带回地球,但你的宇航舱有一定的容量限制。

给定一个宇航舱,最大容量为C。现在有N种不同类型的矿石,每种矿石有一个重量w[i],一个价值v[i],以及最多k[i]个可用。不同类型的矿石在地球上的市场价值不同。你需要计算如何在不超过宇航舱容量的情况下,最大化你所能获取的总价值。输入共包括四行,第一行包含两个整数C和N,分别表示宇航舱的容量和矿石的种类数量。接下来的三行,每行包含N个正整数。具体如下:第二行包含N个整数,表示N种矿石的重量。第三行包含N个整数,表示N种矿石的价格。第四行包含N个整数,表示N种矿石的可用数量上限。

输入:10 3 输出:90

1 3 4

15 20 30

2 3 2

思路:

乍一看非常难,但是我们仔细思考一下,这不就跟我们01背包非常像吗,只不过01背包是只能用1次,而多重背包是能用k次。

我们依旧从一维递推关系式走,01背包问题的关系式是:
dp [ j ] = max ⁡ { dp [ j ] , dp [ j − weight [ i ] ] + value [ i ] } \text{dp}\left[ j \right]=\max \left\{ \text{dp}\left[ j \right],\text{dp}\left[ j-\text{weight}\left[ i \right] \right]+\text{value}\left[ i \right] \right\}\ dp[j]=max{dp[j],dp[j−weight[i]]+value[i]}

那么多重背包只不过在这个基础上多了k次使用,也就变成了:
dp [ j ] = max ⁡ { dp [ j ] , dp [ j − k*weight [ i ] ] + k*value [ i ] } \text{dp}\left[ j \right]=\max \left\{ \text{dp}\left[ j \right],\text{dp}\left[ j-\text{k*weight}\left[ i \right] \right]+\text{k*value}\left[ i \right] \right\}\ dp[j]=max{dp[j],dp[j−k*weight[i]]+k*value[i]}

那么我们的多重背包也就迎刃而解了。

java 复制代码
import java.util.Scanner;
class Main{
    public static void main(String [] args) {
        Scanner sc = new Scanner(System.in);
        int bagWeight, n;
        bagWeight = sc.nextInt();
        n = sc.nextInt();
        int[] weight = new int[n];
        int[] value = new int[n];
        int[] nums = new int[n];
        for (int i = 0; i < n; i++) weight[i] = sc.nextInt();
        for (int i = 0; i < n; i++) value[i] = sc.nextInt();
        for (int i = 0; i < n; i++) nums[i] = sc.nextInt();
        int[] dp = new int[bagWeight + 1];
        for (int i = 0; i < n; i++) {
            for (int j = bagWeight; j >= weight[i]; j--) {
                for (int k = 1; k <= nums[i] && (j - k * weight[i]) >= 0; k++) {
                    dp[j] = Math.max(dp[j], dp[j - k * weight[i]] + k * value[i]);
                }
            }
        }
        System.out.println(dp[bagWeight]);
    }
}

相关推荐
SylviaW088 分钟前
python-leetcode 37.翻转二叉树
算法·leetcode·职场和发展
h^hh18 分钟前
洛谷 P3405 [USACO16DEC] Cities and States S(详解)c++
开发语言·数据结构·c++·算法·哈希算法
玦尘、18 分钟前
位运算实用技巧与LeetCode实战
算法·leetcode·位操作
重生之我要成为代码大佬24 分钟前
Python天梯赛10分题-念数字、求整数段和、比较大小、计算阶乘和
开发语言·数据结构·python·算法
Best_Me071 小时前
【CVPR2024-工业异常检测】PromptAD:与只有正常样本的少样本异常检测的学习提示
人工智能·学习·算法·计算机视觉
HBryce241 小时前
缓存-算法
算法·缓存
eso19831 小时前
Spark MLlib使用流程简介
python·算法·spark-ml·推荐算法
夏末秋也凉1 小时前
力扣-回溯-93 复原IP地址
算法·leetcode
Erik_LinX1 小时前
算法日记27:完全背包(DFS->记忆化搜索->倒叙DP->顺序DP->空间优化)
算法·深度优先
AC使者1 小时前
D. C05.L08.贪心算法入门(一).课堂练习4.危险的实验(NHOI2015初中)
算法·贪心算法