DP 技巧:反推贡献系数
-
常见于 DP 优化。但这个 trick 的本质应该比 DP 优化更深刻。
-
DP 的转移是 DAG,如果枚举起点,从不同起点出发,求到终点的贡献;(当贡献可逆时)可以反过来求终点到起点的贡献,则省略了枚举起点的步骤。
-
可以理解为反求贡献系数。
-
特点:
-
贡献系数固定(DAG 边权固定)。
-
不同起点只有 DP 初值改变。
-
例题一:【CF1810G The Maximum Prefix】
首先考虑对于固定的 \(k\) 怎么做。如果从前往后 DP,需要记录当前位置、最大前缀和、总和,做不了。考虑从后往前 DP,只需要记录位置和最大前缀和即可。\(dp_{i,j}\) 表示考虑了 \(i\sim n\),最大前缀和等于 \(k\) 的概率。初值 \(dp_{n+1,0}=1\)。
于是我们获得了一个 \(O(n^3)\) 的做法:枚举 \(k\),然后 \(O(n^2)\) 做 DP。
考虑优化。
此处用一个经典的 trick:注意到对于不同的 \(k\),转移是相同的,只有初值在变。那么每个 \(dp\) 对答案的贡献系数是固定的,使用反推贡献系数的方法,设 \(g_{i,j}\) 表示 \(dp_{i,j}\) 对答案的贡献系数,初值 \(g_{1,i}=h_i\),那么:
长度 \(k\) 的答案为 \(g_{k+1,0}\)。复杂度 \(O(n^2)\)。
本题还有一个直接 \(O(n^2)\) 的高妙状态定义。不记录最大前缀和的具体值,而是记录一个 "与目标的差",非常高妙。
假设我们钦定了 \(a\) 数组的最大前缀和为 \(x\),那么 \(a\) 的前缀和数组 \(s\) 需要满足:
- \(\forall\ 0\le i\le n\),\(s_i\le x\)。
- \(\exists\ 0\le i\le n\),\(s_i=x\)。
形象地,我们称这个钦定的最大前缀和 \(x\) 为 “目标”。令 \(f_{i,j,0/1}\) 表示仅考虑前 \(i\) 个元素,当前前缀和 \(s_i\) 离目标还差 \(j\),之前达到过 / 未达到过目标的期望得分。则对于长度 \(i\),其答案即为当前所有达到过目标的状态的期望得分之和。
初始化:\(f_{0,i,[i=0]}=h_i\)。
转移方程:\(f_{i,j,o\ |\ [j=0]}=f_{i-1,j+1,o}\times p_i+f_{i-1,j-1,o}\times (1-p_i)\)。
长度为 \(k\) 时的答案:\(\sum_{i=0}^nf_{k,i,1}\)。
注意处理边界问题,具体见代码。
时间复杂度为 \(O(n^2)\)。
代码:倒推系数。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 5050;
const ll mod = 1000000007;
inline ll fpow(ll a, ll b, ll p = mod) {
ll mul = 1;
while (b) {
if (b & 1)
mul = mul * a % p;
a = a * a % p;
b >>= 1;
}
return mul;
}
ll n, p[N], f[N][N], a[N];
void slv() {
cin >> n;
for (int i = 1, x, y; i <= n; i++) {
cin >> x >> y;
p[i] = x * fpow(y, mod - 2) % mod;
}
for (int i = 0; i <= n; i++) {
cin >> a[i];
f[1][i] = a[i];
}
for (int i = 1; i <= n; i++)
for (int j = 0; j <= n; j++)
f[i + 1][j] = ((j < n ? f[i][j + 1] : 0) * p[i] % mod + f[i][max(j - 1, 0)] * (mod + 1 - p[i]) % mod) % mod;
for (int i = 2; i <= n + 1; i++)
cout << f[i][0] << ' ';
cout << '\n';
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int T;
cin >> T;
while (T--)
slv();
return 0;
}
例题二:【P10013 Tree Topological Order Counting】
\(a_u\) 表示 \(u\) 在拓扑序中的位置。
首先考虑一棵树的拓扑序怎么求,记 \(cnt_i\) 为 \(i\) 子树内的拓扑序方案数,则:
看到 \(n\le 5000\),一个直接的想法是枚举 \(u\) 然后 \(O(n)\) 算 \(f(u)\)。考虑到当 \(u\) 要出现在拓扑序里,\((1,u)\) 路径上每个点都要出现,因此这启发我们对 \((1,u)\) 这条链做操作。
把这条链拿出来,记为 \(a_1,a_2,\dots,a_k\),其中 \(a_1=1,a_k=u\)。以这条链为根把这棵树拎起来,每个点下面都会挂若干个子树。考虑按 \(k\rightarrow 1\) 的顺序 DP,状态里也只考虑一个后缀的拓扑序,每次把 \(a_i\) 下方的若干个点插入到 \(u\) 之前进行转移。
记 \(f(i,j)\) 表示考虑了 \(a_i\sim a_k\) 的拓扑序,目前有 \(j\) 个点排在 \(u\) 前面的方案数。初值 \(f(k,0)\) 等于 \(u\) 子树内的拓扑序个数。
考虑转移 \(i\rightarrow i-1\)。先预处理出 \(a_{i-1}\) 子树内排除 \(a_i\) 子树的拓扑序个数,记为 \(C\)。这个 \(C\) 显然可以用 \(cnt_{a_{i-1}}\) 除掉 \(cnt_{a_i}\) 的贡献简单算出。
先在 \(a_i\) 子树里弄出 \(j\) 个排在 \(u\) 前面,方案数 \(f(i,j)\);
然后在目标排在 \(u\) 前面的 \(j+k\) 个位置里选 \(j\) 个放 \(a_i\) 子树里的 \(j\) 个,但是因为 \(a_{i-1}\) 必须排在第一个,所以一号位不能选,因此是 \(j+k-1\) 选 \(j\);
然后类似地选位置放排在 \(u\) 后面的个数,要减去 \(u\) 自己占的位置;
最后 \(a_{i-1}\) 非 \(a_i\) 子树的结点内部有 \(C\) 种方案。
那么 \(u\) 的答案就是 \(\sum_{i\ge 0} b_i\cdot f(1,i+1)\)。于是我们获得了一个 \(O(n^3)\) 的做法。
考虑怎么优化。注意这个 DP 转移的形式非常相似,只有初值不同。类比【CF1810G The Maximum Prefix】。考虑设 \(g(i,j)\) 表示 \(f(i,j)\) 的贡献系数,初值 \(g(i,i+1)=b_i\):
点 \(i\) 的答案为 \(i\) 子树内的拓扑序方案数乘以 \(g(i,0)\),于是优化到 \(O(n^2)\)。
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 2e5 + 5, mod = 1e9 + 7;
int n;
vector<int> e[N];
int fac[N], ifac[N];
int fpow(int a, int b = mod - 2, int p = mod) {
int mul = 1;
while (b) {
if (b & 1)
mul = mul * a % p;
a = a * a % p;
b >>= 1;
}
return mul;
}
void init(int n) {
fac[0] = 1;
for (int i = 1; i <= n; i++)
fac[i] = fac[i - 1] * i % mod;
ifac[n] = fpow(fac[n]);
for (int i = n - 1; i >= 0; i--)
ifac[i] = ifac[i + 1] * (i + 1) % mod;
}
inline int C(int n, int m) {
return m < 0 || m > n ? 0 : fac[n] * ifac[m] % mod * ifac[n - m] % mod;
}
int cnt[N], sz[N];
int g[5005][5005], ans[N];
void dfs1(int x) {
cnt[x] = 1;
sz[x] = 1;
for (auto i: e[x]) {
dfs1(i);
sz[x] += sz[i];
cnt[x] = cnt[x] * cnt[i] % mod * ifac[sz[i]] % mod;
}
cnt[x] = cnt[x] * fac[sz[x] - 1] % mod;
}
void dfs2(int x) {
ans[x] = g[x][0] * cnt[x] % mod;
for (auto i: e[x]) {
int coef = cnt[x] * fac[sz[i]] % mod * fac[sz[x] - sz[i] - 1] % mod * fpow(cnt[i] * fac[sz[x] - 1] % mod) % mod;
for (int j = 0; j < sz[i]; j++)
for (int k = 1; k <= sz[x] - sz[i]; k++)
(g[i][j] += C(j + k - 1, j) * C(sz[x] - j - k - 1, sz[i] - j - 1) % mod * g[x][j + k] % mod * coef % mod) %= mod;
dfs2(i);
}
}
signed main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
init(N - 10);
cin >> n;
for (int i = 2, pr; i <= n; i++) {
cin >> pr;
e[pr].push_back(i);
}
for (int i = 0; i < n; i++)
cin >> g[1][i];
dfs1(1);
dfs2(1);
for (int i = 1; i <= n; i++)
cout << ans[i] << ' ';
cout << '\n';
return 0;
}

浙公网安备 33010602011771号