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\)

\[\begin{aligned} dp_{i,j}\times p_i&\rightarrow dp_{i-1,j+1}\\ dp_{i,j}\times (1-p_i)&\rightarrow dp_{i-1,\max(0, j-1)} \end{aligned} \]

于是我们获得了一个 \(O(n^3)\) 的做法:枚举 \(k\),然后 \(O(n^2)\) 做 DP。

考虑优化。

此处用一个经典的 trick:注意到对于不同的 \(k\),转移是相同的,只有初值在变。那么每个 \(dp\) 对答案的贡献系数是固定的,使用反推贡献系数的方法,设 \(g_{i,j}\) 表示 \(dp_{i,j}\) 对答案的贡献系数,初值 \(g_{1,i}=h_i\),那么:

\[g_{i,j}=p_ig_{i-1,j+1}+(1-p_i)g_{i-1,\max(0,j-1)} \]

长度 \(k\) 的答案为 \(g_{k+1,0}\)。复杂度 \(O(n^2)\)


本题还有一个直接 \(O(n^2)\) 的高妙状态定义。不记录最大前缀和的具体值,而是记录一个 "与目标的差",非常高妙。

假设我们钦定了 \(a\) 数组的最大前缀和为 \(x\),那么 \(a\) 的前缀和数组 \(s\) 需要满足:

  1. \(\forall\ 0\le i\le n\)\(s_i\le x\)
  2. \(\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\) 子树内的拓扑序方案数,则:

\[cnt_i=\binom{sz_i-1}{sz_{j_1},sz_{j_2},\dots}\cdot \prod_{j\in son(i)}cnt_j \]

看到 \(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}\) 的贡献简单算出。

\[f(i-1,j+k)\leftarrow f(i,j)\cdot \binom{j+k-1}{j}\cdot\binom{sz_{a_{i-1}}-(j+k)-1}{sz_{a_i}-j-1} \cdot C \]

先在 \(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\)

\[g(i-1,j+k)\times \binom{j+k-1}{j}\cdot\binom{sz_{a_{i-1}}-(j+k)-1}{sz_{a_i}-j-1} \rightarrow g(i,j) \]

\(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;
}
posted @ 2025-11-15 21:32  FLY_lai  阅读(31)  评论(0)    收藏  举报