UOJ Round Good Bye JiHai D. 新年的追逐战
D 新年的追逐战 [* hard]
对于图 \(G_1,G_2\) 定义图的乘法为:
则 \(H\) 的点集为 \(V^*=\{(u,v),u\in G_1,v\in G_2\}\)
则 \(H\) 的边集为 \(E^*=\{((u_1,v_1),(u_2,v_2)),(u_1,u_2)\in G_1,(v_1,v_2)\in G_2\}\)
现在有 \(n\) 张图,第 \(i\) 张图的大小为 \(m_i\),这张图是随机生成的,即 \(2^{\binom{m_i}{2}}\) 种可能的图等概率随机生成。
你需要求所有可能的 \(H=G_1\times G_2\times G_3....\times G_n\) 的连通块个数。
\(n,m_i\le 10^5\),答案对 \(998244353\) 取模。
Solution
- 因为题解太长所以略做分行。
- 本题的解题思路分为:
- 对模型的简易抽象以及观察出关键性质。
- 对孤立点的计算。
- 如何计算答案以及生成函数的部分。
性质
考虑 \(n=1\),从计数的角度,可以考虑枚举集合 \(S\) 然后钦定其为连通块,那么外部即任意连接的方案数,显然求和即为答案。
设 \(G\) 表示任意无向图的生成函数,即
那么联通图的生成函数即为 \(\ln G\),所以 \(n!G\cdot \ln G[x^n]\) 即为答案。
考虑 \(n\ne 1\) 怎么做,这种题肯定要打表找规律,我们发现对于两个大小 \(>1\) 的连通块生成的图,其连通块个数往往都是 \(1\),不是 \(1\) 的时候是 \(2\)。
考虑抽象问题的模型,我们将点 \((u,v)\) 当作由 \(G_1\) 上的 \(u\) 节点和 \(G_2\) 上的 \(v\) 节点这样的一个组合,那么所连的边可以等价于两个点同时移动一步,连通块等价于这对点可以到达的点对集合 \((U,V)\)
显然的事实是,不妨假设 \(v\) 的出边为 \(v'\),我们存在一种移动方式是让 \(u\) 任意走,然后 \(v\) 每次走到 \(v'\) 再走回来。
这样,从 \(v\) 走回 \(v'\) 经过的步数一定是偶数,那么可以到达的点对 \((u',v)\) 中,所有合法的 \(u'\) 当且仅当存在一条路径 \(u\to u'\) 满足其长度为偶数,当图不为二分图时,任意的 \(x\in [u \text{所在的连通块}]\) 均为合法的 \(u'\),同理于 \(v\)
不难发现,当两边连通块大小均大于 \(1\),且两个连通块均不为二分图时,所生成的新图一定是一张连通图,且这张图不为二分图。
当两边连通块大小均大于 \(1\),且两个连通块均为二分图时,所生成的新图一定是两张二分图。
当两边连通块大小均大于 \(1\),且存在恰好一者为二分图时,所生成的新图一定为二分图。
假设某个连通块大小为 \(1\),那么所生成的新图一定是若干个孤立的节点,此时大小为第二张图的节点个数。
对孤立点的处理
不妨称大小为 \(1\) 的节点为孤立点,那么孤立点的数量是一定不会减少的,假设 \(G_1\times G_2\),且 \(G_1\) 有 \(k_1\) 个孤立点,\(G_2\) 有 \(k_2\) 个孤立点,那么新图的孤立点个数为 \(k_1\times |G_2|+k_2\times |G_1|-k_1\times k_2\)
不难发现孤立点对答案的贡献是独立的,我们可以先考虑孤立点的贡献,然后我们删除孤立点并统计剩余节点的贡献,我们不妨设每张图先有 \(l_1,l_2....l_n\) 个孤立点,那么有 \(l_i\le m_i\),对于所有可能的情况,孤立点对答案的贡献为:
其中,\(f(m_i,l_i)\) 表示大小为 \(m_i\) 的图存在恰好 \(l_i\) 个孤立点的方案数。
基于观察我们发现孤立点的贡献计算式可以认为是枚举一个子集 \(S\),然后计算:
于是答案即:
其中,\(G(m_i)\) 表示大小为 \(m_i\) 的点集生成的无向图的数量。
显然后者可以替换为:
于是我们不难发现后者即大小为 \(m_i\) 的点集,生成的所有无向图的孤立点的和。
显然等价于 \(m_i\times 2^{\binom{m_i-1}{2}}\),即枚举孤立点为谁,剩余的点任意连边。
而前者显然等价于 \(m_i\times 2^{\binom{m_i}{2}}\)
于是答案即为:
考虑忽略 \(S\ne T\),这样答案就是:
考虑加上 \(S=T\),这样就是:
可以直接计算贡献。
对答案的计算
接下来考虑所有大小不为 \(1\) 的连通块的贡献和。
首先我们对图进行分类,这部分图可以表示成联通二分图和联通非二分图的拼接/组合,由于只需要计数,我们只需要考虑所有可能的情况下生成的大小大于 \(1\) 的联通二分图以及联通非二分图的数量和。
其中,联通二分图和联通二分图之间的乘法会使得答案变成连通块数乘积的 \(2\) 倍。
根据前文的讨论(性质),此时我们的答案只有 \(4\) 种新来源(以 T 表示二分图,F 表示非二分图)
- 非二分图数增加:\(F_1\times F_2\)
- 二分图数增加:\(2\times T_1\times T_2\)
- 二分图数增加:\(T_1\times F_2\)
- 二分图数增加 \(T_2\times F_1\)
首先考虑图上的大于 \(1\) 的连通块数是什么,等价于枚举一个大于 \(1\) 的连通块,然后剩余部分任意填,即 \(G\times (\ln G-x)[x^n]\times n!\)
于是只需要考虑联通二分图计数问题(非二分图用连通图减去二分图即可),类似于 ARC106F,可以考虑枚举点集 \(S\),此时点集 \(S\) 为二分图的方案数即所有可能的分集合所导致的二分图(不一定连通,会算重)的方案数减去子集 \(T\) 强制为联通二分图,且外部集合任选的方案数(不难发现计重的部分刚好被减去)
- 好麻烦。
可以更简单的处理,考虑染色二分图的 EGF,显然为:
设连通染色二分图的 EGF 为 \(F\),那么显然有 \(H(x)=\exp F(x)\)
于是联通二分图的 EGF 为 \(\frac{\ln H(x)}{2}\)
同理,\(n\) 个点的连通二分图数即为 \(n!\frac{\ln H(x)G(x)}{2}[x^n]\)
最后问题的瓶颈在于计算 \(H(x)\) 上了,考虑:
这样答案即为
总结一下:
- 对于所有可能的孤立点,贡献式为:
- 对于非孤立点,只需要区分二分图和非二分图,然后递推答案即可。
无向图的 EGF:
联通图的 EGF:
大于 \(1\) 的连通块数的 EGF:
染色二分图的 EGF 计算:
联通二分图的 EGF:
联通二分图数的 EGF:
然后递推答案即可,复杂度 \(\mathcal O(n\log n+m\log m)\)
\(Code:\)
//Author:EmptySoulist
//2020.10.28
#include<bits/stdc++.h>
using namespace std ;
#define drep( i, s, t ) for( register int i = (t); i >= (s); -- i )
#define rep( i, s, t ) for( register int i = (s); i <= (t); ++ i )
#define re register
#define int long long
int read() {
char cc = getchar() ; int cn = 0, flus = 1 ;
while( cc < '0' || cc > '9' ) { if( cc == '-' ) flus = - flus ; cc = getchar() ; }
while( cc >= '0' && cc <= '9' ) cn = ( cn * 10 + cc - '0' ), cc = getchar() ;
return cn * flus ;
}
//视题目调节
const int P = 998244353 ;
const int Gi = 332748118 ;
const int G = 3 ;
// 基本参数
const int N = 6e5 + 5 ;
int m, n, A[N], inv[N], ifac[N], fac[N] ;
int B[N], D[N], zk[N], sk[N], zmy[N], W[N] ;
int fpow( int x, int k ) {
int ans = 1, base = x ;
while( k ) {
if( k & 1 ) ans = ans * base % P ;
base = base * base % P, k >>= 1 ;
} return ans % P ;
}
namespace OGF {
int R[N], L, limit, Inv, iv[N] ;
void _init( int x ) {
rep( i, 1, x ) inv[i] = iv[i] = fpow( i, P - 2 ) ;
}
void Init( int x ) {
limit = 1, L = 0 ; while( limit < x ) limit <<= 1, ++ L ;
rep( i, 0, limit ) R[i] = ( R[i >> 1] >> 1 ) | ( ( i & 1 ) << ( L - 1 ) ) ;
Inv = fpow( limit, P - 2 ) ;
}
void NTT( int *a, int type ) {
for( re int i = 0; i < limit; ++ i ) if( R[i] > i ) swap( a[i], a[R[i]] ) ;
for( re int k = 1; k < limit; k <<= 1 ) {
int d = fpow( ( type == 1 ) ? G : Gi, ( P - 1 ) / ( k << 1 ) ) ;
for( re int i = 0; i < limit; i += ( k << 1 ) )
for( re int j = i, g = 1; j < i + k; ++ j, g = g * d % P ) {
int Nx = a[j], Ny = a[j + k] * g % P ;
a[j] = ( Nx + Ny ) % P, a[j + k] = ( Nx - Ny + P ) % P ;
}
}
if( !type ) rep( i, 0, limit ) a[i] = a[i] * Inv % P ;
}
int MA[N], MB[N] ;
void Mul( int *a, int *b, int x ) {
rep( i, 0, x ) MA[i] = a[i], MB[i] = b[i] ;
Init( x << 1 ), NTT( MA, 1 ), NTT( MB, 1 ) ;
rep( i, 0, limit ) MA[i] = MA[i] * MB[i] % P ;
NTT( MA, 0 ) ; rep( i, 0, limit ) a[i] = MA[i] ;
rep( i, 0, limit ) MA[i] = MB[i] = 0 ;
}
int IF[N], IG[N] ; //(G-G') = 0//G^2+G'^2-2GG'=0//G=2G'-G'^2F
void PolyInv( int *a, int x ) {
IG[0] = fpow( a[0], P - 2 ) ; int lim = 1 ;
while( lim < x ) {
lim <<= 1, Init( lim << 1 ) ; rep( i, 0, lim ) IF[i] = a[i] ;
NTT( IG, 1 ), NTT( IF, 1 ) ;
rep( i, 0, limit ) IG[i] = IG[i] * ( 2ll - IG[i] * IF[i] % P + P ) % P ;
NTT( IG, 0 ) ;
rep( i, lim, limit ) IG[i] = 0 ;
}
rep( i, 0, x ) a[i] = IG[i] ;
rep( i, 0, limit ) IG[i] = IF[i] = 0 ;
}
void PolyInt( int *a, int x ) {
drep( i, 1, x ) a[i] = a[i - 1] * iv[i] % P ; a[0] = 0 ;
}
void PolyDe( int *a, int x ) {
rep( i, 1, x ) a[i - 1] = a[i] * i % P ;
}
int LF[N], LG[N] ;
void Polyln( int *a, int x ) { //G = ln F, G' = F'/F
rep( i, 0, x ) LF[i] = LG[i] = a[i] ;
PolyInv( LF, x ), PolyDe( LG, x ), Init(x + x), Mul( LG, LF, x ), PolyInt( LG, x ) ;
rep( i, 0, x ) a[i] = LG[i] ; rep( i, 0, limit ) LG[i] = LF[i] = 0 ;
}
}
using namespace OGF ;
// 无向图为 B,联通无向图为 D,联通无向图数为 zk
// 联通二分图为 zmy,联通二分图数为 sk,染色二分图为 W
int Q[N], O[N] ;
int C(int x) { return x * (x - 1) / 2 ; }
signed main()
{
m = read() ;
rep( i, 1, m ) A[i] = read(), n = max( n, A[i] ) ;
_init(n << 2), inv[0] = ifac[0] = fac[0] = 1 ;
rep( i, 1, n ) ifac[i] = ifac[i - 1] * inv[i] % P, fac[i] = fac[i - 1] * i % P ;
rep( i, 0, n + 5 ) B[i] = ifac[i] * fpow(2, C(i)) % P ;
rep( i, 0, n + 5 ) D[i] = B[i] ; Polyln(D, n + 5) ;
rep( i, 0, n ) Q[i] = D[i], O[i] = B[i] ; Q[0] = Q[1] = 0 ;
Mul(Q, O, n + 5) ; rep(i, 0, n) zk[i] = Q[i] * fac[i] % P ;
rep(i, 0, limit) Q[i] = O[i] = 0 ;
rep(i, 0, n) Q[i] = ifac[i] * fpow(fpow(2, C(i)), P - 2) % P ;
Mul(Q, Q, n + 5) ; rep(i, 0, n) W[i] = Q[i] * fpow(2, C(i)) % P ;
rep(i, 0, limit) Q[i] = O[i] = 0 ;
rep(i, 0, n) zmy[i] = W[i] ; Polyln(zmy, n + 5) ;
rep(i, 0, n) zmy[i] = zmy[i] * (P + 1) / 2 % P ; zmy[1] = 0 ;
rep(i, 0, n) Q[i] = zmy[i], O[i] = B[i] ; Mul(Q, O, n + 5) ;
rep(i, 0, n) sk[i] = Q[i] * fac[i] % P ; rep(i, 0, limit) Q[i] = O[i] = 0 ;
rep(i, 0, n) zk[i] = (zk[i] - sk[i] + P) % P ;
int Ans = 0, ans = 1 ;
rep(i, 1, m) ans = ans * (A[i] * fpow(2, C(A[i])) % P) % P ;
Ans = ans, ans = 1 ;
rep(i, 1, m) ans = ans * A[i] % P ;
rep(i, 1, m) ans = ans * (fpow(2, C(A[i])) - fpow(2, C(A[i] - 1)) + P) % P ;
Ans = (Ans - ans + P) % P ; int ia = zk[A[1]], ib = sk[A[1]] ;
for(re int i = 2; i <= m; ++ i) {
int fa = ia * zk[A[i]] % P ;
int fb = ( 2ll * ib * sk[A[i]] % P + ia * sk[A[i]] % P + ib * zk[A[i]] % P ) % P ;
ia = fa, ib = fb ;
}
Ans = (Ans + ia + ib) % P ;
cout << Ans << endl ;
return 0 ;
}