【动态规划】通过例题 理解 完全背包问题(完全背包、零钱兑换、完全平方数、C++)


前言

完全背包问题 是一种经典的动态规划问题,通常用于求解优化问题。在这个问题中,我们有一个背包和一组物品,每种物品有一个特定的重量和价值。

  • 与01背包问题不同的是,在完全背包问题中,每种物品可以无限次使用。

问题描述

给定一个背包容量 Wn 种物品,每种物品 i 具有重量 w[i] 和价值 v[i]。我们希望在不超过背包容量的情况下,选择物品使得背包中物品的总价值最大化。


动态规划解法

动态规划解法通过构建一个状态转移表来解决这个问题。我们可以使用一个一维数组 dp 来表示最大价值,其中 dp[j] 表示背包容量为 j 时可以获得的最大价值。

状态转移方程:

dp[j] = max(dp[j], dp[j - w[i]] + v[i])

其中 j 是当前背包容量,w[i]v[i] 分别是第 i 种物品的重量和价值。这个方程的意思是,对于每个容量 j,我们可以选择不选择第 i 种物品,或者选择第 i 种物品,取这两者中的最大值。


算法题

1.【模板】完全背包

思路

  1. 数据读入

    • 读入物品数量 n 和背包容量 V
    • 读入每种物品的价值 v[i] 和重量 w[i]
  2. 问题一:完全背包问题

    • 使用二维数组 dp[i][j] 来表示前 i 个物品中背包容量为 j 时的最大价值。
    • 初始化 dp 数组,dp[i][j] 继承自 dp[i-1][j](不选当前物品)。
    • 如果当前背包容量 j 能容纳第 i 种物品,则更新 dp[i][j] 为选用当前物品后的最大价值。
  3. 问题二:判断背包是否有解的完全背包问题

    • 初始化 dp 数组,设置 dp[0][j] 为 -1(表示不可能达到这些容量)。
    • 对于每种物品,更新 dp[i][j],但前提是当前容量 j 可以通过当前物品达成并且之前的状态不为 -1。
  4. 输出结果

    • 对于问题一,输出最大价值。
    • 对于问题二,输出背包容量 V 时的最大价值,如果为 -1 则输出 0(表示无法达到这个容量)。

代码

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

const int N = 1010;
int n, V, v[N], w[N];
int dp[N][N];

int main() {
    // 读入数据
    cin >> n >> V;
    for (int i = 1; i <= n; i++)
        cin >> v[i] >> w[i];

    // 问题一
    for (int i = 1; i <= n; ++i)
        for (int j = 0; j <= V; ++j) {
            dp[i][j] = dp[i - 1][j];
            if (j >= v[i])
                dp[i][j] = max(dp[i][j], dp[i][j - v[i]] + w[i]);
        }
    cout << dp[n][V] << endl;

    // 问题二
    memset(dp, 0, sizeof(dp));
    for (int j = 1; j <= V; ++j)
        dp[0][j] = -1;

    for (int i = 1; i <= n; ++i)
        for (int j = 0; j <= V; ++j) {
            dp[i][j] = dp[i - 1][j];
            if (j >= v[i] && dp[i][j - v[i]] != -1)
                dp[i][j] = max(dp[i][j], dp[i][j - v[i]] + w[i]);
        }

    cout << (dp[n][V] == -1 ? 0 : dp[n][V]) << endl;
    return 0;
}


2.零钱兑换

在这里插入图片描述

思路

  1. 初始化

    • INF 定义为一个很大的值,用于表示无法达成的情况。
    • dp[i][j] 表示使用前 i 种硬币组合,总金额 j 所需的最少硬币数。
  2. DP 数组初始化

    • dp[0][j] 初始化为 INF(表示使用 0 种硬币无法达到金额 j,除非 j 为 0)。dp[0][0] 被隐式地设置为 0。
  3. 状态转移

    • 对于每个硬币 i 和金额 j,有两种选择:
      1. 不使用当前硬币 i:即 dp[i][j] 继承自 dp[i-1][j]
      2. 使用当前硬币 i:更新 dp[i][j]dp[i][j - coins[i-1]] + 1(即在不使用当前硬币时的最小硬币数基础上加上当前硬币)。
    • 比较这两种情况,取最小值。
  4. 返回结果

    • 如果 dp[n][amount] 大于等于 INF,说明无法用给定的硬币组合成目标金额 amount,返回 -1。
    • 否则,返回 dp[n][amount],即最少硬币数。
  5. 总结

    • dp[i][j]:使用前 i 种硬币时,凑出金额 j 的最小硬币数。
    • 时间复杂度O(n * amount)n 是硬币种类数,amount 是目标金额。
    • 空间复杂度O(n * amount)

