jzoj3336. 【NOI2013模拟】坑带的树(圆方树)

32 篇文章 0 订阅
2 篇文章 0 订阅

题目描述

Description
“我不适合你,你有更好的未来。”
当小A当上主持的那一天,他接受记者采访的时候,回忆起了10年前小N离开自己的那句话。
小A出家,其实是因为他已经勘探到了宇宙的奥秘,他希望遁入佛门,通过自己的可修,创造出超越宇宙的秘法,从而突破宇宙的束缚,达到大无畏之境界。
好吧,小A最近碰到了一个挺恶心的问题。
首先,先介绍仙人掌树。仙人掌树是一张无向图,但是每个节点最多只会在一个环里面,而且这张图的环全部都是简单环,即A->B->C->A这种。
比如下图就是一颗仙人掌树。
在这里插入图片描述
好的,知道了仙人掌树之后,我们现在要计算一个东西。
我们现在已经知道了一个N个节点的仙人掌树,称作为原图。接下来,我们要用1-N的一个排列A[1]-A[N]去变换这棵树,具体的,如果原图中有一条边i-j,那么变换出来的图中必须有一条A[i]-A[j]的边。同样的,如果变换出来的图中有一条A[i]-A[j]的边,那么原图中必有一条i-j的边。(简单而言就是点重新编号)
小A为了超脱宇宙的束缚,必须要知道,有多少种排列,可以使得变换出来的新图和原图是一模一样的,具体的,原图中如果存在一条i-j的边,新图也存在一条i-j的边,新图中存在一条i-j的边,原图中也存在i-j的边。
方案数目答案mod 1000000003。

Input
第一行有两个正整数,N和M,节点个数和边的个数。
接下来M行,每行有2个正整数S,T,表示一条原图的无向边。数据保证没有重边。

Output
一行一个正整数表示方案书目。

Sample Input
5 5
1 2
2 3
3 4
4 5
1 5

Sample Output
10
解释:
所有的答案包括(i,(i+1) % 5 + 1,(i+2) % 5 + 1,(i+3) % 5 + 1,(i+4) % 5 + 1)和(i,(i+4) % 5 + 1,(i+3) % 5 + 1,(i+2) % 5 + 1,(i+1) % 5 + 1)这两种类型。每种类型的i可以是12345,所以答案是2*5=10。

Data Constraint
在这里插入图片描述

点双联通分量

