Sugar Sweet II

Sugar Sweet II

有向基环树,期望

题意

有 \(n\) 个孩子,第 \(i\) 个孩子初始有 \(a_i\) 袋糖。有 \(n\) 个事件以均匀随机顺序发生,第 \(i\) 个事件为:

  • 若第 \(i\) 个孩子当前糖数严格少于第 \(b_i\) 个孩子,则第 \(i\) 个孩子额外获得 \(w_i\) 袋糖;否则什么也不发生。

求所有事件结束后每个孩子糖数的期望,模 \(10^9+7\)。

建图:有向基环树森林

把每个孩子 \(i\) 看成一个点,连有向边 \(i \to b_i\)。每个点出度为 \(1\),因此整个图是有向基环树森林(functional graph)。

关键观察:每个事件只增加自己的糖,依赖关系只沿着 \(i \to b_i\) 单向传播。所以不需要对基环树做复杂的环上 DP。

三类分类

对每个点 \(i\),令 \(v = b_i\),比较 \(a_i\) 与 \(a_v\)、\(a_v + w_v\):

A 类:\(a_i < a_v\)

无论 \(v\) 是否成功,\(v\) 当前糖数至少为 \(a_v > a_i\),所以 \(i\) 一定成功:

\[P(i\text{ 成功}) = 1 \]

B 类:\(a_i \ge a_v + w_v\)

无论 \(v\) 是否成功,\(v\) 当前糖数最多为 \(a_v + w_v \le a_i\),所以 \(i\) 一定失败:

\[P(i\text{ 成功}) = 0 \]

C 类:\(a_v \le a_i < a_v + w_v\)

此时 \(i\) 成功当且仅当:

  1. \(v\) 成功;
  2. 且事件 \(v\) 在事件 \(i\) 之前执行。

即

\[I_i = I_v \cdot [pos_v < pos_i] \]

其中 \(I_i\) 表示事件 \(i\) 是否成功,\(pos_i\) 表示事件 \(i\) 在随机排列中的位置。

沿反向边传播

建反向边:

\[v \to i \quad \text{当且仅当} \quad b_i = v \]

从所有 A 类点开始做 BFS / DFS。A 类点深度为 \(0\),沿反向边每走一步深度加 \(1\)。

  • A 类点自己深度为 \(0\),概率为 \(1\);
  • 若一个 C 类点 \(i\) 到最近的 A 类祖先的距离为 \(d\)(边数),则成功概率为 \(\dfrac{1}{(d+1)!}\);
  • B 类点以及无法到达 A 类的纯环上的点,概率均为 \(0\)。

最终:

\[E[\text{最终糖数}_i] = a_i + w_i \cdot P(i\text{ 成功}) \]

模意义下用逆阶乘 invfact 即可。

为什么 C 类概率是 \(1/(d+1)!\)

设从 \(i\) 沿 \(b\) 指针往上游走,直到遇到一个 A 类点(恒成功点)为止,路径为:

\[x_0 \to x_1 \to x_2 \to \cdots \to x_d = i \]

其中:

  • \(x_0\) 是 A 类点,恒成功;
  • 每个 \(x_j\)(\(j \ge 1\))都是 C 类点,且 \(b_{x_j} = x_{j-1}\);
  • \(d\) 是边数,因此链上一共有 \(d+1\) 个点。

对于 C 类点 \(x_j\),它成功当且仅当:

  1. 它的父亲 \(x_{j-1}\) 成功;
  2. 事件 \(x_{j-1}\) 在事件 \(x_j\) 之前执行。

因此要让 \(i = x_d\) 成功,必须同时满足:

\[x_0 \text{ 在 } x_1 \text{ 前},\quad x_1 \text{ 在 } x_2 \text{ 前},\quad \dots,\quad x_{d-1} \text{ 在 } x_d \text{ 前} \]

也就是说,这 \(d+1\) 个事件在随机排列中必须恰好按顺序

\[x_0,\ x_1,\ x_2,\ \dots,\ x_d \]

出现。

只看这 \(d+1\) 个事件的相对顺序,共有 \((d+1)!\) 种等可能情况,而满足上述固定顺序的只有 \(1\) 种。因此:

\[P(i\text{ 成功}) = \frac{1}{(d+1)!} \]

充分性:若链上事件按 \(x_0, x_1, \dots, x_d\) 的顺序发生,则 \(x_0\) 恒成功;轮到 \(x_1\) 时,父亲 \(x_0\) 已经成功且糖数满足条件,所以 \(x_1\) 成功;依次类推,最终 \(i = x_d\) 成功。

必要性:若 \(i\) 成功,则 \(x_{d-1}\) 必须成功且先于 \(i\) 发生;再递归,得到整条链必须按祖先到后代的顺序发生。

所以二者等价,概率为 \(\dfrac{1}{(d+1)!}\)。

分叉不影响单个点的概率

反向边图里,一个点 \(v\) 可能有很多"孩子" \(i\) 满足 \(b_i = v\),反向看会分叉。但这不影响单个点成功概率是 \(1/(d+1)!\),因为:

每个点 \(i\) 只依赖它唯一的父亲 \(b_i\),不依赖兄弟。

