动态规划之完全背包

简介: 本文详解完全背包问题:作为动态规划经典题型,区别于01背包(每物限选1次),其特点是每种物品可无限次选取。文章从定义、状态转移方程(dp[i][j] = max(dp[i-1][j], dp[i][j-w]+v))、二维/一维实现到遍历顺序对组合数与排列数的影响,结合零钱兑换II、组合总和IV等5道典型例题深入剖析,助力掌握核心思想与编码技巧。

 引言:

完全背包 隶属于动态规划中的背包问题。而 01背包 又是完全背包的基石,所以不懂01背包的,有必要了解一下。

什么是完全背包?

01背包问题:有一个背包承重为V,有N个物品,每个物品的价值(value)为v,重量为(weight)为w

每个物品只能取1次,求问背包,最多能装下多大价值的物品。

完全背包问题:有一个背包承重为V,有N个物品,每个物品的价值(value)为v,重量为(weight)为w,每个物品无限次存取,求问背包,最多能装下多大价值的物品。

如上01背包与完全背包的区别就在于,能无限次存取。

举例

如题:一个能称重为4的背包,共有3件可装物品。对应价值重量如下图所示,求最多能装下多大价值的物品。

image.gif 编辑

1、分析下标的含义:

dp[i][j] 表示,物品[ 0~i ] 每个物品,每个物品能取无数次,放入到容量为 j 的背包内,价值总和最大为多少?

2、递推公式:

image.gif 编辑

拿dp[1][4]举例字,物品1,价值value(20),重量weight(3)。

如上图所示,有两种情况:

  • 不选择物品本身:直接继承物品0,也就dp[0][4] = 60;
  • 选择该物品:dp[ 1 ][ 4-weight ]+value = dp[ 1 ][ 1 ]+20 也就是35;

然后从两种选择中,选取。

最后得出递推公式:dp[i][j] = max(dp[i-1][j] , dp[i][j-weight] + value);

3、推导代码(二维):

下标的含义 与 递推公式都得出来了,代码不就直接出来了吗:

#include <iostream>
#include <vector>
using namespace std;
// 用于存储最终的最大价值,不过在当前代码中未实际使用该变量
int maxValue = 0;
// 物品的重量和价值数组
// weight 数组存储每个物品的重量,例如 weight[0] 是第一个物品的重量
vector<int> weight = {1, 3, 4, 5, 6};
// value 数组存储每个物品的价值,例如 value[0] 是第一个物品的价值
vector<int> value = {1, 3, 4, 5, 6};
int main(){
    // 背包的最大容量
    int bagweight = 6;
    // 定义二维动态规划数组 dp
    // dp[i][j] 表示前 i 个物品放入容量为 j 的背包中所能获得的最大价值
    int dp[weight.size()][bagweight + 1];
    // 初始化 dp 数组的第一行,即只考虑第一个物品的情况
    for(int i = 0; i <= bagweight; ++i){
        // 如果当前背包容量 i 小于第一个物品的重量
        if(i < weight[0]) {
            // 则无法放入第一个物品,最大价值为 0
            dp[0][i] = 0;
        } else {
            // 若能放入第一个物品,最大价值为第一个物品的价值
            // 这里由于是第一个物品,所以可以简单理解为重复放入第一个物品来填满背包
            dp[0][i] = dp[0][i - weight[0]] + value[0];
        }
    }
    // 外层循环遍历物品,从第二个物品开始(索引为 1)
    // weight 数组的大小代表物品的个数
    for(int i = 1; i < weight.size(); ++i){
        // 内层循环遍历背包的容量,从 0 到最大容量 bagweight
        for(int j = 0; j <= bagweight; ++j){
            // 如果当前背包容量 j 小于第 i 个物品的重量
            if(j < weight[i]) {
                // 则无法放入第 i 个物品,最大价值和前 i - 1 个物品放入容量为 j 的背包的最大价值相同
                dp[i][j] = dp[i - 1][j];
            } else {
                // 若能放入第 i 个物品,需要在两种情况中取最大值
                // 情况 1:不放入第 i 个物品,最大价值为 dp[i - 1][j]
                // 情况 2:放入第 i 个物品,此时最大价值为 dp[i - 1][j - weight[i]] + value[i]
                dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
            }
        }
    }
    // 返回 0 表示程序正常结束
    return 0;
}