代码

class Solution {
public:
    const int INF = 0x3f3f3f3f;

    int coinChange(vector<int>& coins, int amount) {
        int n = coins.size();
        // 创建dp数组
        // dp[i][j]: 在前i个数中,选择硬币,使总金额恰好为j的最少硬币个数
        vector<vector<int>> dp(n+1, vector<int>(amount+1));
        // 初始化
        afor(int j = 1; j <= amount; ++j)
            dp[0][j] = INF;
        for(int i = 1; i <= n; ++i)
            for(int j = 0; j <= amount; ++j)
            {
                dp[i][j] = dp[i-1][j];
                if(j >= coins[i-1])
                    dp[i][j] = min(dp[i][j], dp[i][j-coins[i-1]] + 1);
            }

        return dp[n][amount] >= INF ? -1 : dp[n][amount];
    }
};

3.零钱兑换II

在这里插入图片描述
思路

  1. 初始化

    • dp[j] 表示凑成金额 j 的不同组合数。
    • dp[0] = 1 表示凑成金额 0 的组合数为 1(即不选任何硬币)。
  2. 状态转移

    • 对于每种硬币 coins[i-1],更新 dp[j]。这里需要注意的是,我们从金额 coins[i-1] 开始更新,因为金额小于 coins[i-1] 的情况不会受到当前硬币影响。
    • dp[j] += dp[j - coins[i-1]]:这是因为 dp[j - coins[i-1]] 代表了凑成金额 j - coins[i-1] 的组合数,而 dp[j] 的更新表示将当前硬币 coins[i-1] 加入这些组合中,得到新的组合数。
  3. 返回结果

    • dp[amount] 存储了凑成金额 amount 的所有可能组合数。
  4. 总结

    • 时间复杂度O(n * amount),其中 n 是硬币的种类数,amount 是目标金额。每种硬币遍历 amount 次。
    • 空间复杂度O(amount)。我们只使用了一维的 dp 数组来存储状态,减少了空间复杂度。

代码

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        // 空间优化版本:
        int n = coins.size();
        // 创建dp数组
        // dp[i][j]: 从前i个位选,使其总和为i,的选法
        vector<int> dp(amount+1);
        dp[0] = 1;

        for(int i = 1; i <= n; ++i)
            for(int j = coins[i-1]; j <= amount; ++j)
                dp[j] += dp[j-coins[i-1]];

        return dp[amount];
    }
};

4.完全平方数

在这里插入图片描述

思路

  1. 初始化

    • int _sqrt = sqrt(n); 计算不超过 n 的最大整数平方根。
    • vector<vector<int>> dp(_sqrt + 1, vector<int>(n + 1)); 创建二维 dp 数组,其中 dp[i][j] 代表前 i 个平方数中组成 j 的最少数量。
    • for (int j = 1; j <= n; ++j) dp[0][j] = 0x3f3f3f3f; 初始化 dp 表中的不可达状态为一个很大的值(表示初始状态下无法组成 j)。
  2. 填表

    • for (int i = 1; i <= _sqrt; ++i) 遍历每个可能的平方数。
    • dp[i][j] = dp[i-1][j]; 初始状态为不选第 i 个平方数的情况下的结果。
    • if (j >= i * i)j 大于等于当前平方数 i*i 时,尝试使用这个平方数更新 dp
    • dp[i][j] = min(dp[i][j], dp[i][j - i * i] + 1); 更新 dp[i][j] 为包括当前平方数的最小值。
  3. 返回结果

    • return dp[_sqrt][n]; 最终结果在 dp[_sqrt][n] 中,它表示用最少的平方数组合成 n
  4. 总结

  • 时间复杂度O(sqrt(n) * n),其中 sqrt(n) 是平方数的数量,n 是目标值。
  • 空间复杂度O(sqrt(n) * n),由于使用了二维 dp 数组。

代码

class Solution {
public:
    int numSquares(int n) {
        int _sqrt = sqrt(n);
        // 创建dp数组
        // dp[i][j]: 在前i个数中,选择数使其和等于j,时的最少数量
        vector<vector<int>> dp(_sqrt+1, vector<int>(n+1));
        for(int j = 1; j <= n; ++j) dp[0][j] = 0x3f3f3f3f;
        // 填表
        for(int i = 1; i <= _sqrt; ++i)
            for(int j = 1; j <= n; ++j)
            {
                dp[i][j] = dp[i-1][j]; // 不选i位置数
                if(j >= i*i)
                    dp[i][j] = min(dp[i][j], dp[i][j-i*i]+1);
            }

        return dp[_sqrt][n];
    }
};

思路

代码


思路

代码

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值