目录
制作不易,点个赞吧!!!
背包问题又叫动态规划选择问题
一般动态规划都可以用闫式dp分析法来解决
dp问题的学习方法(重点):
比赛的时候做出dp问题,一般不是凭空想出来怎么做,而是我们一起做过类似的dp问题,用之前做过的方法来做dp问题。dp问题是一个以经验为基础的章节,需要我们对各种dp类型都有系统的练习。
优化方法(重点):
优化和分析是分开的,优化是在将状态转移方程求出之后对代码的一个等价变形。
01背包问题
背包问题的理论基础重中之重是01背包,一定要理解透!
推导思路
有N件物品和一个最多能装体积为 V 的背包。第i件物品的体积是volume[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
对于背包问题,有好几种写法,一种是使用二维数组,即dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
对于01背包问题的二维图像就是这样
那么对于01背包的分析思路是:
优化方法:
对于该题,在时间上没法优化,只能在空间上进行优化。
先来看看状态转移方程怎么进行优化
f[i][j] = max(f[i-1][j],f[i-1][j-volumw[i]] + value[i])
可以看到,f[i][j]都是从 i-1 转移来的,那么我们就可以将 i 这一维给取消掉。因为我们需要每次从上一个状态转移过来,那么操作 i,j 的时候就需要是上一个状态的,那么如果 j 正向遍历的话,j 每次都是这一状态得到的,为了解决这个问题,可以逆向遍历,那么每次遍历的 j 就是从上一个状态得到的。
for(int i = 1; i <= n; i++)
for(int j = vs; j >= v[i]; j--)
f[j] = max(f[j],f[j-v[i]] + value[i]);
解题代码
朴素代码
#include<iostream>
using namespace std;
int v[1005];
int value[1005];
int f[1005][1005];
int main (void)
{
int n,vs;
cin >> n >> vs;
for(int i = 1; i <= n; i++) cin >> v[i] >> value[i];
for(int i = 1; i <= n; i++)
{
for(int j = 0; j <= vs; 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]] + value[i]);
}
}
cout << f[n][vs];
return 0;
}
优化代码
#include<iostream>
using namespace std;
int v[1005];
int value[1005];
int f[1005];
int main (void)
{
int n,vs;
cin >> n >> vs;
for(int i = 1; i <= n; i++) cin >> v[i] >> value[i];
for(int i = 1; i <= n; i++)
for(int j = vs; j >= v[i]; j--)
f[j] = max(f[j],f[j-v[i]] + value[i]);
cout << f[vs];
return 0;
}
完全背包问题
推导思路
有N件物品和一个最多能被重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品可以无限用,求解将哪些物品装入背包里物品价值总和最大。
闫式dp分析法:
那么可以看到,要求 f[i,j] 的最大值要求三重循环,又因为n(1~1000),那么三重循环一定会超时,那么就需要进行优化,怎么进行优化:
观察下面两个式子的关系
f[i][j] = max(f[i-1][j],f[i-1][j-v]+w,f[i-1][j-2v]+2w,...)
f[i][j-v] = max(f[i-1][j-v],f[i-1][j-2v]+w,f[i-1][j-3v]+2w...)
我们可以看到得到:
f[i,j] = max(f[i-1][j],f[i][j-v]+w);
那么我们就可以将该代码优化为二维计算。
优化代码:
还可以将二维代码优化为一维的:
还是那句话,代码优化的方法就是对原代码进行一个等价的变形;
可以先进行变换,然后看和原代码有什么关系。
for(int i = 1; i <= n; i++)
{
for(int j = 0; j <= m; j++)
{
f[i][j] = f[i-1][j];
//f[j] = f[j] 无区别
if(j >= volume[i]) f[i][j] = max(f[i][j],f[i][j-volume[i]] + value[i]);
// f[j] = max(f[j],f[j-volume[i]]+value[i]) 无区别
}
}
可以观察到该代码和01背包优化之后的代码很像,只是改了一下 j 的遍历顺序,为什么会这样。
可以观察到,对于完全背包问题, f[i][j-volume[i]] + value[i]这语句是从 i 进行计算的,所以说,我们进行每次进行计算的时候是从这一层状态进行计算的,所以可以正向进行,而01背包是每次进行计算的时候是从上一层状态进行计算的,所以要逆向进行计算。
解题代码
朴素代码
#include<iostream>
using namespace std;
int volume[1005],value[1010],f[1010][1010];
int main (void)
{
int n,m;
cin >> n >> m;
for(int i =1 ; i <= n; i++) cin >> volume[i] >> value[i];
for(int i = 1; i <= n; i++)
{
for(int j = 0; j <= m; j++)
{
f[i][j] = f[i-1][j];
if(j >= volume[i]) f[i][j] = max(f[i][j],f[i][j-volume[i]] + value[i]);
}
}
cout << f[n][m];
return 0;
}
优化代码
#include<iostream>
using namespace std;
int volume[1005],value[1010],f[1010];
int main (void)
{
int n,m;
cin >> n >> m;
for(int i =1 ; i <= n; i++) cin >> volume[i] >> value[i];
for(int i = 1; i <= n; i++)
for(int j = volume[i]; j <= m; j++)
f[j] = max(f[j],f[j-volume[i]] + value[i]);
cout << f[m];
return 0;
}
多重背包问题1
推导思路
有N种物品和一个最多能被重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。第i件物品的个数是s[i]个,求解将哪些物品装入背包里物品价值总和最大。
依然是闫式dp分析法:
由推导公式可以看到,如果想要进行计算需要进行三重循环,观察这题的数据范围可以看到,这题可以进行三重循环进行计算,那么就可以直接进行计算。
解题代码
朴素代码
#include<iostream>
#include<cstring>
using namespace std;
int volume[105],value[105],s[105];
int f[105][105];
int main (void)
{
int n,m;
cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> volume[i] >> value[i] >> s[i];
for(int i = 1; i <= n; i++)
{
for(int j = 1; j <= m; j++)
{
f[i][j] = f[i-1][j];
for(int k = 1; k <= s[i]; k++)
{
if(j - k * volume[i] >= 0) f[i][j] = max(f[i][j],f[i-1][j-k * volume[i]] + k * value[i]);
}
}
}
cout << f[n][m] << endl;
return 0;
}
优化代码:
该题的优化代码就属于多重背包问题2了
多重背包问题2
推导思路
有N种物品和一个最多能被重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。第i件物品的个数是s[i]个,求解将哪些物品装入背包里物品价值总和最大。
与上一题的区别就是数据范围变大了
那么就不能用三重循环进行计算,那么就需要在时间上进行优化,那么怎么进行优化呢?
可以考虑将多重背包问题转化为01背包问题。
转化方法:
可以将一种类型的多重背包打包为多种类型的01背包,就是用组合数的原理进行打包。例如:
背包数量是10
那么就可以将该类型的背包转化为一种类型的多个背包。
那么可以转化为1 2 4 3个背包。
组合数原理我写在另一个博客中了,不懂的可以自行观看:
那么我们可以计算,一种类型的物品最多2000个物品,那么可以最多可以拆成 = 11个物品。
通过计算,可以得到时间复杂度最多为1000 * 2000 * 11 = 2 * ,时间复杂度是能过的。
假设成立,接下来就是实现了,因为该题如果直接用二维的话,空间也会超,那么直接用01背包的优化后的思路进行计算该题
解题代码
朴素代码:
#include<iostream>
#include<vector>
using namespace std;
int f[2005];
struct node{
int volume,value;
};
int main (void)
{
vector<node> item;
int n,m;
cin >> n >> m;
for(int i = 1; i <= n; i++)
{
int v,w,s;
cin >> v >> w >> s;
for(int j = 1; j <= s; j++)
{
item.push_back({j*v,j*w});
s -= j;
}
if(s > 0) item.push_back({s*v,s*w});
}
for(node i : item)
{
for(int j = m; j >= i.volume; j--)
{
f[j] = max(f[j],f[j-i.volume] + i.value);
}
}
cout << f[m];
return 0;
}
优化代码:
优化的代码就是对应的是多重背包问题3了
多重背包问题3
推导思路
有N种物品和一个最多能被重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。第i件物品的个数是s[i]个,求解将哪些物品装入背包里物品价值总和最大。
依然是对于多重背包的优化过程。
上一个写法是将多重背包转化为01背包求解,这一种写法是将多重背包转化为完全背包的方法来优化。
多重背包的原始状态转移方程:
f[i,j] = max(f[i-1][j],f[i-1][j-v]+w,f[i-1][j-2v]+2w,...,f[i-1][j-si*v]+si*w)
考虑用完全背包的优化方式来优化这个方程
f[i,j-v] = max(f[i-1][j-v],f[i-1][j-2v]+w,f[i-1][j-3v]+2w,...,f[i-1][j-(si-1)*v]+si*w)
我们发现好像并没有什么用,因为:
因为 完全背包 是一口气把所有体积全部用掉
然而 多重背包 对于每个物品的个数是有限制的
但是,我们可以把这个式子 继续 推导下去,直到背包体积被用到不能再用为止
也可以理解为 完全背包 下把当前物品 选到不能再选后,剩下的余数得到,我们再利用 完全背包优化思路 往回倒推一遍,会惊奇的发现一个 滑动窗口求最大值 的模型
其中 r = j % vi
具体如下:
该图来自acwing中一位大佬的推导
由此可以推出可以用单调队列进行优化。
时间复杂度为
不理解的话,可以去看看大佬的题解
解题代码
朴素代码:
#include<iostream>
#include<cstring>
using namespace std;
const int N = 1005,M = 20010;
int volume[N],value[N],s[N];
int q[M];
int f[N][M];
int main ()
{
int n,m;
cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> volume[i] >> value[i] >> s[i];
for(int i = 1; i <= n; i++)
{
for(int j = 0; j < volume[i]; j++) // 该语句是为了模拟不同的余数对于得到的值。
{
int head = 0,tail = -1;
for(int k = j; k <= m; k+=volume[i])
{
while(head <= tail && k - q[head] > s[i] * volume[i]) head++;
while(head <= tail && f[i-1][q[tail]] + (k - q[tail])/volume[i]*value[i] <= f[i-1][k]) tail--;
q[++tail] = k;
f[i][k] = f[i-1][q[head]] + (k - q[head])/volume[i]*value[i];
}
}
}
cout << f[n][m] ;
return 0;
}
优化代码:
和 01背包 的优化类似,观察到 状态转移方程,对于 i 阶段,只会用到 i-1 层的状态
滚动数组的写法:
#include<iostream>
#include<cstring>
using namespace std;
const int N = 1005,M = 20010;
int volume[N],value[N],s[N];
int q[M];
int f[2][M];
int main ()
{
int n,m;
cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> volume[i] >> value[i] >> s[i];
for(int i = 1; i <= n; i++)
{
for(int j = 0; j < volume[i]; j++) // 该语句是为了模拟不同的余数对于得到的值。
{
int head = 0,tail = -1;
for(int k = j; k <= m; k+=volume[i])
{
while(head <= tail && k - q[head] > s[i] * volume[i]) head++;
while(head <= tail && f[(i-1) & 1][q[tail]] + (k - q[tail])/volume[i]*value[i] <= f[(i-1) & 1][k]) tail--;
q[++tail] = k;
f[i&1][k] = f[(i-1) & 1][q[head]] + (k - q[head])/volume[i]*value[i];
}
}
}
cout << f[n&1][m] ;
return 0;
}
混合背包问题
推导思路
混合背包就是将01背包,完全背包,多重背包问题混在一起进行计算。
那么做法可以依照多重背包2的做法进行计算,将01背包的个数就定义为1,将完全背包中无限个物品转化为有限个(在有限个数据中其数据的范围必须为大于总体积的个数),比如:如果总体积为 V 那么就可以定义为无限个就可以转为为 V 个。因为我们可以看到该题的数据为:
那么就需要进行二进制优化的方式进行减少时间复杂度。
当然,该题的空间也是需要优化的,那么我们就可以通过01背包一维优化的方式进行计算。
剩下的细节请看解题代码
解题代码
二进制优化和一维优化后的代码
#include<iostream>
#include<vector>
#include<cstring>
using namespace std;
int f[1010];
struct node{
int volume,value;
};
int main (void)
{
vector<node> item;
int n,m;
cin >> n >> m;
for(int i = 1; i <= n; i++)
{
int v,w,s;
cin >> v >> w >> s;
if(s == -1) s = 1;
if(s == 0) s = 1000; // 该数值只要比m大就好了
for(int j = 1; j <= s; j *= 2)
{
item.push_back({j * v,j * w});
s -= j;
}
if(s > 0) item.push_back({s * v,s * w});
}
for(auto i : item)
{
for(int j = m; j >= i.volume; j--)
{
f[j] = max(f[j],f[j-i.volume] + i.value);
}
}
cout << f[m] << endl;
return 0;
}
二维费用的背包问题
推导思路
有N件物品和一个最多能装重量为 W 和最多能装体积为V的背包。第i件物品的重量是weight[i],体积为volume[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
该题就是在01背包的基础上加上了一个重量限制,
闫氏dp分析法:
仔细观察可以发现,与01背包问题就多了一重循环,来判断重量的,那么我们就可以依照01背包问题来写该题
优化方法:
和01背包一样,也是只能从空间上进行优化,优化方法也是和01背包一样。
解题代码
朴素代码:
#include<iostream>
using namespace std;
int f[1010][105][110];
int volume[1005],value[1005],weight[1005];
int main ()
{
int n,v,m;
cin >> n >> v >> m;
for(int i = 1; i <= n; i++) {
cin >> volume[i] >> weight[i] >> value[i];
}
for(int i = 1; i <= n; i++)
{
for(int j = 0; j <= v; j++)
{
for(int k = 0; k <= m; k++)
{
f[i][j][k] = f[i-1][j][k];
if(j >= volume[i] && k >= weight[i])
f[i][j][k] = max(f[i][j][k],f[i-1][j-volume[i]][k-weight[i]] + value[i]);
}
}
}
cout << f[n][v][m];
return 0;
}
二维优化代码:
#include<iostream>
using namespace std;
int f[105][110];
int volume[1005],value[1005],weight[1005];
int main ()
{
int n,v,m;
cin >> n >> v >> m;
for(int i = 1; i <= n; i++) {
cin >> volume[i] >> weight[i] >> value[i];
}
for(int i = 1; i <= n; i++)
{
for(int j = v; j >= volume[i]; j--)
{
for(int k = m; k >= weight[i]; k--)
{
f[j][k] = max(f[j][k],f[j-volume[i]][k-weight[i]] + value[i]);
}
}
}
cout << f[v][m];
return 0;
}
分组背包问题
推导思路
有N种物品和一个最多能被重量为W 的背包。在每种物品中最多选出一个物品来装入背包中,在不超过背包的最大容量的前提下,求解将哪些物品装入背包里物品价值总和最大。
该题的做法可以参考多重背包问题1。
首先进行闫式dp分析法:
我们先逐个分析:
给定 n 组物品,每组物品最多选一个出来放入背包,那么每组物品有两种选择,一种是选这组物品中的一个物品,另一种是不选这组物品
不选的话,我们可以分析出:
f[i][j] = f[i-1][j];
如果是选这组物品中的一个物品,那么我们就需要枚举这一组中的每一个物品,看选这个物品得到的价值大还是不选这个物品得到的价值大。
既然要枚举这一组中的每一个物品,这一步是不是特别像多重背包问题1。
分析结束,那么就来看看时间复杂度:
既然是三重循环那么时间复杂度就是
在时间上是肯定能过的。那么接下来就是实现了
解题代码
朴素代码:
#include<iostream>
#include<vector>
using namespace std;
int s[105];
int f[105][105];
struct node{
int volume,value;
};
vector<node> item[110];
int main ()
{
int n,m;
cin >> n >> m;
for(int i = 1; i <= n; i++) {
cin >> s[i];
for(int j = 1; j <= s[i]; j++)
{
int v,w;
cin >> v >> w;
item[i].push_back({v,w});
}
}
for(int i = 1; i <= n; i++)
{
for(int j = 0; j <= m; j++)
{
f[i][j] = f[i-1][j];
for(int k = 0; k < item[i].size(); k++)
{
if(j >= item[i][k].volume)
f[i][j] = max(f[i][j],f[i-1][j-item[i][k].volume] + item[i][k].value);
}
}
}
cout << f[n][m];
return 0;
}
有依赖的背包问题
该题比较难,需要用到树形dp和图论,最近没时间写了,后来再补。
背包问题求方案数
推导思路
该题就是在01背包的基础上改变了问题。
该题要求的是输出 最优选法的方案数。
那么怎么求最优选法的方案数:
求得到第 i 层中的一个 m 以内的所有空间的方案数:
当体积为 j 的时候我们判断个物品是否能选,当不选的话,那么它的方案数就是 s[i-1][j]
当体积为 j 的时候,选的话,那么它的方案数就是 s[i-1][j-volume[i]]
又因为我们选或不选得到的值都是一样的话,那么它的方案数就是两个相加
那么我们就可以得到这一组语句:
t = max(f[j],f[j-volume[i]] + value[i]);
if(t == f[j]) ans += s[j];
if(t == f[j - volume[i]] + value[i]) ans += s[j - volume[i]];
因为要求的是最优选法的方案数,那么我们就需要先找到最大的那个价值,然后通过最大的那个价值来进行对最优选法的那个方案数进行求和。
解题代码
该题解主要是依照一维讲的,二维的和一维的还是有点区别的,感兴趣的话自己看。
朴素代码(二维代码):
#include<iostream>
#define int long long
using namespace std;
const int mod = 1000000007;
int volume[1010],value[1010];
int f[1010][1010],s[1010][1010];
signed main (void)
{
int n,m;
cin >> n >> m;
int res = 0;
for(int i = 1; i <= n; i++) {
cin >> volume[i] >> value[i];
}
for(int i = 0; i <= m; i++) s[0][i] = 1; // 当空间为0的时候方案数为1
for(int i = 1; i <= n; i++)
{
for(int j = m; j >= 0; j--)
{
if(j < volume[i])
{
s[i][j] = s[i-1][j];
f[i][j] = f[i-1][j];
}
if(j >= volume[i])
{
int t = 0;
int ans = 0;
t = max(f[i-1][j],f[i-1][j-volume[i]] + value[i]);
if(t == f[i-1][j]) ans += s[i-1][j];
if(t == f[i-1][j - volume[i]] + value[i]) ans += s[i-1][j - volume[i]];
if(ans > mod) ans %= mod;
f[i][j] = t;
s[i][j] = ans;
}
}
}
cout << s[n][m] << endl;
return 0;
}
优化代码(一维优化):
#include<iostream>
#define int long long
using namespace std;
const int mod = 1000000007;
int volume[1010],value[1010];
int f[1010],s[1010];
signed main (void)
{
int n,m;
cin >> n >> m;
int res = 0;
s[0] = 1; // 当空间为0的时候方案数为1
for(int i = 1; i <= n; i++) {
cin >> volume[i] >> value[i];
}
for(int i = 1; i <= n; i++)
{
for(int j = m; j >= volume[i]; j--)
{
int t = 0;
int ans = 0;
t = max(f[j],f[j-volume[i]] + value[i]);
if(t == f[j]) ans += s[j]; // 如果该操作为不选,那么就是该空间的方案数就是加上不选的方案
if(t == f[j - volume[i]] + value[i]) ans += s[j - volume[i]]; // 如果该操作为选,那么那么就是该空间的方案数就是加上选的方案
if(ans > mod) ans -= mod; // 取模
f[j] = t;
s[j] = ans;
}
}
int maxx = 0;
for(int i = 0; i <= m; i++) if(f[i] > maxx) maxx = f[i]; // 找到那个最优的选法的值
for(int i = 0; i <= m; i++)
{
if(f[i] == maxx) {
res += s[i];
res %= mod;
}
}
cout << res << endl;
return 0;
}
背包问题求具体方案
推导思路
该题依然是01背包的变形。要求的是满足该条件的最小字典序的方案。
那么该题的做法就和01背包有些区别了。
那么我们来分析一下:
要求的是最小字典序的方案。我们就需要从1开始进行枚举,那么我们得到的那个序列就是最小字典序方案。
从1开始枚举的话,那么就需要先算出 2~n 所有方案的最优解,枚举到 2 的时候我们又需要求出 3~n 所有方案的最优解。
这就可以看出我们需要从后往前进行枚举。
那么就是代码实现了
for(int i = n; i >= 1; i--)
{
for(int j = 0; j <= m; j++)
{
f[i][j] = f[i+1][j];
if(j >= volume[i])
f[i][j] = max(f[i][j],f[i+1][j-volume[i]] + value[i]);
}
}
这就将dp得到的值求出来了。
那么接下来就是枚举了。从1开始枚举,看取到最大值是否时是否含 1 这个物品,然后依次向下枚举就好了。
更多细节请看代码。
解题代码
朴素代码:
#include<iostream>
#include<vector>
using namespace std;
int volume[1010],value[1010];
int f[1010][1010];
signed main ()
{
int n,m;
cin >> n >> m;
int res = 0;
for(int i = 1; i <= n; i++) {
cin >> volume[i] >> value[i];
}
for(int i = n; i >= 1; i--)
{
for(int j = 0; j <= m; j++)
{
f[i][j] = f[i+1][j];
if(j >= volume[i])
f[i][j] = max(f[i][j],f[i+1][j-volume[i]] + value[i]);
}
}
int cur = m;
for(int i =1 ; i <= n; i++)
{
if(volume[i] <= cur && f[i+1][cur-volume[i]] + value[i] == f[i][cur])
{
printf("%d ",i);
cur -= volume[i];
}
}
return 0;
}
背包问题多组例题
最近没时间写了,后来再补