树上算法学习笔记(四):树同构相关
\(\text{Prufer}\) 序列
\(\text{Prufer}\) 序列常用于统计有标号无根树的统计。我们可以将任意一棵有标号无根树映射成一个唯一的 \(\text{Prufer}\) 序列,一个 \(\text{Prufer}\) 序列也对应着一个唯一的有标号无根树。通过 \(\text{Prufer}\) 序列的转化,我们可以将一些比较困难的树计数问题转化成序列计数问题。
其实 \(\text{Prufer}\) 序列的构造比较简单,那就是每次选择编号最小的叶节点并删掉它,然后在序列中记录下它连接到的那个节点编号。我们下面通过构造来证明 \(\text{Prufer}\) 和有标号无根树一一对应。
将树转化成 \(\text{Prufer}\) 序列
由于要求每次选择编号最小的叶节点,因此一个很显然的思路就是,拿堆将所有的叶子节点的编号存下来,再不断地取编号最小的叶子节点删掉,并将它连接到的那个点加入 \(\text{Prufer}\) 序列。如果此时它连接到的点变成了叶子节点,就将父节点加入堆中。这样做的时间复杂度为 \(\mathcal O(n \log n)\)。
我们考虑将它优化成线性,也就是不需要进行比大小的操作。因此我们维护一个指针 \(p\),一开始指向编号最小的节点。随后我们将 \(p\) 不断扩大,如果遇到某个节点 \(p\) 是叶子节点,那么我们就将与 \(p\) 连接的点 \(u\) 加入 \(\text{Prufer}\) 序列,并将其删掉。此时与它连接的节点 \(u\) 可能会成为新的叶子节点。如果 \(u > p\),那么就不进行任何操作,否则就将与 \(u\) 连接的点也加入 \(\text{Prufer}\) 序列,并继续考虑与 \(u\) 连接的点 \(v\)。
这样为什么是正确的呢?由于 \(p\) 是当前编号最小的叶子节点,那么这一步一定会删除 \(p\)。考虑到如果删除了 \(p\),并未产生新的叶子节点,那么就将 \(p\) 继续扩大。否则如果产生了新的叶子节点 \(v\),那么有以下两种情况:
-
\(v > p\),那么 \(p\) 继续扩大一定会扫到 \(v\),于是我们不用做任何操作;
-
\(v < p\),由于 \(p\) 是原先编号最小的叶子节点,而 \(v < p\),证明 \(v\) 是现在编号最小的叶子节点,那么我们就应该把与它连接的点 \(u\) 加入到拓扑排序中,并继续考虑是否又出现了这样的点。
此时我们发现每个节点只被遍历到了 \(1\) 次,因此此时算法的复杂度降到了 \(\mathcal O(n)\)。
此时我们还可以发现,一棵有标号无根树只有一个字典最小的拓扑序列,也只有一个 \(\text{Prufer}\) 序列。
将 \(\text{Prufer}\) 序列转化成树
我们观察 \(\text{Prufer}\) 序列,可以得到以下性质:
-
构造完 \(\text{Prufer}\) 序列后,一定还剩下两个节点,其中一个节点一定为 \(n\)。
-
每个节点出现的次数为这个节点的度数减 \(1\),也就是只有将这个节点的度数减到 \(1\),才有可能将这个节点删掉。
我们利用这两个性质,可以将 \(\text{Prufer}\) 序列重新转化成树转化成树,且方法类似。
我们先统计出树上每个节点的度数,然后我们每次选择编号最小的叶子节点,将它与在 \(\text{Prufer}\) 中对应的节点连边,并删除该叶子节点。如果用堆维护,时间复杂度依然是 \(\mathcal O(n \log n)\)。
我们依然沿用刚才的优化方法,在删度数的时侯会产生新的叶结点 \(u\),于是判断 \(u\) 与指针 \(p\) 的大小关系,如果更小就优先考虑它。复杂度降低至 \(\mathcal O(n)\)。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 5e6 + 9;
int fa[N], p[N], deg[N], n, m, ans;
signed main(){
scanf("%lld%lld", &n, &m);
if(m == 1){
for(int i = 1; i <= n - 1; i++){
scanf("%lld", &fa[i]);
deg[fa[i]]++, deg[i]++;
}
for(int i = 1, pos = 1; i <= n - 2; i++, pos++){
while(deg[pos] > 1)
pos++;
p[i] = fa[pos];
while(i <= n - 2 && (--deg[p[i]] == 1) && p[i] < pos)
p[i + 1] = fa[p[i]], i++;
}
for(int i = 1; i <= n - 2; i++)
ans ^= i * p[i];
printf("%lld", ans);
} else {
for(int i = 1; i <= n - 2; i++){
scanf("%lld", &p[i]);
deg[p[i]]++, deg[i]++;
}
++deg[n - 1], ++deg[n];
p[n - 1] = n;
for(int i = 1, pos = 1; i <= n - 1; i++, pos++){
while(deg[pos] > 1)
pos++;
fa[pos] = p[i];
while(i <= n - 1 && (--deg[p[i]] == 1) && p[i] < pos)
fa[p[i]] = p[i + 1], i++;
}
for(int i = 1; i <= n - 1; i++)
ans ^= i * fa[i];
printf("%lld", ans);
}
return 0;
}
\(\text{AHU}\) 算法
我觉得这是一个没有学过 OI 的人也可能会想出来的判断树同构的方法。
树同构的定义
有根树同构:对于两棵有根树 \(T_1(V_1, E_1, r_1)\) 和 \(T_2(V_2, E_2, r_2)\),如果存在双射 \(\varphi : V_1 \longleftrightarrow V_2\),使得对于所有 \((u, v) \in E_1 \iff (\varphi(u), \varphi(v)) \in E_2\),且 \(\varphi(r_1) = r_2\),那么我们就称这两棵有根树同构。说人话就是如果将 \(T_1\) 中的节点重新编号,并钦定一个根以后,这两棵有根树完全相同,那么我们就称这两棵有根树同构。
无根树同构:将有根树同构中 \(\varphi(r_1) = r_2\) 的限制去掉,如果两棵无根树满足剩余的条件,那么就称这两棵无根树同构。
无根树同构其实特别不方便判断。其实我们有一个想法,那就是给无根树钦定一个根,将其转化成有根树同构。但是钦定根的方案太多了,于是我们考虑将树的重心作为根。显然,两棵同构无根树的重心是一样的,因为重心只跟子树大小有关,而跟节点编号无关。
注意,一颗无根树的重心可能会有两个,此时我们需要判断将两颗重心作为分别根时两棵有根树是否同构。
朴素的 \(\text{AHU}\) 算法
对于一棵有根树,我们知道在确定一棵二叉树的前序和中序遍历的情况下可以唯一确定这棵二叉树,因此为了判断两棵二叉树是否同构,我们可以判断前序和中序遍历是否一样。但是有可能某个节点的两个儿子对调了一下,那么我们钦定小的那个儿子先遍历,此时就可以避免这种情况。
\(\text{AHU}\) 算法就基于这种思路。我们 DFS 这棵有根树的时候,当进入一个节点的子树时,我们将一个左括号压入队列;当离开这个节点的子树时,我们将一个右括号压入队列。那么此时,任何一棵有根树都对应了一个括号序列,而且我们发现,一棵树的括号序列,可以由它儿子子树的括号序列拼接在一起,再在外面套上一个 \(()\) 得到。由于此时我们还是要排除某个节点的几个儿子对调的情况,于是我们将所有儿子子树的括号序列按字典序排列以后,在拼在一起就可以了。
这个算法的时间复杂度上界可以在这棵树形态为一条链时得到,此时从下往上数第 \(i\) 个节点的子树的括号序列长度为 \(2 \times i\),而拼接两个字符串的复杂度是线性的,因此总时间复杂度为 \(\mathcal O(n^2)\)。
优化的 \(\text{AHU}\) 算法
我们考虑到朴素的 \(\text{AHU}\) 算法比较慢在于一个节点子树的括号序列可能过于长了,对于这一点需要优化。
我们考虑一个和倍增求后缀数组一样思路的优化,那么就是对于整棵树分层,每个节点的子树重新定义一个数组,这个数组由它的儿子子树的数组在同一层的排名拼接而成。此时,我们就用一个排名代替了子节点子树冗长的括号序列。这和离散化很像,就是用这个数字的排名代替数字本身。
此时,每一个节点的子树的数组大小,就是这个节点的度数,那么总的数组长度就从 \(O(n^2)\) 降到了 \(O(n)\),而由于节点的子树的数组都要恰好参加一次排序,那么总的时间复杂度就为 \(O(n \log n)\)。
SP7826 TREEISO - Tree Isomorphism
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 9;
struct Edge{
int v, nex;
} e[N << 1];
int head[N], ecnt;
void addEdge(int u, int v){
e[++ecnt] = Edge{v, head[u]};
head[u] = ecnt;
}
int siz[N], maxn[N], maxsiz, n, T;
void dfs1(int u, int fa){
siz[u] = 1;
maxn[u] = 0;
for(int i = head[u]; i; i = e[i].nex){
int v = e[i].v;
if(v == fa)
continue;
dfs1(v, u);
siz[u] += siz[v];
maxn[u] = max(maxn[u], siz[v]);
}
}
vector <int> c[2], dep[N], tag[N];
void dfs2(int rt, int u, int fa, int id){
maxn[u] = max(maxn[u], siz[rt] - siz[u]);
if(maxsiz > maxn[u]){
c[id].clear();
c[id].push_back(u);
maxsiz = maxn[u];
} else if(maxsiz == maxn[u])
c[id].push_back(u);
for(int i = head[u]; i; i = e[i].nex){
int v = e[i].v;
if(v == fa)
continue;
dfs2(rt, v, u, id);
}
}
int f[N], tg[N];
int get_h(int u, int fa, int d){
dep[d].push_back(u);
f[u] = fa;
int h = 0;
for(int i = head[u]; i; i = e[i].nex){
int v = e[i].v;
if(v == fa)
continue;
h = max(h, get_h(v, u, d + 1));
}
return h + 1;
}
bool cmp(int u, int v){
return tag[u] < tag[v];
}
bool check(int rt1, int rt2){
for(int i = 0; i <= 2 * n + 1; i++){
dep[i].clear();
tag[i].clear();
}
int h1 = get_h(rt1, 0, 0), h2 = get_h(rt2, 0, 0);
if(h1 != h2)
return false;
for(auto v : dep[h1 - 1])
tg[v] = 0;
for(int i = h1 - 1; i >= 1; i--){
for(auto v : dep[i])
tag[f[v]].push_back(tg[v]);
sort(dep[i - 1].begin(), dep[i - 1].end(), cmp);
for(int j = 0, cnt = 0; j < (int)dep[i - 1].size(); j++) {
if(j && tag[dep[i - 1][j]] != tag[dep[i - 1][j - 1]])
cnt++;
tg[dep[i - 1][j]] = cnt;
}
}
return tag[rt1] == tag[rt2];
}
void init(){
memset(head, 0, sizeof(head));
ecnt = 0;
c[0].clear();
c[1].clear();
}
int main(){
scanf("%d", &T);
while(T--){
init();
scanf("%d", &n);
for(int i = 1; i < n; i++){
int u, v;
scanf("%d%d", &u, &v);
addEdge(u, v);
addEdge(v, u);
}
dfs1(1, 0);
maxsiz = n;
dfs2(1, 1, 0, 0);
for(int i = 1; i < n; i++){
int u, v;
scanf("%d%d", &u, &v);
addEdge(u + n, v + n);
addEdge(v + n, u + n);
}
dfs1(1 + n, 0);
maxsiz = n;
dfs2(1 + n, 1 + n, 0, 1);
if(c[0].size() == c[1].size()){
if(check(c[0][0], c[1][0]))
printf("YES\n");
else if(c[0].size() > 1 && check(c[0][0], c[1][1]))
printf("YES\n");
else
printf("NO\n");
} else
printf("NO\n");
}
return 0;
}
树哈希
现在我们来讲一个更加 OI 的方法来判断两棵有根树是否同构。
首先,为了比较两个东西相等,这不难想到哈希。但是我们现在是对一棵树进行哈希,如果我们按照字符串哈希的思路,将儿子的哈希值乘以 \(base\) 的若干次方,那么如果对调了若干个儿子,此时的哈希值是要变的。我们这次换一个思路,不再将儿子节点排序,而是将儿子节点的哈希值进行一通神秘的变换 \(f(x)\),再加到父节点的哈希值上来。形式化的,\(h(u) = \displaystyle\sum_{v \in son(u)} f(h(v))\)。
这里,大神 moorhsum 断言,如果此处使用自然溢出哈希,那么哈希冲突的期望冲突次数为 \(\displaystyle\frac{n^2}{2^{\omega}}\),其中 \(\omega\) 取决于哈希表中存储的数据类型的长度。但是笔者实在太菜,不会证明。我们只用知道这个值很小就是了。不过,如果使用我的模数为 \(2^{61} - 1\) 的哈希,那么不用自然溢出也不会被卡。
但是有些时候出题人非常恶毒,会卡这种神秘哈希,但是只需要在哈希时加入一个神秘常数,就可以让出题人失去一切力量手段,但是我依然不会证明,就先将它记住吧!
点击查看代码
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e6 + 9, base = rand(), MOD = (1ll << 61) - 1;
int plu(int x, int y){
return x + y > MOD ? x + y - MOD : x + y;
}
int sub(int x, int y){
return x < y ? x - y + MOD : x - y;
}
int mod(__int128 x){
return sub((MOD & x) + (x >> 61), MOD);
}
int mul(__int128 x, int y){
return mod(x * y);
}
int H(__int128 x){
return plu(mul(mul(mul(x, x), x), 19890535), 19260817);
}
int F(int x){
return plu(H(x & MOD), H(x >> 61));
}
struct Edge{
int v, nex;
} e[N << 1];
int head[N], ecnt;
void addEdge(int u, int v){
e[++ecnt] = Edge{v, head[u]};
head[u] = ecnt;
}
int h[N], ans;
void dfs(int u, int fa){
h[u] = base;
for(int i = head[u]; i; i = e[i].nex){
int v = e[i].v;
if(v == fa)
continue;
dfs(v, u);
h[u] = plu(h[u], F(h[v]));
}
}
int n;
signed main(){
srand(time(NULL));
scanf("%lld", &n);
for(int i = 1; i < n; i++){
int u, v;
scanf("%lld%lld", &u, &v);
addEdge(u, v);
addEdge(v, u);
}
dfs(1, 0);
sort(h + 1, h + n + 1);
int ans = unique(h + 1, h + n + 1) - h - 1;
printf("%lld", ans);
return 0;
}
我们用这种哈希方法的好处,就是可以快速地换根。假设 \(u\) 为 \(v\) 的父亲,我们此时想把根从 \(u\) 转到 \(v\),那么我们就先将 \(h_u\) 减掉 \(f(h_v)\),得到除开 \(v\) 的子树,其它子树的哈希值的和,也就是换根以后 \(u\) 的哈希值,我们将其带入 \(f\) 中,再加上 \(v\) 子树原本的哈希值,就可以得到以 \(v\) 为根,整棵树的哈希值了。
这道题中,由于 \(B\) 中添加了一个叶子,会使得重心发生变化,因此无法沿用之前的套路,于是我们只能将 \(A\) 一每一个节点为根,整棵子树的哈希值。再将 \(B\) 中任意一个叶子节点 \(v\) 删去,查询 \(B\) 的哈希值有无出现在 \(A\) 的哈希值序列中。\(B\) 哈希值可以直接用 \(h_{fa_v} - f(h(v))\) 得到。
点击查看代码
#include <bits/stdc++.h>
#include <ext/pb_ds/assoc_container.hpp>
#include <ext/pb_ds/hash_policy.hpp>
using namespace std;
using namespace __gnu_pbds;
#define int long long
const int N = 1e5 + 9, base = rand(), MOD = (1ll << 61) - 1;
int plu(int x, int y){
return x + y > MOD ? x + y - MOD : x + y;
}
int sub(int x, int y){
return x < y ? x - y + MOD : x - y;
}
int mod(__int128 x){
return sub((MOD & x) + (x >> 61), MOD);
}
int mul(__int128 x, int y){
return mod(x * y);
}
int H(__int128 x){
return plu(mul(mul(mul(x, x), x), 19890535), 19260817);
}
int F(int x){
return plu(H(x & MOD), H(x >> 61));
}
gp_hash_table <int, bool> mp;
struct Edge{
int v, nex;
} e[N << 1], e2[N << 1];
int head[N], ecnt;
void addEdge(int u, int v){
e[++ecnt] = Edge{v, head[u]};
head[u] = ecnt;
}
int head2[N], ecnt2;
void addEdge2(int u, int v){
e2[++ecnt2] = Edge{v, head2[u]};
head2[u] = ecnt2;
}
int h[N], tmp[N], ans, n;
int h2[N], tmp2[N];
void dfs(int u, int fa){
h[u] = base;
for(int i = head[u]; i; i = e[i].nex){
int v = e[i].v;
if(v == fa)
continue;
dfs(v, u);
h[u] = plu(h[u], F(h[v]));
}
}
void dp(int u, int fa){
if(u != 1)
tmp[u] = plu(F(sub(tmp[fa], F(h[u]))), h[u]);
mp[tmp[u]] = true;
for(int i = head[u]; i; i = e[i].nex){
int v = e[i].v;
if(v == fa)
continue;
dp(v, u);
}
}
void dfs2(int u, int fa){
h2[u] = base;
for(int i = head2[u]; i; i = e2[i].nex){
int v = e2[i].v;
if(v == fa)
continue;
dfs2(v, u);
h2[u] = plu(h2[u], F(h2[v]));
}
}
void dp2(int u, int fa){
if(u != 1)
tmp2[u] = plu(F(sub(tmp2[fa], F(h2[u]))), h2[u]);
for(int i = head2[u]; i; i = e2[i].nex){
int v = e2[i].v;
if(v == fa)
continue;
dp2(v, u);
}
}
signed main(){
srand(time(NULL));
scanf("%lld", &n);
for(int i = 1; i < n; i++){
int u, v;
scanf("%lld%lld", &u, &v);
addEdge(u, v);
addEdge(v, u);
}
for(int i = 1; i <= n; i++){
int u, v;
scanf("%lld%lld", &u, &v);
addEdge2(u, v);
addEdge2(v, u);
}
dfs(1, 0);
tmp[1] = h[1];
mp[tmp[1]] = true;
dp(1, 0);
dfs2(1, 0);
tmp2[1] = h2[1];
dp2(1, 0);
for(int i = 1; i <= n + 1; i++){
if(!e2[head2[i]].nex){
int fa = e2[head2[i]].v;
if(mp[sub(tmp2[fa], F(base))]){
printf("%lld", i);
return 0;
}
}
}
return 0;
}
本文来自博客园,作者:Orange_new,转载请注明原文链接:https://www.cnblogs.com/JPGOJCZX/p/19001589

浙公网安备 33010602011771号