樹鏈剖分之長鏈剖分 詳解 題目整理

樹鏈剖分

題目中出現的樹鏈剖分一般分爲兩種,重鏈剖分和長鏈剖分

  • 重鏈剖分:選擇子樹最大的兒子, 將其歸入當前點所在 的同一條重鏈
  • 長鏈剖分:選擇向下能達到的深 度最深的兒子,將其歸 入當前點所在的同一 條長鏈

重剖主要用於維護子樹信息和鏈信息長剖主要用於維護子樹中只與深度有關的信息

長剖

從根開始對樹進行深度優先搜索,同時優先搜索子樹深度最深的兒子

優先搜索子樹深度最深的兒子 (以下以重兒子表示) 使得 每條長鏈在 dfs 序上是連續的

  • 樹被切分爲多條長鏈
  • 一條長鏈頂端點的父親節點所在長鏈一定長於這條長鏈,一個點 k 級祖先所在長鏈一定長於 k
  • 任一點到根最多經過 n\sqrt{n} 條長鏈,所有長鏈 總長度爲 O(n)O(n)
  • 能夠在線性時間維護 子樹中只與深度有關 的信息

k級祖先

重剖

重鏈剖分,還是根據O(logn)O(logn)條輕鏈的性質,如果k級組先就在當前重鏈上則直接找到,否則往上一條重鏈跳。複雜度O(logn)O(logn)

長剖

  1. O(nlogn)O(nlogn) 求祖先 ST 表
  2. O(n) 求出每個長鏈頂端 1 到 len(x) 級祖先和重兒子
  3. O(n) 預處理每個數字最高位 1 的位數 b[i]
  4. 對於 k 級祖先,我們先求 ST 表 2b[k] 級祖先
  5. 其長鏈信息一定大於 k−2b[k],直接 O(1) 查表即可

詳解鏈接

長鏈剖分優化樹上DP

O(n)統計每個點子樹中以深度爲下標的可合併信息

長鏈剖分

我們首先要預處理以下內容

  • 節點的深度
  • 節點的重兒子(子樹深度最深的兒子)
  • 長鏈的長度
int deep[maxn], h_size[maxn], son[maxn];
//deep記錄深度,h_size記錄重鏈長度,son記錄重兒子
void dfs1(int now, int f) {
	deep[now] = deep[f] + 1;
	h_size[now] = deep[now];
	for (int i = head[now]; i; i = edge[i].next) {
		if (f == edge[i].v)continue;
		dfs1(edge[i].v, now);
		h_size[now] = max(h_size[now], h_size[edge[i].v]);
		if (h_size[edge[i].v] > h_size[son[now]])
			son[now] = edge[i].v;		//長度最大的爲重兒子
	}
}

樹上DP

  • 數組大小
    數組大小只需要開tmp[maxn]tmp[maxn],即爲所有長鏈的點的總數
    狀態數組sum[maxn]*sum[maxn],指向tmptmp上,長鏈所在區間
    id*id用於維護長鏈區間的移動

  • 數組維護
    sum[i][d]sum[i][d]表示節點ii的深度爲dd的狀態

  • 重鏈的繼承
    sum[i][j]=sum[i][j+1]sum[i][j]=sum[i][j+1]爲某節點的深度x的狀態可以直接繼承其長鏈深度爲x+1的
    sum[i]=sum[j]+1sum[i]=sum[j]+1

  • 輕鏈的合併
    直接將輕鏈合併到重鏈即可
    由於每個點只會合併一次,複雜度爲O(n)

特別注意,這樣實現也有相應的缺點,即若動規的數組高於一維,那麼數組是一次性的,不能在完成轉移後查詢中間過程的值。
原因是部分內存被共用掉了

