《GMOJ-Senior-6892 树的解构》题解

该博客讨论了一种外向树的删除边操作问题,其中每条边的删除代价与其删除时形成子树的大小有关。博主分析了每个节点对总代价的贡献,并提出自底向上遍历树来计算节点大小和深度的方法。关键在于使用线性求逆元加速计算总期望代价,最后给出了实现该算法的代码。这道题目的难点在于理解和应用线性求逆元技术。
摘要由CSDN通过智能技术生成

题目大意

给出一棵 n n n个结点的根为 1 1 1的外向树。定义删去一条边 a → b a \rightarrow b ab的代价为当时以 b b b为根的子树的大小。现在进行 ( n − 1 ) (n−1) (n1)次操作,其中每一次操作会等概率地选择一条未被删去的边删去,求总代价的期望值对 1 0 9 + 7 10^9+7 109+7取模的值。
对于 10 % 10\% 10%的数据,图为菊花图;
对于另 15 % 15\% 15%的数据,图为链;
对于另 25 % 25\% 25%的数据, n ≤ 500 n \leq 500 n500
对于 100 % 100\% 100%的数据, 1 ≤ n ≤ 2 × 1 0 6 1 \leq n \leq 2 \times10^6 1n2×106

分析

分析以每个结点为根的子树的大小(记为 s i z e 1 ⋯ n size_{1 \cdots n} size1n)对答案的贡献。设结点 v v v为结点 u u u的祖先,可以发现:当先删去连接 u u u和它父亲的边,再删去连接 v v v和它父亲的边时,它们对总代价的贡献相当于先删去连接 v v v和它父亲的边,再不计其代价地删去连接 u u u和它父亲的边。于是可知删去连接 u u u和它父亲的边对总代价有 s i z e u size_u sizeu的贡献当且仅当 u u u到根结点的其它边都被删去,且删去连接 u u u和它父亲的边在其余情况下无贡献。

设根结点到每个结点经过的边数为 d e p t h 1 ⋯ n depth_{1 \cdots n} depth1n,则可得结点 a a a对总代价的期望值有贡献的概率为 1 a \frac{1}{a} a1,最终的答案为 ∑ i = 2 n s i z e i d e p t h i \sum_{i=2}^n \frac{size_i}{depth_i} i=2ndepthisizei。于是可以用搜索求出 s i z e 1 ⋯ n size_{1 \cdots n} size1n d e p t h 1 ⋯ n depth_{1 \cdots n} depth1n,再用线性求逆元(设 i n v i inv_i invi i i i在模 m m m意义下的逆元,则有 i n v 1 = 1 inv_1=1 inv1=1 i n v i ≡ − ⌊ m ÷ i ⌋ × i n v m m o d    i ( m o d m ) inv_i \equiv −⌊m÷i⌋ \times inv_{m \mod i} \pmod m invim÷i×invmmodi(modm))加速求出答案。时间复杂度为 O ( n ) O(n) O(n)

代码

根据思路,可以写出如下代码:

#include<cstdio>
using ll = long long;
constexpr ll mod = 1000000007; //定义模数
inline ll Read() //快速读入
{
    char c;
    for (c = getchar(); c > '9' || c < '0'; c = getchar());
    ll ans = static_cast<ll>(c) - '0';
    for (c = getchar(); c >= '0' && c <= '9'; ans = (ans * 10) + (static_cast<ll>(c) - '0'), c = getchar());
    return ans;
}
struct Node //定义结点
{
    ll father/*当前节点的父亲(没有则为-1)*/, size/*以当前节点为根的子树的大小*/, depth/*根结点到当前结点经过的边数*/, son_count/*当前结点的儿子个数(确定更新顺序用)*/;
    Node() :son_count(0), father(-1), size(1), depth(0) {} //初始化
}nod[2000002];
ll que[2000002]/*队列*/, inv[2000002]/*逆元*/;
int main()
{
    freopen("deconstruct.in", "r", stdin); //定义文件输入输出
    freopen("deconstruct.out", "w", stdout);
    const ll n = Read(); //读入n
    for (ll i = 2; i <= n; ++i)
    {
        nod[i].father = Read(); //读入结点2到n的父亲
        ++nod[nod[i].father].son_count; //更新儿子个数
    }
    ll head = 0, tail = 0;
    for (ll i = 1; i <= n; ++i)
    {
        if (nod[i].son_count == 0)
        {
            que[tail++] = i; //将所有叶子结点加入队列中
        }
    }
    while (head < tail) //自底向上遍历整棵树
    {
        const ll& from = que[head++]/*取出当前结点*/, & to = nod[from].father/*取出当前节点的父亲*/;
        if (to != -1) //若当前节点有父亲
        {
            nod[to].size += nod[from].size; //求出子树的大小
            if ((--nod[to].son_count) == 0) //若父亲已更新完,继续遍历父亲
            {
                que[tail++] = to;
            }
        }
    }
    ll size_inv = 0;
    for (tail -= 2; tail >= 0; --tail)
    {
        const ll& pos = que[tail]; //取出当前结点
        nod[pos].depth = nod[nod[pos].father].depth + 1; //求出根结点到每个结点经过的边数
        if (nod[pos].depth > size_inv) //求出需要求逆元的范围
        {
            size_inv = nod[pos].depth;
        }
    }
    inv[1] = 1;
    for (ll i = 2; i <= size_inv; ++i) //线性求逆元
    {
        inv[i] = (mod - mod / i) * inv[mod % i] % mod;
    }
    ll ans = 0;
    for (ll i = 2; i <= n; ++i) //统计答案
    {
        ans += inv[nod[i].depth] * nod[i].size % mod;
    }
    printf("%lld", ans % mod); //输出
    return 0;
}

总结

这是一道比较容易想出答案的题目,但是考察了线性求逆元,需要有一定的基础才能通过。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值