HDOJ 4661: Message Passing

题目链接:

http://acm.hdu.edu.cn/showproblem.php?pid=4661


题目大意:

有n个人,每个人知道一个独特的信息。

每一次操作,我们可以让一个人把自己知道的所有信息告诉另一个人。

要求经过最少次数的操作,可以让所有人知道所有信息。

求方案数。


算法:

显然最好的方法是所有人把消息先告诉同一个人,然后再由这个人把这个消息传给所有人。

这就构成了一个树形结构。

以某点为根,求出各点先收集自己子树的信息,再传递给父节点的方法数。

由于根节点收集信息的方法数等于把信息传递出去的方法数,所以取平方即可。

实际就是一棵树,以每个节点为根,得到的拓扑序列方法数的平方和。

不过我们要避免枚举根节点,否则会TLE。


有向树的拓扑方案数等于 n! 除以 以每个点为根的子树的大小的乘积。

所以就可以用一个很简单的树形DP来求出以各点为根的拓扑方案数。

具体可参见wzk巨巨的代码

但是我想介绍一个比较麻烦的树形DP,因为这种处理的方式可能在别的地方用到。


要明白这个DP过程,首先要会求解一个问题:

(1)已知若干有序序列,现在把他们穿插组成一个新序列,使各原序列中的相对顺序不变。求方法数。

(2)已知上一个问题(1)的方法数,现在要把某个序列的元素全部从新序列中抽出来,问余下的序列有多少个不重复的。


比如序列1有1个数,序列2有2两个数,序列3有3个数。

问题(1)的方法数就是:C(1,1)*C(1+2,2)*C(1+2+3,3)

现在如果把序列2全部抽出来,

问题(2)的方法数就是:问题(1)的方法数/C(1+2+3,2)

证明方法比较简单,不解释了。


这道题实际要求两个方法数。

第一个是d1[u],代表从u的子树汇集到u的方法数。

第二个是d2[u],代表从u以外的节点汇集到u的方法数。


求解d1[u]的过程要从下向上更新。

假设u有若干棵子树: v1, v2, v3 ......

d1[u] = d1[v1] * C(size[v1], size[v1]) * d1[v2] * C(size[v1] + size[v2], size[v2]) * d1[v3] * C(size[v1] + size[v2] + size[v3], size[v3]) ......

看出来了么,就跟问题(1)是一样的。

对于各子树的每一种方法,它的内部已经是有序的。只要把它们穿插起来就可以了。


求解d2[u]的过程要从上向下更新

假设u的父节点是p[u].

那么信息从p[u]传递到点u时。

注意到此时u的兄弟节点的信息仍然要先汇集到p[u],再传递到点u。

p[u]的子树以外的节点信息,也要先汇集到p[u],再传递到点u。


如上图,我们来计算红色节点的d2[u]。

当信息从红色节点u的蓝色根节点p[u]传递到u时,

也就是树的根由p[u]换到u时,

点u(红色)的子节点(蓝色)仍然是它的子节点。

p[u](蓝色)原本是点u的父节点,现在变成了它的子节点。

p[u]除u以外的子节点(紫色)仍然是它的子节点。

点p[u]的父节点p[p[u]](紫色)也变成了它的子节点

调整后的关系图如下:


现在要求的d2[u],是原图中点u子树以外的点汇集到点u的方法数。

那么,d2[u] = (d2[p[u]] * C(n - size[u] - 1, n - size[p[u]])) * (d1[p[u]]  / (d1[u] * C(size[p[u]] - 1, size[u])))

就是先把p[u]子树以外的点,也就是新图中以p[p[u]]为根的子树,作为p[u]的一棵子树插入。

方法数是d2[p[u]] * C(n - size[u] - 1, n - size[p[u]])。

新图中以p[u]的各子节点为根的子树大小之和是n - size[u] - 1,以p[p[u]]为根的子树大小是n - size[u]

然后要把以点u为根的子树从d1[p[u]]中去除,也就是d1[p[u]]  / (d1[u] * C(size[p[u]] - 1, size[u]))

原图中以p[u]的各子节点为根的子树大小之和是size[p[u]] - 1,以u为根的子树大小是size[u]。


最后,所有点的d1[u] * d2[u]*C(n - 1, n - size[u])得到的乘积和就是答案。

要乘以C(n - 1, n - size[u])是因为把原图中u的子树和原图u子树以外的部分分别作为新图中u的子树,合并。


PS: 感谢xiaodao的解题报告为我提供了两种思路。

PPS:向叉姐学习了求逆元的一行写法,开心 >__<

PPPS:像我这种搞DP但是不搞数学的人。排列组合和数论可是一定要学好的哟  TwT


代码:

#pragma comment(linker,"/STACK:102400000,102400000")
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <sstream>
#include <cstdlib>
#include <cstring>
#include <string>
#include <climits>
#include <cmath>
#include <queue>
#include <vector>
#include <stack>
#include <set>
#include <map>
#define INF 0x3f3f3f3f
#define eps 1e-8
using namespace std;

const int MOD = 1000000007;
const int MAXN = 1000010;

vector<int> mm[MAXN];
int c[MAXN], d1[MAXN], d2[MAXN];
int size[MAXN];
int ans;
int n;

inline int fsqrt(int x)
{
    return (long long)x * x % MOD;
}

int inv(int x)
{
    return x <= 1 ? x : (long long)(MOD - MOD / x) * inv(MOD % x) % MOD;
}

inline int C(int n, int k)
{
    return (long long)c[n] * inv(c[n - k]) % MOD * inv(c[k]) % MOD;
}

void dfs1(int u, int p)
{
    d1[u] = 1;
    size[u] = 0;
    for (int i = 0; i < mm[u].size(); i ++)
    {
        int v = mm[u][i];
        if (v == p)
        {
            continue;
        }
        dfs1(v, u);
        size[u] += size[v];
        d1[u] = (long long)d1[u] * d1[v] % MOD * C(size[u], size[v]) % MOD;
    }
    size[u] ++;
}

void dfs2(int u, int p)
{
    ans = (ans + fsqrt((long long)d1[u] * d2[u] % MOD * C(n - 1, n - size[u]) % MOD)) % MOD;
    for (int i = 0; i < mm[u].size(); i ++)
    {
        int v = mm[u][i];
        if (v == p)
        {
            continue;
        }
        d2[v] = (long long)d1[u] * d2[u] % MOD * C(n - size[v] - 1, n - size[u]) % MOD;
        d2[v] = (long long)d2[v] * inv(d1[v]) % MOD * inv(C(size[u] - 1, size[v])) % MOD;
        dfs2(v, u);
    }
}

void init()
{
    c[0] = 1;
    for (int i = 1; i < MAXN; i ++)
    {
        c[i] = (long long)c[i - 1] * i % MOD;
    }
}

int main()
{
    init();
    int cas;
    scanf("%d", &cas);
    while (cas --)
    {
        ans = 0;
        scanf("%d", &n);
        for(int i = 0; i < n; i ++)
        {
            mm[i].clear();
        }
        for(int i = 1; i < n; i ++)
        {
            int u, v;
            scanf("%d %d", &u, &v);
            u --;
            v --;
            mm[u].push_back(v);
            mm[v].push_back(u);
        }
        dfs1(0, -1);
        d2[0] = 1;
        dfs2(0, -1);
        printf("%d\n", ans);
    }
    return 0;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值