感觉是好久之前看到的题,今天终于有勇气补了。
题目链接。
题意
对于一张连通图,定义它是好的,当且仅当其所有生成树都存在完美匹配。
给定 ,求有 个点的好的连通图数量。
,模数是输入的质数,
为偶数。
题解
首先考虑什么样的图是好的。
因为对于所有生成树来说,各个点双内部的方案是独立的,所以考虑对一个点双进行分析。
对于一个点双,设 表示
这个点外面挂了多少个其他点双的点(不包括 这个点)。
那么可以知道,若 是偶数,则
一定要在这个点双内部照匹配点,否则要在点双外部找匹配。
接下来分类讨论一下。
第一种情况:
假设这个点双内部同时出现了
为偶数和
为奇数的点,会发生什么。
首先,因为点双内部肯定是连通的,所以此时一定存在一条边,满足这条边两侧的点
奇偶性不同。
而由于我们找的是一个点双,所以删掉那个
为偶数的点之后,整张图仍然连通(至少存在一个生成树)。
那么就可以直接构造出来一棵生成树,使得这个
为偶数的点,在这个点双内只连了一条边(另一侧是那个 为奇数的点)。
这显然是不合法的,所以可以知道,一个点双内部,不能同时存在 为奇数和 为偶数的点。
第二种情况:
如果这个点双内部只有
为奇数的点。
显然,所有点都只会向外找匹配,这个点双内部什么影响都没有。
第三种情况:
如果这个点双内部只有
为偶数的点。
那么显然,所有的点都要在这个点双内部找匹配。
我们来看看,假如一个点,度数至少是 ,会发生什么。
假设有一个点 度数至少是
,取其连边中编号最小的点,设为
。
不妨假设,这部分的构造中,生成树的根均为 。
首先按照之前的分析,我们可以构造出一棵生成树 ,满足 在这棵树里面之和 连边。
我们还可以构造出一棵生成树 ,使得其为 删掉边 再加上边 。
如果整个点双要合法,则
都需要有完美匹配,这说明
的其他所有子树都存在完美匹配(不算本身的子树大小为偶数)。
那么我们可以再构造出一棵树 ,满足 和 这写点有连边,在 中
与其父亲的连边都不存在,其余边完全一样的树。
可以发现,此时
子树内都是完美匹配,所以
都需要和
匹配,这就不合法了。
偷一张 AT 官解的图上来:

所以说,在这种情况中,所有的点度数都必须 。
也就是说整张图要么是一个大环,要么只有两个点中间连一条边。
那么我们现在就可以分析出合法图的结构了:
首先这张图最基本的部分是
个点组成了若干个环与若干条单边。
然后再在不同的部分中间加边(形成新的点双),直到连成了一张连通图。
现在我们就可以考虑如何进行计数了。
但在这之前,我们需要解决一个稍微简单一点的问题,如何计算 个点的点双连通图个数。
首先先来计算
个点的连通图数量,设答案为 。
首先, 个点的图总数为 ,再减掉不连通的图方案数就是连通图总数。
直接枚举点
所在连通块大小即可,得到 。
这可以在
的时间内计算。
再来考虑如果对点双连通图计数,设 个点的连通图中,有 个点双的图有 种。
(特殊的,当
时整张图为一个单点,我们定义其有一个点双)。
那么显然,,所以我们只需要考虑如何计算
。
不难发现一张图有
个点双,相当于其圆方树中有
个方点,不妨画一个图出来看看。

(似乎可以直接对着这个圆方树 dp,但是直接 dp 的话我好像只会 )。
我们先固定
号点为根,然后再给每一个方点一个编号(后面方案数除掉 即可)。
设编号为 的方点有 个儿子,那么有 和 。
考虑如果给定一组
的取值,能否算出其对应的方案数。
首先需要把 这 个圆点分配给 个方点,方案数是 。
第 个点双有 个点,所以还要乘上点双的方案数
。
然后我们要给每一个方点找一个父亲(其实是找爷爷),其挂在第 个方点下面的方案数是 。
由于还有直接连到根下面的情况,不妨认为还有一个 号方点只有 号圆点一个儿子(,所以后面有 )。
可以发现连边的方案数不好计算,但是考虑一个不同的问题。
现在有 个点,第 个点有点权 。
一条边 的权值是 ,一棵树的权值是所有边权的乘积。
问所有树的权值之和。
不难发现,这是个经典问题,答案是 。
而我们发现这个问题和我们上面要算的方案数几乎没有区别。
我们要算的东西只有父亲那一端有代价,而这个问题是两条边都有代价。
不同的部分儿子的部分没有代价,而每一个点显然有恰好一条边作为儿子,所以我们只需要给总权值除掉
即可。
总结,若给定一组 ,则方案数为
。
(可能会算错的地方:一共有
个点所以 的指数是 )(还有这里官解好像写错了)。
然后我们直接对这个 跑 dp
就好了,复杂度是 的。
现在回到原题,我们需要计算合法的图方案数。
首先我们需要将 个点分成
个部分,每一个部分都是一条单边或者一个偶数个点的环。
然后这
个部分需要拼成一整个连通块,考虑这样的过程:
每一次调出
个部分,强制编号为
的部分必须拿出,加边让这些部分形成一个连通的新部分,编号为 。
不断重复使得只剩下一个部分,此时整张图连通,不难发现这样计数的过程是不重不漏的。
那么显然,在一次加边的过程中,每一个部分只会存在一个点,有向外的连边,不然会爆炸。

