zoukankan      html  css  js  c++  java
  • [HAOI2018] 苹果树

    题意:

    小 C 在自己家的花园里种了一棵苹果树, 树上每个结点都有恰好两个分支. 经过细心的观察, 小 C 发现每一天这棵树都会生长出一个新的结点.

    第一天的时候, 果树会长出一个根结点, 以后每一天, 果树会随机选择一个当前树中没有长出过结点 的分支, 然后在这个分支上长出一个新结点, 新结点与分支所属的结点之间连接上一条边.

    小 C 定义一棵果树的不便度为树上两两结点之间的距离之和, 两个结点之间 的距离定义为从一个点走到另一个点的路径经过的边数.

    现在他非常好奇, 如果 N 天之后小 G 来他家摘苹果, 这个不便度的期望 E 是多少. 但是小 C 讨厌分数, 所以他只想知道 E×N! P 取模的结果, 可以证明这是一个整数.

    (这里,节点长出的顺序不同,即使最后形态一样,也算是不同的)

    n<=2000,p<=1e9

    分析:

    期望好懵啊。

    考虑每种不同方案的树的产生的概率都是相等的。必然分母就是种类数了。

    我们第一天只有1种方案,第二天2个空位2种,第三天3个空位3种。。。相当于,每放上一个节点,就少一个空位,多出两个。

    所以一共有N!种不同的方案数。

    这下明白了为什么要乘上N!了。

    所以我们要做的就是统计所有方案数里,每种方案任意点对的距离和,再做和。

    发现,我们针对每种方案枚举点对简直丧心病狂。

    又发现,我们枚举每条树边被点对经过的次数,与枚举点对算距离是等价的。

    对于节点i,i到父亲的边被经过的次数,就是size[i]*(n-size[i]),子树内所有的点,与子树外所有的点组成的所有点对都会经过这条边。

    我们肯定要循环一遍i从2到n,表示枚举i到父亲的所有n-1条边。

    但是,子树大小不知道,而且不确定,发现,n<=2000复杂度支持n^2,所以我们内层再枚举一个子树大小siz(包括根)

    对于给定的i和siz,我们要统计,在所有可能情况中,这条边做的总贡献。

    首先,每个方案作出的贡献,都是(n-siz)*siz

    其次,我们要整出方案数。

    利用乘法原理,方案数就是子树的生成方案数,乘上子树外生成的方案数。

    i号点就是我们现在子树的根节点。

    先考虑子树:

    子树的形态,有siz!种,但是不要忘了,生成先后也算不同的方案,即编号不同,子树也不同。

    由于前面i个点已经确定,所以,还剩n-i个点,要选siz-1个点。C(siz-1,n-i)

    再考虑子树外:

    子树外的形态,第一次1个空位一种,第二次两种,……,第i次i种,但是,到了i+1次,它已经不能放在i的两侧了。因为我们已经把它给了i的子树算过了。

    所以,子树外的形态就是,i!*((i-1)*(i-2)*..*(n-(siz-1)-2).

    而子树外的编号就不用再乘了,因为我们考虑子树内的时候,已经预留了编号 ,剩下的就给了子树外。

    所以:

    点对贡献 -> 每条边被点对覆盖次数 -> 枚举边 -> 枚举子树大小 -> siz*(n-siz) 个点对会经过i到父节点的边 -> 子树形态 -> 子树编号 (即生成节点编号的顺序) -> 子树外形态 (编号之前留下的就是,不用再算了) -> 变形 推公式 -> 定范围。

    公式:          

        (2<=i<=n)(1<=size<=n-i+1) (size[i]*(n-size[i]))*size[i]!*C(size[i]-1,n-i)*i!*((i-1)*...*(n-(size[i]-1)-2))

    -> 组合数打表,最后面的阶乘,利用循环顺序处理

    或者:

        (2<=i<=n)(1<=size<=n-i+1) (size[i]*(n-size[i]))*size[i]!*C(size[i]-1,n-i)* (n-siz-1)! * (i-1) * i 

     利用之前打好的阶乘表直接处理,效率会更高。

    复杂度:O(n^2)

    代码:

    #include<bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    const int N=2000+2;
    int n,p;
    ll c[N][N];
    ll fac[N];
    ll ans;
    ll last;
    ll cas;
    int main()
    {
        scanf("%d%d",&n,&p);
        c[0][0]=1;
        for(int i=1;i<=n;i++)
        {
            c[i][0]=1;
            for(int j=1;j<=n;j++)
             c[i][j]=(c[i-1][j-1]+c[i-1][j])%p;//组合数打表
        }
        fac[0]=1;
        for(int i=1;i<=n;i++)
         fac[i]=(fac[i-1]*i)%p;//阶乘打表
        for(int i=2;i<=n;i++)
        {
         last=1;
         cas=i-1;
         for(int siz=n-i+1;siz>=1;siz--)
         {
             ans=(ans+siz*(n-siz)*fac[siz]%p*c[n-i][siz-1]%p*fac[i]%p*last)%p;
             last=(last*cas)%p;//last记录子树外那部分的乘积
             cas++;
         }
        }//计算
        printf("%lld",ans);
        return 0;
    }
  • 相关阅读:
    C语言学习笔记之 程序流程结构
    C语言学习笔记之 类型转换
    C语言学习笔记之 标准输入输出函数
    C语言学习笔记之 运算符
    bzoj 4322 东西分配问题
    bzoj 3240 矩阵乘法+十进制快速幂
    bzoj 4017 子序列和的异或以及异或的和
    bzoj 1934 最小割
    bzoj 3275 最小割
    bzoj 3931 最短路+最大流
  • 原文地址:https://www.cnblogs.com/Miracevin/p/9081149.html
Copyright © 2011-2022 走看看