树哈希(附带树的重心板子)
update:NOI考树哈希了。到底什么树哈希是真的啊。
树哈希是判定树同构的方法。两棵树如果在重新对每个节点赋一个编号之后等价,那么就称这两棵树同构。
我们的思路是:先将树变为有根的,然后从根往下遍历回溯,并求出一个节点所有子树的哈希值。如果两棵子树的哈希值相同,当且仅当这两棵子树同构。然后合并到这个节点的哈希值并继续向上计算。
首先我们要找到树根。一般使用树的重心。树的重心是以它为根所有子树大小都不大于 $ \lfloor\cfrac{n}{2} \rfloor$ 的点,最多有两个,最少有一个,两个的情况仅当树的结点个数为偶数并且这两个点的连边平分整棵树(指大小)。我们使用树的重心作为树根,比较容易对应上。
求法:按照这个定义求就可以了。\(O(n)\)
int n, sz[MAXN], mss[MAXN]; // n:总结点数(请从外部传入),sz:树的大小,mss:最大子树大小
vector<int> ctr; // 重心
void dfs(int p, int fa = 0) // 找重心
{
sz[p] = 1, mss[p] = 0;
for (auto [to, w] : edges[p])
if (to != fa)
{
dfs(to, p);
mss[p] = max(mss[p], sz[to]);
sz[p] += sz[to];
}
mss[p] = max(mss[p], n - sz[p]);
if (mss[p] <= n / 2) ctr.push_back(p);
}
树哈希的写法:有根树之后,我们算出了 \(i\) 所有子节点的哈希值,将子节点按照哈希值排序。(我们不知道子节点的顺序,所以要用一个确定的顺序排序。不可以用子树大小,因为若干个长得不一样的子树可以大小一样)然后遍历子节点,计算:(其中 \(son(u,i)\) 表示 \(u\) 的第 \(i\) 个儿子,\(seed\) 最好是质数,可以是 \(13331\),借鉴字符串哈希)
多个重心怎么办:如果是判断两棵树是否同构,直接取较大的 \(hash\) 值,如果同构的话在较大那个重心处 \(hash\) 值一样。
P5043
#include<bits/stdc++.h>
using namespace std;
#define f(i, a, b) for(long long i = (a); i <= (b); i++)
#define cl(i, n) i.clear(),i.resize(n);
#define endl '\n'
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int inf = 1e9;
ll hst[55][55], sz[55][55], mss[55][55], h[55];
ll Pow[55], n[55]; vector<ll> ctr[55];
ll m; const ll seed = 13331; const ll mod = 998244353;
vector<ll> tree[55][55];
void dfs(int num, int now, int fa) {
sz[num][now] = 1, mss[num][now] = 0;
f(i, 0, (int)tree[num][now].size() - 1) {
int to = tree[num][now][i];
if(to != fa) {
dfs(num, to, now); sz[num][now] += sz[num][to];
mss[num][now] = max(mss[num][now], sz[num][to]);
}
}
mss[num][now] = max(mss[num][now], n[num] - sz[num][now]);
if(mss[num][now] <= n[num] / 2) ctr[num].push_back(now);
}
ll nownum;
bool cmp(ll x, ll y) {
return hst[nownum][x] < hst[nownum][y];
}
void calc(int num, int now, int fa) {
sz[num][now] = 1; hst[num][now] = 1;
f(i, 0, (int)tree[num][now].size() - 1) {
int to = tree[num][now][i];
if(to != fa) {
calc(num, to, now); sz[num][now] += sz[num][to];
}
}
nownum = num;
sort(tree[num][now].begin(), tree[num][now].end(), cmp);
ll szsum = 0;
f(i, 0, (int)tree[num][now].size() - 1) {
int to = tree[num][now][i];
if(to != fa) {
szsum += sz[num][to];
hst[num][now] += hst[num][to] * Pow[szsum]; hst[num][now] %= mod;
}
}
}
int main() {
ios::sync_with_stdio(0);
cin.tie(NULL);
cout.tie(NULL);
Pow[0] = 1;
f(i, 1, 50) Pow[i] = Pow[i - 1] * seed % mod;
cin >> m;
f(i, 1, m) {
cin >> n[i];
f(j, 1, n[i]) {
int k; cin >> k; if(k == 0) continue;
tree[i][j].push_back(k); tree[i][k].push_back(j);
}
dfs(i, 1, 0);
f(j, 0, (int)ctr[i].size() - 1) {
calc(i, ctr[i][j], 0); h[i] = max(h[i], hst[i][ctr[i][j]]);
}
}
f(i, 1, m) f(j, 1, m) if(h[i] == h[j]) {cout << j << endl; break;}
return 0;
}
LOJ 2072
题意:有两棵树 \(A\) 和 \(B\) :树 \(A\) 有 \(N\) 个点,编号为 \(1\) 到 \(N\) ;树 \(B\) 有\(N+1\) 个节点,编号为 \(1\) 到\(N+1\)。树 \(B\) 恰好是由树 \(A\) 加上一个叶节点,然后将节点的编号打乱后得到的。这个多余的叶子到底是树 \(B\) 中的哪一个叶节点呢?求出最小可能值。
\(1 \le N \le 10^5\)
分析:这题是树同构问题,树同构问题需要使用树哈希(是不是还有树的最小表示法这是啥子?)。树哈希就要一个基本的想法就是对 \(A\) 上每一个节点求出以其为根的哈希值放到 map 里面去,然后对 \(B\) 上每一个叶子节点,求出以其为根其子节点的哈希值对 map 中的元素进行比对。
但是树哈希现在常见的做法是类似树形 DP,我们知道它是 \(O(n)\) 的(还有字符串哈希,常见做法是 \(O(n^{维度})\) 的,这都是要知道的)那么这个题目需要 \(O(n^2)\) 跑不过去。
我们考虑换根 DP。第一次扫描之后求出 \(h[i]\)。这个东西应该只和树的子节点的 \(size\) 有关。很多做法运用的是常规的递推式,比如 \(h[i] = \sum \limits_{j \in son_i(sorted)} h(j) \times P^{size[j]}\) 等等。然后还要一系列奇怪的换根操作,但是我们遇到这种题目不妨想到使用异或的性质:自逆性(可减性)。设计哈希:
这样换根的时候,令 \(h[i]\) 表示以 \(i\) 为根整棵树的哈希值,有:
特别地 \(h[1]=f[1]\)。
对 \(A\) 和 \(B\) 的操作都使用这个方法。
但是我们要取 \(B\) 中一个子结点的哈希值怎么办?这好办!因为 \(B\) 被我们考虑的根都是度数为 \(1\) 的,所以 \(h_B[i]=h[son] \times W + ((n+1)-1)\)。(\(B\) 的大小是 \(n+1\))我们为了避免除法(又可以愉快 \(\bmod 2^{64}\) 力)直接把 \(h_A[i] \times W\) 放到 map 里面,然后用 \(h_B[i] - n\) 比对即可。
另外这个 \(W\) 选择 \(13331\) 又被卡了,选 \(17737\) 居然也被卡了,选 \(777737\) 不会被卡。真神奇。(题解区一些人抱怨 \(998244353\) 等等的也被卡了,\(1e9+7\) 却没有。真的很神奇。)
#include<bits/stdc++.h>
using namespace std;
#define f(i, a, b) for(int i = (a); i <= (b); i++)
#define cl(i, n) i.clear(),i.resize(n);
#define endl '\n'
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int inf = 1e9;
ull n;
vector<int> a[100010], b[100010];
ull f[100010], h[100010], sz[100010];
ull g[100010], d[100010];
const ull seed = 777737;
map<ull, int> mp;
void dfs1(int now, int fa) {
sz[now] = 1;
f(i, 0, (int)a[now].size() - 1) {
if(a[now][i] != fa){
dfs1(a[now][i], now);
sz[now] += sz[a[now][i]];
f[now] ^= (f[a[now][i]] * seed + sz[a[now][i]]);
}
}
}
void dfs2(int now, int fa) {
if(fa != 0) h[now] = f[now] ^ ((h[fa] ^ (f[now] * seed + sz[now])) * seed + (n - sz[now]));
f(i, 0, (int)a[now].size() -1 ) {
if(a[now][i] != fa) {
dfs2(a[now][i], now);
}
}
}
void dfs3(int now, int fa) {
sz[now] = 1;
f(i, 0, (int)b[now].size() - 1) {
if(b[now][i] != fa){
dfs3(b[now][i], now);
sz[now] += sz[b[now][i]];
g[now] ^= (g[b[now][i]] * seed + sz[b[now][i]]);
}
}
}
void dfs4(int now, int fa) {
if(fa != 0) d[now] = g[now] ^ ((d[fa] ^ (g[now] * seed + sz[now])) * seed + (n + 1 - sz[now]));
f(i, 0, (int)b[now].size() -1 ) {
if(b[now][i] != fa) {
dfs4(b[now][i], now);
}
}
}
int main() {
ios::sync_with_stdio(0);
cin.tie(NULL);
cout.tie(NULL);
cin >> n;
f(i, 1, n - 1) {
int x, y; cin >> x >> y;
a[x].push_back(y); a[y].push_back(x);
}
f(i, 1, n){
int x, y; cin >> x >> y;
b[x].push_back(y); b[y].push_back(x);
}
dfs1(1, 0);
h[1] = f[1];
dfs2(1, 0);
f(i, 1, n) mp[h[i]*seed] = 1;
int ans = inf;
dfs3(1, 0);
if(b[1].size() == 1 && mp.count(g[b[1][0]])) {cout << 1 << endl; return 0;}
d[1] = g[1];
dfs4(1, 0);
f(i, 1, n + 1)
if(b[i].size() == 1)
if(mp.count(d[i] - n)) ans = min(ans, i);
cout << ans << endl;
return 0;
}
点分治
建立点分树,模板题:
https://atcoder.jp/contests/abc291/tasks/abc291_h
每一次先找树的重心然后递归下去。递归下去的时候要挡住原来的路,这个用 \(vis\) 数组存不能走的点,而不是边。判断是不是树的重心,就是判断是不是最大的 \(size\) 小于等于整棵树 \(size\) 的一半。
写的丑死了,可以现场写,但是唯一注意的一个地方是不能写 set 存不能走的边,否则tmd哇了30分钟不知道为什么(事实上是一个 typo)会变慢。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define f(i, a, b) for(int i = (a); i <= (b); i++)
#define cl(i, n) i.clear(),i.resize(n);
#define endl '\n'
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int inf = 1e9;
//#define cerr if(false)cerr
//#define freopen if(false)freopen
#define watch(x) cerr << (#x) << ' '<<'i'<<'s'<<' ' << x << endl
void pofe(int number, int bitnum) {
string s; f(i, 0, bitnum) {s += char(number & 1) + '0'; number >>= 1; }
reverse(s.begin(), s.end()); cerr << s << endl;
return;
}
void cmax(int &x, int y) {if(x < y) x = y;}
void cmin(int &x, int y) {if(x > y) x = y;}
//调不出来给我对拍!
int n;
vector<int> t[200100];
bool vis[200100];
int mx[200100];
int sz[200100];
int tar, wei;
void bl(int x, int fa) {
mx[x] = 0; sz[x] = 1;
for(int y : t[x]) {
if(vis[y]) continue;
if(y == fa) continue;
bl(y, x);
sz[x] += sz[y];
}
// cout << x << " " << fa << " " << sz[x] << endl;
}
void dp(int x, int fa) {
for(int y : t[x]) {
if(vis[y]) continue;
if(y == fa) continue;
dp(y, x);
cmax(mx[x], sz[y]);
}
cmax(mx[x], tar - sz[x]);
// cout << x << " " << mx[x] << endl;
if(mx[x] * 2 <= tar) {wei = x;}
}
int fa[200100];
int solve(int x) {
// cout << x << endl;
bl(x, 0); tar = sz[x];
// f(i, 1, n) cout << sz[i] << " ";
// cout<<endl;
dp(x, 0); //cout << wei << endl;
int tmp = wei;
vis[wei] = 1;
for(int y : t[tmp]) {
if(vis[y]) continue;
fa[solve(y)] = tmp;
}
return tmp;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(NULL);
cout.tie(NULL);
//freopen();
//freopen();
//time_t start = clock();
//think twice,code once.
//think once,debug forever.
cin >> n;
f(i, 1, n-1) {
int u, v; cin >> u >> v;
t[u].push_back(v); t[v].push_back(u);
}
fa[solve(1)] = -1;
f(i, 1, n) cout << fa[i] << " ";
cout << endl;
//time_t finish = clock();
//cout << "time used:" << (finish-start) * 1.0 / CLOCKS_PER_SEC <<"s"<< endl;
return 0;
}
/*
2023/x/xx
start thinking at h:mm
start coding at h:mm
finish debugging at h:mm
*/