动态规划——背包问题

背包问题完全指南

  • 算法

  • 背包问题

  • 01 背包

  • 完全背包

  • 动态规划 categories:

  • 算法入门


背包问题是动态规划最经典的入门题型,也是算法竞赛、刷题中最常考的考点之一。本文从最基础的 01 背包开始,带你一步步搞定完全背包、多重背包、分组背包、二维费用背包等所有常见的背包变种,配套完整可直接套用的 C++ 代码,看完就能上手刷题。


一、什么是背包问题?

背包问题的核心场景非常简单:

你有一个容量有限的背包,还有一堆物品,每个物品有重量、价值等属性,你要选一些物品放进背包,在不超过背包容量的前提下,最大化总价值。

根据物品的不同限制,背包问题衍生出了非常多的变种,我们一个个来看。


二、01 背包:最基础的背包

问题描述

有 N 件物品和一个容量为 V 的背包。第 i 件物品的重量是w[i],价值是v[i]每件物品最多只能用 1 次。 求解:将哪些物品装入背包,可使这些物品的重量总和不超过背包容量,且价值总和最大。

这是所有背包问题的基础,所有其他的背包都是从它衍生出来的。


状态定义

dp[i][j]:前 i 件物品,背包容量为 j 时,能获得的最大价值。

转移方程

对于第 i 件物品,我们有两种选择:

  1. 不选这件物品 :那么状态就是前 i-1 件物品,容量 j 的状态,也就是dp[i][j] = dp[i-1][j]

  2. 选这件物品 :如果背包容量够的话,那么我们就把这件物品放进去,状态就是前 i-1 件物品,容量减去当前物品重量的状态,加上当前物品的价值,也就是dp[i][j] = dp[i-1][j-w[i]] + v[i]

所以最终的转移方程是:

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


空间优化

我们发现,每次计算 i 的状态,只依赖 i-1 的状态,所以我们完全可以把二维的 dp 数组,优化成一维的,用dp[j]直接表示容量为 j 时的最大价值,这样可以把空间复杂度从 O (nV) 降到 O (V)。

但是要注意:必须逆序遍历 j! 因为逆序遍历的话,我们计算 dp [j] 的时候,dp [j-w [i]] 还是上一轮 i-1 的状态,这样就保证了每个物品只会被选一次,不会重复选。


完整代码

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=1010;
ll n,V,w[maxn],v[maxn],dp[maxn];
void sol(){
    cin>>n>>V;
    for(ll i=1;i<=n;i++)cin>>w[i]>>v[i];
    for(ll i=1;i<=n;i++){
        for(ll j=V;j>=w[i];j--){
            dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
        }
    }
    cout<<dp[V]<<endl;
}
int main(){
    sol();
    return 0;
}

三、完全背包:物品可以无限选

问题描述

有 N 件物品和一个容量为 V 的背包。第 i 件物品的重量是w[i],价值是v[i]每件物品可以用无限次。 求解:最大总价值。


核心区别

和 01 背包的区别就是,物品可以选无限次,所以当我们选了这件物品之后,还可以再选它。

所以空间优化之后,我们只需要把逆序遍历改成顺序遍历就可以了! 顺序遍历的话,计算 dp [j] 的时候,dp [j-w [i]] 已经是当前 i 轮的状态了,也就是我们已经选过当前物品了,这样就可以重复选,刚好符合完全背包的要求。


完整代码

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=1010;
ll n,V,w[maxn],v[maxn],dp[maxn];
void sol(){
    cin>>n>>V;
    for(ll i=1;i<=n;i++)cin>>w[i]>>v[i];
    for(ll i=1;i<=n;i++){
    for(ll j=w[i];j<=V;j++){
    dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
    }
}
    cout<<dp[V]<<endl;
}
int main(){
sol();
return 0;
}

四、多重背包:物品有有限个

问题描述

有 N 件物品和一个容量为 V 的背包。第 i 件物品的重量是w[i],价值是v[i]最多有 s[i] 。 求解:最大总价值。


二进制优化

如果我们直接暴力,把每个物品拆成s[i]个 01 背包的物品,时间复杂度会很高,所以我们用二进制优化来优化这个过程。

二进制优化的原理是:任何一个数,都可以拆成 2 的幂次的和,比如s=5,我们可以拆成1,2,2,这样这三个数就可以组合出 0~5 的所有数量,刚好覆盖了选 0 个、1 个、2 个、3 个、4 个、5 个的所有情况。

这样我们就把s[i]个物品,拆成了log s[i]个物品,然后把这些拆出来的物品当成 01 背包的物品,就可以用 01 背包解决了,时间复杂度从 O (nVs) 降到了 O (nV log s)。