(原来存在三个部分(紫色),新加入三条边(红色),形成了新的点双(蓝色))。
那么考虑方案数如何计算。
假设这 个部分又形成了 个点双,考虑和上面类似的思路:
首先,有 个圆点,第 个圆点代表一个有 个小点的部分;还有 个方点,编号为 。
号方点是我们虚构出来的,和
号圆点一起代表整棵树的根(我们强制小点 需要在 号圆点内,其余不强制)。
所有方点至少都要有一个圆点作为儿子,除了
号方点的其余所有方点需要找一个方点作为爷爷。
考虑方案数,首先若确定了树的形态,要将所有圆点连成点双,所以对于每一个方点首先有
的贡献。
其次还要考虑每一个点在一个点双内部,选哪个点往外连边。
对于一个圆点,其向父亲只有一条边,所以这部分相当于所有部分的大小之积(编号为
的部分不算)。
对于向儿子的方案数,因为儿子是方点,所有可以直接考虑每一个方点爷爷是谁,也就是说我们不关注每一个方点父亲是谁,只关注爷爷是谁。
这样的话,一个方点到其爷爷的方案数,就是其爷爷下面的所有圆点内部,小点的个数和。
考虑预处理一个
表示一个方点下面有
个小点的方案数权值和。
假设第 个方点下面挂了
个小点,则方案数(不考虑分配小点给方点)大概是 。
(这里需要注意可能会出现
的情况)。
那么瞎 dp 一下就好了。
时间复杂度 。
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
| #include<bits/stdc++.h> #define inf 0x3f3f3f3f3f3f3f3fll #define debug(x) cerr<<#x<<"="<<x<<endl using namespace std; using ll=long long; using ld=long double; using pli=pair<ll,int>; using pi=pair<int,int>; template<typename A> using vc=vector<A>; template<typename A,const int N> using aya=array<A,N>; inline int read() { int s=0,w=1;char ch; while((ch=getchar())>'9'||ch<'0') if(ch=='-') w=-1; while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar(); return s*w; } inline ll lread() { ll s=0,w=1;char ch; while((ch=getchar())>'9'||ch<'0') if(ch=='-') w=-1; while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar(); return s*w; } ll p2[250005]; ll dp[505][505]; ll g[505][505]; ll C[505][505]; ll jc[505],inv[505]; ll f[505],h[505]; int n,mod=998244353; inline ll qow(ll a,ll b) { ll ans=1; while(b) { if(b&1) ans=ans*a%mod; a=a*a%mod; b>>=1; } return ans; } int main() { n=read(),mod=read(); for(int i=0;i<=n;i++) { C[i][0]=C[i][i]=1; for(int j=1;j<i;j++) C[i][j]=(C[i-1][j]+C[i-1][j-1])%mod; }
p2[0]=1; for(int i=1;i<=n*n;i++) p2[i]=p2[i-1]*2%mod;
f[0]=jc[0]=inv[0]=1; for(int i=1;i<=n;i++) { f[i]=p2[i*(i-1)/2],jc[i]=jc[i-1]*i%mod,inv[i]=qow(jc[i],mod-2); for(int j=1;j<i;j++) (f[i]-=C[i-1][j-1]*f[j]%mod*p2[(i-j)*(i-j-1)/2])%=mod; f[i]=(f[i]%mod+mod)%mod; }
dp[0][0]=1; for(int i=1;i<=n;i++) { g[i][1]=f[i];ll val=1; for(int j=2;j<=n;j++) { val=val*i%mod; g[i][j]=val*jc[i-1]%mod*inv[j]%mod*dp[i-1][j]%mod; g[i][1]=(g[i][1]-g[i][j]+mod)%mod; }
for(int x=1;x<i;x++) (dp[i][2]+=g[x+1][1]*inv[x]%mod*g[i-x+1][1]%mod*inv[i-x])%=mod; for(int x=3;x<=i;x++) for(int y=1;y<i;y++) (dp[i][x]+=dp[i-y][x-1]*g[y+1][1]%mod*inv[y])%=mod; }
memset(dp,0,sizeof(dp));dp[0][0]=1; for(int i=2;i<=n;i+=2) { for(int j=2;j<=i;j+=2) { ll val=(j==2?1:(jc[j-1]*inv[2]%mod))*j%mod*C[i][j]%mod; for(int p=1;2*p<=i;p++) (dp[i][p]+=dp[i-j][p-1]*val)%=mod; } for(int j=1;2*j<=i;j++) (h[i]+=dp[i][j]*g[j+1][1]%mod*inv[j])%=mod; }
ll ans=n==2?1:(jc[n-1]*inv[2]%mod); memset(dp,0,sizeof(dp)); for(int j=2;j<=n;j+=2) dp[0][j]=(j==2?1:(jc[j-1]*inv[2]%mod))*j%mod;
for(int i=1;2*i<=n;i++) for(int j=2;j<=n;j++) for(int p=2;p<j;p++) (dp[i][j]+=dp[i-1][j-p]*C[j-1][p]%mod*h[p])%=mod;
ll val=1; for(int m=1;2*m<=n;m++) { (ans+=dp[m][n]*val%mod*inv[m])%=mod; val=val*n%mod; }
printf("%lld\n",ans); return 0; }
|