魔王消失day2T1 uni【树哈希+Burnside】

题目描述:

给定一棵n个点n条边的基环树,有m种颜色,给这棵树染色,求本质不同的染色方案。
对于树,本质不同的含义与树同构类似。
对于环,本质不同指一种方案不能通过另一种旋转环来得到。

题目分析:

首先对于环上的每个点,DP出它下面的树的染色方案。
用树哈希来判断树同构,哈希时需要将儿子的哈希值排序然后合并,合并完后需要再乘上一个Seed、跟size取个异或或者乘上size。(不然后面的点会WA到自闭。神奇的树哈希。。)
考虑DP,用 f [ u ] f[u] f[u]表示 u u u子树的染色方案,如果有 k k k个儿子同构,枚举 k k k个子树的颜色数 i i i,从 f [ v ] f[v] f[v]种颜色里选 i i i种,分配给 k k k个儿子,所以总贡献就是 ∑ i = 1 m i n ( f [ v ] , k ) C ( f [ v ] , i ) ∗ C ( k − 1 , i − 1 ) \sum_{i=1}^{min(f[v],k)}C(f[v],i)*C(k-1,i-1) i=1min(f[v],k)C(f[v],i)C(k1,i1)。(Upd:我傻了,直接看做每种颜色有多少棵子树选了,就是 k k k个相同的球放进 f [ v ] f[v] f[v]个盒子里的方案数即 C f [ v ] + k − 1 f [ v ] − 1 = C f [ v ] + k − 1 k C_{f[v]+k-1}^{f[v]-1}=C_{f[v]+k-1}^k Cf[v]+k1f[v]1=Cf[v]+k1k,虽然复杂度是一样的。)
把不同的儿子的贡献乘给 f [ u ] f[u] f[u]即可, f [ u ] f[u] f[u]初值为 m m m

现在知道了环上每个点的哈希值和方案数,对哈希值做KMP找循环节。如果没有循环节,说明不可能通过环旋转来得到完全相同的方案,把所有点方案数相乘即为答案。
如果有循环节,把一个循环节看做一个点,每个循环节的方案记为 M M M,循环节数记为 N N N,,由Burnside引理知
a n s = ∑ i = 1 N M g c d ( i , N ) N ans={\sum_{i=1}^NM^{gcd(i,N)}\over N} ans=Ni=1NMgcd(i,N)
关于Burnside引理可以看这里

Code:

#include<bits/stdc++.h>
#define maxn 100005
using namespace std;
typedef unsigned long long ULL;
const int mod = 998244353;
const ULL Seed = 37;
int n,m,stk[maxn],top,dep[maxn],siz[maxn],f[maxn],son[maxn];
int fac[maxn],inv[maxn],fail[maxn];
bool onc[maxn],vis[maxn];
vector<int>cir;
ULL H[maxn];
int fir[maxn],nxt[maxn<<1],to[maxn<<1],tot=1;
inline void line(int x,int y){nxt[++tot]=fir[x],fir[x]=tot,to[tot]=y;}
inline int ksm(int a,int b){
	int s=1;
	for(;b;b>>=1,a=1ll*a*a%mod) if(b&1) s=1ll*s*a%mod;
	return s;
}
inline int C(int n,int m){return 1ll*fac[n]*inv[m]%mod*inv[n-m]%mod;}
inline int gcd(int a,int b){return b?gcd(b,a%b):a;}
void dfs(int u,int pre){
	vis[u]=1,stk[++top]=u;
	for(int i=fir[u],v;i;i=nxt[i])
		if(!vis[v=to[i]]) {dfs(v,i^1);if(!cir.empty()) return;}
		else if(i!=pre){
			do cir.push_back(stk[top]),onc[stk[top]]=1;
			while(stk[top--]!=v);
			return;
		}
	top--;
}
bool cmp(int i,int j){return H[i]<H[j];}
void solve(int u,int pre){
	dep[u]=dep[to[pre]]+1,siz[u]=1;
	for(int i=fir[u],v;i;i=nxt[i])
        if(i!=pre&&!onc[v=to[i]]) solve(v,i^1),siz[u]+=siz[v];
	son[0]=0;
	for(int i=fir[u],v;i;i=nxt[i])
        if(i!=pre&&!onc[v=to[i]]) son[++son[0]]=v;
	sort(son+1,son+1+son[0],cmp);
	f[u]=m;
	for(int i=1,k=1,v;i<=son[0];i++){
		H[u]=H[u]*Seed+H[v=son[i]];
		if(i==son[0]||H[v]!=H[son[i+1]]){
			int s=0,F=f[v];
			for(int j=1;j<=min(k,f[v]);F=1ll*F*(f[v]-j)%mod,j++)
				s=(s+1ll*F*inv[j]%mod*C(k-1,j-1))%mod;
			f[u]=1ll*f[u]*s%mod;
			k=1;
		}
		else k++;
	}
	H[u]=(H[u]*Seed)^(siz[u]*10)^dep[u];
}
void KMP(){
	fail[0]=-1;
	for(int i=0,j=-1;i<cir.size();fail[++i]=++j)
		while(j!=-1&&H[cir[i]]!=H[cir[j]]) j=fail[j];
}
int main()
{
	//freopen("uni.in","r",stdin);
	//freopen("uni.out","w",stdout);
	scanf("%d%d",&n,&m),m%=mod;
	fac[0]=fac[1]=inv[0]=inv[1]=1;
	for(int i=2;i<=n;i++) fac[i]=1ll*fac[i-1]*i%mod,inv[i]=1ll*(mod-mod/i)*inv[mod%i]%mod;
	for(int i=2;i<=n;i++) inv[i]=1ll*inv[i]*inv[i-1]%mod;
	for(int i=1,x;i<=n;i++) scanf("%d",&x),line(i,x),line(x,i);
	dfs(1,0);
	for(int i: cir) solve(i,0);
	KMP();
	int num=cir.size(),len=num-fail[num],ans;
	if(!fail[num]||num%len){
		ans=1;
		for(int i: cir) ans=1ll*ans*f[i]%mod;
		return printf("%d\n",ans),0;
	}
	else{
		m=1,num/=len,ans=0;
		for(int i=0;i<len;i++) m=1ll*m*f[cir[i]]%mod;
		for(int i=1;i<=num;i++) ans=(ans+ksm(m,gcd(i,num)))%mod;
		return printf("%d\n",1ll*ans*ksm(num,mod-2)%mod),0;
	}
}

Upd (on 2020 2.10):
关于树哈希似乎有人指出用 H [ u ] ∗ S e e d H[u]*Seed H[u]Seed ^ H [ v ] H[v] H[v]会很有效,最后令 H [ u ] ∗ = S e e d ∗ S e e d H[u]*=Seed*Seed H[u]=SeedSeed达到分层效果,这样的话哈希时就不需要用siz和dep了:

	f[u]=m,H[u]=2333333;
	for(int i=1,v,j,k;i<=*son;i=j){
		v=son[i];
		for(j=i,k=0;j<=*son&&H[son[j]]==H[v];k++,j++) 
			H[u]=H[u]*Seed^H[son[j]];
		int ret=0;
		for(int F=f[v],x=1;x<=min(f[v],k);F=1ll*F*(f[v]-x)%mod,x++)
			ret=(ret+1ll*F*inv[x]%mod*C(k-1,x-1))%mod;
		f[u]=1ll*f[u]*ret%mod;
	}
	H[u]=H[u]*Seed*Seed;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值