樹上啓發式合併算法概述及習題

樹上啓發式合併概述

一、適用問題

樹上啓發式合併作爲樹上問題三劍客之一(點分治、長鏈剖分),以其優雅的暴力而聞名於江湖之中。

通常來說,如果一個問題可以被劃分爲一個個子樹進行求解的問題,而且各個子兒子對答案的貢獻容易添加與刪除,就可以考慮使用樹上啓發式合併來求解。

本文主要介紹樹上啓發式合併的一些習題,可以從習題中仔細感受該算法的一系列特點。

二、算法介紹

樹上啓發式合併需要兩次 dfsdfs,第一次 dfsdfs 進行重鏈剖分,第二次 dfsdfs 進行求解。

通常有一個全局的數組用於信息記錄。dfsdfs 之前,需要將這個數組賦初值。dfsdfs 時,先遞歸處理輕兒子,處理完之後清空輕兒子,最後再處理重兒子,處理完之後不清空。

計算完兒子的答案之後,再遞歸所有輕兒子,邊遞歸邊計算答案,並將輕兒子的信息添加到全局數組中。

這個做法的時間複雜度是 O(nlogn)O(nlogn),因爲每個節點直接繼承了其子樹中的重兒子,即每次只有輕兒子會被重複訪問,訪問完之後,輕兒子即會和重兒子進行合併,每次合併 szsz 至少乘 22,因此每個點最多被重複訪問 lognlogn 次,即總時間複雜度爲 O(nlogn)O(nlogn)

三、算法模板

其實樹上啓發式合併並沒有什麼模板,只需要處理好兩次 dfsdfs 的過程,然後實現插入、刪除、更新三個函數即可。

以下面第一題的代碼爲例,給出一個大致的模板。

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] 的一個字母。現對於每個樹上點,求出最長的一條 “迴文” 路徑。“迴文” 路徑的含義是將路徑上所有的字母取出,可以組成一個迴文串。(1n5105)(1\leq n\leq 5*10^5)

思路: 樹上的這類問題,我們可以依次思考點分治、樹上啓發式合併、長鏈剖分,根據該題題意,不難識別這是一道樹上啓發式合併問題。

確認是樹上啓發式合併之後,我們需要定狀態。由於 “迴文” 路徑只要求所有字母可以組成一個迴文串,因此我們可以利用狀壓的思想給每一個字母進行賦值,然後對於每個點求一個從根節點到當前點的異或和。

f[i]f[i] 表示對於當前點 nownow,其子樹中存在一個點 xxvalue[x]=i,f[i]=dep[x]value[x]=i,f[i]=dep[x]xx 爲此中情況下深度最深的點。然後計算點 nownow 答案時,我們只需考慮三種情況。

  1. nownow 爲最長路徑的一個端點
  2. 最長路徑經過 nownow
  3. 最長路徑在 nownow 子樹中,不經過 nownow

三種情況在代碼中有比較清晰的註釋,不太清楚細節的朋友可以看看代碼。總體來說,這題應該屬於樹上啓發式合併的經典問題。

總結: 此題有幾個思想比較可取。

  1. 對每一個字母進行狀壓編碼
  2. 每一個點維護的權值是從根到該點的異或和
  3. 33 種情況對答案進行了枚舉

代碼:

#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

題意: 給定一棵有根樹,一共有 nn 個節點,mm 個葉子,每個葉子節點都有一個權值。現對於每個非葉子節點,要求求出其子樹中任意兩個葉子權值絕對值之差的最小值。 (1n5104)(1\leq n\leq 5*10^4)

思路: 相比上一個問題,這個問題是更加典型的樹上啓發式合併問題,我們只需要維護一個全局的 setset,然後每次往 setset 加點時,用其左右兩個點求一個差值,然後更新答案即可。

代碼:

#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;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章