树哈希(Tree Hash)
顾名思义,对树进行哈希,经常判断两个树是否同构。一下均为对有根树的算法,而无根树只需要找重心
无权树
方法一
f n o w = s i z e n o w × ∑ f s o n ( n o w , i ) × s e e d i − 1 f_{now}=size_{now} \times \sum f_{son(now,i)}\times seed^{i-1} fnow=sizenow×∑fson(now,i)×seedi−1
其中 f x f_x fx 为以节点 x x x 为根的子树对应的哈希值。特殊地,我们令叶子节点的哈希值为 1 1 1。
s i z e x size_{x} sizex 表示以节点 x x x 为根的子树大小。
s o n x , i son_{x,i} sonx,i 表示 x x x 所有子节点以 f f f 作为关键字排序后排名第 i i i 的儿子。
s e e d seed seed 为选定的一个合适的种子(最好是质数,对字符串 hash 有了解的人一定不陌生)
上述哈希过程中,可以适当取模避免溢出或加快运行速度。
方法二
f n o w = ⨁ f s o n ( n o w , i ) × s e e d + s i z e s o n ( n o w , i ) f_{now}=\bigoplus f_{son(now,i)}\times seed+size_{son(now,i)} fnow=⨁fson(now,i)×seed+sizeson(now,i)
其中 f x f_x fx 为以节点 x x x 为根的子树对应的哈希值。特殊地,我们令叶子节点的哈希值为 1 1 1。
s i z e x size_{x} sizex 表示以节点 x x x 为根的子树大小。
s o n x , i son_{x,i} sonx,i 表示 x x x 所有子节点之一(不用排序)。
s e e d seed seed 为选定的一个合适的质数。
⨁ \bigoplus ⨁ 表示异或和。
方法三
f n o w = 1 + ∑ f s o n ( n o w , i ) × p r i m e ( s i z e s o n ( n o w , i ) ) f_{now}=1+\sum f_{son(now,i)} \times prime(size_{son(now,i)}) fnow=1+∑fson(now,i)×prime(sizeson(now,i))
其中 f x f_x fx 为以节点 x x x 为根的子树对应的哈希值。
s i z e x size_{x} sizex 表示以节点 x x x 为根的子树大小。
s o n x , i son_{x,i} sonx,i 表示 x x x 所有子节点之一(不用排序)。
p r i m e ( i ) prime(i) prime(i) 表示第 i i i 个质数。
#include <bits/stdc++.h>
#define FR freopen("in.txt", "r", stdin)
#define FW freopen("out.txt", "w", stdout)
using namespace std;
typedef long long ll;
struct Edge
{
int to;
int nxt;
} e[500];
int head[100];
int siz[100];
ll has[100][100];
int tot = 0;
ll base = 233;
ll mod = 1000000007;
void add(int u, int v)
{
tot++;
e[tot].to = v;
e[tot].nxt = head[u];
head[u] = tot;
}
ll hashTree(int u, int r)
{
siz[u] = 1;
vector<ll> subhash;
for (int ne = head[u]; ne; ne = e[ne].nxt)
{
int to = e[ne].to;
if (to == r)
continue;
subhash.push_back(hashTree(to, u));
siz[u] += siz[to];
}
if (siz[u] == 1)
return 1;
sort(subhash.begin(), subhash.end());
ll ans = 0;
for (int i = 0; i < subhash.size(); i++)
{
ans = (ans * base + subhash[i]) % mod;
}
ans *= siz[u];
return ans;
}
int main()
{
int M;
cin >> M;
for (int i = 1; i <= M; i++)
{
tot = 0;
memset(head, 0, sizeof(head));
int n;
cin >> n;
int root = 0;
for (int j = 1; j <= n; j++)
{
int fa;
cin >> fa;
if (fa == 0)
{
root = i;
}
else
{
add(fa, j);
add(j, fa);
}
}
for (int j = 1; j <= n; j++)
{
has[i][j] = hashTree(j, 0);
}
for (int j = 1; j <= M; j++)
{
for (int r1 = 1; r1 <= n; r1++)
{
for (int r2 = 1; r2 <= n; r2++)
{
if (has[i][r1] == has[j][r2])
{
printf("%d\n", j);
goto gg;
}
}
}
}
gg:
continue;
}
return 0;
}
有权树
只需要将权值加入进公式即可:
f n o w = s i z e n o w × ∑ f s o n ( n o w , i ) × s e e d i − 1 × w n o w f_{now}=size_{now} \times \sum f_{son(now,i)}\times seed^{i-1} \times w_{now} fnow=sizenow×∑fson(now,i)×seedi−1×wnow
另外,我们可以使用字符串hash的方式:
例如题目:LeetCode 1948
我们定义一个叶子节点的序列化字符串为空字符串,一个非叶子节点的字符串序列化为:
f n o w = H a s h ( ∑ w s o n ( f s o n ) ) f_{now} = Hash(\sum w_{son}(f_{son})) fnow=Hash(∑wson(fson))
其中求和表示拼接字符串, H a s h Hash Hash为某一个字符串哈希函数。此时拼接顺序一定要根据权值进行排序。
如果是无权树,那么只剩下括号序列,就是树的最小括号表示法。