树上启发式合并算法概述及习题

树上启发式合并概述

一、适用问题

树上启发式合并作为树上问题三剑客之一(点分治、长链剖分),以其优雅的暴力而闻名于江湖之中。

通常来说,如果一个问题可以被划分为一个个子树进行求解的问题,而且各个子儿子对答案的贡献容易添加与删除,就可以考虑使用树上启发式合并来求解。

本文主要介绍树上启发式合并的一些习题,可以从习题中仔细感受该算法的一系列特点。

二、算法介绍

树上启发式合并需要两次 d f s dfs ,第一次 d f s dfs 进行重链剖分,第二次 d f s dfs 进行求解。

通常有一个全局的数组用于信息记录。 d f s dfs 之前,需要将这个数组赋初值。 d f s dfs 时,先递归处理轻儿子,处理完之后清空轻儿子,最后再处理重儿子,处理完之后不清空。

计算完儿子的答案之后,再递归所有轻儿子,边递归边计算答案,并将轻儿子的信息添加到全局数组中。

这个做法的时间复杂度是 O ( n l o g n ) O(nlogn) ,因为每个节点直接继承了其子树中的重儿子,即每次只有轻儿子会被重复访问,访问完之后,轻儿子即会和重儿子进行合并,每次合并 s z sz 至少乘 2 2 ,因此每个点最多被重复访问 l o g n logn 次,即总时间复杂度为 O ( n l o g n ) O(nlogn)

三、算法模板

其实树上启发式合并并没有什么模板,只需要处理好两次 d f s dfs 的过程,然后实现插入、删除、更新三个函数即可。

以下面第一题的代码为例,给出一个大致的模板。

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

题意: 给定一棵有根树,每条边的权值是 [ a , v ] [a,v] 的一个字母。现对于每个树上点,求出最长的一条 “回文” 路径。“回文” 路径的含义是将路径上所有的字母取出,可以组成一个回文串。 ( 1 n 5 1 0 5 ) (1\leq n\leq 5*10^5)

思路: 树上的这类问题,我们可以依次思考点分治、树上启发式合并、长链剖分,根据该题题意,不难识别这是一道树上启发式合并问题。

确认是树上启发式合并之后,我们需要定状态。由于 “回文” 路径只要求所有字母可以组成一个回文串,因此我们可以利用状压的思想给每一个字母进行赋值,然后对于每个点求一个从根节点到当前点的异或和。

f [ i ] f[i] 表示对于当前点 n o w now ,其子树中存在一个点 x x v a l u e [ x ] = i , f [ i ] = d e p [ x ] value[x]=i,f[i]=dep[x] x x 为此中情况下深度最深的点。然后计算点 n o w now 答案时,我们只需考虑三种情况。

  1. n o w now 为最长路径的一个端点
  2. 最长路径经过 n o w now
  3. 最长路径在 n o w now 子树中,不经过 n o w now

三种情况在代码中有比较清晰的注释,不太清楚细节的朋友可以看看代码。总体来说,这题应该属于树上启发式合并的经典问题。

总结: 此题有几个思想比较可取。

  1. 对每一个字母进行状压编码
  2. 每一个点维护的权值是从根到该点的异或和
  3. 3 3 种情况对答案进行了枚举

代码:

#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

题意: 给定一棵有根树,一共有 n n 个节点, m m 个叶子,每个叶子节点都有一个权值。现对于每个非叶子节点,要求求出其子树中任意两个叶子权值绝对值之差的最小值。 ( 1 n 5 1 0 4 ) (1\leq n\leq 5*10^4)

思路: 相比上一个问题,这个问题是更加典型的树上启发式合并问题,我们只需要维护一个全局的 s e t set ,然后每次往 s e t set 加点时,用其左右两个点求一个差值,然后更新答案即可。

代码:

#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;
}
发布了244 篇原创文章 · 获赞 115 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/qq_41552508/article/details/102775070