int* sum[maxn], tmp[maxn], * id = tmp;
//sum爲數組指針,數組總大小爲maxn(所有長鏈加起來爲n個點),id爲指針
void dfs(int now, int f) {
	sum[now][0] = 1;
	if (son[now]) {
		sum[son[now]] = sum[now] + 1;	//繼承其長鏈,sum[fx][i+1]=sum[x][i]
		dfs(son[now], now);
	}
	for (int i = head[now]; i; i = edge[i].next) {
		if (edge[i].v == f || edge[i].v == son[now])continue;
		sum[edge[i].v] = id;		//數組開始點
		id += h_size[edge[i].v] - deep[edge[i].v] + 1;	//開數組長度爲長鏈長度
		dfs(edge[i].v, now);
		for (int j = 1; j <= h_size[edge[i].v] - deep[edge[i].v] + 1; j++) {
			//DP() ,dp方程
		}
	}
	
}

CF1009F Dominant Indices

題意

給定一棵根爲 1 的樹, 對於每個節點,求子樹 中,哪個距離下的節點 數量最多
當數量相同時,取較小 的那個距離值

思路

我們對樹進行長鏈剖分,記錄sum[x]sum[x]數組表 示節點x不同距離的點的數量
同一條長鏈中sum[fx][i+1]=sum[x][i]sum[fx][i+1]=sum[x][i], 直接繼承長鏈,暴力輕 兒子即可

CF570D Tree Requests

題意:

給定一棵樹,樹的邊長均爲1,每個節點上標着一個字母
多次詢問,每次給出一個v和一個d,要求查詢v的子樹 中深度爲 d 的節點所標字母能否通過合理排列形成迴文串

思路:

我們發現能組合成迴文串的字母集包含奇數個字母最多隻有一種
因此我們對字母集進行狀壓,1和0分別表示 該字母出現奇數次或者偶數次
a[x][i]a[x][i] 表示 x 爲子樹,距離 x 深度爲 i 的字母集
對樹進行長鏈剖分,在同一條長鏈上有 a[fx][i+1]=a[x][i]a[fx][i+1]=a[x][i]長鏈繼承
短鏈對應深度暴力 xor 即可詢問需保存在點上,在對應的點統計答案

BZOJ4543 Hotel加強版

題意

求一棵樹上三點距離 兩兩相等的三元組數

思路

f[i][j]f[i][j]表示i子樹距離 i 爲 j 的點數量
g[i][j]g[i][j] 表示 i 子樹兩點lcalca距離彼此爲d,且 該lcalca距離i點爲d-j的 點對數

  • g[x][j+1]+=f[x][j+1]f[y][j]g[x][j+1]+=f[x][j+1]*f[y][j]
  • g[x][j1]+=g[y][j]g[x][j-1]+=g[y][j]
  • f[x][j+1]+=f[y][j]f[x][j+1]+=f[y][j]
  • ans=f[x][j]g[y][j+1]+g[x][j]f[y][j1]ans=f[x][j]*g[y][j+1]+g[x][j]*f[y][j-1]
    我們發現狀態轉移只跟節點深度有關,因此可以長鏈剖 分優化
    同一條長鏈上對於 f 數組有 f[fx][i+1]=f[x][i]f[fx][i+1]=f[x][i],對於g數組有 g[fx][i1]=g[x][i]g[fx][i-1]=g[x][i]
    繼承重兒子的 g 和 f 函數, 暴力統計輕鏈,同時計算 ans 即可

例題代碼

Dominant Indices

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
#pragma warning (disable:4996)
typedef long long LL;
const int maxn = 1000005;
const int maxm = 1000005;
int n, res[maxn];

int head[maxn], tot;
struct Edge
{
	int v;
	int next;
}edge[maxm << 1];
void init() {
	memset(head, 0, sizeof(head));
	tot = 0;
}
inline void AddEdge(int u, int v) {
	edge[++tot].v = v;
	edge[tot].next = head[u];
	head[u] = tot;
}

int deep[maxn], h_size[maxn], son[maxn];
//deep記錄深度,h_size記錄重鏈長度,son記錄重兒子
void dfs1(int now, int f) {
	deep[now] = deep[f] + 1;	
	h_size[now] = deep[now];
	for (int i = head[now]; i; i = edge[i].next) {
		if (f == edge[i].v)continue;
		dfs1(edge[i].v, now);
		h_size[now] = max(h_size[now], h_size[edge[i].v]);
		if (h_size[edge[i].v] > h_size[son[now]])
			son[now] = edge[i].v;		//長度最大的爲重兒子
	}
}