image.gif

4、压缩(一维滚动数组)

得出二维数组之后,经观察得到,每一行代码,都是由本行,或者上一行推导出来的:

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

image.gif

故:我们可以用叠加的思维,将二维数组 压缩 成一维数组,因不断地叠加,滚动之名也由此而来。

int v[3]={15,20,30};
    int w[3] = {1,3,4};
    int dp[5];
    for(int i=0; i<3; ++i){
        for(int j=w[i]; j<5; ++j){
          dp[j] = max(dp[j],dp[j-w[i]]+v[i]);
        }
    }

image.gif

理论到此结束


大纲:(经典算法题)

1、零钱兑换 II--01背包,过渡,首次接触动规数据越界问题

2、组合总和 Ⅳ--排列问题-完全背包,深度思考遍历背包与容量的顺序问题

3、爬楼梯--排列问题-动态规划

4、零钱兑换--组合问题,巧妙求最小值

5、单词拆分--结合哈希表


1、零钱兑换 II

给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。

请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0

假设每一种面额的硬币有无限个。

题目数据保证结果符合 32 位带符号整数。

    示例 1:

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

    输出:4

    解释:有四种方式可以凑成总金额:

    5=5

    5=2+2+1

    5=2+1+1+1

    5=1+1+1+1+1


    示例 2:

    输入:amount = 3, coins = [2]

    输出:0

    解释:只用面额 2 的硬币不能凑成总金额 3 。


    示例 3:

    输入:amount = 10, coins = [10]

    输出:1


    提示:

    • 1 <= coins.length <= 300
    • 1 <= coins[i] <= 5000
    • coins 中的所有值 互不相同
    • 0 <= amount <= 5000
    class Solution {
    // 本题相当于是(等和子集、粉碎石头、目标和、一和零的降智难度)
    // 这道题目,跟神经病一样,数组非开到最大
    public:
        int change(int amount, vector<int>& coins) {
            vector<unsigned long long> 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]+dp[j-coins[i]];
                }
            }
            return dp[amount];
        }
    };

    image.gif

    2、组合总和 Ⅳ

    给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。

    题目数据保证答案符合 32 位整数范围。

    示例 1:

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

    输出:7

    解释:

    所有可能的组合为:

    (1, 1, 1, 1)

    (1, 1, 2)

    (1, 2, 1)

    (1, 3)

    (2, 1, 1)

    (2, 2)

    (3, 1)

    请注意,顺序不同的序列被视作不同的组合。


    示例 2:

    输入:nums = [9], target = 3

    输出:0


    提示:

    • 1 <= nums.length <= 200
    • 1 <= nums[i] <= 1000
    • nums 中的所有元素 互不相同
    • 1 <= target <= 1000
    class Solution {
        // 理解dp的,遍历顺序,比推导公式难多了!!挑战思维
        // 总是有超出界限的数目,因为指数级增加,(2的多少次方)
    public:
        int combinationSum4(vector<int>& nums, int target) {
            vector<int> dp(target+1);
            dp[0] = 1;
            for(int i=0; i<=target; ++i){
                for(int j=0; j<nums.size(); ++j){ // 叠加,指数爆炸性增长,2的多少次方,所以需要限制dp[i]+dp[i-nums[j]]<INT32_MAX
                    if(i>=nums[j] && dp[i]<INT32_MAX - dp[i-nums[j]]) dp[i] = dp[i] + dp[i-nums[j]];
                }
            }
            return dp[target];
        }
    };

    image.gif

    3、爬楼梯

    题目描述

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

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

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

    输入描述

    输入共一行,包含两个正整数,分别表示n, m

    输出描述

    输出一个整数,表示爬到楼顶的方法数。

    输入示例

    3 2
    image.gif

    输出示例

    3
    image.gif

    提示信息

    数据范围:

    1 <= m < n <= 32;

    当 m = 2,n = 3 时,n = 3 这表示一共有三个台阶,m = 2 代表你每次可以爬一个台阶或者两个台阶。

    此时你有三种方法可以爬到楼顶。

    1. 1 阶 + 1 阶 + 1 阶段
    2. 1 阶 + 2 阶
    3. 2 阶 + 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 i = 0; i<=n; ++i){
            for(int j=1; j<=m; ++j){
                if(i>=j) dp[i] = dp[i-j]+dp[i];
            }
        }
        cout<<dp[n];
        return 0;
    }

    image.gif

    4、零钱兑换

    给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

    计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1

    你可以认为每种硬币的数量是无限的。

    示例 1:

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

    输出:3

    解释:11 = 5 + 5 + 1

    示例 2:

    输入:coins = [2], amount = 3

    输出:-1

    示例 3:

    输入:coins = [1], amount = 0

    输出:0


    提示:

    • 1 <= coins.length <= 12
    • 1 <= coins[i] <= 231 - 1
    • 0 <= amount <= 104
    class Solution {
    public:
        int coinChange(vector<int>& coins, int amount) {
            vector<long long> dp(amount+1,INT32_MAX);
            dp[0] = 0;
            for(int i=0; i<coins.size(); ++i){
                for(int j=coins[i]; j<=amount; ++j){
                    dp[j] = min(dp[j], dp[j-coins[i]]+1); 
                }
            }
            return dp[amount]==INT32_MAX?-1:dp[amount];
        }
    };

    image.gif

    5、单词拆分

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

    注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

    示例 1:

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

    输出: true

    解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。


    示例 2:

    输入: s = "applepenapple", wordDict = ["apple", "pen"]

    输出: true

    解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。

        注意,你可以重复使用字典中的单词。


    示例 3:

    输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]

    输出: false


    提示:

    • 1 <= s.length <= 300
    • 1 <= wordDict.length <= 1000
    • 1 <= wordDict[i].length <= 20
    • swordDict[i] 仅由小写英文字母组成
    • wordDict 中的所有字符串 互不相同
    class Solution {
        // 相当于利用动态规划,卡空档位
    public:
        bool wordBreak(string s, vector<string>& wordDict) {
            unordered_set<string> uset(wordDict.begin(),wordDict.end()); // 存
            vector<bool> dp(s.size()+1,false);
            dp[0] = true;
            for(int i=1; i<=s.size(); ++i){
                for(int j=1; j<=i; ++j){
                    string str = s.substr(i-j,j);
                    if(uset.find(str)!=uset.end() && dp[i-j]) dp[i] = true;
                }
            }
            return dp[s.size()];
        }
    };

    image.gif

    知识点:

    1、遍历顺序:(组合数与排列数

    coins存放硬币数组amount 是要组成的金额数目

    组合数
    for(int i=0; i<coins.size(); i++){ // 先遍历物品
        for(int j=coins[i]; j<=amount; j++){ // 在遍历背包
            dp[j] = dp[j] + dp[j-coins[i]];
        }    
    }

    image.gif

    核心逻辑:每次固定一个物品,然后更新所有能容纳该背包容量的组合数。

    举例分析(coins = [1,5],amount = 6):

    1. 处理物品 1
    • 从 j=1 开始,所有 j>=1 的背包容量都会加上 dp [j-1]。
    • 此时 dp 数组记录的是仅使用物品 1 的组合数(全 1 的序列)。
    1. 处理物品 5
    • 从 j=5 开始,所有 j>=5 的背包容量会加上 dp [j-5]。
    • 此时计算的是在已使用物品 1 的基础上,添加物品 5 的组合。
    • 最终得到的组合是类似 {1,1,1,1,1,1}、{1,1,1,1,5} 等,不会出现 5 在前 1 在后的情况,因为物品是按顺序处理的,每个物品只能在其之后的容量中被添加。
    排列数
    for(int j=0; j<=amount; ++j){
        for(int i=0; i<coins.size(); ++i){
            if(j>=coins[i]) dp[j] += dp[j-coins[i]];
        }    
    }

    image.gif

    核心逻辑:每次固定一个数,尝试将所有物品都放入,从而更新组合数。

    举例分析(coins = [1,5],amount = 6):

    1. 当 j=1 时
    • 遍历物品 1,dp [1] += dp [0](即 1 种方式:{1})。
    1. 当 j=5 时
    • 遍历物品 1,dp [5] += dp [4](此时 dp [4] 可能已包含多个 1 的组合)。
    • 遍历物品 5,dp [5] += dp [0](即 1 种方式:{5})。
    1. 当 j=6 时
    • 遍历物品 1,dp [6] += dp [5](包含 {1,5} 和 {5,1} 吗?)
    • 遍历物品 5,dp [6] += dp [1](即 {5,1})。
    2、多重背包:

    多重背包,其实与01背包非常的相似。

    有N种物品和一个容量为V 的背包。第i种物品最多有Mi件可用,每件耗费的空间是Ci ,价值是Wi 。求解将哪些物品装入背包可使这些物品的耗费的空间 总和不超过背包容量,且价值总和最大。

    所以,往往只需要将重复的物品摊开即可。


    借鉴博客:

    1、算法之动态规划(DP)求解完全背包问题

    2、动态规划---完全背包问题详解

    3、完全背包理论基础-二维DP数组



    目录
    相关文章
    |
    7月前
    |
    机器学习/深度学习 存储 算法
    动态规划算法深度解析:0-1背包问题
    0-1背包问题是经典的组合优化问题,目标是在给定物品重量和价值及背包容量限制下,选取物品使得总价值最大化且每个物品仅能被选一次。该问题通常采用动态规划方法解决,通过构建二维状态表dp[i][j]记录前i个物品在容量j时的最大价值,利用状态转移方程避免重复计算子问题,从而高效求解最优解。
    802 1
    |
    1月前
    |
    存储 人工智能 关系型数据库
    OpenClaw怎么可能没痛点?用RDS插件来释放OpenClaw全部潜力
    OpenClaw插件是深度介入Agent生命周期的扩展机制,提供24个钩子,支持自动注入知识、持久化记忆等被动式干预。相比Skill/Tool,插件可主动在关键节点(如对话开始/结束)执行逻辑,适用于RAG增强、云化记忆等高级场景。
    832 56
    OpenClaw怎么可能没痛点?用RDS插件来释放OpenClaw全部潜力
    |
    18天前
    |
    人工智能 Linux API
    阿里云/本地部署OpenClaw多Bot群内协作指南:一键配置提示词+大模型API完整方案及避坑指南
    OpenClaw真正强大的地方,在于支持多Bot在同一群组内自动协作,通过1个Boss Bot+多个执行Bot的分工模式,实现一句话下发需求、全流程自动执行、自动汇总结果。本文将复杂的多智能体配置完全整理为可直接使用的配置与提示词,同时提供2026年阿里云部署、MacOS/Linux/Windows11本地部署流程,以及阿里云千问大模型API、免费Coding Plan API配置方法,搭配全套代码命令与常见问题解答,让你无需理解复杂配置,即可拥有一支全自动AI协作团队。
    581 4
    |
    20天前
    |
    算法 机器人 测试技术
    动态规划入门详解
    等待了好久好久,今天终于可以对动态规划动手了(☆▽☆)
    146 2
    |
    20天前
    |
    算法
    动态规划-01背包
    本文深入解析动态规划经典问题——01背包及其四大变式:分割等和子集、最后一块石头的重量II、目标和、一和零。从暴力回溯切入,对比O(2ⁿ)与O(N·W)动态规划解法,详解状态定义、递推公式、二维/一维滚动数组优化,并配以清晰代码与图示,助你透彻掌握背包问题核心思想与实战技巧。
    167 1
    |
    20天前
    动态规划之打家劫舍
    最后在此,送坚持到这里的读者一句话。简单题,用来培养方法;难题,用来突破自我;两者结合,方能突破至高;当难题,难得你受不了时,恰恰是因为你没有重视简单题!希望大家有所收获。
    87 1
    |
    1月前
    |
    Arthas 人工智能 Java
    我们做了比你更懂 Java 的 AI-Agent -- Arthas Agent
    Arthas Agent 是基于阿里开源Java诊断工具Arthas的AI智能助手,支持自然语言提问,自动匹配排障技能、生成安全可控命令、循证推进并输出结构化报告,大幅降低线上问题定位门槛。
    879 64
    我们做了比你更懂 Java 的 AI-Agent -- Arthas Agent
    |
    1月前
    |
    人工智能 安全 前端开发
    阿里开源 Team 版 OpenClaw,5分钟完成本地安装
    HiClaw 是 OpenClaw 的升级版,通过引入 Manager Agent 架构和分布式设计,解决了 OpenClaw 在安全性、多任务协作、移动端体验、记忆管理等方面的核心痛点。
    1822 60
    阿里开源 Team 版 OpenClaw,5分钟完成本地安装