首先最知名的当属dd大牛的背包九讲(点击下载)
DD engi 背包九讲的个人整理
背包不一定要装满,或许会有一定空余,但是此时价值是最大的
一、01背包
特点:对于每个物品只有选和不选
两种状态,每件物品最多用一次
d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − w [ i ] ] + v [ i ] ) dp[i][j]=max(dp[i-1][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])
例题
样例
Input
4 5
1 2
2 4
3 4
4 5
Output
8
代码
所有状态 d p [ 0 − > n ] [ 0 − > c ] dp[0->n][0->c] dp[0−>n][0−>c],如果从0件物品中选,所有价值都是0
初始情况:
d
p
[
0
]
[
0
−
>
c
]
=
0
dp[0][0->c]=0
dp[0][0−>c]=0
#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstring>
using namespace std;
const int maxn=1e4+5;
int v[maxn],w[maxn];//二维动态规划
int dp[maxn][maxn];//表示前i个物品,当前使用重量为j的最大价值
int main()
{
int n,c;
cin>>n>>c;
for(int i=1;i<=n;i++)
cin>>w[i]>>v[i];
for(int i=1;i<=n;i++)
{
for(int j=0;j<=c;j++)//c是容量
{
dp[i][j]=dp[i-1][j];//一定存在,而右边状态不一定存在
if(w[i]<=j)//要判断能不能装下
dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
}
}
// cout<<dp[n][c]<<endl; 可以重量不达到c,也可以不装满所有n个物品,注意题目中的“前”
int ans=0;
for(int i=0;i<=c;i++)
ans=max(ans,dp[n][i]);//找出最大价值
cout<<ans<<endl;
return 0;
}
一维优化
d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − w [ i ] ] + v [ i ] ) dp[i][j]=max(dp[i-1][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])
在求解dp[i][j]
时,只用到了dp[i-1][k]
这一层,所以可以用滚动数组
来做。另外对于j
来说,j≤j
,j-w[i]≤j
,都分布在j
的一侧,所以可以该成用一维数组来算。
从大到小去枚举j
d p [ j ] = m a x ( d p [ j ] , d p [ j − w [ i ] ] + v [ i ] ) dp[j]=max(dp[j],dp[j-w[i]]+v[i]) dp[j]=max(dp[j],dp[j−w[i]]+v[i])
核心优化过程
for(int i=1;i<=n;i++)//遍历所有物品
//for(int j=w[i];j<=c;j++)
for(int j=c;j>=w[i];j--)
{
//dp[j]=dp[j];//恒等式 直接去掉
//if(w[i]<=j)//改成直接从w[i]开始
//dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);//如果直接删掉第一维,会出现一些问题
//<=>dp[i][j]=max(dp[i][j],dp[i][j-w[i]]+v[i]);
//j-w[i]<j,所以在第i层会是dp[j-w[i]]+v[i]先被算了,实际会是dp[i][j-w[i]]
//这样不对,应该要是dp[i-1],所以我们应该从大到小枚举
//这样使得上式在用dp[j-w[i]]的时候还是i-1层的
}
完整代码
#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstring>
using namespace std;
const int maxn=1e3+5;
int v[maxn],w[maxn];//二维动态规划
int dp[maxn];//当前使用重量为j的最大价值
//注意:初始化很关键
//这里所有的位置都被初始化为0,所以dp[i]的含义变成了重量小于等于i时的最大价值
//k<m时
//dp[k]=max_v;
//dp[0]=0 --> f[w[0]]=v[0] -->...
//dp[m-k]=0 --> f[m-k+w[0]]=v[0] -->... 相比较而言 不过是多了一个偏移量m-k
//所以不用去枚举所有质量啦,结果就是dp[c]
//而如果 最开始初始化成 dp[0]=0,dp[其他]=-inf
//则所有状态都将由dp[0][0]转移而来,最后就要for循环去找最大的dp[i]
//若题意变成了 重量恰好是c时的最大价值
//则初始化dp[0]=0,dp[i]=-inf, 最终结果就是dp[c]
int main()
{
int n,c;
cin>>n>>c;
for(int i=1;i<=n;i++)
cin>>w[i]>>v[i];
for(int i=1;i<=n;i++)
for(int j=c;j>=w[i];j--)//换一下 从大到小枚举,w[i]>0 则j-w[i]一定是没有被算过的,保证j-w[i]是i-1的 而不是i的
{
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}
// cout<<dp[n][c]<<endl; 可以重量不达到c,也可以不装满所有n个物品,注意题目中的“前”
//int ans=0;
//for(int i=0;i<=c;i++)
//ans=max(ans,dp[i]);//找出最大价值
cout<<dp[c]<<endl;
return 0;
}
二、完全背包
特点:这些物品都可以被选择无穷多次
<=>该物品有无穷多个
从小到大去枚举j
题面
样例
Input
4 5
1 2
2 4
3 4
4 5
Output
10
朴素做法
直接写时间复杂度会很高,最坏情况
O
(
n
∗
w
2
)
O(n*w^2)
O(n∗w2) ->
109 370ms
for(int i=1;i<=n;i++)
for(int j=0;j<=m;j++)
for(int k=0;k*w[i]<=j;k++)
dp[i][j]=max(dp[i][j],dp[i-1][j-k*w[i]]+k*v[i]);
cout<<dp[n][m]<<endl;
64ms
for(int i=1;i<=n;i++)
for(int j=0;j<=m;j++)
{
dp[i][j]=dp[i-1][j];
if(w[i]<=j)
dp[i][j]=max(dp[i-1][j],dp[i][j-w[i]]+v[i]);
}
cout<<dp[n][m]<<endl;
优化
需要注意的是,这里从小到大枚举
,是为了让dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
中的dp[j-w[i]]+v[i]
是i-1
层的。
从小到大去枚举j,保持 dp[j-w[i]] 是第 i 层的
d
p
[
j
]
=
m
a
x
(
d
p
[
j
]
,
d
p
[
j
−
w
[
i
]
]
+
v
[
i
]
)
dp[j]=max(dp[j],dp[j-w[i]]+v[i])
dp[j]=max(dp[j],dp[j−w[i]]+v[i])
40ms
#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstring>
using namespace std;
const int maxn=1e3+5;
int v[maxn],w[maxn];//二维动态规划
int dp[maxn];//当前使用重量为i的最大价值
//仍然可以从二维优化到一维,所以我们直接用一维做就好了
//与01背包的区别在于,每件物品可以用无限次 这个可以用dfs?
//把所有值都初始化成了0
int main()
{
int n,c;
cin>>n>>c;
for(int i=1;i<=n;i++)
cin>>w[i]>>v[i];
for(int i=1;i<=n;i++)
{
for(int j=w[i];j<=c;j++)//换回来,就可以表示可以取无限多次,就变成了完全背包
{
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
//从小到大的话,dp[j-w[i]]其实就已经被算过了, 其中可能已经包括了若干个第i个物品了
//(所有比j小的都被算过了,注意这里的dp[j]=...dp[j-w[i]])
//所以一定可以枚举到最优解
}
/*通过数学归纳法证明
1.假设考虑前i-1个物品之后,所有的dp[j]都是正确的
2.现在要证明,考虑前i个物品后,所有的dp[j]也都是正确的
对于某个j而言,如果最优解中包含k个v[i]
从小到大枚举时,一定可以枚举到dp[j-k*w[i]] 我们会用dp[j-k*w[i]-w[i]]+v[i]来更新它
// --> dp[j-(k-1)*w[i]-w[i]]+v[i]=dp[j+"w[i]"-k*w[i]]+v[i] 包含一个i物品
// --> dp[j-(k-2)*w[i]-w[i]]+v[i] 包含两个i物品
// ......
// --> dp[j-w[i]]+v[i] ==dp[j-(k-(k-1))*w[i]] 计算过只包含k-1个i物品这种状态
// --> dp[j] 就包含了k个i物品 -->找到最优解
*/
/*
for(int j=c;j>=w[i],j--)
for(int k=0;k*w[i]<=j;k++)
dp[j]=max(dp[j],dp[j-k*w[i]]+k*v[i]);//可以选无穷多个
//dp[j-k*w[i]] 这个里面是一定没有选第i个物品的 是i-1的
//自己列一下01背包的动态规划二维表也许更好理解?
*/
}
// cout<<dp[n][c]<<endl; 可以重量不达到c,也可以不装满所有n个物品,注意题目中的“前”
//int ans=0;
//for(int i=0;i<=c;i++)
//ans=max(ans,dp[i]);//找出最大价值
cout<<dp[c]<<endl;
return 0;
}
三、多重背包问题
特点:每种物品有个数限制
,是01背包的扩展
状态划分:枚举每种物品选几个
完全背包,受背包容量限制
,最多到dp[i-1][j-k*w[i]]+(k-1)*v[i]
, 所以dp[i][j-w[i]] = max(…)
最多有k项。
而上述多重背包,是受物品个数限制
,最多可以到dp[i-1][j-(s+1)*w[i]]+s*v[i]
导致会多出一项, 所以dp[i][j-w[i]]=max(…)
最多有s+1项
完全背包是求前缀的最值,而多重背包是求滑动窗口的最值
题面
样例
Input
4 5
1 2 3
2 4 1
3 4 3
4 5 2
Output
10
0≤n,c≤100 0≤w,v,s≤100
通过输入规模可以判断复杂度 可用三次方
暴力解法: O(ncs)
二维
for(int i=1;i<=n;i++)
for(int j=0;j<=m;j++)
for(int k=0;k<=s[i]&&k*w[i]<=j;k++)
dp[i][j]=max(dp[i][j],dp[i-1][j-k*w[i]]+k*v[i]);
//max对每一轮的dp[i][j]取最大值
/* 01背包的扩展
做状态转移的时候多加一重循环即可
dp[j]=max(dp[j],dp[j-w[i]]+v[i] 选一个,dp[j-2*w[i]]+2*v[i] 选两个 ...);
关于初始化,还是和之前一样
1.dp[i]=0
ans=dp[c]
2.dp[0]=0,dp[i]=-inf
ans=max{dp[0....c]}
*/
#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstring>
using namespace std;
const int maxn=1e3+5;
int v[maxn],w[maxn],s[maxn];//二维动态规划
int dp[maxn];//当前使用重量为i的最大价值
int main()
{
int n,c;
cin>>n>>c;
for(int i=1;i<=n;i++)
cin>>w[i]>>v[i]>>s[i];
for(int i=1;i<=n;i++)
{
for(int j=c;j>=w[i];j--)
{
for(int k=1;k<=s[i]&&k*w[i]<=j;k++)//注意这里k从1开始噢,如果是0就没啥意思了诶,直接dp[j]=dp[j] 注意它后面与的条件,进行一个优化
dp[j]=max(dp[j],dp[j-k*w[i]]+k*v[i]);//虽然从0开始也能ac 唔...
}
}
cout<<dp[c]<<endl;
return 0;
}
多重背包的二进制优化
数据范围扩大 0≤n≤1000,0≤c≤2000,w,v,s≤2000
暴力做的话,计算次数可能为1000*2000*2000=4*10^9,会超时。
二进制优化:复杂度O(nclogs)
分组存储
#include <iostream>
#include <cmath>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
const int maxn=2e4+5;//1000*log2000 ~ 12000
const int mod=1e9+7;
int dp[2010];
int w[maxn],v[maxn];
int main()
{
int n,c;
cin>>n>>c;
int k=0;
for(int i=1;i<=n;i++)
{
int ww,vv,s;
cin>>ww>>vv>>s;
int t=1;//从1开始依次打包成1,2,4,8...
while(t<=s)
{
w[k]=ww*t;
v[k]=vv*t;
s-=t;
t<<=1;
k++;
}
if(s>0)
{
w[k]=ww*s;
v[k]=vv*s;
k++;
}
}
for(int i=0;i<k;i++)
for(int j=c;j>=w[i];j--)
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
cout<<dp[c]<<endl;
return 0;
}
结构体实现
/*
要把一个多重背包变成01背包,怎么做? 把s个物品拆开嘛,那不就是单个单个的了?-->01背包
最多会有1000*2000=2*10^6个物品,复杂度很高噢2*10^6 * 2000 =4*10^9 也会超时啦
所以我们要用二进制拆法
eg:7 最少选多少个数,能够把<7的所有数表示出来,每个数可以选也可以不选
1 1 1 1 1 1 1 (7个1)
至少要三个数 因为:每个数两种选法 ,log2 (8)=3 所以下界一定是3
1 2 4 (2^0 2^1 2^2)
给定任意一个数s,问最少需要多少个数能够把<s的所有数表示出来 每个数有选和不选两种状态
log2 (s) 上取整
but s=10怎么办呢 不能1 2 4 8 会超过10的 (11 12 13 14 15都是不能选的,因为s是物品的上限个数)
1 2 4 3(s-1-2-4) -->每个数分成的是log2 (s)份
--> c=1000*log(2000)个物品 复杂度 =1000*11 *2000 =2*10^7 刚刚好
*/
#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <vector>
using namespace std;
const int maxn=2e3+5;
//int v[maxn],w[maxn],s[maxn];//二维动态规划
int dp[maxn];//当前使用重量为i的最大价值
struct node{
int w,v;
};
int main()
{
vector<node> goods;
int n,c;
cin>>n>>c;
for(int i=1;i<=n;i++)
{
int w,v,s;
cin>>w>>v>>s;
for(int k=1;k<=s;k*=2)
{
s-=k;//开始拆分咯
goods.push_back({w*k,v*k});//k份k份看成一个整体 注意这样一整个整体放进去的写法
}
if(s>0)
goods.push_back({w*s,v*s});
}
for(auto g:goods)//auto自动类型 遍历goods
{
for(int j=c;j>=g.w;j--)
{
dp[j]=max(dp[j],dp[j-g.w]+g.v);
}
}
cout<<dp[c]<<endl;
return 0;
}
多重背包的单调队列优化
题面
数据范围再扩大 0≤n≤1000,0≤c≤20000 w,v,s≤20000
硬算 O ( n c s ) = 1 0 3 ∗ 4 ∗ 1 0 8 = 4 ∗ 1 0 1 1 O(ncs)=10^3*4*10^8=4*10^11 O(ncs)=103∗4∗108=4∗1011 必炸
二进制优化的复杂度: 1000 ∗ l o g 2 ( 20000 ) ∗ 20000 = 1000 ∗ 15 ∗ 20000 = 3 ∗ 1 0 8 1000*log_2 (20000)*20000=1000*15*20000=3*10^8 1000∗log2(20000)∗20000=1000∗15∗20000=3∗108 会炸
楼天城的《男人八题》什么时候去写一下? 噢还有网络流14题
代码
因为要求的是滑动窗口最大值,所以可以用单调队列来优化。
for(int i=1;i<=n;i++)
{
for(int j=c;j>=w[i];j--)
{
for(int k=1;k<=s[i]&&k*w[i]<=j;k++)//观察它这个决策部分,看能不能用什么数据结构去优化掉
dp[j]=max(dp[j],dp[j-k*w[i]]+k*v[i]);
}
}
/*
把枚举的质量归个类 以%w不同余进行归类,一共会有w类,两两之间相互独立
*/
四、分组背包问题
特点:物品有n组,每组中有若干个物品,每组只能选一个
例题
输入
3 5
2
1 2
2 4
1
3 4
1
4 5
输出
8
代码
#include <iostream>
#include <cmath>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
const int maxn=105;
const int mod=1e9+7;
int dp[2010];
int w[maxn][maxn],v[maxn][maxn],s[maxn];
int main()
{
int n,c;
cin>>n>>c;
for(int i=1;i<=n;i++)
{
cin>>s[i];
for(int j=0;j<s[i];j++)
cin>>w[i][j]>>v[i][j];
}
for(int i=1;i<=n;i++)
for(int j=c;j>=0;j--)
for(int k=0;k<s[i];k++)
if(w[i][k]<=j)
dp[j]=max(dp[j],dp[j-w[i][k]]+v[i][k]);
cout<<dp[c]<<endl;
return 0;
}
五、二维费用的背包问题
六、混合背包问题
七、有依赖的背包问题
本篇致谢yxc老师的背包九讲专题,获益匪浅。