导读:本期聚焦于小伙伴创作的《如何在有限预算下用0/1背包问题最大化收集物品数量》,敬请观看详情。当活动经费被卡死而待选物料单价与效用参差不齐时,直接按喜好下单往往超支。0/1背包模型把每件物品抽象为重量等于花费、价值等于收集数量的背包件,在总花费不越预算的前提下求最大件数。传统动态规划以二维数组记录前i件在容量j下的最优选,时间空间皆为O(nW)。若只关心数量而非异质价值,可把价值统一置1并做容量剪枝,或用滚动数组把空间压到一维。实际编码要注意花费为整数才能离散化容量,浮点预算需先乘倍率转整。下文给出基础实现与空间优化两段可运行代码,并对比贪心为何在0/1约束下失效。

在采购、抽奖或是游戏内资源兑换等场景里,我们常遇到一个现实约束:手里的钱或者积分固定,面前每件东西都有自己的价格和是否值得拿的属性。目标很直接,就是在不能超支的情况下,尽可能多地把东西收入囊中。这类问题在数学上可以规约为0/1背包问题的一个特例,把预算当作背包容量,单件花费当作物品重量,而每件物品对收集数量的贡献固定为1。

如何在有限预算下用0/1背包问题最大化收集物品数量

0/1背包的基础建模与动态规划思路

标准的0/1背包描述如下:有n件物品,第i件花费为cost[i],在只统计件数时价值val[i]可全部取1。背包总容量为预算B。每件物品要么拿要么不拿,不能拆分,也不能重复拿。我们定义dp[i][j]表示考虑前i件物品、使用不超过j预算时能拿到的最大件数。

状态转移时,对于第i件,如果当前预算j小于cost[i],那肯定没法拿,只能沿用之前的结果;如果j大于等于cost[i],则可以选择拿或者不拿,取两者中较大的:不拿就是dp[i-1][j],拿则是dp[i-1][j-cost[i]] + 1。这种递推保证了无后效性,最终答案落在dp[n][B]。由于所有val都是1,其实我们就是在数最多能装几件。

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

int maxItems(vector<int> &cost, int B) {
    int n = cost.size();
    // dp[i][j] 前i件在预算j下最大件数
    vector<vector<int>> dp(n + 1, vector<int>(B + 1, 0));
    for (int i = 1; i <= n; i++) {
        int c = cost[i - 1];
        for (int j = 0; j <= B; j++) {
            if (j < c) {
                dp[i][j] = dp[i - 1][j];
            } else {
                dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - c] + 1);
            }
        }
    }
    return dp[n][B];
}

int main() {
    vector<int> cost = {3, 2, 4, 1, 5};
    int B = 8;
    cout << maxItems(cost, B) << endl; // 输出最多能拿几件
    return 0;
}

上面的代码用二维数组直观表达了递推过程。时间复杂度是O(nB),空间是O(nB)。当预算是整数且范围不大时,这种方法既好写又稳定。但如果预算很大或者物品极多,二维数组会占用过多内存,这就需要后续的优化。

还要注意一个前提:代码里的预算B和cost都应是整数。如果原始数据是浮点金额,比如12.5元,就需要统一乘以10或者100转成整数分,否则无法作为数组下标使用。这也是实际项目里最容易踩的坑。

空间优化:滚动数组压成一维

观察转移方程会发现,dp[i][j]只依赖dp[i-1][*]这一行。因此我们完全可以用一个长度为B+1的一维数组,从后往前更新,覆盖掉旧值而不影响本次计算。关键在于内循环必须逆序遍历预算,避免同一件物品被重复选取,那就变成完全背包了。

一维写法把空间降到O(B),在嵌入式或者大规模运算时优势明显。逻辑上和二维等价,只是把行维度隐式丢弃。下面给出改造后的版本,结构更紧凑。

def max_items(cost, B):
    dp = [0] * (B + 1)
    for c in cost:
        # 逆序更新,防止重复拿
        for j in range(B, c - 1, -1):
            dp[j] = max(dp[j], dp[j - c] + 1)
    return dp[B]

cost = [3, 2, 4, 1, 5]
B = 8
print(max_items(cost, B))

这段Python代码里,外层遍历每件物品,内层从B向下到c。由于j-c一定小于j,而我们是逆序走,dp[j-c]还是上一轮(即前i-1件)的值,完美符合0/1语义。如果反过来正序,就会在某件物品上累加多次。

在空间敏感的场景,比如前端小程序计算促销组合,一维DP几乎是无脑首选。它牺牲了一点可读性,换来了实打实的内存下降,并且运算速度也因缓存局部性变好而略有提升。

为什么贪心策略在这里会失效

有人可能想到:那我按花费从小到大排,依次拿最便宜的不就行了?这其实是部分背包的贪心思路。在0/1约束下,它不一定最优。因为便宜的东西凑一起可能留下无法利用的零碎预算,而稍贵但组合更紧的物品反而能塞满。

举个简单反例:预算为6,物品花费分别是3、3、4。贪心拿两个3刚好满,得2件;但如果物品是3、4、4,贪心拿一个3后剩3拿不了任何4,只拿1件,而最优其实是拿不了两个4但选一个4加别的若没有则仍1件,此处仅说明零碎问题。更极端的,预算5,花费为2、2、3,贪心拿2+2=4得2件,剩1浪费;若存在组合3+2=5也是2件,数量持平但说明贪心不保证探索全部。

策略是否保证最多件数适用条件
动态规划0/1不可拆,花费整数
按价贪心仅部分背包可拆分时最优

表格里对比得很清楚。动态规划穷举了所有子集的可能,自然能拿到理论最大值;贪心省时间却丢了最优性。所以在预算卡死且物品不可拆的业务里,别迷信排序,老老实实DP更稳。

实际工程中的小技巧

如果物品数量极多但预算中等,可以先用花费做一遍过滤,去掉那些单价超过预算的无效项。另外当多件物品花费相同时,它们等价,只需保留一件代表,因为件数贡献都是1,这能缩短n。

在Web端用JavaScript跑这类计算时,建议把预算放大系数和花费预处理放在接口层,前端只收整数数组。如下片段展示了过滤与调用:

function solve(costs, budget) {
    const valid = costs.filter(c => c <= budget);
    const dp = new Array(budget + 1).fill(0);
    for (const c of valid) {
        for (let j = budget; j >= c; j--) {
            dp[j] = Math.max(dp[j], dp[j - c] + 1);
        }
    }
    return dp[budget];
}
// 假设后台给的是元,转成分
const costs = [300, 200, 400, 100, 500].map(x => x / 100);
console.log(solve(costs, 8));

这段代码演示了前端视角的处理:过滤掉超预算项,一维DP出结果。注意map里除以100是为了贴合上文整数化示例,实际中常是乘100。工程上把这些细节封装成纯函数,方便单测和复用。

总结来看,有限预算下最大化收集数量就是0/1背包的件数最大化形态。掌握二维推演、一维压缩以及贪心误区,就能在采购系统、福利发放等模块里写出既正确又高效的代码。

0/1_knapsack动态规划budget_optimization修改时间:2026-08-02 12:06:38

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。