树上启发式合并概述
一、适用问题
树上启发式合并作为树上问题三剑客之一(点分治、长链剖分),以其优雅的暴力而闻名于江湖之中。
通常来说,如果一个问题可以被划分为一个个子树进行求解的问题,而且各个子儿子对答案的贡献容易添加与删除,就可以考虑使用树上启发式合并来求解。
本文主要介绍树上启发式合并的一些习题,可以从习题中仔细感受该算法的一系列特点。
二、算法介绍
树上启发式合并需要两次 ,第一次 进行重链剖分,第二次 进行求解。
通常有一个全局的数组用于信息记录。 之前,需要将这个数组赋初值。 时,先递归处理轻儿子,处理完之后清空轻儿子,最后再处理重儿子,处理完之后不清空。
计算完儿子的答案之后,再递归所有轻儿子,边递归边计算答案,并将轻儿子的信息添加到全局数组中。
这个做法的时间复杂度是 ,因为每个节点直接继承了其子树中的重儿子,即每次只有轻儿子会被重复访问,访问完之后,轻儿子即会和重儿子进行合并,每次合并 至少乘 ,因此每个点最多被重复访问 次,即总时间复杂度为 。
三、算法模板
其实树上启发式合并并没有什么模板,只需要处理好两次 的过程,然后实现插入、删除、更新三个函数即可。
以下面第一题的代码为例,给出一个大致的模板。
int sz[N],son[N];
void dfs1(int x){
/* 求解重儿子 */
sz[x] = 1;
for(int i = head[x]; i; i = e[i].next){
int y = e[i].to;
dfs1(y); sz[x] += sz[y];
if(sz[y] > sz[son[x]]) son[x] = y;
}
}
void Delete(int x){
/* 删除的内容 */
for(int i = head[x]; i; i = e[i].next) Delete(e[i].to);
}
void modify(int x,int fa){
/* 更新的内容 */
for(int i = head[x]; i; i = e[i].next) modify(e[i].to,fa);
}
void ins(int x){
/* 插入的内容 */
for(int i = head[x]; i; i = e[i].next) ins(e[i].to);
}
void dfs2(int x){
/* 求解轻儿子并清空 */
for(int i = head[x]; i; i = e[i].next)
if(e[i].to != son[x]) dfs2(e[i].to), Delete(e[i].to);
/* 求解重儿子并保留 */
if(son[x]) dfs2(son[x]);
/* 用重儿子更新答案 */
/* 枚举轻儿子更新答案,并加入轻儿子 */
for(int i = head[x]; i; i = e[i].next)
if(e[i].to != son[x]) modify(e[i].to,x), ins(e[i].to);
/* 用所有儿子更新答案 */
}
树上启发式合并系列习题
1. Mehrdad’s Dokhtar-kosh paths
题意:
给定一棵有根树,每条边的权值是
的一个字母。现对于每个树上点,求出最长的一条 “回文” 路径。“回文” 路径的含义是将路径上所有的字母取出,可以组成一个回文串。
思路:
树上的这类问题,我们可以依次思考点分治、树上启发式合并、长链剖分,根据该题题意,不难识别这是一道树上启发式合并问题。
确认是树上启发式合并之后,我们需要定状态。由于 “回文” 路径只要求所有字母可以组成一个回文串,因此我们可以利用状压的思想给每一个字母进行赋值,然后对于每个点求一个从根节点到当前点的异或和。
令 表示对于当前点 ,其子树中存在一个点 , , 为此中情况下深度最深的点。然后计算点 答案时,我们只需考虑三种情况。
- 为最长路径的一个端点
- 最长路径经过
- 最长路径在 子树中,不经过
三种情况在代码中有比较清晰的注释,不太清楚细节的朋友可以看看代码。总体来说,这题应该属于树上启发式合并的经典问题。
总结:
此题有几个思想比较可取。
- 对每一个字母进行状压编码
- 每一个点维护的权值是从根到该点的异或和
- 分 种情况对答案进行了枚举
代码:
#include <bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof a);
#define rep(i,a,b) for(int i = a; i <= b; i++)
#define per(i,a,b) for(int i = a; i >= b; i--)
#define __ ios::sync_with_stdio(0);cin.tie(0);cout.tie(0)
typedef long long ll;
typedef double db;
const int inf = 1e8;
const int N = 5e5+10;
const db EPS = 1e-9;
using namespace std;
void dbg() {cout << "\n";}
template<typename T, typename... A> void dbg(T a, A... x) {cout << a << ' '; dbg(x...);}
#define logs(x...) {cout << #x << " -> "; dbg(x);}
int n,a[N],tot,head[N],sz[N],son[N],f[1<<22],dep[N],ans[N]; //f[i]:表示子树中异或和为f[i]的最大深度
struct Node{
int to,next;
}e[N];
void add(int x,int y){
e[++tot].to = y, e[tot].next = head[x], head[x] = tot;
}
void dfs1(int x){
ans[x] = -inf; sz[x] = 1;
for(int i = head[x]; i; i = e[i].next){
int y = e[i].to;
dep[y] = dep[x]+1; a[y] ^= a[x];
dfs1(y); sz[x] += sz[y];
if(sz[y] > sz[son[x]]) son[x] = y;
}
}
void Delete(int x){
f[a[x]] = -inf;
for(int i = head[x]; i; i = e[i].next) Delete(e[i].to);
}
void modify(int x,int fa){
ans[fa] = max(ans[fa],f[a[x]]+dep[x]-2*dep[fa]);
for(int i = 0; i < 22; i++)
ans[fa] = max(ans[fa],f[a[x]^(1<<i)]+dep[x]-2*dep[fa]);
for(int i = head[x]; i; i = e[i].next) modify(e[i].to,fa);
}
void ins(int x){
f[a[x]] = max(f[a[x]],dep[x]);
for(int i = head[x]; i; i = e[i].next) ins(e[i].to);
}
void dfs2(int x){
ans[x] = 0;
for(int i = head[x]; i; i = e[i].next)
if(e[i].to != son[x]) dfs2(e[i].to), Delete(e[i].to);
if(son[x]) dfs2(son[x]);
f[a[x]] = max(f[a[x]],dep[x]);
//路径经过x
for(int i = head[x]; i; i = e[i].next)
if(e[i].to != son[x]) modify(e[i].to,x), ins(e[i].to);
//x为路径端点
ans[x] = max(ans[x],f[a[x]]-dep[x]);
for(int i = 0; i < 22; i++)
ans[x] = max(ans[x],f[a[x]^(1<<i)]-dep[x]);
//路径不经过x
for(int i = head[x]; i; i = e[i].next) ans[x] = max(ans[x],ans[e[i].to]);
}
int main()
{
scanf("%d",&n); tot = 1;
rep(i,0,(1<<22)-1) f[i] = -inf; //不要忘记赋初值
rep(i,2,n){
int p; char s[10];
scanf("%d%s",&p,s);
add(p,i); a[i] = 1<<(s[0]-'a');
}
dfs1(1); dfs2(1);
rep(i,1,n) printf("%d%c",ans[i]," \n"[i==n]);
return 0;
}
2. Treediff
题意:
给定一棵有根树,一共有
个节点,
个叶子,每个叶子节点都有一个权值。现对于每个非叶子节点,要求求出其子树中任意两个叶子权值绝对值之差的最小值。
思路:
相比上一个问题,这个问题是更加典型的树上启发式合并问题,我们只需要维护一个全局的
,然后每次往
加点时,用其左右两个点求一个差值,然后更新答案即可。
代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <set>
#define mem(a,b) memset(a,b,sizeof a);
#define rep(i,a,b) for(int i = a; i <= b; i++)
#define per(i,a,b) for(int i = a; i >= b; i--)
#define __ ios::sync_with_stdio(0);cin.tie(0);cout.tie(0)
typedef long long ll;
typedef double db;
const int inf = (1ll<<31)-1ll;
const int N = 5e4+10;
const db EPS = 1e-9;
using namespace std;
void dbg() {cout << "\n";}
template<typename T, typename... A> void dbg(T a, A... x) {cout << a << ' '; dbg(x...);}
#define logs(x...) {cout << #x << " -> "; dbg(x);}
int n,m,a[N],tot,head[N],sz[N],son[N],ans[N]; //f[i]:表示子树中异或和为f[i]的最大深度
struct Node{
int to,next;
}e[N];
set<int> st;
void add(int x,int y){
e[++tot].to = y, e[tot].next = head[x], head[x] = tot;
}
void dfs1(int x){
sz[x] = 1;
for(int i = head[x]; i; i = e[i].next){
int y = e[i].to;
dfs1(y); sz[x] += sz[y];
if(sz[y] > sz[son[x]]) son[x] = y;
}
}
void ins(int x,int fa){
if(a[x] != inf){
if(st.find(a[x]) != st.end()) ans[fa] = 0;
else{
st.insert(a[x]);
// logs(x,ans[x],fa,ans[fa]);
auto it = st.find(a[x]), tmp = it;
if(it != st.begin()){
tmp = --it; it++;
ans[fa] = min(ans[fa],(*it)-(*tmp));
}
tmp = ++it; it--;
if(tmp != st.end()) ans[fa] = min(ans[fa],(*tmp)-(*it));
// for(auto &v:st) logs(v);
}
}
for(int i = head[x]; i; i = e[i].next) ins(e[i].to,fa);
}
void dfs2(int x){
for(int i = head[x]; i; i = e[i].next)
if(e[i].to != son[x]) dfs2(e[i].to), st.clear();
if(son[x]) dfs2(son[x]), ans[x] = min(ans[x],ans[son[x]]);
if(a[x] != inf) st.insert(a[x]);
for(int i = head[x]; i; i = e[i].next)
if(e[i].to != son[x]) ins(e[i].to,x);
}
int main()
{
scanf("%d%d",&n,&m); tot = 1;
rep(i,2,n){
int p; scanf("%d",&p);
add(p,i);
}
rep(i,1,n) ans[i] = inf, a[i] = inf;
rep(i,n-m+1,n) scanf("%d",&a[i]);
dfs1(1); dfs2(1);
rep(i,1,n-m) printf("%d%c",ans[i]," \n"[i==n]);
return 0;
}