该篇文章为了自己回顾基础,也为了下学期为社团讲解做个铺垫,本来想做ppt但是公式真是太折磨人了。
主要是自己太菜了,想一直刷题,发现到后面想不出来,看题解也要看半天,仔细回想觉得是因为经典问题不牢固的原因。
所有解题思路学习来源于acwing。
01背包问题
问题叙述:
有
n
n
n 件物品和一个容量为
v
v
v 的背包,每件物品只能选择一次。
对于第
i
i
i 件物品,体积为
v
[
i
]
v[i]
v[i] ,价值为
w
[
i
]
w[i]
w[i] 。
问不超过背包容量的情况下,拿哪些物品得到的总价值最大。
问题分析:
采用闫式dp分析法,在集合的角度进行分析 (状态表示:
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j] )。
集合:只考虑前
i
i
i 个物品在容量为
j
j
j 的背包下,拿到的最大价值。
属性:Max.
集合划分:如图
得到状态转移方程:
d
p
[
i
]
[
j
]
=
m
a
x
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
−
1
]
[
j
−
v
]
+
w
)
dp[i][j]=max(dp[i-1][j],dp[i-1][j-v]+w)
dp[i][j]=max(dp[i−1][j],dp[i−1][j−v]+w)
完整代码:
#include<bits/stdc++.h>
using namespace std;
int n,m;
int v,w;
int dp[1010][1010];
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>v>>w;
for(int j=0;j<=m;j++){
dp[i][j]=dp[i-1][j];
if(j-v>=0) dp[i][j]=max(dp[i][j],dp[i-1][j-v]+w);
}
}
cout<<dp[n][m]<<endl;
return 0;
}
优化空间:
由上文分析可知,所有的
[
i
]
[i]
[i] 都是由
[
i
−
1
]
[i-1]
[i−1] 转移来的。那么可以用滚动数组代替第一维 or 可以把体积由大到小枚举,这样在判断的时候当前物品的时候,
[
i
]
[i]
[i]中的状态还没有更新,相当于
[
i
−
1
]
[i-1]
[i−1] 。
//滚动数组方法
#include<bits/stdc++.h>
using namespace std;
int n,m;
int v,w;
int dp[2][1010];
int id;
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>v>>w;
id^=1;
for(int j=0;j<=m;j++){
dp[id][j]=dp[id^1][j];
if(j-v>=0) dp[id][j]=max(dp[id][j],dp[id^1][j-v]+w);
}
}
cout<<dp[id][m]<<endl;
return 0;
}
//一维数组写法
#include<bits/stdc++.h>
using namespace std;
int n,m;
int v,w;
int dp[1010];
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>v>>w;
for(int j=m;j>=v;j--){
dp[j]=max(dp[j],dp[j-v]+w);
}
}
cout<<dp[m]<<endl;
return 0;
}
完全背包问题
问题叙述:
有
n
n
n 件物品和一个容量为
v
v
v 的背包,每件物品可以选择无数次。
对于第
i
i
i 件物品,体积为
v
[
i
]
v[i]
v[i] ,价值为
w
[
i
]
w[i]
w[i] 。
问不超过背包容量的情况下,拿哪些物品得到的总价值最大。
问题分析:
采用闫式dp分析法,在集合的角度进行分析 (状态表示:
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j] )。
集合:只考虑前
i
i
i 个物品在容量为
j
j
j 的背包下,拿到的最大价值。
属性:Max.
集合划分:如图
更正:不是 k 种选法,是 k*v 不大于 j 种选法
得到状态转移方程:
d
p
[
i
]
[
j
]
=
m
a
x
{
d
p
[
i
−
1
]
[
j
−
k
∗
v
]
+
k
∗
w
}
dp[i][j]=max\{dp[i-1][j-k*v]+k*w\}
dp[i][j]=max{dp[i−1][j−k∗v]+k∗w}
k
=
0
,
1
,
2...
k=0,1,2...
k=0,1,2...
//三维写法 时间复杂度O(n^3)
#include<bits/stdc++.h>
using namespace std;
int n,m;
int v,w;
int dp[1010][1010];
int main(){
ios::sync_with_stdio(false);
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>v>>w;
for(int j=0;j<=m;j++){
for(int k=0;k*v<=j;k++){
dp[i][j]=max(dp[i][j],dp[i-1][j-k*v]+k*w);
}
}
}
cout<<dp[n][m];
return 0;
}
因为上一个写法的时间复杂度太大,我们对状态转移方程再次进行整理,得到: d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j − v ] + w , d p [ i − 1 ] [ j − 2 ∗ v ] + 2 ∗ w + d p [ i − 1 ] [ j − 3 ∗ v ] + 3 ∗ w . . . ) dp[i][j]=max(dp[i-1][j-v]+w,dp[i-1][j-2*v]+2*w+dp[i-1][j-3*v]+3*w...) dp[i][j]=max(dp[i−1][j−v]+w,dp[i−1][j−2∗v]+2∗w+dp[i−1][j−3∗v]+3∗w...) d p [ i ] [ j − v ] = m a x ( d p [ i − 1 ] [ j − 2 ∗ v ] + w , d p [ i − 1 ] [ j − 3 ∗ v ] + 2 ∗ w . . . ) dp[i][j-v]=max(dp[i-1][j-2*v]+w,dp[i-1][j-3*v]+2*w...) dp[i][j−v]=max(dp[i−1][j−2∗v]+w,dp[i−1][j−3∗v]+2∗w...)由两公式得到: d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − v ] + w ) dp[i][j]=max(dp[i-1][j],dp[i][j-v]+w) dp[i][j]=max(dp[i−1][j],dp[i][j−v]+w)解释: d p [ i ] [ j ] dp[i][j] dp[i][j] 的最大值为不选或者选一个的最大值,因为选一个的最大值,就是选一个或者选两个的最大值…
#include<bits/stdc++.h>
using namespace std;
int n,m;
int v,w;
int dp[1010][1010];
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>v>>w;
for(int j=0;j<=m;j++){
dp[i][j]=dp[i-1][j];
if(j-v>=0) dp[i][j]=max(dp[i-1][j],dp[i][j-v]+w);
}
}
cout<<dp[n][m];
return 0;
}
最后再优化一下空间:
对于为什么dp[j]可以表示
[
i
−
1
]
[i-1]
[i−1]的状态,可以思考一下,从小到大枚举的意义。
#include<bits/stdc++.h>
using namespace std;
int n,m;
int v,w;
int dp[1010];
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>v>>w;
for(int j=v;j<=m;j++){
dp[j]=max(dp[j],dp[j-v]+w);
}
}
cout<<dp[m];
return 0;
}
多重背包问题
问题叙述:
有
n
n
n 件物品和一个容量为
v
v
v 的背包。
对于第
i
i
i 件物品,可以选择的次数为
s
[
i
]
s[i]
s[i] ,体积为
v
[
i
]
v[i]
v[i] ,价值为
w
[
i
]
w[i]
w[i] 。
问不超过背包容量的情况下,拿哪些物品和该多少数量得到的总价值最大。
问题分析:
采用闫式dp分析法,在集合的角度进行分析 (状态表示:
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j] )。
集合:只考虑前
i
i
i 个物品在容量为
j
j
j 的背包下,拿到的最大价值。
属性:Max.
集合划分:如图
状态转移方程:
d
p
[
i
]
[
j
]
=
m
a
x
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
−
1
]
[
j
−
k
∗
v
]
+
k
∗
w
)
dp[i][j]=max(dp[i-1][j],dp[i-1][j-k*v]+k*w)
dp[i][j]=max(dp[i−1][j],dp[i−1][j−k∗v]+k∗w)
朴素方法
当数据范围特别小时(满足n^3):
#include<bits/stdc++.h>
using namespace std;
const int maxn=105;
int n,m;
int s,v,w;
int dp[maxn][maxn];
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>v>>w>>s;
for(int j=0;j<=m;j++){
dp[i][j]=dp[i-1][j];
for(int k=0;k<=s;k++){
if(j-k*v>=0) dp[i][j]=max(dp[i][j],dp[i-1][j-k*v]+k*w);
}
}
}
cout<<dp[n][m]<<endl;
return 0;
}
二进制优化
可以先看一下这篇博客
二进制优化顾名思义是把原本需要遍历完成的事情通过二进制的方式完成。
因为1,2,4能表示[1,7]中的所有数字,所以我们把每种物品的s份拆成二进制去表示。
因为背包容量(第二维维护的东西)是枚举的,所以我们用二进制表示的状态和十进制表示的状态是一致的。
比如我的5是选1不选3选4的结果,完全可以用二进制来表示得到,谁选谁不选,又大大减少了枚举的次数,降低了时间。
#include<bits/stdc++.h>
using namespace std;
#define v first
#define w second
int n,m;
int dp[2010];
int v,w,s;
vector<pair<int,int> >ve;
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>v>>w>>s;
for(int j=1;j<=s;j<<=1){
s-=j;//为了s能全用j表示的最小j到多少
ve.push_back({j*v,j*w});
}
if(s) ve.push_back({s*v,s*w});
}
for(auto t:ve){
for(int j=m;j>=t.v;j--){
dp[j]=max(dp[j],dp[j-t.v]+t.w);
}
}
cout<<dp[m];
return 0;
}
优先队列优化
解释写在注释中了,至于怎么想到,为什么这么想,回头再补吧。
#include<bits/stdc++.h>
using namespace std;
const int maxn=20010;
int n,m;
int v,w,s;
int dp[2][maxn];// 滚动数组 dp[][i] 代表体积为i的最大价值
int vol[maxn];// 单调队列模拟 代表体积(容量)维护:s+1长度内dp[][j-k*w]的最大值
int id;
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>v>>w>>s;
id^=1;
for(int j=0;j<v;j++){
int l=0,r=-1;
for(int t=j;t<=m;t+=v){
//以下注释中的j为背包容量,但程序中的j为余数 代表意义不同
//以下注释中的t为拿到物品需要消耗的容量,但程序中的t为背包容量 代表意义不同
//程序中 t是真正的容量 vol[]队列中存的是真正的$我在这个区间中拿最大价值的物品要花费的容量$
//t=j-k*v k为选择该物品的个数
//k=(j-t)/v
//dp[i][t]=max{dp[i-1][t],dp[i-1][j-k*v]+k*w} --> dp[i-1][t]+(j-t)/v*w
//首先计算长度 若超长(>s) 把最小的出队
if(l<=r&&(t-vol[l])/v>s) l++;
//找到当前容量t的最大的价值
if(l<=r) dp[id][t]=max(dp[id^1][t],dp[id^1][vol[l]]+(t-vol[l])/v*w);
//若当前的放入价值大于队列中最右端的价值
//为了维护队列的单调性 r--
//最后将该价值所对应的“容量”放入队列中
while(l<=r&&dp[id^1][vol[r]]-(vol[r]-j)/v*w <= dp[id^1][t]-(t-j)/v*w) r--;
vol[++r]=t;
}
}
}
cout<<dp[id][m];
return 0;
}
分组背包问题
问题叙述:
有
n
n
n 件物品和一个容量为
v
v
v 的背包。
每件物品,体积为
v
[
i
]
[
j
]
v[i][j]
v[i][j] ,价值为
w
[
i
]
[
j
]
w[i][j]
w[i][j] ,其中
i
i
i 是组号,
j
j
j 是组内编号。
问不超过背包容量的情况下,拿哪些物品和该多少数量得到的总价值最大。
- 每组数据第一行有一个整数 s [ i ] s[i] s[i],表示第 i i i 个物品组的物品数量;
- 每组数据接下来有 s [ i ] s[i] s[i] 行,每行有两个整数 v [ i ] [ j ] v[i][j] v[i][j] , w [ i ] [ j ] w[i][j] w[i][j] 用空格隔开,分别表示第 i i i 个物品组的第 j j j 个物品的体积和价值;
问题分析:
采用闫式dp分析法,在集合的角度进行分析 (状态表示:
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j] )。
集合:只考虑前
i
i
i 个物品,在前
k
k
k 个数中选,在容量不大于
j
j
j 的背包下,拿到的最大价值。
属性:Max.
集合划分:如图
得到状态转移方程:
d
p
[
i
]
[
j
]
=
m
a
x
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
−
1
]
[
j
−
v
[
i
]
[
k
]
]
+
w
[
i
]
[
k
]
)
dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i][k]]+w[i][k])
dp[i][j]=max(dp[i−1][j],dp[i−1][j−v[i][k]]+w[i][k])其中
k
k
k 代表每个组内的决策,即选第k个物品。
思路: 把分组背包看成是01背包的一个变型,可以采用01背包的思想,每个组中的最大值为
d
p
[
j
]
,
d
p
[
j
−
v
[
1
]
]
+
w
[
1
]
,
d
p
[
j
−
v
[
2
]
+
w
[
2
]
,
.
.
.
,
d
p
[
j
−
v
[
s
]
]
+
w
[
s
]
dp[j],dp[j-v[1]]+w[1],dp[j-v[2]+w[2],...,dp[j-v[s]]+w[s]
dp[j],dp[j−v[1]]+w[1],dp[j−v[2]+w[2],...,dp[j−v[s]]+w[s] 中的一个。按照是否选择,将最大价值维护出来即可。
#include<bits/stdc++.h>
using namespace std;
const int maxn=105;
int n,m;
int v[maxn][maxn],w[maxn][maxn];
int s[maxn];
int dp[maxn][maxn];
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>s[i];
for(int j=1;j<=s[i];j++){
cin>>v[i][j]>>w[i][j];
}
}
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
dp[i][j]=dp[i-1][j];
for(int k=1;k<=s[i];k++){
if(j>=v[i][k]) dp[i][j]=max(dp[i][j],dp[i-1][j-v[i][k]]+w[i][k]);
}
}
}
cout<<dp[n][m]<<endl;
return 0;
}
优化一下空间
#include<bits/stdc++.h>
using namespace std;
const int maxn=105;
int n,m;
int v[maxn],w[maxn];
int s;
int dp[maxn];
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>s;
for(int j=1;j<=s;j++){
cin>>v[j]>>w[j];
}
for(int j=m;j>=0;j--){
for(int k=1;k<=s;k++){
if(j>=v[k]) dp[j]=max(dp[j],dp[j-v[k]]+w[k]);
}
}
}
cout<<dp[m]<<endl;
return 0;
}
有依赖的背包问题
首先用了邻接表把树形存了进去:记录一下根的下标,然后把其余的点自己和自己的父节点相连。
有了记录,我们可以从根节点开始做深搜,因为只有选了根节点的物品,才能选其它物品,不妨以这个思路出发,先用深搜搜到当前 (
u
u
u) 物品,看要不要选,选了之后要回溯回去,看看背包内的容量还够不够装选择 (
u
u
u) 物品前需要选择的物品,dp[u][i]=dp[u][i-v[u]]+w[u];
为必须要选上的物品,若背包容量不够,dp[u][i]=0;
把之前选择的全部清零,换下一条子路径进行判断。
#include<bits/stdc++.h>
using namespace std;
const int maxn=105;
int n,m;
int v[maxn],w[maxn],p;
int head[maxn],edge[maxn],next_edge[maxn],idx;
int dp[maxn][maxn];
void add(int a,int b){
edge[idx]=b;
next_edge[idx]=head[a];
head[a]=idx++;
}
void dfs(int u){
for(int i=head[u];~i;i=next_edge[i]){
int son=edge[i];
dfs(son);
for(int j=m-v[u];j>=0;j--){
for(int k=0;k<=j;k++){
dp[u][j]=max(dp[u][j],dp[u][j-k]+dp[son][k]);
}
}
}
for(int i=m;i>=v[u];i--){
dp[u][i]=dp[u][i-v[u]]+w[u];//必须要加
}
for(int i=0;i<v[u];i++){
dp[u][i]=0;//从叶子到根,装不下就都不能要
}
}
int main(){
memset(head,-1,sizeof(head));
int rt;
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>v[i]>>w[i]>>p;
if(p==-1) rt=i;
else add(p,i);
}
dfs(rt);
cout<<dp[rt][m]<<endl;
return 0;
}