【题解】CF917D Stranger Trees(prufer序列+二项式反演)

【题解】CF917D Stranger Trees(prufer序列+二项式反演)

考虑有一个东西叫做\(prufer\)序列,然后个东西叫做图联通方案数

然后我们考虑先算一下至少\(k\)条边在方案里的方案数,我们可以用树形dp

\(dp(i,j,k)\)表示对于\(i\)节点及其子树,共有\(j\)条边被选中,且和\(i\)共有\(k\)个点是在一个联通块里的\(\prod siz[]\)之和。转移很简单啦。(但是CF的内存访问极慢!要用对内存友好的写法)

通过\(dp()\)可以很方便的求出来是\(f(k)\)表示至少有\(k\)条边在方案里的方案数。

我们设\(g(k)\)为答案,那么考虑\(f(k)\)和\(g(k)\)的关系

值得注意的事情是,由于我们是在树上选择边,所以我们钦定选择的边是不会成环的,也就是说任何一个选树边的方案都是合法的,所以我们有
\[
f(k)=\sum_{j\ge k} {j\choose k} g(j)
\]
根据二项式反演直接得到
\[
g(k)=\sum_{j\ge k}{j\choose k}(-1)^{k-j}f(j)
\]

//@winlere
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
#include<assert.h>

using namespace std;  typedef long long ll;
inline int qr(){
    int ret=0,f=0,c=getchar();
    while(!isdigit(c))f|=c==45,c=getchar();
    while(isdigit(c)) ret=ret*10+c-48,c=getchar();
    return f?-ret:ret;
}

const int maxn=105;
const int mod=1e9+7;
int dp[maxn][maxn][maxn],n,siz[maxn],ans[maxn];
int invs[maxn*maxn],c[maxn][maxn];
int inv[maxn][maxn*maxn];
vector<int>e[maxn];
void add(int fr,int to){e[fr].push_back(to);e[to].push_back(fr);}
int MOD(const int&x){return x>=mod?x-mod:x;}
int MOD(const int&x,const int&y){return 1ll*x*y%mod;}
int MOD(const int&x,const int&y,const int&z,const int&b){return 1ll*x*y%mod*b%mod*z%mod;}
int ksm(const int&ba,const int&p){
    int ret=1;
    for(int t=p,b=ba;t;t>>=1,b=MOD(b,b))
        if(t&1) ret=MOD(ret,b);
    return ret;
}

void pre(const int&n){
    invs[1]=1;
    for(int t=2;t<=n;++t) invs[t]=MOD(mod-mod/t,invs[mod%t]),assert(MOD(t,invs[t])==1);
    for(int t=n+1;t<=n*n;++t) invs[t]=MOD(mod-mod/t,invs[mod%t]);
    for(int t=1;t<=n*n;++t)
        for(int i=1;i<=n;++i)
            inv[i][t]=MOD(i,invs[t]);
    for(int t=0;t<=n;++t)
        for(int i=c[t][0]=1;i<=t;++i)
            c[t][i]=MOD(c[t-1][i-1]+c[t-1][i]);
    //cerr<<"c[5][2]="<<c[5][2]<<endl;
}

