背包常见 trick
1. 缺一背包
1.1 Gym104090C No Bug No Game
因为触发最后一种情况的条件是 \(sum < k,sum + p_i > k\),所以本次操作后一定有 \(sm > k\)。由此可以得到最多有一个物品被执行了最后一次操作的结论。
由此问题被转化为:有一个背包,每次询问扣掉一个物品(即执行最后一个操作的物品),对所有情况分别求背包。
这是缺一背包的经典模型。具体而言,我们采用最经典的分治结构:
- 每次将整个序列分为两块:\([l, mid], [mid + 1, r]\)。
- 将 \([mid + 1, r]\) 的物品加入背包中,开一个数组记录背包的历史版本,然后对 \([l, mid]\) 进行分治。分治完后,用历史版本将背包回退。
- 将 \([l, mid]\) 的物品加入背包中,开一个数组记录背包的历史版本,然后对 \([mid + 1, r]\) 进行分治。分治完后,用历史版本将背包回退。
- 如果当前是叶子节点,则不进行分治,而是直接枚举自己最后的体积,贡献到答案里即可。容易发现我们在分治的过程中已经把到根链上其他子树的物品全加进来了,因此此时的背包就是 \([1, l - 1]\cup[l + 1,r]\) 的背包了。
由于递归层数只有 \(O(\log n)\) 层,且同一时刻每层最多只有一个正在执行的递归,因此记录历史版本可以统一对每一层开一个数组存储答案,而不用每次递归都开一个大小为 \(n\) 的。
时间复杂度可以使用主定理分析,\(T(n) = 2T(\dfrac{n}{2})+O(nk) = O(nk\log n)\)。
WA 了神秘数据,对拍 10w 组没拍出来,不想调了,代码就不放了吧。
1.2 CF1442D Sum
对选数的过程进行观察。假设有两个序列,下一个选的数分别是 \(x, y\),满足 \(x > y\)。因为序列是递增的,所以 \(x\) 的下一个数一定比 \(y\) 大。根据调整法,我们不选 \(y\),而连续选两个 \(x\) 所处的序列一定更优。
由此可以得到一个结论:最多只有一个序列不被全部选,其余序列要么全部被选中,要么一个都不选。
把那个唯一的只选一部分的序列看做缺失的那一个物品,跑缺一背包即可。时间复杂度 \(O(nk\log n)\)。
#include <bits/stdc++.h>
#define fi first
#define se second
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
#define lc(x) (tr[x].ls)
#define rc(x) (tr[x].rs)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
using pi = pair<int, int>;
const int N = 3005;
ll n, m, len[N], sm[N], ans, dp[N], f[20][N];
vector<ll> a[N];
void divide(int l, int r, int dep)
{
if(l == r)
{
ll sma = 0;
for(int i = 0; i <= min(m, len[l]); i++)
{
if(i > 0) sma += a[l][i - 1];
ans = max(ans, sma + dp[m - i]);
}
return;
}
int mid = (l + r) >> 1;
memcpy(f[dep], dp, sizeof(f[dep]));
for(int i = mid + 1; i <= r; i++)
for(int j = m; j >= len[i]; j--)
dp[j] = max(dp[j], dp[j - len[i]] + sm[i]);
divide(l, mid, dep + 1);
memcpy(dp, f[dep], sizeof(dp));
memcpy(f[dep], dp, sizeof(f[dep]));
for(int i = l; i <= mid; i++)
for(int j = m; j >= len[i]; j--)
dp[j] = max(dp[j], dp[j - len[i]] + sm[i]);
divide(mid + 1, r, dep + 1);
memcpy(dp, f[dep], sizeof(dp));
}
int main()
{
//freopen("sample.in", "r", stdin);
//freopen("sample.out", "w", stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> m;
for(int i = 1; i <= n; i++)
{
cin >> len[i];
for(int j = 1; j <= len[i]; j++)
{
ll x;
cin >> x;
sm[i] += x;
a[i].push_back(x);
}
sort(a[i].begin(), a[i].end());
}
divide(1, n, 1);
cout << ans;
return 0;
}
2. 拆分数背包
2.1 P6189 [NOI Online #1 入门组] 跑步
拆分数背包:有 \(n\) 种物品,体积为 \(1\sim n\),每个物品可以放任意多个,求最终体积为 \(n\) 的方案数。
普通的完全背包是 \(O(n^2)\) 的,显然不可做。
考虑根号分治。对于体积 \(< \sqrt n\) 的数,只有 \(O(\sqrt n)\) 个,于是定义 \(f_{i, j}\) 表示前 \(i\) 种物品中体积为 \(j\) 的方案数,直接暴力做背包即可。
对于体积 \(\ge \sqrt n\) 的物品,显然一个背包内最多只能有 \(O(\dfrac{n}{\sqrt n}) = O(\sqrt n)\) 个,因为再放多就会超过背包容量了。
但是你发现,这里依然无法直接设计 DP。于是我们来编一个转化:我们有两种操作:
- 操作 \(1\):将背包里的所有物品的体积加 \(1\)。
- 操作 \(2\):向背包里加入一个体积为 \(\sqrt n\) 的数。
发现此时所有的背包方案都可以通过此类操作构造出来。构造是容易的,只需要对所有物品排序后,从大到小加入背包,对于差量直接使用操作 \(1\) 即可。
因此设计 DP:\(g_{i, j}\) 表示背包里此时有 \(i\) 个物品,背包内的体积为 \(j\) 的方案数。转移可以考虑套用上面的转化:
- 操作 \(1\):\(g_{i, j + i}\overset{+}\leftarrow g_{i, j}\)。
- 操作 \(2\):\(g_{i + 1, j + \sqrt n}\overset{+}\leftarrow g_{i, j}\)。
最后我们将两个背包合并即可。注意合并的时候要枚举 \(g\) 的第一维进行求和。
时间复杂度 \(O(n\sqrt n)\)。
#include <bits/stdc++.h>
#define fi first
#define se second
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
#define lc(x) (tr[x].ls)
#define rc(x) (tr[x].rs)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
using pi = pair<int, int>;
const int N = 100005, SQRT = 505;
int n, mod, m, f[SQRT][N], g[SQRT][N], ans;
int main()
{
//freopen("sample.in", "r", stdin);
//freopen("sample.out", "w", stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> mod;
m = sqrt(n);
f[0][0] = 1;
for(int i = 1; i < m; i++)
{
for(int j = 0; j <= n; j++)
{
f[i][j] += f[i - 1][j];
if(f[i][j] >= mod) f[i][j] -= mod;
if(j - i >= 0) f[i][j] += f[i][j - i];
if(f[i][j] >= mod) f[i][j] -= mod;
}
}
g[0][0] = 1;
for(int i = 1; i <= n / m; i++)
{
for(int j = 0; j <= n; j++)
{
if(j - m >= 0) g[i][j] += g[i - 1][j - m];
if(g[i][j] >= mod) g[i][j] -= mod;
if(j - i >= 0) g[i][j] += g[i][j - i];
if(g[i][j] >= mod) g[i][j] -= mod;
}
}
for(int i = 0; i <= n; i++)
{
ll smg = 0;
for(int j = 0; j <= n / m; j++) smg = (smg + g[j][n - i]) % mod;
ans = (ans + f[m - 1][i] * smg % mod) % mod;
}
cout << ans;
return 0;
}
3. 回退背包
通常需要比缺一背包具有更强的性质,即更新背包的可以回退。因此求 \(\max, \min\) 的背包是无法回退的,只有算方案数 / 概率 / 期望的背包能够回退。
3.1 P4141 消失之物
对于背包问题,一般来说物品添加的顺序是没有影响的。也就是说背包的转移具有交换律。
因此我们在跑过一次背包之后,如果要对某次操作做撤销,则可以直接把这个操作看做将最后一个物品撤销,因为操作是具有交换律的。
而对最后一个元素做撤销是容易的,直接反着枚举 DP 值,把状态更新反过来即可。
时间复杂度 \(O(nm)\)。
#include <bits/stdc++.h>
#define fi first
#define se second
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
#define lc(x) (tr[x].ls)
#define rc(x) (tr[x].rs)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
using pi = pair<int, int>;
const int N = 2005, mod = 10;
int n, m, dp[N], w[N], g[N];
int main()
{
//freopen("sample.in", "r", stdin);
//freopen("sample.out", "w", stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> m;
dp[0] = 1;
for(int i = 1; i <= n; i++)
{
cin >> w[i];
for(int j = m; j >= w[i]; j--)
{
dp[j] += dp[j - w[i]];
if(dp[j] >= mod) dp[j] -= mod;
}
}
for(int i = 1; i <= n; i++)
{
memcpy(g, dp, sizeof(g));
for(int j = w[i]; j <= m; j++)
{
g[j] -= g[j - w[i]];
if(g[j] < 0) g[j] += mod;
}
for(int j = 1; j <= m; j++) cout << g[j];
cout << "\n";
}
return 0;
}

浙公网安备 33010602011771号