int* sum[maxn], tmp[maxn], * id = tmp;
//sum爲數組指針,數組總大小爲maxn(所有長鏈加起來爲n個點),id爲指針
void dfs(int now, int f) {
	sum[now][0] = 1;
	if (son[now]) {
		sum[son[now]] = sum[now] + 1;	//繼承其長鏈,sum[fx][i+1]=sum[x][i]
		dfs(son[now], now);
		res[now] = res[son[now]] + 1;	//同上,繼承
	}
	for (int i = head[now]; i; i = edge[i].next) {
		if (edge[i].v == f || edge[i].v == son[now])continue;
		sum[edge[i].v] = id;		//數組開始點
		id += h_size[edge[i].v] - deep[edge[i].v] + 1;	//開數組長度爲長鏈長度
		dfs(edge[i].v, now);
		for (int j = 1; j <= h_size[edge[i].v] - deep[edge[i].v] + 1; j++) {
			sum[now][j] += sum[edge[i].v][j - 1];	//對於輕鏈dp
			if (sum[now][j] > sum[now][res[now]] || sum[now][j] == sum[now][res[now]] && j < res[now])
				res[now] = j;
		}
	}
	if (sum[now][res[now]] == 1)res[now] = 0;	//特判,若爲1個,那麼0是最小的
}

int main() {
	int n; scanf("%d", &n);
	int u, v;
	for (int i = 1; i < n; i++) {
		scanf("%d%d", &u, &v);
		AddEdge(u, v);
		AddEdge(v, u);
	}
	dfs1(1, 0);	//長鏈剖分
	sum[1] = id; id += h_size[1];
	dfs(1, 0);		//樹上dp
	for (int i = 1; i <= n; i++)
		printf("%d\n", res[i]);
}


Tree Requests

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <vector>
using namespace std;
#pragma warning (disable:4996)
typedef pair<int, int> pii;
typedef long long LL;
const int maxn = 500005;
const int maxm = 500005;
int n, m, x;
bool res[maxn];

int head[maxn], tot;
struct Edge
{
	int v;
	int next;
}edge[maxm << 1];
void init() {
	memset(head, 0, sizeof(head));
	tot = 0;
}
inline void AddEdge(int u, int v) {
	edge[++tot].v = v;
	edge[tot].next = head[u];
	head[u] = tot;
}

int deep[maxn], h_size[maxn], son[maxn];
//deep記錄深度,h_size記錄重鏈長度,son記錄重兒子
void dfs1(int now, int f) {
	deep[now] = deep[f] + 1;	
	h_size[now] = deep[now];
	for (int i = head[now]; i; i = edge[i].next) {
		if (f == edge[i].v)continue;
		dfs1(edge[i].v, now);
		h_size[now] = max(h_size[now], h_size[edge[i].v]);
		if (h_size[edge[i].v] > h_size[son[now]])
			son[now] = edge[i].v;		//長度最大的爲重兒子
	}
}

int w[maxn];
char s[maxn];
vector<pii> E[maxn];
int* sum[maxn], tmp[maxn], * id = tmp;
void dfs(int now, int f) {
	sum[now][0] = w[now];
	if (son[now]) {
		sum[son[now]] = sum[now] + 1;
		dfs(son[now], now);
	}
	for (int i = head[now]; i; i = edge[i].next) {
		if (edge[i].v == f || edge[i].v == son[now])continue;
		sum[edge[i].v] = id;
		id += h_size[edge[i].v] - deep[edge[i].v] + 1;
		dfs(edge[i].v, now);
		for (int j = 1; j <= h_size[edge[i].v] - deep[edge[i].v] + 1; j++)
			sum[now][j] ^= sum[edge[i].v][j - 1];
	}
	int depth = deep[now], son_depth;
	for (int i = 0; i < E[now].size(); i++) {
		son_depth = E[now][i].first - depth;
		if (son_depth <= 0 || E[now][i].first > h_size[now]) {
			res[E[now][i].second] = true;
			continue;
		}
		if (sum[now][son_depth] == 0) {
			res[E[now][i].second] = true;
			continue;
		}
		int cnt = 0, j;
		for (j = 0; j < 26; j++) {
			if (sum[now][son_depth] & (1 << j))
				cnt++;
			if (cnt == 2)break;
		}
		if (j < 26)res[E[now][i].second] = false;
		else res[E[now][i].second] = true;
	}
}