类似于边双,就是一个删掉其中任意一个点仍能连通的点集
然后就需要求割边
割边的定义是删去点t后能使以son为根的子树分离出去的边t–>s
(其实就是求割顶但我不屑于这样理解

求法很简单,当dfn[t]<=low[son]时,t–>son这条边就是割边
显然如果dfn[t]>low[son]时,son以及其子树中一定有一个点可以走到t的祖先(返祖边)
在这里插入图片描述
那么当dfn[t]<=low[son]时,这条边就是割边了
类似于求边双,以son为结尾的点集可以构成一个点双

如果当前点为根,那么只有当根在Tarjan树上有≥2个儿子时,根连出去的每一条边都是割边
因为如果有两个儿子,那么这两个儿子之间一定没有边相连(否则就是一个儿子了)
所以删掉根可以把原图分成两个块
感性理解

至于dfn和low的求法,其实和边双一样
但是为了防止子树之间相互影响,要在low求出后再赋值
我原来写的是假的Tarjan

code(点双)

void Tarjan(int fa,int t)
{
	int i,Low;
	
	dfn[t]=++j;
	low[t]=j;
	Low=j;
	
	bz[t]=1;
	for (i=ls[t]; i; i=a[i][1])
	if (a[i][0]!=fa)
	{
		if (!dfn[a[i][0]])
		{
			Tarjan(t,a[i][0]);
			l+=(t==1);
			Low=min(Low,low[a[i][0]]);
		}
		else
		if (bz[a[i][0]])
		Low=min(Low,low[a[i][0]]);
	}
	bz[t]=0;
	low[t]=Low;
}

void find(int t)
{
	if (d[l][0]==t)
	{
		NEW(d[l][0],d[l][1]);
		l--;
	}
	else
	{
		N++;
		while (d[l][0]!=t)
		NEW(d[l--][0],N);
		NEW(d[l--][0],N);
	}
}

void tarjan(int fa,int t)
{
	int i;
	
	bz[t]=1;
	Bz[t]=1;
	for (i=ls[t]; i; i=a[i][1])
	if (a[i][0]!=fa)
	{
		if (!bz[a[i][0]])
		{
			l++;
			d[l][0]=t;
			d[l][1]=a[i][0];
			tarjan(t,a[i][0]);
			
			if (dfn[t]<=low[a[i][0]] && (t>1 || BZ))
			find(t);
		}
		else
		if (Bz[a[i][0]])
		{
			l++;
			d[l][0]=t;
			d[l][1]=a[i][0];
		}
	}
	Bz[t]=0;
}

圆方树

因为这道题给出的是一颗仙人掌,所以不能直接求树的同构方案
所以考虑把仙人掌转成一颗树
在这里插入图片描述
如图所示,只需要在每个点双中建一个点(称之为方点),然后点双里的每个点(称之为圆点)都向它连边
新建出来的树就叫圆方树

圆方树有一些奇♂妙的性♂质,最基本的就是每个圆点代表一个点,每个方点代表一个点双(在本题中就是环)
然后就可以求仙人掌的同构了

求同构

先假设节点1映射自己,求以某个点为根时的同构方案数tot[t],之后向上合并
对于一个圆点,因为没有限制,所以方案数就是所有子树方案数之积*所有(一种子树的出现次数的阶乘)之积
显然
因为没有限制,所以每个子树都有tot[son]种方案,一共就是Πtot[son]种
对于相同形态的子树,可以互相映射,所以有Π(一种子树的个数!)种
结果就是两者之积

(不用考虑自己是因为当前点已经确定了映射的位置)


因为方点代表的是一个环,所以不能随便重构
显然环可以翻转,所以每次以当前点为中心判断对称,如果可以翻转答案就*2

至于为什么要以当前点为中心,是因为每个点在处理时都已经确定了儿子的映射情况,而当前点的父亲已经被考虑过了,所以当前点不能再与其他点映射


还有一个问题,就是如何判断子树的形态相同
考虑哈希,具体实现可以自由选择,但要分圆点和方点来计算

圆点:因为不考虑顺序,所以把子树的哈希值乘起来
方点:因为考虑顺序,所以要以当前点按顺/逆时针分别求出两个不同的值,然后相乘(或者取min,总之随便搞)
处理时可以先在建圆方树时按顺序连边,然后把得出的序列对齐,最后计算两个方向的值反正随便搞能AC就行

实现过程中为了减少重复的可能,要尽量把子树的特征记录到哈希值里
子树深度:每次求完哈希值后取平方
儿子个数:乘以儿子数的阶乘
方点的顺序:每次平方后再加
最后加上双哈希好像不加也可以

code

#include <iostream>
#include <cstdlib>
#include <cstring>
#include <cstdio>
#define fo(a,b,c) for (a=b; a<=c; a++)
#define fd(a,b,c) for (a=b; a>=c; a--)
#define min(a,b) (a<b?a:b)
#define mod 1000000003
#define Mod 998244353
using namespace std;

int a[10001][2];
int A[10001][2];
int ls[2001];
int Ls[2001];
int dfn[1001];
int low[1001];
int C[2001][2];
bool bz[1001];
bool Bz[1001];
int d[10001][2];
long long tot[2001];
long long hash[2001][2];
long long jc[1001];
int N,n,m,i,j,k,K,l,len,Len;
long long ans;
bool BZ,_bz;

void swap(int &a,int &b) {int c=a;a=b;b=c;}
void qsort(int l,int r)
{
	int i,j,mid,Mid;
	
	i=l;
	j=r;
	mid=C[(l+r)/2][0];
	Mid=C[(l+r)/2][1];
	
	while (i<=j)
	{
		while (C[i][0]<mid || C[i][0]==mid && C[i][1]<Mid) i++;
		while (C[j][0]>mid || C[j][1]==mid && C[j][1]>Mid) j--;
		
		if (i<=j)
		{
			swap(C[i][0],C[j][0]);
			swap(C[i][1],C[j][1]);
			
			i++,j--;
		}
	}
	
	if (l<j) qsort(l,j);
	if (i<r) qsort(i,r);
	
	return;
}

void New(int x,int y)
{
	len++;
	a[len][0]=y;
	a[len][1]=ls[x];
	ls[x]=len;
}

void _new(int x,int y)
{
	Len++;
	A[Len][0]=y;
	A[Len][1]=Ls[x];
	Ls[x]=Len;
}
void NEW(int x,int y) {_new(x,y),_new(y,x);}

void Tarjan(int fa,int t)
{
	int i,Low;
	
	dfn[t]=++j;
	low[t]=j;
	Low=j;
	
	bz[t]=1;
	for (i=ls[t]; i; i=a[i][1])
	if (a[i][0]!=fa)
	{
		if (!dfn[a[i][0]])
		{
			Tarjan(t,a[i][0]);
			l+=(t==1);
			Low=min(Low,low[a[i][0]]);
		}
		else
		if (bz[a[i][0]])
		Low=min(Low,low[a[i][0]]);
	}
	bz[t]=0;
	low[t]=Low;
}

void find(int t)
{
	if (d[l][0]==t)
	{
		NEW(d[l][0],d[l][1]);
		l--;
	}
	else
	{
		N++;
		while (d[l][0]!=t)
		NEW(d[l--][0],N);
		NEW(d[l--][0],N);
	}
}

void tarjan(int fa,int t)
{
	int i;
	
	bz[t]=1;
	Bz[t]=1;
	for (i=ls[t]; i; i=a[i][1])
	if (a[i][0]!=fa)
	{
		if (!bz[a[i][0]])
		{
			l++;
			d[l][0]=t;
			d[l][1]=a[i][0];
			tarjan(t,a[i][0]);
			
			if (dfn[t]<=low[a[i][0]] && (t>1 || BZ))
			find(t);
		}
		else
		if (Bz[a[i][0]])
		{
			l++;
			d[l][0]=t;
			d[l][1]=a[i][0];
		}
	}
	Bz[t]=0;
}

void init()
{
	jc[0]=1;
	fo(i,1,1000)
	jc[i]=jc[i-1]*i%mod;
	
	scanf("%d%d",&n,&m); N=n;
	fo(i,1,m)
	{
		scanf("%d%d",&j,&k);
		New(j,k);
		New(k,j);
	}
	
	j=0;l=0;
	Tarjan(0,1);
	BZ=(l>1);
	
	l=0;
	tarjan(0,1);
	if (l) find(1);
	
	memcpy(a,A,sizeof(a));
	memcpy(ls,Ls,sizeof(ls));
	len=Len;
}

void dfs(int fa,int t)
{
	int i,j,k,L,l=0,l2;
	bool bz;
	tot[t]=1;
	hash[t][0]=2;
	hash[t][1]=2;
	
	for (i=ls[t]; i; i=a[i][1])
	if (a[i][0]!=fa)
	dfs(t,a[i][0]);
	
	if (t>n && !fa)
	{
		l=1;
		C[1][0]=-1;
	}
	
	for (i=ls[t]; i; i=a[i][1])
	if (a[i][0]!=fa)
	{
		l++;
		C[l][0]=hash[a[i][0]][0]%mod;
		C[l][1]=hash[a[i][0]][1]%Mod;
		tot[t]=tot[t]*tot[a[i][0]]%mod;
	}
	else
	if (t>n)
	C[++l][0]=-1;
	
	l2=l;
	
	if (t<=n) //circle
	{
		qsort(1,l);
		
		j=1;
		fo(i,2,l)
		if (C[i-1][0]==C[i][0] && C[i-1][1]==C[i][1])
		j++;
		else
		{
			tot[t]=tot[t]*jc[j]%mod;
			j=1;
		}
		tot[t]=tot[t]*jc[j]%mod;
		
		fo(i,1,l)
		{
			hash[t][0]=hash[t][0]*C[i][0]%mod;
			hash[t][1]=hash[t][1]*C[i][1]%Mod;
		}
	}
	else //square
	{
		L=1;
		while (C[L][0]!=-1)
		{
			l++;
			C[l][0]=C[L][0];
			C[l][1]=C[L][1];
			L++;
		}
		L++;
		
		long long h1=2,h2=2;
		fo(i,L,l) h1=(h1*h1+C[i][0])%mod;
		fd(i,l,L) h2=(h2*h2+C[i][0])%mod;
		hash[t][0]=h1*h2%mod;
		h1=2,h2=2;
		fo(i,L,l) h1=(h1*h1+C[i][1])%Mod;
		fd(i,l,L) h2=(h2*h2+C[i][1])%Mod;
		hash[t][1]=h1*h2%Mod;
		
		if (_bz)
		{
			bz=1;
			j=L;k=l;
			while (j<k)
			{
				if (C[j][0]!=C[k][0] || C[j][1]!=C[k][1])
				{
					bz=0;
					break;
				}
				j++;k--;
			}
			if (bz) ans=(ans<<1)%mod;
		}
	}
	
	hash[t][0]=hash[t][0]*hash[t][0]%mod*(jc[l2]+1)%mod;
	hash[t][1]=hash[t][1]*hash[t][1]%Mod*(jc[l2]+1)%Mod;
}

void work()
{
	ans=1;
	_bz=1;
	dfs(0,1);
	_bz=0;
	
	ans=ans*tot[1]%mod;
	k=hash[1][0];
	K=hash[1][1];
	
	l=1;
	fo(i,2,n)
	{
		dfs(0,i);
		l+=(hash[i][0]==k && hash[i][1]==K);
	}
	ans=ans*l%mod;
}

int main()
{
	init();
	work();
	
	printf("%lld\n",ans);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值