1. 程式人生 > >HDU 5732 2016多校Contest 1 Subway【找樹的重心,判斷樹的同構】

HDU 5732 2016多校Contest 1 Subway【找樹的重心,判斷樹的同構】

題目大意:

給定一棵樹,這兩棵樹肯定是同構的。

問你,第一棵樹的每個節點,可以對應第二個樹的那個節點。 顯然對應方法不唯一,SPJ來檢測結果正確。

方法:

首先找樹的重心, 樹的重心最多2個。

一個重心的情況很多,兩個重心的情況如圖:


有人說這個圖太對稱了……

那給個不對稱的。

 這個圖很重要……涉及到一些奇怪的情況

求樹的重心,是一個簡單的tree dp

f[i]表示i如果為根(為整棵樹的根的情況下),所有兒子節點的size的max。

s[i]表示在做DP中,i的所有兒子的節點總數。

f[i] = max(s[j],  n-sum(s[j])-1)  其中j為i的兒子。

然後找f[]陣列中元素的最小值,就是樹的重心了。

如果第一棵樹的重心為A,B。 第二顆數的重心是C,D

那麼就檢查以A為根,以C為根的兩棵樹是否同構,再檢查(A,D),(B,C),(B,D)是否同構。同構則相對著輸出即可。

題解這裡判斷同構的是給樹進行雜湊。給的方案是:

(我理解的……)

照抄題解“

這裡提供一種方法:首先求解樹的中點,然後將中點作為根。只有一個結點的子樹雜湊值為 1。選一個比較大的質數P和一個特別大的質數Q。對於每一顆樹,把它的所有子樹的雜湊 值排序。然後hash=sum(P^{i}*hash[i])\%Qhash=sum(Pihash[i])%Q,就能算出來總體的雜湊值。有兩個中點的樹兩個 中點都試一下。為了保險可以檢查下雜湊值有沒有重的。


葉子節點的雜湊值為1,假設要求K節點的雜湊值。

K有兒子節點A,B,C。並且A<=B<=C(排序好了)

hash[k] =  (P^1*hash[A])+ (P^2*hash[B])+(P^3*hash[C]) % Q

P,Q為素數。

然後就出問題了……

我上面圖上的那個情況,按照我理解的題解的做法,不管你素數取多少,都會重複。導致不管選哪一對重心,都會導致雜湊值相同……

然後我給雜湊函式改一下,排序好的A,B,C.... 並不都用P這一個素數,每5個數字迴圈一次,用5個素數來

hash[k] =  (P[1]^1*hash[A])+ (P[2]^2*hash[B])+(P[3]^3*hash[C])  + ....+ [P[I%5+1]^I * hash[?]] % Q

這樣…… 然後就AC了。 

當然題解說,需要檢查雜湊值是否有重的。。。那樣好麻煩,還是選一些比較不太容易重的雜湊函式吧~

程式跑了2秒7,幾乎馬上就要超時了……

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <cstring>
#include <vector>
#include <map>
#include <string>
using namespace std;

typedef long long LL;
const int maxn = 100000 + 100;
int n;

map<string,LL>mp;
int mp_tail=0;
vector<LL> g1[maxn];
vector<LL> g2[maxn];
const LL P[] = {435617, 999983, 327827, 674249, 986191}; //一共5個素數
const LL mod = 1e9+7;
string a1[maxn],a2[maxn];//車站下標,對應的車站名

LL powMod( LL a , LL b , LL p = mod )//a^b % p
{
    LL r = 1 ;
    a %= p ;
    while( b )
    {
        if( b&1 ) r = r*a%p ;
        b >>= 1 ;
        a = a*a%p ;
    }
    return r ;
}

LL get(char s[])	//判斷這個字串是否出現過,並根據情況新增進map,並返回這個字串對應的數字
{
	string x="";
	for (int i = 0; s[i]!=0; x+=s[i++]);
	//cout<<"!!!"<<x<<endl;

	if (mp.find(x) == mp.end())
	{
		mp[x]= ++mp_tail;
	}else return mp[x];
	return mp_tail;
}

void init()
{
	for (int i = 0; i<= n; ++ i)	
	{
		g1[i].clear();
		g2[i].clear();
	}
	mp.clear();
	mp_tail=0;
	char a[100],b[100];

	for (int i = 1; i < n; ++ i)
	{
		scanf("%s%s",a, b);
		LL A = get(a);
		a1[A]=a;
		LL B = get(b);
		a1[B]=b;
		g1[A].push_back(B);
		g1[B].push_back(A);
	}
	for (int i = 1; i < n; ++ i)
	{
		scanf("%s%s",&a, &b);
		LL A = get(a);
		a2[A]=a;
		LL B = get(b);
		a2[B]=b;
		g2[A].push_back(B);
		g2[B].push_back(A);
	}
}