完整代码

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=20010;
ll n,V,w[maxn],v[maxn],dp[1010];
void sol(){
    cin>>n>>V;
    ll cnt=0;
    for(ll i=1;i<=n;i++){
        ll wi,vi,si;
        cin>>wi>>vi>>si;
        for(ll j=1;j<=si;j<<=1){
            cnt++;
            w[cnt]=wi*j;
            v[cnt]=vi*j;
            si-=j;
        }
        if(si>0){
            cnt++;
            w[cnt]=wi*si;
            v[cnt]=vi*si;
        }
    }
    for(ll i=1;i<=cnt;i++){
        for(ll j=V;j>=w[i];j--){
            dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
        }
    }
    cout<<dp[V]<<endl;
}
int main(){
    sol();
    return 0;
}

五、分组背包:每组最多选一个

问题描述

有 N 件物品,分成了 k 组,每组里的物品最多选一个,背包容量为 V,求最大总价值。


核心思路

分组背包的核心是:把每一组当成一个整体,遍历每一组,然后对于组里的物品,做 01 背包的操作,保证每组最多选一个。

遍历顺序是:先遍历组,然后逆序遍历容量,最后遍历组内的物品,这样就保证了每组只会选一个物品。


完整代码

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=110;
ll n,V,group[maxn][maxn],w[maxn],v[maxn],dp[maxn];
void sol(){
    cin>>n>>V;
    ll cnt=0;
    for(ll i=1;i<=n;i++){
        ll s;
        cin>>s;
        group[i][0]=s;
        for(ll j=1;j<=s;j++){
            cnt++;
            cin>>w[cnt]>>v[cnt];
            group[i][j]=cnt;
        }
    }
    for(ll i=1;i<=n;i++){
        for(ll j=V;j>=0;j--){
            for(ll k=1;k<=group[i][0];k++){
                ll id=group[i][k];
                if(j>=w[id])dp[j]=max(dp[j],dp[j-w[id]]+v[id]);
            }
        }
    }
    cout<<dp[V]<<endl;
}
int main(){
    sol();
    return 0;
}

六、二维费用背包:两个限制条件

问题描述

背包有两个限制条件,比如不仅有重量的限制,还有体积的限制,物品也有对应的两个重量,求最大总价值。

这就是我们之前做过的 01 字符串选物品的题,刚好是二维费用背包的经典应用。


核心思路

状态定义加一维就可以了,dp[j][k]表示重量为 j,体积为 k 的时候,最大的价值,然后转移和 01 背包一样,逆序遍历两个维度就可以了。


完整代码

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=610;
ll N,m,n,dp[110][110],z[maxn],o[maxn];
void sol(){
    cin>>N>>m>>n;
    for(ll i=1;i<=N;i++){
        string s;
        cin>>s;
        for(char c:s)z[i]+=(c=='0'),o[i]+=(c=='1');
    }
    for(ll i=1;i<=N;i++){
        for(ll j=m;j>=z[i];j--){
            for(ll k=n;k>=o[i];k--){
                dp[j][k]=max(dp[j][k],dp[j-z[i]][k-o[i]]+1);
            }
        }
    }
    cout<<dp[m][n]<<endl;
}
int main(){
    sol();
    return 0;
}

七、背包问题的常见坑点

  1. 遍历顺序:01 背包逆序,完全背包顺序,分组背包先组后物品,这个绝对不能搞反,搞反了答案就错了。

  2. 数据溢出:背包的价值是累加的,很容易超过 int 的范围,所以一定要用 long long,不然会溢出。

  3. 初始化:如果是求最大价值,初始化为 0 就可以,如果是求恰好装满的最小价值,要初始化为无穷大。

  4. 二进制拆分:多重背包的二进制拆分,最后剩下的余数不要忘了加进去。


八、总结

背包问题是动态规划的基础,它的核心思想非常简单:选或者不选,根据物品的限制调整转移的顺序和条件

学会了这五种最常见的背包,你就已经搞定了 99% 的背包问题,不管是 LeetCode 刷题,还是算法竞赛,都能轻松应对。


如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、关注,我会持续更新算法入门的系列文章~

相关推荐
Yingye Zhu(HPXXZYY)1 小时前
洛谷 P15553 [CCPC 2025 哈尔滨站] 液压机
算法
谭欣辰2 小时前
LCS(最长公共子序列)详解
开发语言·c++·算法
m0_629494732 小时前
LeetCode 热题 100-----17.缺失的第一个正数
数据结构·算法·leetcode
Cando学算法2 小时前
鸽笼原理(抽屉原理)
c++·算法·学习方法
Tisfy2 小时前
LeetCode 0796.旋转字符串:暴力模拟
算法·leetcode·题解·模拟·字符串匹配
BlockChain8882 小时前
AI+区块链深度探索:算法与账本的共生时代
人工智能·算法·区块链
生成论实验室2 小时前
《源·觉·知·行·事·物:生成论视域下的统一认知语法》第一章 源:不可言说的生成之源
人工智能·科技·算法·生活·创业创新
2zcode3 小时前
基于低光照增强与轻量型CNN道路实时识别算法研究(UI界面+数据集+训练代码)
人工智能·算法·cnn·低光照增强·自动驾驶技术
小雅痞3 小时前
[Java][Leetcode middle] 209. 长度最小的子数组
java·算法·leetcode