为什么我们需要树链剖分
大家都知道用线段树对一连续区间进行整体操作,如:将区间[l,r]的值增加x,或者求[l,r]的值的和。但是当我们将操作的对象由连续的区间变为树,这时候就不是很方便了。
不过,还有一种大家比较熟悉的,用于树上对某一子树进行整体操作的方法,那便是用dfs序构建线段树,这个方法的好处在于,当我们访问一棵子树的时候,我们将连续地访问完其所有的子节点,使得从根结点 x 到遍历完整个树的过程中的形成dfs序是连续的,此时,我们利用dfs序构建线段树,就可以很好的对树上某个子树进行整体操作了。
但是,普通的dfs序构建的线段树还是有所欠缺,比如我们要修改树上任意两点的最短路上所有的结点,显然,普通的dfs序不够用了,因此,我们需要引入树链剖分的方法求dfs序并构建线段树。
树链剖分基本术语
重儿子:最大的子树,也就是节点个数最多的子树
轻儿子:不是重儿子的子树
重边:由当前结点和重儿子之间的边
轻边:由当前结点和轻儿子之间的边
重链:所有的重边的集合
轻边:所有轻边的集合
链头:每条重链或轻链中深度最小的点
为了帮助理解,我们画一个图来说明一下这些术语,红边代表重边,蓝边代表轻边,红圈代表链头
树链剖分原理
在上图中,我们把树分成数个轻链和重链,这个是重要的,因为相比于普通的dfs序构建线段树,此时我们的dfs序不仅保留了某子树中所有结点的dfs序连续,而且我们也将保证同一条链上的点的dfs也是连续的(具体实现请看代码,这里说明思路)
此时我们就会发现一个方便之处,假设此时我们需要执行操作:将s和e之间的结点的权值增加x,设想,如果s和e处于同一条链上的时候,由于同一条链上的点的dfs连续,那么我们就可以直接利用由dfs序构建的线段树来更新s和e之间的结点了,因为此时s和e之间的结点相当于线段树上一段连续的区间。
那么,如果s和e不在同一条链上呢?假设此时的s = 7,e = 12(见上面的图),此时s和e不在同一条链上,通过观察我们可以知道s->e 的路径为: 7 - 5 - 4 -3- 2 -11 - 12 ,显然,我们可以将这个路径分为三段: 7 , 5 ~2 , 11 ~12 ,同时,这三段路径分别处于三条链上,由之前的推导,我们知道对这三段路径进行操作是比较容易的,而且我们发现,对这三段路径上的点进行操作,相当于对 7~12这条路径上的点进行操作,两者效果等价,这便是树链剖分的特点了。
树链剖分的特点:将两点之间dfs不连续的路径分隔为数个dfs序连续的路径,对这数个dfs连续的路径进行相当于对线段树区间的操作,并且此操作的效果等价于对两点之间所有结点的操作
代码区(P3384,树链剖分模板题)
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
#include<string>
#include<cmath>
#include<fstream>
#include<vector>
#include<stack>
#include <map>
#include <iomanip>
#define bug cout << "**********" << endl
#define out cout<<"---------------"<<endl
#define show(x, y) "["<<x<<","<<y<<"] "
//#define LOCAL = 1;
using namespace std;
typedef long long ll;
const int inf = 0x3f3f3f3f;
//const int mod = 1e8;
const int Max = 1e5 + 10;
struct Tree
{
int deep, father, val; //结点深度,父节点,权值
int size, heavy; //以此结点为根节点的树的大小。重儿子的编号
int id, top; //对应于线段树上的编号(dfs序),链头编号
}tree[Max]; //原本树的结点信息
int n, m, root, mod;
int toTree[Max], cnt; //记录线段树上某一点对应的树的编号,也就是根据dfs序找到对应的树
//以下为线段树操作
struct Node
{
int l, r;
int sum, lazy;
}node[Max<<2];
void build(int l, int r, int num)
{
node[num].l = l;
node[num].r = r;
node[num].lazy = 0;
if (l == r)
{
node[num].sum = tree[toTree[l]].val;
return;
}
int mid = (l + r) >> 1;
build(l, mid, num << 1);
build(mid + 1, r, num << 1 | 1);
node[num].sum = node[num << 1].sum + node[num << 1 | 1].sum;
}
void push_down(int num)
{
if (node[num].lazy != 0)
{
int &lazy = node[num].lazy;
node[num << 1].sum += (node[num << 1].r - node[num << 1].l + 1) * lazy;
node[num << 1].sum %= mod;
node[num << 1 | 1].sum += (node[num << 1 | 1].r - node[num << 1 | 1].l + 1) * lazy;
node[num << 1 | 1].sum %= mod;
node[num << 1].lazy += lazy;
node[num << 1 | 1].lazy += lazy;
node[num << 1].lazy %= mod;
node[num << 1 | 1].lazy %= mod;
lazy = 0;
}
}
void upData(int l, int r, int val, int num) //更新区间
{
if (l <= node[num].l && node[num].r <= r)
{
node[num].sum += (node[num].r - node[num].l + 1)*val;node[num].sum %= mod;
node[num].lazy += val;node[num].lazy %= mod;
return;
}
push_down(num);
int mid = (node[num].l + node[num].r) >> 1;
if (l <= mid)
upData(l, r, val, num << 1);
if (r > mid)
upData(l, r, val, num << 1 | 1);
node[num].sum = node[num << 1].sum + node[num << 1 | 1].sum;
}
int query(int l, int r, int num) //查询区间和
{
if (l <= node[num].l && node[num].r <= r)
return node[num].sum%mod;
push_down(num);
int mid = (node[num].l + node[num].r) >> 1;
int ans = 0;
if (l <= mid)
ans += query(l, r, num << 1);
if (r > mid)
ans += query(l, r, num << 1 | 1);
return ans % mod;
}
//以下为树链剖分的部分
struct Edge
{
int to, next;
}edge[Max<<1]; //边
int head[Max], tot;
int val[Max]; //记录各个结点的权值
void init()
{
memset(head, -1, sizeof(head));tot = 0;
memset(toTree, -1, sizeof(toTree));cnt = 0;
}
void add(int u, int v)
{
edge[tot].to = v;
edge[tot].next = head[u];
head[u] = tot++;
}
void dfs1(int now, int fa, int deep)
{
tree[now].val = val[now];
tree[now].father = fa;
tree[now].deep = deep;
tree[now].size = 1;
tree[now].heavy = -1; //初始化为无重边(也就是看作叶子节点了)
int max_son = -1;
for (int i = head[now]; i != -1; i = edge[i].next)
{
int v = edge[i].to;
if (v == fa) continue;
dfs1(v, now, deep + 1);
tree[now].size += tree[v].size;
if (tree[v].size > max_son) //更新重边
tree[now].heavy = v, max_son = tree[v].size;
}
}
void dfs2(int now, int top) //top为当前链的链头
{
tree[now].top = top;
tree[now].id = ++cnt;
toTree[cnt] = now;
if (tree[now].heavy == -1) return; //叶子结点
dfs2(tree[now].heavy, top); //先处理重链
for (int i = head[now]; i != -1; i = edge[i].next) //处理轻链
{
int v = edge[i].to;
if (v == tree[now].father || v == tree[now].heavy) continue;
dfs2(v, v); //此时v为一条轻链的链头(画一下就知道为什么了)
}
}
void upData2(int s, int e, int val) //更新s->e上的结点,将dfs不连续的路径分为数个dfs序连续的路径,对这个数个dfs序连续的路径进行操作
{
while (tree[s].top != tree[e].top) //不断地将深度大的上移,使得最后两个点都在同一个链上
{
if (tree[tree[s].top].deep < tree[tree[e].top].deep) swap(s, e);
upData(tree[tree[s].top].id, tree[s].id, val, 1); //同时更新分出来的dfs序连续的路径
s = tree[tree[s].top].father;
}
if (tree[s].deep > tree[e].deep) swap(s, e);
upData(tree[s].id, tree[e].id, val, 1);
}
int query2(int s, int e) //思路和upData2的一样:将dfs不连续的路径分为数个dfs序连续的路径,对这个数个dfs序连续的路径进行操作
{
int sum = 0;
while (tree[s].top != tree[e].top)
{
if (tree[tree[s].top].deep < tree[tree[e].top].deep) swap(s, e);
sum += query(tree[tree[s].top].id, tree[s].id, 1);
sum %= mod;
s = tree[tree[s].top].father;
}
if (tree[s].deep > tree[e].deep) swap(s, e);
sum += query(tree[s].id, tree[e].id, 1);
sum %= mod;
return sum;
}
int main()
{
#ifdef LOCAL
freopen("input.txt", "r", stdin);
freopen("output.txt", "w", stdout);
#endif
while (scanf("%d%d%d%d", &n, &m, &root, &mod) != EOF)
{
init();
for (int i = 1;i <= n;i++)
scanf("%d", val + i);
for (int i = 1, u, v;i < n;i++)
{
scanf("%d%d", &u, &v);
add(u, v);add(v, u);
}
dfs1(root, -1, 0); //先预处理出每个结点的基本信息,比如每个根结点对应的重边,以为构建链做准备
dfs2(root, root); //根据之前已经求出的每个结点的信息,获得链上结点dfs连续的dfs序
build(1, n, 1); //根据之前得到的dfs序构建线段树
while (m--)
{
int type, x, y, z;
scanf("%d", &type);
if (type == 1)
scanf("%d%d%d", &x, &y, &z), upData2(x, y, z); //将不连续的dfs划分,使得对数个连续的dfs序路径进行操作,以达所需效果
else if (type == 2)
scanf("%d%d", &x, &y), printf("%d\n", query2(x, y)); //将不连续的dfs划分,使得对数个连续的dfs序路径进行操作,以达所需效果
else if (type == 3)
scanf("%d%d", &x, &z), upData(tree[x].id, tree[x].id + tree[x].size - 1, z,1); //同一子树中的点,其dfs序是连续的,直接操作即可
else
scanf("%d", &x), printf("%d\n", query(tree[x].id, tree[x].id + tree[x].size - 1,1)); //同一子树中的点,其dfs序是连续的
}
}
return 0;
}