樹鏈剖分
題目中出現的樹鏈剖分一般分爲兩種,重鏈剖分和長鏈剖分
- 重鏈剖分:選擇子樹最大的兒子, 將其歸入當前點所在 的同一條重鏈
- 長鏈剖分:選擇向下能達到的深 度最深的兒子,將其歸 入當前點所在的同一 條長鏈
重剖主要用於維護子樹信息和鏈信息,長剖主要用於維護子樹中只與深度有關的信息
長剖
從根開始對樹進行深度優先搜索,同時優先搜索子樹深度最深的兒子
優先搜索子樹深度最深的兒子 (以下以重兒子表示) 使得 每條長鏈在 dfs 序上是連續的
- 樹被切分爲多條長鏈
- 一條長鏈頂端點的父親節點所在長鏈一定長於這條長鏈,一個點 k 級祖先所在長鏈一定長於 k
- 任一點到根最多經過 條長鏈,所有長鏈 總長度爲
- 能夠在線性時間維護 子樹中只與深度有關 的信息
k級祖先
重剖
重鏈剖分,還是根據條輕鏈的性質,如果k級組先就在當前重鏈上則直接找到,否則往上一條重鏈跳。複雜度
長剖
- 求祖先 ST 表
- O(n) 求出每個長鏈頂端 1 到 len(x) 級祖先和重兒子
- O(n) 預處理每個數字最高位 1 的位數 b[i]
- 對於 k 級祖先,我們先求 ST 表 2b[k] 級祖先
- 其長鏈信息一定大於 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
-
數組大小
數組大小只需要開,即爲所有長鏈的點的總數
狀態數組,指向上,長鏈所在區間
用於維護長鏈區間的移動 -
數組維護
表示節點的深度爲的狀態 -
重鏈的繼承
爲某節點的深度x的狀態可以直接繼承其長鏈深度爲x+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 的樹, 對於每個節點,求子樹 中,哪個距離下的節點 數量最多
當數量相同時,取較小 的那個距離值
思路
我們對樹進行長鏈剖分,記錄數組表 示節點x不同距離的點的數量
同一條長鏈中, 直接繼承長鏈,暴力輕 兒子即可
CF570D Tree Requests
題意:
給定一棵樹,樹的邊長均爲1,每個節點上標着一個字母
多次詢問,每次給出一個v和一個d,要求查詢v的子樹 中深度爲 d 的節點所標字母能否通過合理排列形成迴文串
思路:
我們發現能組合成迴文串的字母集包含奇數個字母最多隻有一種
因此我們對字母集進行狀壓,1和0分別表示 該字母出現奇數次或者偶數次
表示 x 爲子樹,距離 x 深度爲 i 的字母集
對樹進行長鏈剖分,在同一條長鏈上有 長鏈繼承
短鏈對應深度暴力 xor 即可詢問需保存在點上,在對應的點統計答案
BZOJ4543 Hotel加強版
題意
求一棵樹上三點距離 兩兩相等的三元組數
思路
設表示i子樹距離 i 爲 j 的點數量
設 表示 i 子樹兩點距離彼此爲d,且 該距離i點爲d-j的 點對數
我們發現狀態轉移只跟節點深度有關,因此可以長鏈剖 分優化
同一條長鏈上對於 f 數組有 ,對於g數組有 ,
繼承重兒子的 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);
}