LL dp(int now, vector<LL> g[], LL f[], int fa)	//求一棵樹的重心的程式
{
	LL sum = 0;
	f[now] =0;
	if (g[now].size()==0)
	{
		return 1;
	}
	for (int i = 0; i != g[now].size(); ++ i)
	{
		int will = g[now][i];
		if (will == fa)	continue;
		LL tmp =dp(will, g, f, now);
		sum += tmp;
		f[now] = max(f[now], tmp);
	}
	if (now != fa)	f[now] = max(f[now], n - sum - 1);
	return sum + 1;
}

LL f1[maxn], f2[maxn];
LL f3[maxn];
vector<LL>zhong1;
vector<LL>zhong2;



struct node
{
	LL hash, id;
	node(){}
	node(LL _hash, LL _id)
	{
		hash = _hash;
		id = _id;
	}
};

bool operator < (node a, node b)
{
	return a.hash < b.hash;
}

vector<node> tmp[maxn];
vector<LL>g3[maxn], g4[maxn];

LL heav(int now, vector<LL> g[], LL f[], int fa, vector<LL> h[])
{
	if (g[now].size() == 1 && g[now][0] == fa)
		//這裡有個程式陷阱。。。判斷是否為葉子節點,並不是size=0,而是size=1,並且這個節點為父親。
	{
		return f[now] = 1;
	}
	LL hash = 0;
	tmp[now].clear();
	for (int i = 0; i != g[now].size(); ++ i)
	{
		int will = g[now][i];
		if (will == fa)	continue;
		LL t = heav(will, g, f, now, h);	//得到兒子節點的hash值,儲存進臨時陣列中,為了重新構圖用。
		tmp[now].push_back(node(t,will));
	}
	sort(tmp[now].begin(), tmp[now].end());//根據兒子節點的雜湊值排序

	for (int i = 0; i != tmp[now].size(); ++ i)//根據兒子節點的雜湊值,計算自己節點的雜湊值
	{
		h[now].push_back(tmp[now][i].id);
		hash += powMod(P[i%5], i + 1, mod) * tmp[now][i].hash;
		hash %= mod;
	}
	f[now] = hash;
	return f[now];
}

void pg(int a, int b)	//第一棵樹在a節點,第二課樹在b節點,這2個節點彼此對應,同時遍歷兩棵樹,輸出他們。
{
	printf("%s %s\n", a1[a].c_str(), a2[b].c_str());
	//這裡a1,a2儲存的是編號對應的字元。 題目給的是a,b,c之類的字串,然後字串被化為了數字,這裡要重新輸出字串
	for (int i = 0; i != g3[a].size(); ++ i)
	{
		int will1 = g3[a][i];
		int will2 = g4[b][i];
		pg(will1, will2);
	}
}


bool check(int a, int b)
{
	//cout<<a<<" "<<b<<endl;
	for (int i = 0; i <=n;++i)
	{
		g3[i].clear();
		g4[i].clear();
	}
	heav(a, g1, f1, 0, g3);	
	//去計算第一棵樹,以a為根,g1儲存的第一棵樹的邊的情況,f1是返回每個節點的雜湊值。 0表示父親,為了遍歷樹避免訪問到父親。 g3為返回一棵有根樹。
	heav(b, g2, f2, 0, g4);
	//計算第二顆數的資訊
	
	if (f1[a] == f2[b]) //判斷兩棵樹的根節點的雜湊值是否相同
	{
		pg(a,b); //相同,則輸出答案
		return 1;
	}
	return 0;
}

void doit()
{
	dp(1, g1, f1, 1);
	dp(1, g2, f2, 1);
	
	zhong1.clear();
	zhong2.clear();
	memmove(f3,f1,sizeof(f3));
	sort(f3+1,f3+1+n);
	int tmp = f3[1];
	for (int i = 1; i <= n; ++ i)
	{
		if (f1[i]==tmp)	zhong1.push_back(i);
		if (f2[i]==tmp) zhong2.push_back(i);
	}

	for (int i = 0; i != zhong1.size(); ++ i)
		for (int j = 0; j != zhong2.size(); ++ j)
		{
			if (check(zhong1[i], zhong2[j]))	return;
		}
}

int main()
{
	while (~scanf("%d", &n))
	{
		init();
		doit();
	}
	return 0;
}