NFLSOJ 1060 - 【2021 六校联合训练 NOI #40】白玉楼今天的饭(子集 ln)

由于 NFLSOJ 题面上啥也没有就把题意贴这儿了(

没事儿,反正是上赛季的题,你们非六校学生看了就看了,况且看了你们也没地方交就是了

题意:

  • 给你一张 \(n\) 个点 \(m\) 条边的图 \(G=(V,E)\)
  • 要求有多少张子图 \(G'=(V',E')\)​ 满足 \(G'\) 是连通二分图。
  • \(n\le 20,m\le\dfrac{n(n-1)}{2}\)
  • 答案对 \(998244353\) 取模。

输入样例 #1:

3 3
1 2
1 3
2 3

输出样例 #1:

9

众所周知,这场比赛现场六校就我一个人打(

首先考虑 \(n\le 17\)​ 怎么做,也就是本题的 80 分做法,注意到二分图计数这个东西是不太好直接求的,因为它涉及点集和边集两个决策对象,而这两个决策对象之间没有直接关联,因此考虑换个求解方式,我们将每个二分图与给二分图每个顶点进行黑白染色的方案看作一个整体,也就是二分染色图的数量,这样我们可以枚举染黑的点集 \(B\) 和染白的点集 \(W\),这样点集显然就是 \(B\cup W\),边集的限制就变为了不能存在一条连接两个 \(B\) 中节点的边,也不能存在一条连接两个 \(W\)​ 中节点的边,而 \(B\)\(W\) 之间的边可以随意选择,因此假设 \(c\)\(B\)\(W\) 之间的边数,\(f_S\)\(S\) 中点为点集的二分染色图的数量,那么有 \(f_{B\cup W}=2^c\)

接下来考虑求出 \(f_S\) 之后怎样求解答案,注意到对于一个连通图而言,假如它是一个二分图,那么它对应的染色方法必然恰有 \(2\) 种,也就是说,假设以 \(S\) 为点集的连通二分图数量为 \(x\),那么以 \(S\) 为点集的连通二分染色图的数量就是 \(2x\),因此我们只需要求出 \(g_S\) 表示以 \(S\) 为点集的连通二分染色图数量即可求出答案,怎么求呢?其实做过这题的同学思考到这一步应该比较有感觉了,我们考虑再设 \(h_S\) 表示以 \(S\)​ 为点集的不连通二分染色图数量,那么显然 \(g_S+h_S=f_S\),而 \(h_S\) 的转移是容易的,就设 \(S\)\(x\) 中最小的元素,然后枚举 \(x\) 所在的连通块 \(x\in T\subsetneq S\)​,贡献为 \(g_Tf_{S\backslash T}\),简单枚举个子集即可,时间复杂度 \(3^n\)

注:现场 80pts 的代码,部分变量名称可能与上面不符,同时为了卡常我在求解 \(h_S\) 那一步强制要求 \(S\)最大的元素(instead of 最小的元素)必须属于 \(T\),这样一旦不符合即可 break 掉:

const int MAXM=136;
const int MAXP=1<<17;
const int MOD=998244353;
int n,m,pw2[MAXM+5];
int f[MAXP+5],g[MAXP+5],tot[MAXP+5],ed[MAXP+5];
int high(int x){return 31-__builtin_clz(x);}
int main(){
//	freopen("youmu.in","r",stdin);freopen("youmu.out","w",stdout);
	scanf("%d%d",&n,&m);
	for(int i=(pw2[0]=1);i<=m;i++) pw2[i]=pw2[i-1]*2%MOD;
	for(int i=1,u,v;i<=m;i++) scanf("%d%d",&u,&v),ed[(1<<u-1)|(1<<v-1)]++;
	for(int i=0;i<n;i++) for(int j=0;j<(1<<n);j++) if(j>>i&1) ed[j]+=ed[j^(1<<i)];
	for(int i=1;i<(1<<n);i++){
		for(int j=i;j;j=(j-1)&i){
			if(high(j)!=high(i)) break;
			tot[i]=(tot[i]+pw2[ed[i]-ed[j]-ed[i^j]])%MOD;
		}
	} f[0]=1;int ans=0;
	for(int i=1;i<(1<<n);i++){
		int lb=high(i&(-i));
		for(int j=i;j;j=(j-1)&i){
			if(j>>lb&1) g[i]=(g[i]+2ll*f[j]*tot[i^j])%MOD;
		} f[i]=(tot[i]-g[i]+MOD)%MOD;ans=(ans+f[i])%MOD;
//		printf("%d %d %d\n",i,f[i],g[i]);
	} printf("%d\n",ans);
	return 0;
}

接下来考虑优化,首先是第一步求解 \(f_S\) 的优化,注意到在第一步中,\(B\)\(W\) 之间的边的个数可以用 \(ed_{B\cup W}-ed_B-ed_W\) 来表示,其中 \(ed_S\) 表示 \(S\)​ 中边的个数,那么 \(f_S=\sum\limits_{B\cup W=S,B\cap W=\varnothing}2^{ed_S-ed_B-ed_W}=2^{ed_S}\sum\limits_{B\cup W=S,B\cap W=\varnothing}\dfrac{1}{2^{ed_B}2^{ed_W}}\),容易发现这东西是一个子集卷积的形式,可以 \(2^nn^2\) 做掉。

然后是第二步的优化,这东西现场看来无法优化,所以就没写正解。现在学了子集卷积之后看就很 easy 了,我们记集合幂级数 \(A(x)\)\(x^S\) 前的系数表示以 \(S\) 为点集的连通二分图染色数量,同理 \(B(x)\) 则表示二分染色图的数量,那么不难发现这题中 \(A(x)\)\(B(x)\)​ 的关系就相当于 P4841 [集训队作业2013]城市规划 中,带标号图的数量的 OGF 带标号连通图的数量的 OGF 的关系一样,因此 \(B(x)=\sum\limits_{n\ge 0}\dfrac{A^n}{n!}\),其中乘法表示子集卷积,也就是说 \(B=\exp A\),因此 \(A=\ln B\),不难发现 \([x^S]B(x)=f_S\),因此求个子集 \(\ln\) 即可求出 \(A\),也就是上文中的 \(g_S\)

时间复杂度就降到了 \(2^nn^2\)

代码(后面附有两个比较强的样例,没错你没看错,样例 #1 就是这题的样例,样例 #2 就是这题的样例):

const int MAXN=20;
const int MAXM=190;
const int MAXP=1<<20;
const int MOD=998244353;
const int INV2=499122177;
int qpow(int x,int e){
	int ret=1;
	for(;e;e>>=1,x=1ll*x*x%MOD) if(e&1) ret=1ll*ret*x%MOD;
	return ret;
}
int n,m,ed[MAXP+5],ppc[MAXP+5],tmp[MAXN+5],inv[MAXN+5],res=0;
int f[MAXN+5][MAXP+5],g[MAXN+5][MAXP+5],pw2[MAXM+5],ipw2[MAXM+5];
void FWTor(int *a,int len,int type){
	for(int i=2;i<=len;i<<=1)
		for(int j=0;j<len;j+=i)
			for(int k=0;k<(i>>1);k++)
				a[(i>>1)+j+k]=(a[(i>>1)+j+k]+1ll*a[j+k]*type)%MOD;
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=(inv[0]=inv[1]=1)+1;i<=n;i++) inv[i]=1ll*inv[MOD%i]*(MOD-MOD/i)%MOD;
	for(int i=(pw2[0]=ipw2[0]=1);i<=m;i++) pw2[i]=(pw2[i-1]<<1)%MOD,ipw2[i]=1ll*ipw2[i-1]*INV2%MOD;
	for(int i=1;i<(1<<n);i++) ppc[i]=ppc[i&(i-1)]+1;
	for(int i=1,u,v;i<=m;i++) scanf("%d%d",&u,&v),ed[(1<<u-1)|(1<<v-1)]++;
	for(int i=0;i<n;i++) for(int j=0;j<(1<<n);j++) if(j>>i&1) ed[j]+=ed[j^(1<<i)];
	for(int i=0;i<(1<<n);i++) f[ppc[i]][i]=ipw2[ed[i]];
	for(int i=0;i<=n;i++) FWTor(f[i],1<<n,1);
	for(int i=0;i<=n;i++) for(int j=0;j<(1<<n);j++) for(int l=0;l<=i;l++)
		g[i][j]=(g[i][j]+1ll*f[l][j]*f[i-l][j])%MOD;
	for(int i=0;i<=n;i++) FWTor(g[i],1<<n,MOD-1);
	for(int i=0;i<(1<<n);i++) for(int j=0;j<=n;j++) if(ppc[i]!=j) g[j][i]=0;
	for(int i=0;i<(1<<n);i++)
		g[ppc[i]][i]=1ll*g[ppc[i]][i]*pw2[ed[i]]%MOD;
//	for(int i=0;i<(1<<n);i++) printf("%d %d\n",i,g[ppc[i]][i]);
	for(int i=0;i<=n;i++) FWTor(g[i],1<<n,1);
	for(int i=0;i<(1<<n);i++){
		memset(tmp,0,sizeof(tmp));
		for(int j=1;j<=n;j++){
			tmp[j]=g[j][i];
			for(int k=1;k<j;k++)
				tmp[j]=(tmp[j]-1ll*k*tmp[k]%MOD*inv[j]%MOD*g[j-k][i]%MOD+MOD)%MOD;
		} for(int j=1;j<=n;j++) g[j][i]=tmp[j];
	} for(int i=0;i<=n;i++) FWTor(g[i],1<<n,MOD-1);
//	for(int i=0;i<(1<<n);i++) printf("%d %d\n",i,g[ppc[i]][i]);
	for(int i=1;i<(1<<n);i++) res=(res+1ll*g[ppc[i]][i]*INV2)%MOD;
	printf("%d\n",res);
	return 0;
}
/*1
Input:
4 5
1 2
2 4
1 3
1 4
2 3
Output:
26
*/
/*2
Input:
6 13
1 2
1 3
2 3
1 4
4 2
3 4
5 2
3 5
5 4
6 2
6 3
6 4
6 5
Output:
1866
*/
posted @ 2021-08-11 16:45  tzc_wk  阅读(201)  评论(2编辑  收藏  举报