blog III | 回退背包
回退背包 / 可逆背包
题外话:鉴于我的知识体系大多数都来源于 OI Wiki,然后 OI Wiki 上没有回退背包的内容,所以只能自己写一个了。
讲解
我们通过一个例题来引入回退背包:
P4141 消失之物
题目大意:有
n 件物品,每件物品都有一个体积w_i 。对于每一个i=1,2,\dots,n ,需要回答在不选第i 件物品的情况下,有多少种方案可以填满容积为x 的背包。
不考虑限制,这题就是一个朴素的 01 背包。而对于“不选某件物品”的限制,可以看作是从原背包中退掉了这个物品,所以叫回退背包。
一种最暴力的想法是,每次,举不能选的物品,然后对剩下的物品做 01 背包,一共做
每次都重新求一次背包显然太慢了,我们尝试用回退的思想。先对所有的物品做一次背包,得到一个 dp 数组。
然后对每个限制,开一个 ans 数组,记
设我们不能选的是物品
对于
对于
这个要怎么理解呢?我们实际上是要把
这样就做完了。可以看到,我们处理一个限制的复杂度是
main code
void solve()
{
int n, m; cin >> n >> m;
vector <int> v(n + 1);
for(int i = 1; i <= n; i ++) cin >> v[i];
vector <int> dp(m + 1);
dp[0] = 1;
for(int i = 1; i <= n; i ++){
for(int j = m; j >= v[i]; j --){
dp[j] = (dp[j] + dp[j - v[i]]) % 10;
}
}
for(int i = 1; i <= n; i ++){
vector <int> ans(m + 1);
for(int j = 0; j < v[i]; j ++) ans[j] = dp[j];
for(int j = v[i]; j <= m; j ++) ans[j] = (dp[j] - ans[j - v[i]] + 10) % 10;
for(int j = 1; j <= m; j ++) cout << ans[j];
cout << endl;
}
}
值得一提的是,具有回退性质的背包种类很少,大多数都是 01 背包,这与回退背包的主要思想有关。回退背包的思想是:背包问题与物品的求解顺序无关,因此每一个物品都可以认为是最后一个被求解的,那就可以对着 dp 的式子逆向操作,退回这个物品的 dp 状态。
其实回退背包的回退转移式更多是通过对原转移式逆向操作得到的,因为有一些题目很难通过纯分析来推导回退转移式。
如何通过逆向操作得到回退转移式呢?我们用上面的题目为例,原转移式有:
遍历顺序
对于
对于
那么逆向操作即为:
遍历顺序
对于
对于
显然
上面的题作为最基础的回退背包还是太简单了,接下来给大伙上上强度。
题目
1.
Nowcoder890A Blackjack,2019 牛客暑期多校 Day10
题目大意:有
n 张牌,每张牌有一个点数x_i 。你可以不断抽牌并随时中止,每次抽牌随机等概率从牌堆抽取一张,抽的牌点数和大于a 则获胜,但点数和大于b 则失败。求最优操作下获胜的概率。1 \leq n \leq 500, 1 \leq a < b \leq 500,1 \le x_i \le 500,\sum_{i=1}^n x_i > b.
首先我们很容易想到 dp。
记
转移式即为:
那么这似乎就已经做完了。对于选了
但是这样做其实是不对的,样例 2 就是 hack。假如
那如何正确计算呢?有一个好的思路是,我们可以钦定最后一张牌选的是哪张,这张牌点数是
但是显然,每次钦定一张牌都做一次背包是很慢的,所以我们考虑对所有牌做完一次背包后,每次把钦定的那张牌给退掉,这样也达到了相同的效果。
逆向操作一下转移式,可以得到回退转移式。
回退转移式:
(由于转移不会影响同一层
这样做出来的
需要注意的是,计算 dp 数组的过程中会爆 long long,所以要开 long double。
时间复杂度
main code
void solve()
{
int n, a, b; cin >> n >> a >> b;
vector <int> x(n + 1);
for(int i = 1; i <= n; i ++) cin >> x[i];
vector <vector <long double> > dp(n + 1, vector <long double> (b + 1, 0));
dp[0][0] = 1;
for(int i = 1; i <= n; i ++){
for(int j = i; j >= 1; j --){
for(int k = x[i]; k <= b; k ++){
dp[j][k] += dp[j - 1][k - x[i]];
}
}
}
long double ans = 0;
for(int i = 1; i <= n; i ++){
vector <vector <long double> > tdp = dp;
for(int j = 1; j <= n; j ++){
for(int k = x[i]; k <= b; k ++){
tdp[j][k] -= tdp[j - 1][k - x[i]];
}
}
for(int j = 0; j < n; j ++){
long double sum = 0;
for(int k = max(0, a - x[i] + 1); k <= min(a, b - x[i]); k ++){
sum += tdp[j][k];
}
for(int k = n - j - 1; k >= 1; k --)
sum = sum * k / (k + j + 1);
ans += sum / (j + 1);
}
}
cout << fixed << setprecision(12) << ans << endl;
}
2.
Gym105459E Marble Race,2024 CCPC 哈尔滨站 E
题目大意:
m 个球,速度为每单位时间v_1,v_2,\dots,v_m ,n 个在负半轴的位置x_1,\dots,x_n ,每个球都会随机选择一个位置,然后向正方向移动,求坐标中位数在原点的期望时间,对10^9+7 取模。
首先有一个不太难想的
由于
所以我们要求的是在这个时间
如何去优化它?观察球的状态数量,虽然一共有
所以这启发我们将所有时间
所以我们可以先对所有的球进行一次 dp,然后从小到大遍历每个
来看一下转移式。对于一个时间
遍历顺序
对于
对于
那么逆向操作即为:
遍历顺序
对于
对于
我们每次是先进行逆向操作,统计贡献后,再正向操作回去。统计贡献前后,因为时间推进,其能够到达原点的位置会多一个,也就是有
为了方便统计,我们可以把
这样做的时间复杂度降到了
main code
constexpr int mod = 1e9 + 7;
ll qpow(ll b, ll k)
{
ll res = 1;
while(k){
if(k & 1) res = (res * b) % mod;
b = (b * b) % mod;
k >>= 1;
}
return res;
}
ll inv(ll b)
{
return qpow(b, mod - 2);
}
void solve()
{
int n, m; cin >> n >> m;
vector <ll> x(n + 1), v(m + 1);
vector <ll> invv(m + 1), invi(n + 1);
for(int i = 1; i <= n; i ++) cin >> x[i], x[i] = -x[i], invi[i] = inv(i);
for(int i = 1; i <= m; i ++) cin >> v[i], invv[i] = inv(v[i]);
sort(x.begin() + 1, x.end());
vector <array <int, 2> > t;
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= m; j ++){
t.push_back({i, j});
}
}
sort(t.begin(), t.end(), [&](auto A, auto B){
return x[A[0]] * v[B[1]] < x[B[0]] * v[A[1]];
});
ll ans = 0;
vector <ll> dp(m + 1, 0);
dp[0] = 1; //最开始可以视作时间为 0,没有球能到原点
for(auto [a, b] : t){
dp[0] = dp[0] * (n * invi[n - a + 1] % mod) % mod;
for(int i = 1; i <= m; i ++) dp[i] = (dp[i] - dp[i - 1] * ((a - 1) * invi[n] % mod) % mod + mod) % mod * (n * invi[n - a + 1] % mod) % mod;
ans = (ans + dp[m / 2] * (x[a] * invv[b] % mod) % mod) % mod;
for(int i = m; i >= 0; i --){
dp[i] = dp[i] * ((n - a) * invi[n] % mod) % mod;
if(i) dp[i] = (dp[i] + dp[i - 1] * (a * invi[n] % mod) % mod) % mod;
}
}
cout << ans * invi[n] % mod << endl;
}