代码随想录算法训练营32天 | ​​完全背包理论基础、518.零钱兑换II、377. 组合总和 Ⅳ、70. 爬楼梯(进阶版)

题目链接:携带研究材料518.零钱兑换II377. 组合总和 Ⅳ爬楼梯(进阶版)
文章链接:代码随想录

动态规划


1. 完全背包理论基础

 有 n 件物品和一个最多能背重量为 w 的背包。第 i 件物品的重量是 weight[i],得到的价值是 value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。

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

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

注意:每件商品有无数个,可以无限次放入背包中。问背包能背的物品最大价值是多少?

方法一:二维 dp 数组

1. 确定 dp 数组(dp table)以及下标的含义:dp[i][j] 表示从下标为 [0-i] 的物品,每个物品可以取无限次,放进容量为 j 的背包,价值总和最大是多少

2. 推导递推公式:求取 dp[i][j] 有两种情况:放物品 i & 不放物品 i

  • 不放物品 i:背包容量为 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 的价值)就是背包放物品 i 得到的最大价值

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

  • 注意与01背包区别:01背包是 dp[i - 1][j - weight[i]] + value[i]

3. dp 数组如何初始化:

  • 首先从 dp[i][j] 的定义出发,如果背包容量 j 为 0 的话,即 dp[i][0],无论是选取哪些物品,背包价值总和一定为 0
  • 再由状态转移方程 dp[i][j] = max ( dp[i - 1][j], dp[i][j - weight[i]] + value[i] ) 可以看出有一个方向 i 是由 i-1 推导出来,那么 i 为 0 的时候,也就是 dp[0][j] 一定要初始化。
    for (int i = 1; i < weight.size(); i++) {  // 当然这一步,如果把dp数组预先初始化为0了,这一步就可以省略,但很多同学应该没有想清楚这一点。
        dp[i][0] = 0;
    }
    
    // 正序遍历,如果能放下就一直装物品0
    for (int j = weight[0]; j <= bagWeight; j++)
        dp[0][j] = dp[0][j - weight[0]] + value[0];
    • 当 j < weight[0] 时,dp[0][j] 应该是 0,因为背包容量比物品 0 重量还小

    • 当 j > weight[0] 时,dp[0][j] 如果能放下 weight[0] 的话,就一直装,因为每一种物品有无限个

  • 除了 dp[0][j] 和 dp[i][0],从递归公式可以看出,dp[i][j] 是由其上方和左上方的数值推导而来,因此,其他下标初始值可以任意设置,因为都会被覆盖

此时 dp 数组初始化情况如图所示:

4. 确定遍历顺序:对于完全背包二维 dp 数组,先遍历物品还是先遍历背包都是可以的

5. 举例推导 dp 数组:因为物品 0 的性价比是最高的,而且在完全背包中,每一类物品都有无限个,因此,既然物品 0 的性价比最高,当然是优先放物品 0

#include <iostream>
#include <vector>
using namespace std;

int main() {
    int n, bagSize;
    cin >> n >> bagSize;

    vector<int> weight(n);
    vector<int> value(n);
    for(int i = 0; i < n; i++) {
        cin >> weight[i] >> value[i];
    }

    vector<vector<int>> dp(n, vector<int>(bagSize + 1, 0));
    for(int j = weight[0]; j <= bagSize; j++) {
        dp[0][j] = dp[0][j - weight[0]] + value[0];
    }

    for(int i = 1; i < n; i++) {
        for(int j = 0; j <= bagSize; j++) {
            if(j < weight[i]) dp[i][j] = dp[i - 1][j];
            else dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]);
        }
    }

    cout << dp[n - 1][bagSize];
}

方法二:一维 dp 数组(滚动数组)

对于递推公式:去掉 i 这个维度,完全背包以为--一维 dp 数组的递推公式变成: dp[j] = max( dp[j], dp[j - weight[i]] + value[i] )

对于遍历顺序:

已知 01 背包中,二维 dp 数组的两个 for 循环的先后顺序是可以颠倒的,一维 dp 数组的两个 for 循环的先后循序一定是先遍历物品,再遍历背包容量(且背包是倒序遍历)

而对于完全背包一维 dp 数组,两个for循环嵌套顺序是可以颠倒的,而且都是正序遍历

#include <iostream>
#include <vector>
using namespace std;

int main() {
    int n, bagSize;
    cin >> n >> bagSize;

    vector<int> weight(n);
    vector<int> value(n);
    for(int i = 0; i < n; i++) {
        cin >> weight[i] >> value[i];
    }

    vector<int> dp(bagSize + 1, 0);

    for(int i = 0; i < n; i++) {
        for(int j = weight[i]; j <= bagSize; j++) {
            dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
        }
    }

    cout << dp[bagSize];
}

 2. 零钱兑换II

方法一:二维数组dp[i][j]

1. 确定dp数组(dp table)以及下标的含义:dp[i][j]:使用下标为 [0, i] 的 nums[i] 能够凑满 j(包括 j)这么大容量的包,有 dp[i][j] 种方法

2. 确定递推公式:dp[i][j] = dp[i - 1][j] + dp[i][j - nums[i]]