void dfs(const int&now,const int&last){
    dp[now][0][1]=1;
    siz[now]=1;
    for(auto t:e[now]){
        if(t^last){
            dfs(t,now);
            int g[maxn][maxn];
            memset(g,0,sizeof g);
            /*
            for(int i=siz[now]-1;~i;--i){// edge_totol
                for(int j=0,edj=min(siz[t]-1,i);j<=edj;++j){// edge_target
                    for(int p=1,edp=siz[t];p<=edp;++p){// vertices_target
                        if(dp[t][j][p]){
                            for(int k=siz[now];k;--k){// vertices_total
                                int&s=g[i][k];
                                //connect
                                if(i-j-1>=0&&k>p) s=(s+1ll*dp[now][i-j-1][k-p]*dp[t][j][p]%mod*inv[k][(k-p)*p])%mod;
                                //dont connect
                                if(i>=j) s=(s+1ll*dp[now][i-j][k]*dp[t][j][p])%mod;
                            }
                        }
                    }
                }
            }这种写法很不优秀,内存访问非常不连续!
            */
            for(int i=0;i<siz[now];++i)
                for(int k=1;k<=siz[now];++k)
                    for(int j=0;j<siz[t];++j)
                        for(int p=1;p<=siz[t];++p){
                            ll l=1ll*dp[now][i][k]*dp[t][j][p]%mod;
                            g[i+j+1][k+p]=(g[i+j+1][k+p]+l*inv[k+p][k*p])%mod;
                            g[i+j][k]=MOD(g[i+j][k]+l);
                        }
            siz[now]+=siz[t];
            memcpy(dp[now],g,sizeof g);
        }
    }
}

int rt,val=1e9;
void dfsrt(int now,int last){
    siz[now]=1;
    int k=0;
    for(auto t:e[now])
        if(!siz[t])
            siz[now]+=siz[t],k=max(k,siz[t]);
    k=max(k,n-siz[now]);
    if(k<val) rt=now;
}

int main(){
    n=qr();
    pre(101);
    for(int t=1;t<n;++t) add(qr(),qr());
    int k=min(n,55);
    dfs(k,0);
    for(int e=0;e<=n-1;++e){
        for(int i=1;i<=e+1;++i)
            ans[e]=MOD(ans[e]+dp[k][e][i]);
        if(e<=n-2) ans[e]=MOD(ksm(n,n-2-e),ans[e]);
        if(e==n-1) ans[e]=1;
    }
    for(int t=0;t<=n-1;++t){
        int ret=0;
        for(int i=t;i<=n-1;++i){
            int d=MOD(c[i][t],ans[i]);
            if((t^i)&1) ret=MOD(ret-d+mod);
            else ret=MOD(ret+d);
        }
        cout<<ret<<' ';
    }
    cerr<<endl;
    return 0;
}

原文地址:https://www.cnblogs.com/winlere/p/12240444.html

时间: 2024-07-30 16:39:12

【题解】CF917D Stranger Trees(prufer序列+二项式反演)的相关文章

CF917D Stranger Trees

Link 众所周知Kirchhoff定理中的边权可以是多项式. 直接NTT的复杂度是\(O(n^4\log n)\). 带\(n\)个值进去算然后快速插值Lagrange插值或者Gauss消元是\(O(n^4)\)的. #include<bits/stdc++.h> #define LL long long using namespace std; namespace IO { char ibuf[(1<<21)+1],obuf[(1<<21)+1],st[15],*i

【题解】CJOI2019 登峰造鸡境 (Prufer序列+斯特林数)

[题解]CJOI2019 登峰造鸡境 (Prufer序列+斯特林数) 题目背景 舒服了. 题目描述 你有一颗n个点的无根树,每个点有有一个标号(1~n). 现在你知道,总共有m个叶子节点,求不同的树的形态方案数. 答案对\(10^9+7\)取模. 下面是一些可能有用的定义: 叶子:度数为1的点. 不同:若对于两颗标号相同的树\(T1=(V,E_1),T2=(V,E_2)\),\(T1\neq T2\)当且仅当存在\((u,v) \in E_1 ,(u,v) \notin E_2\) 输入格式 一

codeforces 917D Stranger Trees

题目链接 正解:矩阵树定理+拉格朗日插值. 一下午就搞了这一道题,看鬼畜英文题解看了好久.. 首先这道题出题人给了两种做法,感觉容斥+$prufer$序列+$dp$的做法细节有点多所以没看,然而这个做法似乎更难想.. 我们先构造一个函数$f(x)$,表示用一个完全图和$x-1$棵原树的边,构成的生成树的方案数. 也就是说,原树的每条边复制成$x$条,不在原树的边都变成一条边,求这个图的生成树的方案数. 然后我们可以发现,这个方案数实际上就等于$\sum_{i=0}^{n-1}x^{i}*ans_

