背包问题
01背包
主要思想
DP主要从两个角度进行考虑:
- 状态表示 f ( i , j ):用几维来表示状态,每一个状态的含义是什么。
- 集合:是所有选法的集合,选择过程有两个条件:
- 只能从前 i 个物品中选
- 当前总体积 <= j
- 属性:最大值max,最小值min,元素数量
- 集合:是所有选法的集合,选择过程有两个条件:
- 状态计算:如何一步步把每一个状态算出来
一般来说,对应的是集合的划分。例如 在算到某一个物品 i 时,将集合划分为两类:
一类包含 i :最大值为 f ( i - 1 , j - vi) + wi
一类不包含 i :最大值为 f ( i - 1 , j )
所以最终的 f ( i , j ) 为:上面二者取 max。
DP的优化一般都是对DP问题的代码或计算方程进行变形。
例题
解1(二维):
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N][N];
int main(){
cin>>n>>m;
for(int i = 1; i <= n; i ++) cin>>v[i]>>w[i];
for(int i = 1; i <= n; 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]);
}
}
cout<<f[n][m];
return 0;
}
解2(优化,滚动数组,一维):
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N];
int main(){
cin>>n>>m;
for(int i = 1; i <= n; i ++) cin>>v[i]>>w[i];
for(int i = 1; i <= n; i ++)
for(int j = m; j >= v[i]; j--)
f[j] = max(f[j], f[j - v[i]] + w[i]);
cout<<f[m];
return 0;
}
完全背包
主要思想
主要从两个角度进行考虑:
- 状态表示 f ( i , j ):用几维来表示状态,每一个状态的含义是什么。
- 集合:是所有选法的集合,选择过程有两个条件:
- 只能从前 i 个物品中选
- 当前总体积 <= j
- 属性:最大值max,最小值min,元素数量
- 集合:是所有选法的集合,选择过程有两个条件:
- 状态计算:如何一步步把每一个状态算出来
一般来说,对应的是集合的划分。例如 在算到某一个物品 i 时,将集合划分为 k (k 为第 i 个物品的数量)类:
包含 k 个 i :最大值为 f ( i - 1 , j - k * vi) + k * wi
不包含 i :最大值为 f ( i - 1 , j )
所以最终的 f ( i , j ) 为:上面取 max。
DP的优化一般都是对DP问题的代码或计算方程进行变形。
例题
解1(朴素做法,会超时):
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;
int v[N],w[N];
int f[N][N];
int main(){
cin>>n>>m;
for(int i = 1; i <= n; i++) cin>>v[i]>>w[i];
for(int i = 1; i <= n; i++){
for(int j = 0; j <= m; j++){
for(int k = 0; k * v[i] <= j; k++){
f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
}
}
}
cout<<f[n][m];
return 0;
}
解2(优化为二维):
根据 f [ i ][ j ] 与 f [ i ][ j - v ] 的 max 值的相似性,可以将状态转移方程优化为:f [ i ][ j ] = max(f [ i - 1 ][ j ], f [ i ][ j - v ] + w)
这样就不需要进行第三重循环中的 k 项枚举,降为二维。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;
int v[N],w[N];
int f[N][N];
int main(){
cin>>n>>m;
for(int i = 1; i <= n; i++) cin>>v[i]>>w[i];
for(int i = 1; i <= n; 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][j - v[i]] + w[i]);
}
}
cout<<f[n][m];
return 0;
}
解3(一维):
根据 01 背包和完全背包状态转移方程的相似性,所以完全背包也可以降为1维。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;
int v[N],w[N];
int f[N];
int main(){
cin>>n>>m;
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 <= m; j++){
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
cout<<f[m];
return 0;
}
多重背包
主要思想
与完全背包基本相同,主要从两个角度进行考虑:
- 状态表示 f ( i , j ):用几维来表示状态,每一个状态的含义是什么。
- 集合:是所有选法的集合,选择过程有两个条件:
- 只能从前 i 个物品中选
- 当前总体积 <= j
- 属性:最大值max,最小值min,元素数量
- 集合:是所有选法的集合,选择过程有两个条件:
- 状态计算:如何一步步把每一个状态算出来
一般来说,对应的是集合的划分。例如 在算到某一个物品 i 时,将集合划分为 k (k 为第 i 个物品的数量,最大为 si)类:
包含 k 个 i :最大值为 f ( i - 1 , j - k * vi) + k * wi
不包含 i :最大值为 f ( i - 1 , j )
所以最终的 f ( i , j ) 为:上面取 max。
例题
解(朴素做法):
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N], s[N];
int f[N][N];
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] && k * v[i] <= j; k++){
f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
}
}
}
cout<<f[n][m];
return 0;
}
解(二进制优化):
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 25000;
int n, m;
int v[N], w[N];
int f[N];
int main(){
cin>>n>>m;
int cnt = 0;
for(int i = 1; i <= n; i++){
int a, b, s;
cin>>a>>b>>s;
int k = 1;
while(k <= s){
cnt++;
v[cnt] = k * a;
w[cnt] = k * b;
s -= k;
k *= 2;
}
if(s > 0){
cnt++;
v[cnt] = a * s;
w[cnt] = b * s;
}
}
n = cnt;
for(int i = 1; i <= n; i++){
for(int j = m; j >= v[i]; j --){
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
cout<<f[m];
return 0;
}
分组背包
主要思想
与完全背包基本相同,主要从两个角度进行考虑:
- 状态表示 f ( i , j ):用几维来表示状态,每一个状态的含义是什么。
- 集合:是所有选法的集合,选择过程有两个条件:
- 只能从前 i 个物品中选
- 当前总体积 <= j
- 属性:最大值max,最小值min,元素数量
- 集合:是所有选法的集合,选择过程有两个条件:
- 状态计算:如何一步步把每一个状态算出来
一般来说,对应的是集合的划分。例如 在算到某一个物品 i 时,将集合划分为 k (k 为第 i 个物品的数量,最大为 si)类:
包含第 k 个 i :最大值为 f ( i - 1 , j - vi , k) + wi , k
不包含 i :最大值为 f ( i - 1 , j )
所以最终的 f ( i , j ) 为:上面取 max。
例题
解1:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 110;
int n, m;
int v[N][N], w[N][N], s[N];
int f[N][N];
int main(){
cin>>n>>m;
for(int i = 1; i <= n; i++){
cin>>s[i];
for(int j = 0; 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++){
for(int k = 0; k <= s[i]; k++){
if(v[i][k] <= j)
f[i][j] = max(f[i][j], f[i - 1][j - v[i][k]] + w[i][k]);
}
}
}
cout<<f[n][m];
return 0;
}
解2(优化):
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 110;
int n, m;
int v[N][N], w[N][N], s[N];
int f[N];
int main(){
cin>>n>>m;
for(int i = 1; i <= n; i++){
cin>>s[i];
for(int j = 0; j < s[i]; j++){
cin>>v[i][j]>>w[i][j];
}
}
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]);
}
}
}
cout<<f[m];
return 0;
}
线性DP
区间DP
附
有些较复杂的懒得写特别细,建议AcWing学一下y总的课效果更好。
以上截图和模板均来源:AcWing
链接:https://www.acwing.com/blog/content/404/