acm-(NTT,方案计数,Kruskal重构树)Grakn Forces 2020 G. Clusterization Counting

题面
传送门
关于Kruskal重构树的知识点在这里
题意翻译过来就是要求你将n个点的图划分成k个连通块,满足连通块内部的所有连边权值都小于从连通块连出的边的权值。
如果我们将所有边从小到大来建图会怎样呢?每次加入一条边有两种情况,一是 u , v \mathbf{u,v} u,v已经同属一个连通块,故只是在这个连通块内增加一条边,二是将 u , v \mathbf{u,v} u,v连通。
对于第一种情况而言,如果在某一次加边之后我们发现这个连通块内部的所有边数与点数的关系恰好符合完全图的点数和边数关系(即 边 数 = 点 数 ∗ ( 点 数 − 1 ) 2 \mathbf{边数=\frac {点数*(点数-1)}{2}} =2(1)),那么这个连通块显然是符合划分的条件的一个点集,因为我们是按照边权从小到大加的,所以边权比该点集内部连边更大的边还没有被加入图中,也就是说该点集内部的边权一定小于从该点集连出的边的边权(注意题目中说所有边权两两不相同)。
对于第二种情况而言,加边的时候回导致两个连通块变成一个新的连通块,并且这个新的连通块点数为这两个连通块之和,边数为这两个连通块的边数之和再加一。先考虑旧的这两个连通块,如果它们都不是完全图,那么它们显然是不可能作为划分的单位的,因为它们不满足内部连边权值小于从内部连出边的权值,因为它们还没变成完全图的时候就有边从连通块内部连出去了,而我们又是从小到大加边,故不符合条件。而新的连通块我们也要check一下是否符合划分的条件。

我们发现这个过程跟求解最小生成树的过程是一样的,但不太容易转化为求方案数。于是考虑Kruskal重构树,容易发现那些满足划分条件的连通块都对应着Kruskal重构树上的一个节点,因为Kruskal重构树上的每一个点都是最小生成树建立过程中形成的连通块。那么在Kruskal重构树上可以很容易看出各个连通块之间的关系,或是真包含关系,或是不包含关系。然后我们要做的是将所有点不重不漏划分为k个连通块,可以考虑在树上做dp。由于Kruskal重构树是一颗二叉树,我们直接对两个儿子节点的方案数组做卷积即可,对于当前节点符合划分连通块的情况我们还需要给它的方案数组中k=1的时候的方案数加一。

这里卷积使用ntt即可,注意初始化数组长度别倍增太大。
具体细节看代码。

int n,dp[maxn*2][maxm],etot,fa[maxn*2],ee=1,h[maxn*2],tot,mark[maxn*2],szp[maxn*2],
	sze[maxn*2],len=1,pos[maxm];//dp[u]代表u的方案数组 
const int mod = 998244353;
struct EDGE{
	int u,v,w;
	bool operator <(const EDGE a)const{
		return w<a.w;
	}
}E[maxn*maxn/2];
struct Edge{
	int v,next;
}e[maxn*2];
void addedge(int u,int v){
	e[ee]=Edge{v,h[u]};
	h[u]=ee++;
}
int fd(int rt){
	return rt==fa[rt]?rt:(fa[rt]=fd(fa[rt]));
}

void ntt(int *a,int n,int fg){
	if(n==1)return;
	int g=3,gi=(mod+1)/3;
	FOR(i,0,n)if(i<pos[i])swap(a[i],a[pos[i]]);
	for(register int len=2,mid=1;len<=n;len*=2,mid*=2){
		int wn=qpow(fg==1?g:gi,(mod-1)/len,mod);
		for(register int i=0;i<n;i+=len){
			int w=1;
			FOR(j,0,mid){
				ll c=a[i+j],d=1ll*w*a[i+j+mid]%mod;
				a[i+j]=(c+d)%mod;
				a[i+j+mid]=(c-d+mod)%mod;
				w=1ll*w*wn%mod;
			} 
		}
	}
	int invn=qpow(n,mod-2,mod);
	if(fg==-1)FOR(i,0,n)a[i]=1ll*a[i]*invn%mod;
}
void dfs(int u){
	vector<int>s;
	for(register int i=h[u];i;i=e[i].next){
		int v=e[i].v;
		dfs(v);
		s.push_back(v);
	}
	if(!s.empty()){
		int a=s[0],b=s[1];
		ntt(dp[a],len,1);//做个卷积 
		ntt(dp[b],len,1);
		FOR(i,0,len)dp[u][i]=1ll*dp[a][i]*dp[b][i]%mod;
		ntt(dp[u],len,-1);
	}
	dp[u][1]=mark[u];//特殊情况:如果连通块u符合条件需要特判给dp[u][1]+1 
}
int main(){
	rd(&n);
	int bt=0;
	while(len<n+1)len*=2,bt++;//初始化len别太大 
	FOR(i,0,len)pos[i]=(pos[i>>1]>>1)|((i&1)<<(bt-1));
	FOR(i,1,n+1){
		fa[i]=i;
		mark[i]=1;
		szp[i]=1;
		FOR(j,1,n+1){
			int w;
			rd(&w);
			if(i<j)E[etot++]={i,j,w};
		}
	}
	sort(E,E+etot);
	tot=n;
	FOR(i,0,etot){
		int u=E[i].u,v=E[i].v,w=E[i].w;
		int fu=fd(u),fv=fd(v);
		if(fu==fv){
			sze[fu]++;
			if(sze[fu]==szp[fu]*(szp[fu]-1)/2)mark[fu]=1;//通过连通块中点数与边数的关系判断是否为团 
			continue;
		}
		++tot; 
		fa[fu]=fa[fv]=fa[tot]=tot;
		sze[tot]=sze[fu]+sze[fv]+1;
		szp[tot]=szp[fu]+szp[fv];
		if(sze[tot]==szp[tot]*(szp[tot]-1)/2)mark[tot]=1;//通过连通块中点数与边数的关系判断是否符合条件 
		addedge(tot,fu),addedge(tot,fv);
	}
	dfs(tot);//树形dp 
	FOR(i,1,n+1)printf("%d ",dp[tot][i]);
	wrn("");
} 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值