二项式反演及其应用

概念 二项式反演为一种反演形式,常用于通过 "指定某若干个" 求 "恰好若干个" 的问题. 注意:二项式反演虽然形式上和多步容斥极为相似,但它们并不等价,只是习惯上都称之为多步容斥. 引入 既然形式和多步容斥相似,我们就从多步容斥讲起. 我们都知道:$|A\cup B|=|A|+|B|-|A\cap B|$ ,这其实就是容斥原理. 它的一般形式为: $$|A_1\cup A_2\cup...\cup A_n|=\sum\limits_{1\le i\le n}|A_

【BZOJ1005/1211】[HNOI2008]明明的烦恼/[HNOI2004]树的计数 Prufer序列+高精度

[BZOJ1005][HNOI2008]明明的烦恼 Description 自从明明学了树的结构,就对奇怪的树产生了兴趣......给出标号为1到N的点,以及某些点最终的度数,允许在任意两点间连线,可产生多少棵度数满足要求的树? Input 第一行为N(0 < N < = 1000),接下来N行,第i+1行给出第i个节点的度数Di,如果对度数不要求,则输入-1 Output 一个整数,表示不同的满足要求的树的个数,无解输出0 Sample Input 3 1 -1 -1 Sample Outp

【bzoj1005】[HNOI2008]明明的烦恼 Prufer序列+高精度

题目描述 给出标号为1到N的点,以及某些点最终的度数,允许在任意两点间连线,可产生多少棵度数满足要求的树? 输入 第一行为N(0 < N < = 1000),接下来N行,第i+1行给出第i个节点的度数Di,如果对度数不要求,则输入-1 输出 一个整数,表示不同的满足要求的树的个数,无解输出0 样例输入 3 1 -1 -1 样例输出 2 题解 Prufer序列+高精度 Prufer序列:由一棵 $n$ 个点的树唯一产生的一个 $n-2$ 个数的序列. 生成方法:找到这棵树编号最小的叶子节点,将其

【XSY1295】calc n个点n条边无向连通图计数 prufer序列

题目大意 求\(n\)个点\(n\)条边的无向连通图的个数 \(n\leq 5000\) 题解 显然是一个环上有很多外向树. 首先有一个东西:\(n\)个点选\(k\)个点作为树的根的生成森林个数为: \[ \binom{n}{k}\times n^{n-k-1}\times k \] 前面\(\binom{n}{k}\)是这些根的选编号的方案数,后面是prufer序列得到的:前面\(n-k-1\)个数可以是\(1\)~\(n\),第\(n-k\)个数是\(1\)~\(k\). 我的理解是:每个

[bzoj3622]已经没有什么好害怕的了——容斥or二项式反演+DP

题目大意: 给定两个长度为\(n\)的序列,求有多少种匹配方式,使得\(a_i<b_i\)的个数恰好为\(k\)个. 思路: 据说是一道二项式反演的经典例题了. 首先如果要求正好等于\(k\)个的是不太好求的,我们可以考虑求出至少为\(k\)个的方案数. 首先先把两个序列都按照从小到大的顺序排好序,然后以序列\(b\)为对象dp. 我们设\(f_{i,j}\)表示前\(i\)个数里面强制确定了\(j\)个\(a_i<b_i\)关系的方案数,记\(c_i\)表示在\(a\)中有多少个数<\

[bzoj4665]小w的喜糖_二项式反演

小w的喜糖 题目链接:https://lydsy.com/JudgeOnline/problem.php?id=4665 数据范围:略. 题解: 二项式反演裸题. $f_{i,j}$表示,前$i$种钦定$j$拿到自己种类糖果的方案数. 求完了之后可以二项式反演回来即可. 代码: #include <bits/stdc++.h> using namespace std; typedef long long ll; const int mod = 1000000009 ; int n, m; ll