背包问题 - DP
背包问题属于线性DP,但相对难
01背包
\(n\)个物品,背包体积为\(m\),每个物品最多拿一个,问能拿的最大价值
二维解法
- 状态表示: \(f[i][j]表示只选前i个物品,背包体积为j,背包的最大价值\)
- 集合划分: 选或不选第\(i\)个物品
if(选第i个物品) f[i - 1][j] // 即为上次的最大价值 else f[i][j - v[i]] + w[i] // 即减去此物品的体积,加上它的价值
- 状态转移: \(f[i][j] = max(f[i-1][j],\ f[i][j - v[i]]+w[i])\)
其中\(v[i]\)表示第\(i\)个物品的体积,\(w[i]\)表示第\(i\)个物品的价值
一维解法
根据观察我们发现,每一个状态\(f[i][j]\)实际上都只会用到上一次状态\(f[i-1][j]\)的值,因此舍弃第一维,将原数组作为滚动数组
,倒序枚举,从而降为为一维数组
- 状态表示: \(f[i]表示背包体积为j,背包的最大价值\)
- 集合划分: 选或不选第\(i\)个物品
if(选第i个物品) f[j] // 即为上次的最大价值 else f[j - v[i]] + w[i] // 即减去此物品的体积,加上它的价值
- 状态转移: \(f[j] = max(f[j],\ f[j - v[i]]+w[i])\)
其中\(v[i]\)表示第\(i\)个物品的体积,\(w[i]\)表示第\(i\)个物品的价值
练习:T1290. 采药
AC代码
此题时间为体积,草药的价值就是价值
#include <iostream>
#include <cstring>
#include <algorithm>
#define INF 0x3f3f3f3f
using namespace std;
const int N = 1100;
typedef long long LL;
int v[N], w[N], f[N];
int main()
{
int V, n;
cin >> V >> n;
for(int i = 1; i <= n; i++) cin >> w[i] >> v[i];
for(int i = 1; i <= n; i++)
{
for(int j = V; j >= w[i]; j --)
{
f[j] = max(f[j], f[j - w[i]] + v[i]);
}
}
cout << f[V];
return 0;
}
完全背包
\(n\)个物品,背包体积为\(m\),每个物品无限拿,问能拿的最大价值
-
状态表示: \(f[i][j]表示只选前i个物品,背包体积为j,背包的最大价值\)
-
集合划分: 选\(k\)个第\(i\)个物品
-
状态转移: \(f[i][j] = max(f[i][j],f[i-1][j-k*v[i]]+k*w[i])\)
其中\(v[i]\)表示第\(i\)个物品的体积,\(w[i]\)表示第\(i\)个物品的价值,\(k <= j\)
优化
f[i , j ] = max( f[i-1,j] , f[i-1,j-v]+w , f[i-1,j-2*v]+2*w , f[i-1,j-3*v]+3*w , .....)
f[i , j-v]= max( f[i-1,j-v] , f[i-1,j-2*v] + w , f[i-1,j-3*v]+2*w , .....)
发现他们之间只差了一个w,因此,原方程可以优化为\(f[i][j] = max(f[i][j],f[i][j-v[i]]+w[i])\)
发现他和01背包的\(f[i][j] = max(f[i-1][j],\ f[i][j - v[i]]+w[i])\)很像,其实他也可以优化为一维,但是和01背包的概念并不同
\(f[j] = max(f[j],f[j-v[i]]+w[i])\)
还是滚动数组,只不过这次是正序
练习:P1616 疯狂的采药
AC代码
#include <iostream>
#include <cstring>
#include <algorithm>
#define INF 0x3f3f3f3f
using namespace std;
const int N = 1e7+10;
typedef long long LL;
LL v[N], w[N], f[N]; // 要开long long……
int main()
{
int V, n;
cin >> V >> n;
for(int i = 1; i <= n; i++) cin >> v[i] >> w[i];
for(int i = 1; i <= n; i++)
{
for(int j = v[i]; j <= V; j ++)
{
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
cout << f[V];
return 0;
}
多重背包
\(n\)个物品,背包体积为\(m\),每个物品最多拿\(s[i]\)个,问能拿的最大价值
-
状态表示: \(f[i][j]表示只选前i个物品,背包体积为j,背包的最大价值\)
-
集合划分: 选\(k\)个第\(i\)个物品
-
状态转移: \(f[i][j] = max(f[i-1][j-v[i]*k] + w[i]*k, f[i][j])\)
其中\(v[i]\)表示第\(i\)个物品的体积,\(w[i]\)表示第\(i\)个物品的价值,\(k <= min(j, s[i])\)
暴力代码
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int f[N][N], v[N], w[N], s[N], n, m;
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i] >> s[i];
for (int i = 1; i <= n; i ++ )
for (int j = 0; j <= m; j ++ )
for (int k = 0; k <= s[i] && v[i] * k <= j; k ++ )
f[i][j] = max(f[i - 1][j - v[i] * k] + w[i] * k, f[i][j]);
cout << f[n][m] << endl;
return 0;
}
二进制优化
多重背包不能采用完全背包的一维优化
f[i , j ] = max( f[i-1,j] ,f[i-1,j-v]+w ,f[i-1,j-2v]+2*w ,..... f[i-1,j-Sv]+S*w, )
f[i , j-v]= max( f[i-1,j-v] ,f[i-1,j-2v]+w, ..... f[i-1,j-Sv]+(S-1)*w, f[i-1,j-(S+1)v]+S*w )
为什么多出来了一项!
这是因为完全背包可以无限取,所以没有最后一项的问题
那么我们就只能采用另一种优化方式:二进制优化
- 我们知道用\(2^0,2^1,2^2,...,2^k\)可以表示任意\(2^{k+1}\)内的数字(不含\(2^{k+1}\))
那么\(s[i]\)自然可以分成\(2^0,2^1,2^2,...,2^k\)的形式,取\(1~s[i]\)个物品就变成了,在\(k\)个物品\(2^0,2^1,2^2,...,2^k\)中01背包,时间复杂度从\(O(n^3)\)大大降低成\(O(n^2logS)\)
代码
题目:多重背包问题 II
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 12010, M = 2010;
int w[N], v[N], f[M], cnt;
int main()
{
int n, m; // n是物品数,m是容量
cin >> n >> m;
for (int i = 1; i <= n; i ++ )
{
int a, b, s;
cin >> a >> b >> s;
int k = 1;
while(k <= s) // 拆分
{
v[++ cnt] = k * a;
w[cnt] = k * b;
s -= k;
k *= 2;
}
if(s > 0) // 有边角料
{
v[++ cnt] = s * a;
w[cnt] = s * b;
}
}
n = cnt; // 更新物品数量
for (int i = 1; i <= n; i ++ ) // 01背包
for (int j = m; j >= v[i]; j -- )
f[j] = max(f[j], f[j - v[i]] + w[i]);
cout << f[m] << endl;
return 0;
}
其实还有一个单调队列优化,但是本蒟蒻不会╮(╯﹏╰)╭
分组背包
\(n\)个物品,背包体积为\(m\),每个物品属于一个组,每组中最多取一个物品,问能拿的最大价值
状态表示:\(f[i][j]表示从前i组中,背包体积为j的最大价值\)
状态转移: \(f[i][j]=max(f[i][j],f[i-1][j-v[i][k]]+w[i][k])\)
其中\(v[i][k]\)表示第\(i\)个组,第\(k\)个物品的体积,\(w[i][k]\)表示第\(i\)组,第\(k\)个物品的价值
写出代码
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 < s[i]; k++){
if(j >= v[i][k])
f[i][j] = max(f[i][j], f[i - 1][j - v[i][k]] + w[i][k]);
}
}
}
和01背包很像,所以同理,也可以进行一维优化,因为只用到了上次的状态,所以倒序
for (int i = 1; i <= n; i ++ )
for (int j = m; j >= 0; j -- )
for(int k = 0; k < s[i]; k++)
if(v[i][k] <= j)
f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
例题:分组背包
代码
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
#define INF 0x3f3f3f3f
using namespace std;
const int N = 210, M = 35;
typedef long long LL;
int f[N];
struct node
{
int w, v;
}s[M]; // 记录物品属性
vector<int> idxs[M]; // 记录组数
int main()
{
int V, n, t;
cin >> V >> n >> t;
for(int i = 1; i <= n; i ++)
{
int p;
cin >> s[i].v >> s[i].w >> p;
idxs[p].push_back(i); // 放入对应组
}
for(int i = 1; i <= n; i ++)
{
for(int j = V; j >= 0; j --)
{
for(int k = 0; k < idxs[i].size(); k ++)
{
int idx = idxs[i][k]; // 取出编号
if(j >= s[idx].v) // 背包装得下?
f[j] = max(f[j], f[j - s[idx].v] + s[idx].w); // 根据编号取出属性
}
}
}
cout << f[V] << endl;
return 0;
}
混合背包就是混合,进行if判断,判断要用到背包类型,没什么特别之处,因此不记录