题目大意
给出一棵
n
n
n个结点的根为
1
1
1的外向树。定义删去一条边
a
→
b
a \rightarrow b
a→b的代价为当时以
b
b
b为根的子树的大小。现在进行
(
n
−
1
)
(n−1)
(n−1)次操作,其中每一次操作会等概率地选择一条未被删去的边删去,求总代价的期望值对
1
0
9
+
7
10^9+7
109+7取模的值。
对于
10
%
10\%
10%的数据,图为菊花图;
对于另
15
%
15\%
15%的数据,图为链;
对于另
25
%
25\%
25%的数据,
n
≤
500
n \leq 500
n≤500;
对于
100
%
100\%
100%的数据,
1
≤
n
≤
2
×
1
0
6
1 \leq n \leq 2 \times10^6
1≤n≤2×106。
分析
分析以每个结点为根的子树的大小(记为 s i z e 1 ⋯ n size_{1 \cdots n} size1⋯n)对答案的贡献。设结点 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} depth1⋯n,则可得结点 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} size1⋯n和 d e p t h 1 ⋯ n depth_{1 \cdots n} depth1⋯n,再用线性求逆元(设 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 invi≡−⌊m÷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;
}
总结
这是一道比较容易想出答案的题目,但是考察了线性求逆元,需要有一定的基础才能通过。