1. 程式人生 > >jzoj3336. 【NOI2013模擬】坑帶的樹(圓方樹)

jzoj3336. 【NOI2013模擬】坑帶的樹(圓方樹)

題目描述

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][