虽然反向看会分叉,但从任意一个点沿 \(b\) 指针往上游走,路径是唯一的。对于某个点 \(i\),它要成功,只要求它自己到最近 A 类祖先这条唯一链上的事件按固定顺序发生。兄弟节点的事件怎么排、是否成功,都不会改变这条链上 \(d+1\) 个事件的相对顺序概率。

例子

假设 \(x_0\) 是 A 类点,\(b_{x_1} = x_0\),\(b_{x_2} = x_0\),\(b_{x_3} = x_1\)。反向边图为:

\[x_0 \to x_1,\quad x_0 \to x_2,\quad x_1 \to x_3 \]

  • \(x_1\) 的链:\(x_0, x_1\),长度 \(d = 1\),成功概率 \(\dfrac{1}{2!}\);
  • \(x_2\) 的链:\(x_0, x_2\),长度 \(d = 1\),成功概率 \(\dfrac{1}{2!}\);
  • \(x_3\) 的链:\(x_0, x_1, x_3\),长度 \(d = 2\),成功概率 \(\dfrac{1}{3!}\)。

注意 \(x_1\) 和 \(x_2\) 是兄弟,但计算 \(x_1\) 成功时不需要管 \(x_2\)。在随机排列中,\(x_2\) 的事件可以出现在任意位置,它只是"插入"到 \(x_0, x_1\) 的相对顺序中,不会改变 \(x_0\) 在 \(x_1\) 前这一相对顺序的概率。

所以 BFS 传播仍然正确:分叉只是让 BFS 同时访问多个分支,每个分支的深度按各自路径计算,不会出现"多条路径同时决定一个点"的情况。

算法流程

  1. 读入 \(a_i, b_i, w_i\);
  2. 对每个 \(i\),令 \(v = b_i\),比较 \(a_i\) 与 \(a_v\)、\(a_v + w_v\),标记为 A / B / C 类;
  3. 建反向边 \(v \to i\);
  4. 所有 A 类点入队,vis = 1,pro = 1,dist = 0;
  5. BFS:对队首 \(u\) 的每个孩子 \(v\),若 \(v\) 是 C 类且未访问,则 dist[v] = dist[u] + 1,pro[v] = invfact[dist[v] + 1],入队;
  6. 对每个 \(i\),输出 \(a_i + w_i \cdot pro[i] \bmod (10^9+7)\)。

复杂度

每个点只会被访问一次,反向边总数为 \(n\),因此单组数据:

\[O(n) \]

总复杂度:

\[O\left(\sum n\right) \]

满足 \(\sum n \le 5 \cdot 10^5\) 的限制。

#include<bits/stdc++.h>
using namespace std;
using ll = long long;
using pii = pair<int, int>;
using tpi = tuple<int, int, int>;
const ll MOD = 1e9 + 7;
const int N = 1e6;

ll modpow(ll a, ll b) {
    ll r = 1;
    while(b) {
        if(b & 1) r = r * a % MOD;
        a = a * a % MOD;
        b >>= 1;
    }
    return r;
}
ll fact[N], invfact[N];

void init() {
    fact[0] = 1;
    for(int i = 1; i < N; i ++) fact[i] = fact[i - 1] * i % MOD;
    invfact[N - 1] = modpow(fact[N - 1], MOD - 2);
    for(int i = N - 2; i >= 0; i --) invfact[i] = invfact[i + 1] * (i + 1) % MOD;
}

void solve() {
    int n; cin >> n;
    vector<vector<int>> g(n + 1, vector<int>());
    vector<ll> a(n + 1), w(n + 1);
    vector<int> b(n + 1);
    for(int i = 1; i <= n; i ++) cin >> a[i];
    for(int i = 1; i <= n; i ++) cin >> b[i];
    for(int i = 1; i <= n; i ++) cin >> w[i];

    vector<int> type(n + 1, -1); // 1必定发生  0必定不发生  2可能发生
    for(int i = 1; i <= n; i ++) {
        int v = b[i];
        if(a[i] < a[v]) type[i] = 1;
        else if(a[i] >= a[v] + w[v]) type[i] = 0;
        else type[i] = 2;

        g[v].push_back(i);
    }


    vector<int> dist(n + 1, 0);
    vector<int> vis(n + 1, 0);
    vector<ll> pro(n + 1, 0);
    queue<int> q;
    for(int i = 1; i <= n; i ++) {
        if(type[i] == 1) q.push(i), vis[i] = 1, pro[i] = 1;
    }

    while(!q.empty()) {
        int u = q.front(); q.pop();
        for(int v : g[u]) {
            if(vis[v] || type[v] != 2) continue;
            dist[v] = dist[u] + 1;
            pro[v] = invfact[dist[v] + 1];
            vis[v] = 1;
            q.push(v);
        }
    }
    for(int i = 1; i <= n; i ++) {
        ll ans = (a[i] + (w[i] % MOD) * pro[i]) % MOD;
        cout << ans << " \n"[i == n];
    }
}

int main() {
    ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
    init();
    int _ = 1; cin >> _;
    while(_--) solve();
    return 0;
}
posted @ 2026-11-03 22:08  thenrify  阅读(2)  评论(0)    收藏  举报