疯狂的背包问题(4) - 完全背包问题

GitHub跳转原题关系图返回列表

使用完全背包DP,dp[c]表示容量c时的最大总价值,容量正序枚举支持每件物品无限次使用。

OJ: luogu

题目 ID: U661988

难度:入门

标签:动态规划完全背包背包

创建: 2026-08-08 23:11

更新: 2026-08-09 00:41

后置题目
roj 2041usaco-3.1.2 总分A 教的容量正序枚举=物品可重复选,正是 B 完全背包解法中 range(minutes, limit+1) 这一步的直接复用,B 只是在其上叠加被支配种类剪枝roj 2033usaco-2.3.4 货币系统B 把 A 教的「一维数组容量/金额正序枚举,让 dp[c-v] 已含本物品贡献」这一完全背包关键步骤,直接用到计数版上(main.py 的 for amount in range(coin, n+1): ways[amount] += ways[amount-coin]),只在 A 的空间压缩基础上叠加二维递推降维与组合计数(外层按面值分组)的额外流程。roj 1272【例9.16】分组背包B 的分组背包直接复用 A 讲的「容量倒序读不到当前物品」这一具体判定:main.cpp 里 cap 从 V 递减,使 dp[cap-W_j] 只含上一组的旧值,从而同组物品不在同一格叠加、天然满足每组最多选一件;A 只用倒序说明 0/1 与完全背包的差异,B 在此基础上叠加「外层枚举组、内层枚举组内物品」的三层流程,难度从入门升到普及-。luogu U661992疯狂的背包问题(8) - 多重背包问题 IA 讲清的「倒序保证每件物品只用一次」被 B 直接当作展平后 01 物品的枚举方向依据,B 只是在同一 dp[c] 转移上内层叠加件数枚举luogu U663710疯狂的背包问题(6) - 完全背包问题(计数组合问题)B 的 main.cpp(第 21-22 行 for(c=v;c<=V;++c))直接复用 A 教的『容量正序枚举使 dp[c-v] 已含当前物品』这一无限件机制,只把取 max 的价值转移换成 dp[c]+=dp[c-v] 的方案数转移luogu P2904[USACO08MAR] River Crossing SB 把 A 教的完全背包容量正序枚举原样用在 `for (int j = i; j <= n; j++)` 上,从而让「一趟运 k 头牛」可重复使用,再叠加 trip_cost 预处理与末尾减 Mluogu P1679神奇的四次方数B 直接沿用 A 教的一维完全背包骨架(外层枚举物品、内层容量正序枚举以允许同一对象重复使用),只把目标从 max 价值改成 min 项数并把初始化改为 INF,B 的 main.cpp 也确为同型一维正序 dp。luogu P2725[USACO3.1] 邮票 StampsA 教的『容量正序枚举让同一物品无限复用』被 B 的原样搬进 main.cpp 的 for (x = v; x <= limit; x++) dp[x] = min(dp[x], dp[x-v]+1),只把 max/+价值 换成 min/+1 张数;B 在此完全背包模板之上再叠加 limit = k*max_a 与扫描最长连续可达前缀这一额外流程。luogu P1832A+B Problem(再升级)B 把 A 教的「容量正序枚举即允许物品无限次使用」当成完全背包内层骨架,直接套在筛出的素数集合上(main.cpp 内层 for(j=p;j<=n;++j) 正序),只把 A 的 max 转移换成 dp[j]+=dp[j-p] 的计数转移,并叠加埃氏筛与组合计数(外层物品、内层容量的顺序论证)这一层。luogu P2834纸币问题 3A 讲透的完全背包正序枚举理由(dp[c-v] 已含当前物品贡献)正是 B 内层金额 j 正序的那一步,B 只是把 max 求和改成计数取模,并新增先枚举纸币以合并支付顺序luogu P2840纸币问题 2B 直接复用 A 教的『容量正序枚举 ⇒ dp 前驱已含当前物品 ⇒ 可无限次使用』这一判定与一维 dp[容量] 状态设计:main.cpp 内层 dp[j]=(dp[j]+dp[j-a[i]])%MOD 且 j 正序,把 A 的物品换成一维被无限张使用的纸币,只把 max 价值换成方案数累加;B 在此完全背包骨架之上再叠加取模与『先金额后纸币』的排列计数顺序辨析,故 A 是它的入门模板。luogu U663733疯狂的背包问题(7) - 完全背包问题(计数排列问题)B 的 main.cpp(for c=1..V 内层 for v in a 的 dp[c]=(dp[c]+dp[c-v])%MOD)直接复用 A 教的关键观察『容量正序推进时 dp[c-v] 可能已包含当前物品的贡献』,从而让同一体积可被无限次追加;B 只是把外层换成容量、把取 max 换成 +1 计数取模,额外叠加的台阶是『枚举顺序决定排列/组合』这一 A 未涉及的区分(以及恰好装满的 dp[0]=1 初值),难度从 入门 升到 普及-,方向正确。

题意

N 种物品,每种可以选任意多件。第 i 种体积 v_i,价值 w_i。背包容量 V。挑选物品使总体积不超过 V 且总价值最大,输出最大价值。

思路

一句话本质:每件物品可选任意次,容量有限制,求最大总价值。

brute.py 的写法为什么不可行?

brute.py 用 dfs 枚举每种物品选 0 到 max_k 件,递归到下一件。每种物品的选择数量不确定,搜索树巨大。N=1000 时指数级分支无法承受。

和 01 背包(每件只能选一次)的本质区别在哪里?

01 背包中,每件物品要么选要么不选,是一次性决策。完全背包中,同一件物品可以反复选——这意味着处理物品 i 时,从 dp[c - v_i] 转移过来后,可能 dp[c - v_i] 本身已经包含了物品 i 的贡献(即已经选过物品 i 了),再转移一次就等于又选了一次物品 i。

为什么把容量枚举方向从倒序改为正序就能实现"无限次使用"?

在 01 背包中,容量倒序保证了 dp[c - v] 是"没选过当前物品"时的值,避免同一物品被重复使用。

而在完全背包中,我们希望允许重复使用。容量正序时,先处理小的 c,dp[c] 可能已经包含物品 i 的贡献;后面更大的 c’ 再从 dp[c’ - v] 转移时,dp[c’ - v] 里可能已经有了物品 i——这就自然实现了"可以再选一次物品 i"。

状态定义和转移是什么?

定义 dp[c] 表示占用容量 c 时能获得的最大价值。

初始 dp[0] = 0。

对于每种物品 (v, w),容量 c 从 v 到 V 正序:

text
dp[c] = max(dp[c], dp[c - v] + w)

代码

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-08-08 23:11
 * update_at: 2026-08-08 23:11
 */
#include <bits/stdc++.h>
using namespace std;

int main() {
    ios::sync_with_stdio(false); cin.tie(0);
    int N, V;
    cin >> N >> V;
    vector<int> dp(V + 1);
    for (int i = 0; i < N; ++i) {
        int v, w;
        cin >> v >> w;
        for (int c = v; c <= V; ++c)
            dp[c] = max(dp[c], dp[c - v] + w);
    }
    cout << dp[V] << '\n';
    return 0;
}

复杂度

  • 时间:O(N × V),每种物品遍历 V 个容量
  • 空间:O(V),dp 数组大小 V+1

总结

完全背包与 01 背包的唯一代码差异是容量枚举方向:倒序保证每件物品只用一次,正序支持多次使用。本质是 dp[c - v] 是否已经包含了当前物品的贡献——倒序时没包含,正序时可能已包含。