背包问题及一些应用
1.基础
(1) 01背包
// 状态表示
f[i][j]表示从前i个物品中选且总体积不超过j的所有方案。
// 转移
针对第i个物品做出选择。
1.不选第i个物品。 相当于从前i - 1个物品中选且总体积不超过j的所有方案表示为f[i - 1][j].
2.选择第i个物品。剔除第i个物品后,相当于从前i - 1个物品中选且总体积不超过j - v[i]的所有方案.
表示为f[i - 1][j - v] + w。【w,v分别为第i个物品的价值和体积】
注意:前提是背包剩余体积不少于v.
3.综上,f[i][j] = max(f[i - 1][j],f[i - 1][j - v] + w).
// 边界处理
i == 1时,会用到f[0][j]这个状态,根据状态表示,这个意味着从前0个物品中选,价值当然为0.
所以f[0][j] = 0;
// 优化空间
滚动数组 + 从大到小枚举j
#include<iostream>
using namespace std;
const int N = 1010;
int f[N];
int main()
{
int n,v;
cin >> n >> v;
for(int i = 1; i <= n; i ++)
{
int vv,w;
cin >> vv >> w;
for(int j = v; j >= vv; j --)
{
f[j] = max(f[j - vv] + w,f[j]);
}
}
cout << f[v] << endl;
}
(2) 完全背包
// 状态表示
f[i][j] 表示 从前i个物品中选且总体积不超过j的所有方案。
// 转移
根据第i个物品转移
1.不选择第i个物品: f[i - 1][j].
2.选择第i个物品:
(1) 选1个: f[i - 1][j - v] + w;
(2) 选2个: f[i - 1][j - 2 * v] + 2 * w.
...
(k)选k个: f[i - 1][j - k * v] + k * w.
综上: f[i][j] = max(f[i - 1][j - k * v] + k * w) k >= 0 && k * v <= 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 - 1][j - k * v] + k * 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...f[i - 1][j - k * v] + (k - 1) * w)
所以f[i][j] = max(f[i - 1][j],f[i][j - v] + w).
// 优化空间:
滚动数组 + 体积从小到大循环
#include<iostream>
using namespace std;
const int N = 1010;
int f[N];
int main()
{
int n,m;
cin >> n >> m;
for(int i = 1; i <= n; i ++)
{
int v,w;
cin >> v >> w;
for(int j = v; j <= m; j ++) f[j] = max(f[j],f[j - v] + w);
}
cout << f[m] << endl;
return 0;
}
(3) 多重背包
// 状态表示
f[i][j] 表示 从前i个物品中选且总体积不超过j的所有方案。
// 转移
根据第i个物品转移
1.不选择第i个物品: f[i - 1][j].
2.选择第i个物品:
(1) 选1个: f[i - 1][j - v] + w;
(2) 选2个: f[i - 1][j - 2 * v] + 2 * w.
...
(s)选s个: f[i - 1][j - s * v] + s * w.
所以f[i][j] = max(f[i - 1][j - k * v] + k * w) k >= 0 && k <= s
// 优化
二进制优化
// 真得需要每个物品枚举s次吗?
可不可以用一些数通过它们之间相互组合 表示 1 ~ s 内 的所有数。
每个数只有选或不选两种情况,所以转变为了 01 背包。
这些数的组成为1,2,4,8...2 ^ k,c。
2^k <= s && 2^(k + 1) > s
c = s - (1 + 2 + 4 + ... + 2 ^ k)。
#include<iostream>
using namespace std;
const int N = 11010,M = 2010;
int f[M];
int v[N],w[N];
int cnt;
int main()
{
int 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)
{
cnt ++;
v[cnt] = k * a;
w[cnt] = k * b;
s -= k;
k *= 2;
}
if(s > 0)
{
cnt ++;
v[cnt] = s * a;
w[cnt] = s * b;
}
}
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] << endl;
return 0;
}
(4) 分组背包
// 状态表示
f[i][j] 表示从前i组内选,且总体积不超过j的所有方案的集合
// 转移
根据选择第i组的哪个物品:
不选: f[i - 1][j];
选i组第一个物品: f[i - 1][j - v[i][1]] + w[i][1]。
选i组第二个物品: f[i - 1][j - v[i][2]] + w[i][2]。
...
选i组第k个物品: f[i - 1][j - v[i][k]] + w[i][k]。
所以 f[i][j] = max(f[i - 1][j - v[i][k]] + w[i][k]) k >= 0 && k <= 第i组物品总数。
// 注意循环顺序。
#include<iostream>
using namespace std;
const int N = 110;
int f[N];
int s[N],v[N][N],w[N][N];
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 ++) 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(j >= v[i][k]) f[j] = max(f[j],f[j - v[i][k]] + w[i][k]);
cout << f[m] << endl;
}
(5) 求装满背包的方案数
// 状态表示
f[i][j] 表示从前i个物品中选且总体积恰好为j的方案的集合。
// 根据具体的背包类型进行转移
01背包 : f[i][j] = f[i - 1][j] + f[i - 1][j - v].
完全背包 :f[i][j] = f[i - 1][j] + f[i][j - v]。
多重背包 :f[i][j] = f[i - 1][j] + f[i - 1][j - k * v]。k >= 1 && k <= s。
分组背包 :f[i][j] = f[i - 1][j] + f[i - 1][j - v[i][k]]。k >= 1 && k <= 第i组物品总数。
// 边界
注意这里的状态定义是:体积恰好为j。
这意味着初始时只有f[i][0]是合法的。因为根据定义
从前i个物品中选,体积恰好为0的方案数只有什么都不选这1种。
其它状态都无法确定,所以为0。(因为需要计数,0在计数的过程中是不会产生任何影响的)
(6) 求背包价值达到最优时的方案数
// 01 背包
f[i][j] 表示 从前i个物品中选,且总体积恰好为j的所有方案的集合
用g[i][j]表示 f[i][j] 取最大值时的方案数
f[i][j] = max(f[i - 1][j],f[i - 1][j - v] + w).
1.如果f[i - 1][j] > f[i - 1][j - v] + w
说明f[i][j]由f[i - 1][j] 转移过来
所以g[i][j] = g[i - 1][j].
2.如果f[i - 1][j] < f[i - 1][j - v] + w
说明f[i][j]由f[i - 1][j - v] + w转移过来.
所以g[i][j] = g[i - 1][j - v].
3.如果f[i - 1][j] = f[i - 1][j - v] + w
说明这两种情况都能使得f[i][j]值最大
所以总的方案数g[i][j] = g[i - 1][j] + g[i - 1][j - v].
(7) 求背包价值达到最优时的具体方案
// 记录终点状态由谁转移过来即可
// 01 背包
f[i][j] = f[i - 1][j] 或 f[i - 1][j - v] + w.
分别代表着 不选第i个物品 和 选第i个物品
可以令g[i][j] = 0 或 1 表示 f[i][j] 这个状态由 f[i - 1][j] 或 f[i - 1][j - v] + w 转移。
(8) 二维费用的背包
(9) 混合背包
(10) 有依赖的背包
2.应用
(1)正整数分组(01背包)
题目链接:正整数分组
// 思路
假设数组总和为sum
最理想的情况下分成两组,每一组和都为sum / 2,差值为0.
故只要让两组的值尽可能的接近sum / 2即可。
又因为sum1 + sum2 = sum,
所以只要操作一组的值接近sum / 2,另一组可通过sum - sum1 得出。
于是问题转换成:
在n个数中选,选出若干数,使之和在不超过sum / 2的情况下最大
这就是一个01背包问题:
1.背包容积:sum / 2;
2.物品体积:a[i]; 物品价值:a[i].
#include<iostream>
using namespace std;
const int N = 10010;
int f[N],a[N];
int main()
{
int n;
cin >> n;
int sum = 0;
for(int i = 0; i < n; i ++)
{
cin >> a[i];
sum += a[i];
}
for(int i = 0; i < n; i ++)
{
for(int j = sum / 2; j >= a[i]; j --)
f[j] = max(f[j - a[i]] + a[i],f[j]);
}
cout << sum - 2 * f[sum / 2] << endl;
}
扩展
题目链接:正整数分组v2
// 经典二分【最大值最小】
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N = 50010;
int n,k;
int a[N];
int check(ll mid)
{
ll sum = 0;
int cnt = 0;
for(int i = 0; i < n; i ++)
{
sum += a[i];
if(sum > mid) sum = a[i],cnt ++;
}
if(sum) cnt ++;
return cnt;
}
int main()
{
cin >> n >> k;
ll sum = 0;
for(int i = 0; i < n; i ++)
{
cin >> a[i];
sum += a[i];
}
ll l = 1,r = sum;
while(l < r)
{
ll mid = l + r >> 1;
if(check(mid) <= k) r = mid;
else l = mid + 1;
}
cout << l << endl;
}