good link:
第一讲 01背包问题
f [ i ][ v ]表示前i个物品,放入容量为v的背包中可以获得的最大价值
状态转移方程:
f[i][v] = max( f[i-1][v], f[i-1][v-c[i]] + w[i])
优化空间复杂度:
for i = 1.. N
for v = V..0
f[v] = max(f[v],f[v-c[i]]+w[i])
进一步优化:
for i = 1.. N
for v = V..cost
f[v] = max(f[v],f[v-c[i]]+w[i])
初始化的细节问题
在求解最优解背包问题中有两种问法
①恰好装满背包的最优解
②没有要求必须把背包装满这两种情况的区别就是在初始化的时候做些不同的处理
对于第一种问法,在初始化时除了f[0]=0,其余f[1…V]均设为-∞
对于第二种问法,只要将f[0…V]全设为0
一个常数优化
由于只需要最后f[v]的值,倒推前一个物品,其实只要知道f[v-w[n]]即可。以此类推,对以第j个背包,其实只需要知道到f[v-sum{w[j…n]}]即可,即代码中的
for i=1..N
for v=V..0
可以改成
for i=1..n
bound=max{V-sum{w[i..n]},c[i]}
for v=V..bound
这对于V比较大时是有用的。
第二讲 完全背包问题
例题:https://www.acwing.com/problem/content/3/
该问题与01背包不同的是每种物品都可以无限件可用。
如果按照解01背包时的思路,仍可以得出状态转移方程:
f[i][v]=max{f[i-1][v-kc[i]]+kw[i]|0<=k*c[i]<=v}
01背包一维代码上的改进后的 code :
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N=1100;
int n,m;
int v[N];
int w[N];
int f[N];
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%d%d",&v[i],&w[i]);
}
memset(f,0,sizeof(f));
for(int i=1;i<=n;i++)
{
for(int j=m;j>=v[i];j--)
{
for(int k=0;k*v[i]<=j;k++)
f[j]=max(f[j],f[j-k*v[i]]+k*w[i]);
}
}
printf("%d\n",f[m]);
return 0;
}
另一种 : 二维实现code:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N=1100;
int n,m;
int v[N];
int w[N];
int f[N][N];
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%d%d",&v[i],&w[i]);
}
memset(f,0,sizeof(f));
for(int i=1;i<=n;i++)
{
for(int j=0;j<=m;j++)
{
if(j<v[i])
f[i][j]=f[i-1][j];
else
{
f[i][j]=max(f[i-1][j],f[i][j-v[i]]+w[i]);
}
}
//一维代码 ,和01背包的一维代码不同之处在j遍历的方向变了
/*
for(int j=v[i];j<=m;j++)
f[j]=max(f[j],f[j-v[i]]+w[i]);
*/
}
printf("%d\n",f[n][m]);
return 0;
}
第三讲 多重背包问题
与完全背包不同的地方就是每种背包的个数有个上限。
例题:多重背包问题 I
Code :代码类似完全背包的第一份代码
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N=1100;
int n,m;
int v[N];
int w[N];
int f[N];
int mx[N];
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%d%d%d",&v[i],&w[i],&mx[i]);
}
memset(f,0,sizeof(f));
for(int i=1;i<=n;i++)
{
for(int j=m;j>=v[i];j--)
{
for(int k=1;k<=mx[i]&&k*v[i]<=j;k++)
f[j]=max(f[j],f[j-k*v[i]]+k*w[i]);
}
}
printf("%d\n",f[m]);
return 0;
}
例题二:多重背包问题 II
二进制优化:将每种物品的上限s,将其传化成01背包,就是将s拆成log(s)向上取整个(kv[i],kw[i])的背包,然后这个几个背包直接的组合可以表示0~s的所有个数
s=7, 拆成 k=1,2,4三种背包,这三种背包可以表示0~7的每个数
0= 0
1=1;
2=2
3=1+2
4=4
5=1+4
6=2+4
7=1+2+4
s=13,log2(13)=4(向上取整)拆成k=1,2,4,6(最后一个6,是通过13-1-2-4的来的)
因为1,2,4,可以表示0~7,要使能表示的只有0~13,故最后一种为13-7=6;
拆完之后,就是01背包了
复杂度:n*log2(s)+n*log2(s)*m =O(n*m*log(s))
code:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N=2100;
int n,m;
int v,w,s;
int f[N];
struct Good
{
int v,w;
};
vector<Good> goods;
int main()
{
goods.clear();
scanf("%d%d",&n,&m);
memset(f,0,sizeof(f));
for(int i=1;i<=n;i++)
{
scanf("%d%d%d",&v,&w,&s);
for(int k=1;k<=s;k*=2)
{
s-=k;
goods.push_back({k*v,k*w});
}
if(s>0)
{
goods.push_back({s*v,s*w});
}
}
for(auto good: goods)
{
for(int j=m;j>=good.v;j--)
f[j]=max(f[j],f[j-good.v]+good.w);
}
printf("%d\n",f[m]);
return 0;
}
例题三: 多重背包问题 III
需要利用多重背包的单调队列优化方法
这个优化最开始是从这个代码中来的:(也就是多重背包的第一份代码里的)
for(int i=1;i<=n;i++)
{
scanf("%d%d%d",&v[i],&w[i],&mx[i]);
for(int j=m;j>=v[i];j--)
{
for(int k=1;k<=mx[i]&&k*v[i]<=j;k++)
{
f[j]=max(f[j],f[j-k*v[i]]+k*w[i]);
}
}
}
优化的部分就是内部的两层for循环。此时可以把物品种类数n,背包容量m,某件物品的大小v[i],价值w[i],数量mx[i]都看成已知。
以m=20 ,v[i]=4,s=5,为例(s取5是为了好叙述,若s<5的话,后面单调队列中得考虑窗口的大小)
优化根据递推公式f[j]=max(f[j],f[j-kv[i]]+kw[i]),发现每次f[j]更新是与f[j-k*v[i]]有关,故将m对于v[i]取余,余数相同的划分到同一个集合,共有v[i]个集合。这样f[j]的更新只在与j同余的数的集合中进行。
当遍历到j时,以j=20,和j=19为例,f[20]与f[19]的更新所关联的f,同时f[20]=max(f[20]+0w,f[16]+1w,f[12]+2*w, ···)。写个表
f[ ]= | 20 | 16 | 12 | 8 | 4 | 0 |
---|---|---|---|---|---|---|
+k*w | 0*w | 1*w | 2*w | 3*w | 4*w | 5*w |
f[ ]= | 19 | 15 | 11 | 7 | 3 |
---|---|---|---|---|---|
+k*w | 0*w | 1*w | 2*w | 3*w | 4*w |
当遍历到j=16,和j15时,f[16]和f[15]的更新所关联的f为:
f[ ]= | 16 | 12 | 8 | 4 | 0 |
---|---|---|---|---|---|
+k*w | 0*w | 1*w | 2*w | 3*w | 4*w |
f[ ]= | 15 | 11 | 7 | 3 |
---|---|---|---|---|
+k*w | 0*w | 1*w | 2*w | 3*w |
可以发现,当遍历到j=20时,只会更新f[20], 而f[16],f[12]等都会被更新,也就是说当遍历到j=16时,f[20]的更新不会影响到f[16]。
同理,f[12],f[8],f[4],f[0]的更新,都是相互独立的
不同的地方在,(这里将已经更新过的f[0],f[4],···,都记为F[0],F[4],···,
而原始的用小f表示)
F
[
0
]
=
m
a
x
(
f
[
0
]
+
0
∗
w
)
F
[
4
]
=
m
a
x
(
f
[
0
]
+
1
∗
w
,
f
[
4
]
+
0
∗
w
)
F
[
8
]
=
m
a
x
(
f
[
0
]
+
2
∗
w
,
f
[
4
]
+
1
∗
w
,
f
[
8
]
+
0
∗
w
)
F
[
12
]
=
m
a
x
(
f
[
0
]
+
3
∗
w
,
f
[
4
]
+
2
∗
w
,
f
[
8
]
+
1
∗
w
,
f
[
12
]
+
0
∗
w
)
⋮
F[0]=max(f[0]+0*w )\\ F[4]=max(f[0]+1*w,\;\;f[4]+0*w)\\ F[8]=\;max(f[0]+2*w,\;\;f[4]+1*w,\;\;f[8]+0*w)\\ F[12]=max(f[0]+3*w,\;\;f[4]+2*w,\;\;f[8]+1*w,\;\;f[12]+0*w)\\ \vdots
F[0]=max(f[0]+0∗w)F[4]=max(f[0]+1∗w,f[4]+0∗w)F[8]=max(f[0]+2∗w,f[4]+1∗w,f[8]+0∗w)F[12]=max(f[0]+3∗w,f[4]+2∗w,f[8]+1∗w,f[12]+0∗w)⋮
对于F[12],如何找出中最大的呢,
将
f
[
0
]
+
3
w
,
f
[
4
]
+
2
w
,
f
[
8
]
+
1
w
,
f
[
12
]
+
0
w
,
都
减
去
3
w
得
到
f
[
0
]
−
0
w
,
f
[
4
]
−
1
w
,
f
[
8
]
−
2
w
,
f
[
12
]
−
3
w
可
以
发
现
F
[
8
]
=
(
f
[
0
]
−
0
w
,
f
[
4
]
−
1
w
,
f
[
8
]
−
2
w
)
+
2
w
F
[
4
]
=
(
f
[
0
]
−
0
w
,
f
[
4
]
−
1
w
)
+
w
F
[
0
]
=
(
f
[
0
]
−
0
w
)
+
w
因
此
我
们
只
要
求
出
(
f
[
0
]
−
0
w
,
f
[
4
]
−
1
w
,
f
[
8
]
−
2
w
,
f
[
12
]
−
3
w
)
的
[
1
,
1
]
,
[
1
,
2
]
,
[
1
,
3
]
,
[
1
,
4
]
的
最
大
值
,
就
可
以
求
出
F
[
0
]
,
F
[
4
]
,
F
[
8
]
,
F
[
12
]
了
将f[0]+3w,f[4]+2w,f[8]+1w,f[12]+0w,都减去3w得到\\ f[0]-0w,f[4]-1w,f[8]-2w,f[12]-3w\\ 可以发现\\ F[8]=(f[0]-0w,f[4]-1w,f[8]-2w)+2w\\ F[4]=(f[0]-0w,f[4]-1w)+w\\ F[0]=(f[0]-0w)+w\\ 因此我们只要求出(f[0]-0w,f[4]-1w,f[8]-2w,f[12]-3w)的[1,1],[1,2],[1,3],[1,4]的最大值,\\就可以求出F[0],F[4],F[8],F[12]了
将f[0]+3w,f[4]+2w,f[8]+1w,f[12]+0w,都减去3w得到f[0]−0w,f[4]−1w,f[8]−2w,f[12]−3w可以发现F[8]=(f[0]−0w,f[4]−1w,f[8]−2w)+2wF[4]=(f[0]−0w,f[4]−1w)+wF[0]=(f[0]−0w)+w因此我们只要求出(f[0]−0w,f[4]−1w,f[8]−2w,f[12]−3w)的[1,1],[1,2],[1,3],[1,4]的最大值,就可以求出F[0],F[4],F[8],F[12]了
这里需要利用单调队列,在求F[12]的过程中,并且把F[0],F[4],F[8]都求了
为此,还特意回去学了下单调队列
单调队列就是一个固定长度的窗口在数列上移动,也就3种操作:
遍历到a[i]时,
1.首先,要将超出窗口大小的元素弹去:若队列中的首末位置的元素跨度大于窗口的大小,则弹掉队首元素,直到小于等于窗口的大小
2.若队列为空,入队
3.队列非空,
若 a[i]小于队尾元素,那么就弹掉队尾元素,一直弹到a[i]大于队尾元素
若a[i]大于队尾元素,直接放入队尾
注:等于的情况可以根据题目而定
f
[
0
]
−
0
w
,
f
[
4
]
−
1
w
,
f
[
8
]
−
2
w
,
f
[
12
]
−
3
w
f[0]-0w,f[4]-1w,f[8]-2w,f[12]-3w
f[0]−0w,f[4]−1w,f[8]−2w,f[12]−3w这4个数,分别用编号0,4,8,12表示,
这样就可求出同时F[0],F[4],F[8],F[12]的值就是(0,),(0,4),(0,4,8),(0,4,8,12)的最大值加上对应的k*w
具体实现code:
单调队列中窗口大小是 物品的上限mx[i]*物品体积v[i]
hh,tt分别表示队列的首末位置
q[hh]存最大值
cin >> n >> m;
for(int i=0;i<n;i++)
{
int c,w,s;
cin>>c>>w>> s;
memcpy(g,f,sizeof(f));
for(int j=0;j<c;j++)
{
int hh=0,tt=-1;
for(int k=j;k<=m;k+=c)
{
f[k]=g[k];
if(hh<=tt&&k-s*c>q[hh]) hh++;
if(hh<=tt) f[k]=max(f[k],g[q[hh]]+(k-q[hh])/c*w);
while(hh<=tt&&g[q[tt]]-(q[tt]-j)/c*w<=g[k]-(k-j)/c*w) tt--;
q[++tt]=k;
}
}
}
cout<<f[m]<<endl;
例题:多重背包问题 III
利用单调队列优化
代码:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N=2e4+100;
int n,m;
int q[N],f[N],g[N];
int main()
{
cin >> n >> m;
for(int i=0;i<n;i++)
{
int c,w,s;
cin>>c>>w>> s;
memcpy(g,f,sizeof(f));
for(int j=0;j<c;j++)
{
int hh=0,tt=-1;
for(int k=j;k<=m;k+=c)
{
f[k]=g[k];
if(hh<=tt&&k-q[hh]>s*c) hh++;
if(hh<=tt) f[k]=max( f[k],g[q[hh]]+(k-q[hh])/c*w ) ;
while(hh<=tt&&g[q[tt]]-(q[tt]-j)/c*w<=g[k]-(k-j)/c*w) tt--; // 将比k大的都弹掉,hh->tt是递减的
q[++tt]=k;
}
}
}
cout<<f[m]<<endl;
return 0;
}
第四讲 混合背包问题
就是含01背包,完全背包,多重背包的混合型题目
例题:混合背包问题
code:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N=1005;
struct Thing
{
int kind;
int v,w;
};
vector<Thing> things;
int f[N];
int main()
{
int n,m;
int v,w,s;
cin >> n >> m;
for(int i=0;i<n;i++)
{
scanf("%d%d%d",&v,&w,&s);
if(s<0) things.push_back({-1,v,w});
else if(s==0) things.push_back({0,v,w});
else
{
for(int k=1;k<=s;k<<=1)
{
s-=k;
things.push_back({-1,k*v,k*w});
}
if(s>0)
things.push_back({-1,s*v,s*w});
}
}
for(auto thing: things)
{
if(thing.kind==0)
for(int i=thing.v;i<=m;i++) f[i]=max(f[i],f[i-thing.v]+thing.w);
else if(thing.kind==-1)
{
for(int i = m;i>= thing.v;i--) f[i]=max(f[i], f[i-thing.v]+thing.w);
}
}
cout<<f[m]<<endl;
return 0;
}
第五讲 二维费用背包问题
例题:二维费用的背包问题
在01背包的基础上加上一层循环
code :
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N=105;
int f[N][N]; //体积为i,重量为j的最大价值
int N, V, M, v, m , w;
int main()
{
cin>> N >> V >> M;
for(int i=0;i<N;i++)
{
cin >> v >> m >> w;
for(int j=V;j>=v;j--)
{
for(int k=M;k>=m;k--)
f[j][k]=max(f[j][k],f[j-v][k-m]+w);
}
}
cout<< f[V][M] << endl;
return 0;
}
第六讲 分组背包问题
例题:分组背包问题
就是有n组物品,每组物品中最多只能拿一件物品,类似于01背包,在遍历j=m->0时,对于f[j]的更新,直接遍历每组物品中的s件物品,这样做时为了使f[j]的代表的方案中最多有一件该组的物品;
如果将s放外层循环,j=m->0放内层,f[j]所代表的方案有可能就包含该组2件及2件以上的物品。
code:
#include <bits/stdc++.h>
using namespace std;
const int N=110;
int n, m, s;
int f[N],v[N],w[N];
int main()
{
cin >> n >> m;
for(int i=0;i<n;i++)
{
cin >> s;
for(int j=0;j<s;j++)
{
scanf("%d%d",&v[j],&w[j]);
}
for(int j=m;j>=0;j--)
{
for(int k=0;k<s;k++)
{
if(j>=v[k])
f[j]=max(f[j],f[j-v[k]]+w[k]);
}
}
}
cout<<f[m]<<endl;
return 0;
}
第七讲 有依赖的背包问题
例题:有依赖的背包问题
有依赖的背包,类似于选课的时候,有的课程有先修课,先修课就是这种依赖关系;
这类题目将这种依赖关系化成树的形状,若选结点i的某个儿子,就必须选结点i;
这道题就是树形dp中的某类题目
如果懂树形dp,对于这个的理解应该没什么问题;如果不太了解,理解这个问题也问题不大;
定义:
f[i][j]表示选结点i,体积为j,以i为根的子树的最大价值;
那么对于如何更新节点i(f[i][j])?
利用递归,由下往上更新
依次遍历i的儿子,每个儿子都有选与不选两种状态,而选这种状态包含了(只选这个儿子,选以这个儿子为根的子树),这个儿子的情况与结点i的更新完成后的情况是一样的
code:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N=105;
int h[N], e[N], ne[N], idx ;
int n,m;
int v[N],w[N],f[N][N], p;
void add(int u, int v)
{
e[idx]=v, ne[idx]=h[u], h[u]=idx++;
}
void dfs(int u)
{
for(int i=h[u];~i;i=ne[i])
{
int son=e[i];
dfs(son);
for(int j=m-v[u];j>=0;j--)
for(int k=0;k<=j;k++)
// j放外层,f[u][j]的j由大到小,不会影响更新结果;
//要使f[u][j-k]的值为原先的值,故j-k从大到小,即k从小到大
f[u][j]=max(f[u][j],f[u][j-k]+f[son][k]);
}
for(int i=m;i>=v[u];i--) f[u][i]=f[u][i-v[u]]+w[u];// 由于这里是必选结点u,就不需要去最大值了
for(int i=0;i<v[u];i++) f[u][i]=0;
}
int main()
{
memset(h,-1,sizeof h);
idx=0;
cin >> n >> m;
int root;
for(int i=1;i<=n;i++)
{
cin >> v[i] >> w[i] >> p;
if(p==-1) root=i;
else add(p,i);
}
dfs(root);
cout << f[root][m] <<endl;
return 0;
}
第八讲 背包问题求方案数
例题:背包问题求方案数
code:
#include <bits/stdc++.h>
using namespace std;
#define inf 0x3f3f3f3f
#define ll long long
const int mod = 1e9+7;
const int N=1005;
int n, m;
int v, w;
int f[N],g[N];// f[j]表示空间恰为j的最大值,g[j]表示f[j]对应的方案数
int main()
{
cin >> n >> m;
g[0]=1;//初始为1
f[0]=0;
for(int i=1;i<=m;i++) {
f[i]=-inf;//保证恰好
g[i]=0;
}
for(int i=0;i<n;i++)
{
cin>> v >> w;
for(int j=m;j>=v;j--)
{
int t=max(f[j],f[j-v]+w);
int s=0;// 暂存方案数
if(t==f[j]) s+=g[j];
if(t==f[j-v]+w) s+=g[j-v];
s%=mod;
f[j]=t;
g[j]=s;
}
}
int maxw=0;
for(int i=0;i<=m;i++) maxw=max(maxw,f[i]);//枚举得到最优解
int res=0;
for(int i=0;i<=m;i++)
{
if(maxw==f[i])// 将最优解的方案累加
{
res=(res+g[i])%mod;
}
}
cout<<res<<endl;
return 0;
}
第九讲 背包问题求具体方案
例题:背包问题求具体方案
与逆着打印路径类似
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=1010;
int n, m;
int v[N], w[N],f[N][N];
int main()
{
cin >> n >> m;
for(int i=1;i<=n;i++) cin>> v[i] >> w[i];
for(int i=n;i>=1;i--)
for(int j=0;j<=m;j++)
{
f[i][j]=f[i+1][j];
if(j>=v[i]) f[i][j]=max(f[i][j],f[i+1][j-v[i]]+w[i]);
}
int vol=m;
for(int i=1;i<=n;i++)
if(vol>=v[i]&&f[i][vol]==f[i+1][vol-v[i]]+w[i])
{
cout<< i << ' ';
vol-=v[i];
}
return 0;
}
论述下贪心与动态规划的区别:(略略略略略)
图片来源:https://wenku.baidu.com/view/cacb5d1b2b160b4e767fcfee.html?from=search