int main() {
	scanf("%d%d", &n, &m);
	for (int i = 2; i <= n; i++) {
		scanf("%d", &x);
		AddEdge(i, x);
		AddEdge(x, i);
	}
	scanf("%s", s + 1);
	for (int i = 1; i <= n; i++) {
		w[i] = 1 << (s[i] - 'a');
	}
	int d;
	for (int i = 1; i <= m; i++) {
		scanf("%d%d", &x, &d);
		E[x].push_back(pii(d, i));
	}
	dfs1(1, 0);
	sum[1] = id; id += h_size[1];
	dfs(1, 0);
	for (int i = 1; i <= m; i++)
		if (res[i])printf("Yes\n");
		else printf("No\n");

}

Hotel加強版

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <vector>
using namespace std;
#pragma warning (disable:4996)
typedef pair<int, int> pii;
typedef long long LL;
const int maxn = 100005;
const int maxm = 100005;
int n;

int head[maxn], tot;
struct Edge
{
	int v;
	int next;
}edge[maxm << 1];
void init() {
	memset(head, 0, sizeof(head));
	tot = 0;
}
inline void AddEdge(int u, int v) {
	edge[++tot].v = v;
	edge[tot].next = head[u];
	head[u] = tot;
}

int deep[maxn], h_size[maxn], son[maxn];
//deep記錄深度,h_size記錄重鏈長度,son記錄重兒子
void dfs1(int now, int f) {
	deep[now] = deep[f] + 1;
	h_size[now] = deep[now];
	for (int i = head[now]; i; i = edge[i].next) {
		if (f == edge[i].v)continue;
		dfs1(edge[i].v, now);
		h_size[now] = max(h_size[now], h_size[edge[i].v]);
		if (h_size[edge[i].v] > h_size[son[now]])
			son[now] = edge[i].v;		//長度最大的爲重兒子
	}
}

LL* f[maxn], * g[maxn], tmp[maxn << 2], * id = tmp;
LL ans;
void dfs(int now, int fa) {
	if (son[now]) {
		f[son[now]] = f[now] + 1;
		g[son[now]] = g[now] - 1;
		dfs(son[now], now);
	}
	f[now][0] = 1;
	ans += g[now][0];
	for (int i = head[now]; i; i = edge[i].next) {
		if (edge[i].v == fa || edge[i].v == son[now])continue;
		f[edge[i].v] = id; id += ((h_size[edge[i].v] - deep[edge[i].v]) << 1) + 2;
		g[edge[i].v] = id; id += (h_size[edge[i].v] - deep[edge[i].v]) + 1;
		dfs(edge[i].v, now);
		for (int j = 0; j <= h_size[edge[i].v] - deep[edge[i].v]; j++) {
			if (j)ans += g[edge[i].v][j] * f[now][j - 1];	//長鏈上距離(j-1)的點數量
			ans += f[edge[i].v][j] * g[now][j + 1];			//短鏈上的點*長鏈上的點對
		}
		for (int j = 0; j <= h_size[edge[i].v] - deep[edge[i].v]; j++) {
			g[now][j + 1] += f[edge[i].v][j] * f[now][j + 1];	//短鏈一個點+長鏈一個點的合併
			if (j)g[now][j - 1] += g[edge[i].v][j];				//短鏈點對加入
			f[now][j + 1] += f[edge[i].v][j];
		}
	}	
}

int main() {
	scanf("%d", &n);
	int u, v;
	for (int i = 2; i <= n; i++) {
		scanf("%d%d", &u, &v);
		AddEdge(u, v);
		AddEdge(v, u);
	}
	dfs1(1, 0);
	f[1] = id; id += (h_size[1] << 1) + 2;	//給g數組預留向前移動的空間
	g[1] = id; id += h_size[1] + 1;
	dfs(1, 0);
	printf("%lld\n", ans);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章