洛谷 P3384 樹鏈剖分(詳解)
題目描述
如題,已知一棵包含N個結點的樹(連通且無環),每個節點上包含一個數值,需要支援以下操作:
操作1: 格式: 1 x y z 表示將樹從x到y結點最短路徑上所有節點的值都加上z
操作2: 格式: 2 x y 表示求樹從x到y結點最短路徑上所有節點的值之和
操作3: 格式: 3 x z 表示將以x為根節點的子樹內所有節點值都加上z
操作4: 格式: 4 x 表示求以x為根節點的子樹內所有節點值之和
輸入格式:
第一行包含4個正整數N、M、R、P,分別表示樹的結點個數、操作個數、根節點序號和取模數(即所有的輸出結果均對此取模)。
接下來一行包含N個非負整數,分別依次表示各個節點上初始的數值。
接下來N-1行每行包含兩個整數x、y,表示點x和點y之間連有一條邊(保證無環且連通)
接下來M行每行包含若干個正整數,每行表示一個操作,格式如下:
操作1: 1 x y z
操作2: 2 x y
操作3: 3 x z
操作4: 4 x
輸出格式:
輸出包含若干行,分別依次表示每個操作2或操作4所得的結果(對P取模)
輸入樣例#1
5 5 2 24
7 3 7 8 0
1 2
1 5
3 1
4 1
3 4 2
3 2 2
4 5
1 5 1 3
2 1 3
輸出樣例#1
2
21
說明
時空限制:1s,128M
資料規模:
對於30%的資料:
對於70%的資料:
對於100%的資料:
(
其實,純隨機生成的樹LCA+暴力是能過的,可是,你覺得可能是純隨機的麼233)樣例說明:
樹的結構如下:
各個操作如下:
故輸出應依次為2、21(重要的事情說三遍:記得取模)
思路
這是一道樹鏈剖分的模板題:
因為樹的狀態是不變的,因此樹鏈剖分的策略是將這些點按某種方式組織起來,剖分為若干條鏈,每條鏈就相當於一個序列。這樣,操作的路徑可以拆分為某幾條鏈,也就是若干個完整序列或是某個序列上的一段區間,此時可以用線段樹等處理序列上區間新操作的資料結構去解決問題。樹鏈剖分的核心是如何恰當的將樹剖分為若干條鏈,當鏈的劃分方式確定時,只需要將他們看作一個個序列,將所有的序列按照順序拼接起來,每條鏈就形成了一個區間,而序列上的區間問題使我們擅長解決的.
關於樹鏈剖分,有如下幾個定義:
- 重兒子:以任意點為根,記size(u)為以u為根的子樹的節點個數,令v為u的所有兒子size值最大的一個,則v為重兒子
- 輕兒子:除了重兒子,其餘都為輕兒子
- 重邊:令v為u的所有兒子size值最大的一個,則(u,v)為重邊
- 輕邊:重邊之外的邊
- 重鏈:一條鏈為重鏈,當且僅當他全部由重邊組成(一個點也算一條重鏈)
在剖分過程中,要計算如下7個值:
- fa[u]:u在樹中的父親
- dep[u]:u節點的深度
- size[u]:u的子樹節點數(子樹大小)
- son[u]:u的重兒子
- top[u]:u所在重鏈的頂部節點
- id[x]:新的編號
- wt[x]:新編號的點權
第一遍dfs時可以計算前4個值,第二遍dfs後可以計算後3個值,在計算id時,同一條重鏈上的點需要按順序排在連繼續的一段位置,也就是一段區間.
將一條路徑(u,v)拆分成若干重鏈的過程,實際上就是尋找最近公共祖先的過程。我們會選擇u,v中深度較大的點向上走一步,知道u==v.現在有了重鏈,由於我們記錄了重鏈的頂部節點top[x],還記錄了每個點在序列中的位置,因為我們不需要一步步走。假定top[u]和top[v]不同,那麼他們的最近公共祖先可能在其中的一條重鏈上,也可能在其他重鏈上。因為LCA顯然不可能在top深度較大的那條重鏈上,所以我們先處理top深度較大的。首先我們找出u,v中top深度較大的點,假設是u,則可以直接調到fa[top[u]]
處,且跳過的這一段,線上段樹中是一段區間,若我們按照深度從小到大來儲存點,則這段區間為:[id[top[x]],id[x]]
。當u,v中的top相同時,說明他們走到了同一條重鏈上,這時他們之間的路徑也是序列上的一段區間,且u,v中深度較小的點是原路徑的LCA.這樣我們就可以將給出的任意路徑拆分為若干個重鏈,也就是若干個區間,可以用線段樹來處理操作。
程式碼
#include <bits/stdc++.h>
using namespace std;
#define mem(a, b) memset(a, b, sizeof(a))
#define lson l, m, rt << 1
#define rson m + 1, r, rt << 1 | 1
const int N = 2e5 + 10;
int sum[N << 2], lazy[N << 2]; //線段樹求和
int n, m, r, mod; //節點數,運算元,根節點,模數
int first[N], tot; //鄰接表
//分別為:重兒子,每個節點新編號,父親,編號,深度,子樹個數,所在重鏈的頂部
int son[N], id[N], fa[N], cnt, dep[N], siz[N], top[N];
int w[N], wt[N]; //初始點權,新編號點權
int res = 0; //查詢答案
struct edge
{
int v, next;
} e[N];
void add_edge(int u, int v)
{
e[tot].v = v;
e[tot].next = first[u];
first[u] = tot++;
}
void init()
{
mem(first, -1);
tot = 0;
cnt = 0;
}
int pushup(int rt)
{
sum[rt] = (sum[rt << 1] + sum[rt << 1 | 1]) % mod;
}
void pushdown(int rt, int m) //下放lazy標記
{
if (lazy[rt])
{
lazy[rt << 1] += lazy[rt]; //給左兒子下放lazy
lazy[rt << 1 | 1] += lazy[rt]; //給右兒子下放lazy
sum[rt << 1] += lazy[rt] * (m - (m >> 1)); //更新sum
sum[rt << 1] %= mod;
sum[rt << 1 | 1] += lazy[rt] * (m >> 1);
sum[rt << 1 | 1] %= mod;
lazy[rt] = 0;
}
}
void build(int l, int r, int rt)
{
lazy[rt] = 0;
if (l == r)
{
sum[rt] = wt[l]; //新的編號點權
sum[rt] %= mod;
return;
}
int m = (l + r) >> 1;
build(lson);
build(rson);
pushup(rt);
}
void update(int L, int R, int c, int l, int r, int rt)
{
if (L <= l && r <= R)
{
lazy[rt] += c;
sum[rt] += c * (r - l + 1);
sum[rt] %= mod;
return;
}
pushdown(rt, r - l + 1);
int m = (l + r) >> 1;
if (L <= m)
update(L, R, c, lson);
if (R > m)
update(L, R, c, rson);
pushup(rt);
}
void query(int L, int R, int l, int r, int rt)
{
if (L <= l && r <= R)
{
res += sum[rt];
res %= mod;
return;
}
pushdown(rt, r - l + 1);
int m = (l + r) >> 1;
if (L <= m)
query(L, R, lson);
if (R > m)
query(L, R, rson);
}
//----------------------------------------------------------------
//處理出fa[],dep[],siz[],son[]
void dfs1(int u, int f, int deep)
{
dep[u] = deep; //標記深度
fa[u] = f; //標記節點的父親
siz[u] = 1; //記錄每個節點子樹大小
int maxson = -1; //記錄重兒子數量
for (int i = first[u]; ~i; i = e[i].next)
{
int v = e[i].v;
if (v == f)
continue;
dfs1(v, u, deep + 1);
siz[u] += siz[v];
if (siz[v] > maxson) //兒子裡最多siz就是重兒子
{
son[u] = v; //標記u的重兒子為v
maxson = siz[v];
}
}
}
//處理出top[],wt[],id[]
void dfs2(int u, int topf)
{
id[u] = ++cnt; //每個節點的新編號
wt[cnt] = w[u]; //新編號的對應權值
top[u] = topf; //標記每個重鏈的頂端
if (!son[u]) //沒有兒子時返回
return;
dfs2(son[u], topf); //搜尋下一個重兒子
for (int i = first[u]; ~i; i = e[i].next)
{
int v = e[i].v;
if (v == fa[u] || v == son[u]) //處理輕兒子
continue;
dfs2(v, v); //每一個輕兒子都有一個從自己開始的鏈
}
}
void updrange(int x, int y, int k)
{
k %= mod;
while (top[x] != top[y])
{
if (dep[top[x]] < dep[top[y]]) //使x深度較大
swap(x, y);
update(id[top[x]], id[x], k, 1, n, 1);
x = fa[top[x]];
}
if (dep[x] > dep[y]) //使x深度較小
swap(x, y);
update(id[x], id[y], k, 1, n, 1);
}
int qrange(int x, int y)
{
int ans = 0;
while (top[x] != top[y]) //當兩個點不在同一條鏈上
{
if (dep[top[x]] < dep[top[y]]) //使x深度較大
swap(x, y);
res = 0;
query(id[top[x]], id[x], 1, n, 1);
//ans加上x點到x所在鏈頂端這一段區間的點權和
ans += res;
ans %= mod;
x = fa[top[x]]; //x跳到x所在鏈頂端的這個點的上面一個點
}
//當兩個點處於同一條鏈
if (dep[x] > dep[y]) //使x深度較小
swap(x, y);
res = 0;
query(id[x], id[y], 1, n, 1);
ans += res;
return ans % mod;
}
void upson(int x, int k)
{
update(id[x], id[x] + siz[x] - 1, k, 1, n, 1); //子樹區間右端點為id[x]+siz[x]-1
}
int qson(int x)
{
res = 0;
query(id[x], id[x] + siz[x] - 1, 1, n, 1);
return res;
}
int main()
{
// freopen("in.txt", "r", stdin);
int u, v;
scanf("%d%d%d%d", &n, &m, &r, &mod);
init();
for (int i = 1; i <= n; i++)
scanf("%d", &w[i]);
for (int i = 1; i <= n - 1; i++)
{
scanf("%d%d", &u, &v);
add_edge(u, v);
add_edge(v, u);
}
dfs1(r, 0, 1);
dfs2(r, r);
build(1, n, 1); //用新點權建立線段樹
while (m--)
{
int op, x, y, z;
scanf("%d", &op);
if (op == 1)
{
scanf("%d%d%d", &x, &y, &z);
updrange(x, y, z);
}
else if (op == 2)
{
scanf("%d%d", &x, &y);
printf("%d\n", qrange(x, y));
}
else if (op == 3)
{
scanf("%d%d", &x, &z);
upson(x, z);
}
else if (op == 4)
{
scanf("%d", &x);
printf("%d\n", qson(x));
}
}
return 0;
}