3. dp数组如何初始化:首先考虑 dp[0][0],背包容量为 0,能装满物品 0 的方法有0 种,由于题目中不存在硬币面额为 0 的情况,因此不考虑无限个 0 的组合和为 0 的情况(与494. 目标和不同

  • 最上行dp[0][j](用物品 0 装满容量为 j 的背包有几种组合方法):只要 j 可以整除 coins[i],就有一种组合方法

    for (int j = 0; j <= bagSize; j++) {
        if (j % coins[0] == 0) dp[0][j] = 1;
    }
  • 最左列dp[i][0](用物品 [0, i] 装满容量为 0 的背包有几种组合方法):只有一种方法,即不装,因此 dp[i][0] = 1

4. 确定遍历顺序:使用二维 dp 数组,两种遍历方式都可以(先从上到下再从左到右,先从左到右再从上到下)

5. 举例推导dp数组:以 amount 为 5,coins 为:[2,3,5] 为例:

1 0 1 0 1 0
1 0 1 1 1 1
1 0 1 1 1 2
class Solution {
public:
    int change(int amount, vector<int>& coins) {
        int bagSize = amount;
        vector<vector<uint64_t>> dp(coins.size(), vector<uint64_t>(bagSize + 1, 0));

        for(int i = 0; i < coins.size(); i++) {
            dp[i][0] = 1;
        }
        for(int j = 0; j <= bagSize; j++) {
            if(j % coins[0] == 0) dp[0][j] = 1;
        }

        for(int i = 1; i < coins.size(); i++) {
            for(int j = 0; j <= bagSize; 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.size() - 1][bagSize];
    }
};

方法二:一维数组dp[j]

1. 确定dp数组(dp table)以及下标的含义:dp[j] 表示凑满总金额为 j 的组合数为 dp[j]

2. 确定递推公式:dp[j] = dp[j] + dp[j - coins[i]] 即 dp[j] += dp[j - coins[i]]

3. dp数组如何初始化:dp[0] 初始为 1,即装满容量为 0 的背包有一种方法,放 0 件物品

4. 确定遍历顺序:对于纯完全背包使用一维 dp 数组,两种 for 循环遍历方式是可以颠倒的,而由于本题求的是组合数,两种遍历方式的结果并不相同

  • 情况一:外层 物品(硬币),内层 背包容量(金额)
    for (int i = 0; i < coins.size(); i++) {   // 遍历硬币
        for (int j = coins[i]; j <= amount; j++) { // 遍历金额
            dp[j] += dp[j - coins[i]];
        }
    }
    含义:每次固定一种硬币,计算它能对所有金额产生的影响。这样不会重复统计排列,只会统计组合
  • 情况二:外层 背包容量(金额),内层 物品(硬币)
    for (int j = 0; j <= amount; j++) {   // 遍历金额
        for (int i = 0; i < coins.size(); i++) { // 遍历硬币
            if (j - coins[i] >= 0) dp[j] += dp[j - coins[i]];
        }
    }
    含义:每次固定一个金额,尝试从所有硬币转移过来。这会把不同顺序的结果都算进去,等于计算排列数

5. 举例推导dp数组:

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<uint64_t> dp(amount + 1, 0);
        dp[0] = 1;
        for (int i = 0; i < coins.size(); i++) {
            for (int j = coins[i]; j <= amount; j++) {
                dp[j] += dp[j - coins[i]];
            }
        }

        return dp[amount];
    }
};

3. 组合总和 Ⅳ

本题的思路和“518.零钱兑换II”一致,只是从求组合(不强调顺序)变成了求排列(强调顺序)

1. 确定dp数组(dp table)以及下标的含义:dp[j] 表示凑成目标正整数为 j 的排列个数为 dp[j]

2. 确定递推公式:dp[j] = dp[j] + dp[j - nums[i]] 即 dp[j] += dp[j - nums[i]]

3. dp数组如何初始化:根据递推公式 dp[j] += dp[j - nums[j]],dp[0] 要初始化为 1,这样递归其他 dp[j] 的时候才会有数值基础

4. 确定遍历顺序:对于纯完全背包使用一维 dp 数组,两种 for 循环遍历方式是可以颠倒的,而由于本题求的是排列数,两种遍历方式的结果并不相同

  • 如果求组合数,就是外层 for 循环遍历物品,内层 for 循环遍历背包
  • 如果求排列数,就是外层 for 循环遍历背包,内层 for 循环遍历物品

5. 举例推导dp数组:

//本题的思路和“518.零钱兑换II”一致,只是从求组合(不强调顺序)变成了求排列(强调顺序)

class Solution {
public:
    int combinationSum4(vector<int>& nums, int target) {
        int bagSize = target;
        vector<uint64_t> dp(bagSize + 1, 0); //C++测试用例有两个数相加超过int的数据
        dp[0] = 1;
        for(int j = 0; j <= bagSize; j++) {
            for(int i = 0; i < nums.size(); i++) {
                if(j >= nums[i]) dp[j] += dp[j - nums[i]];
            }
        }

        return dp[bagSize];
    }
};

4. 爬楼梯(进阶版)

之前做的爬楼梯每次至多只能爬两个台阶,进阶版改为:每次至多可以爬 m 个台阶(1 <= m < n),问有多少种不同的方法可以爬到楼顶呢?

进阶版的爬楼梯其实也是一个完全背包问题。1 阶、2 阶、.... m 阶就是物品,楼顶 n 就是背包的容量。每一阶可以重复使用,例如跳了 1 阶,还可以继续跳 1 阶。问跳到楼顶有几种方法其实就是问装满背包有几种方法。

本题和“”基本就是一道题了。

#include <iostream>
#include <vector>
using namespace std;

int main() {
    int n, m;
    cin >> n >> m;
    vector<int> dp(n + 1, 0);
    dp[0] = 1;
    for(int j = 0; j <= n; j++) {
        for(int i = 1; i <= m; i++) {
            if(j >= i) dp[j] += dp[j - i];
        }
    }
    cout << dp[n];
}

5. 阶段总结

(待更新...)


 相关题目和后续提高:


心得:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值