树链剖分(轻重链剖分)

树链剖分,是一种将树剖分为链,在链上进行各种操作以达到目的的算法.树链剖分(轻重链剖分)可以解决lca(最近公共祖先),树上操作(对树上两点及经过的路的权值进行求和,修改等操作)等一类操作.对于这些问题建议逐个进行理解.

在理解问题之前,要对树链剖分的某些概念有了解:

1.对于每个点,存在一个size来记录它的子树大小,对于每个父节点,它的子节点的子树的大小(即子树上点的个数)size值最大的就是它的重儿子,其余的点都是它的轻儿子.每两个重儿子间靠重边相连,连接每个重儿子的边连在一起叫做重链.对于一个有了重儿子的节点来说,其余的可遍历的点(除了父节点)都是它的轻儿子,它与轻儿子之间的连线就叫轻边.如果某个父节点没有子节点,那么它本身就可以看做一条重链.如图:

 那么图中的重链就是:

1.  1->4->5->7

2.  2->3

3.  6

然后还要引入深度deep,父节点fa,top节点,还有一个记录重儿子的son,包括上面的size,五个数组.deep,顾名思义,就是求当前节点在树上的深度(从根开始往下遍历的深度),上图根节点1的深度是1,那么3的深度就是3,7的深度就是4.fa节点,是单向的关系,fa节点就是当前节点的上一层节点(接近根节点的那层),例如4的fa节点就是1.top节点记录的是当前节点所在的重链的最上方的节点,在2->3这条重链上,每个节点的top节点都是2.重儿子son的含义就是当前节点往树的下方遍历(不会去遍历父节点)size的值最大的,也就是它的重儿子son.

这就是解决一些问题基本概念了.

LCA(最近公共祖先)

lca有两种求法,一种是倍增算法,还有一种就是树链剖分(当然,没有完全利用树链剖分).

首先先理解一下lca的基本思路:

 要是要找2和6的最近公共祖先,所谓找最近公共祖先,就是将这两个点向上搜索,搜索到的第一点,在此点两条路相遇,并且保证走的路最少.一开始我就想用dfs直接搜索,但是由于没有预处理父节点这些,就很容易搜爆.树链剖分的思路就是先判断两点的深度是否相等,不相等就将深度深的那个点往上寻找父亲节点,直到两者在同一深度上,这一步是因为有一种情况是两者在一条重链上,从更深这样一直找就可以直接找到它的祖先就是浅一点的那个点.由此两者最近公共祖先就是较浅的那个点,但是也有两点不在一条重链的情况.当两者不在一条重链,我们仍然按照之前的做法,当两者在同一平面后,在将两者一起向上搜索父节点,直到两者重合,找到的该点就是他们的最近公共祖先.

下面结合一道杭电oj题目来理解lca;


How far away ?

Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)
Total Submission(s): 88    Accepted Submission(s): 37

Problem Description
There are n houses in the village and some bidirectional roads connecting them. Every day peole always like to ask like this "How far is it if I want to go from house A to house B"? Usually it hard to answer. But luckily int this village the answer is always unique, since the roads are built in the way that there is a unique simple path("simple" means you can't visit a place twice) between every two houses. Yout task is to answer all these curious people.
Input
First line is a single integer T(T<=10), indicating the number of test cases.
  For each test case,in the first line there are two numbers n(2<=n<=40000) and m (1<=m<=200),the number of houses and the number of queries. The following n-1 lines each consisting three numbers i,j,k, separated bu a single space, meaning that there is a road connecting house i and house j,with length k(0<k<=40000).The houses are labeled from 1 to n.
Next m lines each has distinct integers i and j, you areato answer the distance between house i and house j.
Output
For each test case,output m lines. Each line represents the answer of the query. Output a bland line after each test case.
Sample Input
2 3 2 1 2 10 3 1 15 1 2 2 3 2 2 1 2 100 1 2 2 1
Sample Output
10 25 100 100

题目大意就是第一行输入t,有t组样例,对于每个样例都存在一个n,表示有n个节点,第二行就输入n,和m,m表示询问次数,第三行开始的n-1行每行输入三个数,u,v,l,其中u和v是两个节点,l为连接两个节点间的长度.接下来是m行询问,每行两个整数i,j,表示询问i,j两点间的长度.对应输出.

先上ac代码:

#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;
int fa[40001],deep[40001];
long long ans=0;
struct node
{
	int to;//记录后继结点
	int len;//记录边的权值
};
vector<node>g[40001];//建立邻接表存图(树)
void dfs(int x,int fa1)//x为当前节点,da1为父节点,初始可以填1,0
{
	fa[x]=fa1,deep[x]=deep[fa1]+1;//存父节点,子节点的深度就是父节点深度加一
	for(int i=0;i<g[x].size();i++)..遍历图
	{
		if(g[x][i].to==fa[x])continue;//当遍历子节点的子节点是,不能遍历到其父节点
		dfs(g[x][i].to,x);//递归
	}
	return ;
}
void dfs1(int x,int y)
{
	if(deep[x]<deep[y])swap(x,y);//寻找深度深的点
	while(deep[x]!=deep[y])//深度深的点向上搜索,知道和另外一个点在同一深度
	{
		for(int i=0;i<g[x].size();i++)//遍历图
		{
			if(g[x][i].to==fa[x])//同上,不可遍历父节点
			{
				ans+=g[x][i].len;//将经过的边权加起来
				x=fa[x];//向子节点上方走
			}
		}
	}
	while(x!=y)//两者一起往上走知道相遇,此即为最近公共祖先
	{
		for(int i=0;i<g[x].size();i++)//遍历图
		{
			if(g[x][i].to==fa[x])
			{
				ans+=g[x][i].len;//记录边权
				x=fa[x];//寻找
			}
		}
		for(int i=0;i<g[y].size();i++)//与x点同步往上找最近公共祖先
		{
			if(g[y][i].to==fa[y])
			{
				ans+=g[y][i].len;
				y=fa[y];
			}
		}
	}
	return;
}
int main()
{
	int t;
	cin>>t;
	while(t--)//t个样例
	{
		int n,q,u,v,we;//n房屋数量,q询问次数
		scanf("%d%d",&n,&q);
		for(int i=0;i<n-1;i++)
		{
			scanf("%d%d%d",&u,&v,&we);//两个端点和路径长度
			g[u].push_back({v,we});//存树
			g[v].push_back({u,we});//存树
		}
		dfs(1,0);
		while(q--)
		{
			ans=0;//初始化
			cin>>u>>v;
			dfs1(u,v);
			cout<<ans<<endl;
		}
		for(int i=0;i<n;i++)g[i].clear();//初始化图
	}
	return 0;
}

树上操作

上文中的lca算法,并没有提及之前预处理的top,size,son这些数组,那是因为这些数组记录的值可以在树上操作系列中解决问题.

所谓的树上操作,主要包含以下问题:

1.对两点间最短路径上所有点的点权和边的边权进行加减求和等操作;

2.对某个点的子树进行操作

下面来看例题:


Aragorn's Story

Time Limit: 10000/3000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)
Total Submission(s): 237    Accepted Submission(s): 48


 
Problem Description
Our protagonist is the handsome human prince Aragorn comes from The Lord of the Rings. One day Aragorn finds a lot of enemies who want to invade his kingdom. As Aragorn knows, the enemy has N camps out of his kingdom and M edges connect them. It is guaranteed that for any two camps, there is one and only one path connect them. At first Aragorn know the number of enemies in every camp. But the enemy is cunning , they will increase or decrease the number of soldiers in camps. Every time the enemy change the number of soldiers, they will set two camps C1 and C2. Then, for C1, C2 and all camps on the path from C1 to C2, they will increase or decrease K soldiers to these camps. Now Aragorn wants to know the number of soldiers in some particular camps real-time.
 
Input
Multiple test cases, process to the end of input.

For each case, The first line contains three integers N, M, P which means there will be N(1 ≤ N ≤ 50000) camps, M(M = N-1) edges and P(1 ≤ P ≤ 100000) operations. The number of camps starts from 1.

The next line contains N integers A1, A2, ...AN(0 ≤ Ai ≤ 1000), means at first in camp-i has Ai enemies.

The next M lines contains two integers u and v for each, denotes that there is an edge connects camp-u and camp-v.

The next P lines will start with a capital letter 'I', 'D' or 'Q' for each line.

'I', followed by three integers C1, C2 and K( 0≤K≤1000), which means for camp C1, C2 and all camps on the path from C1 to C2, increase K soldiers to these camps.

'D', followed by three integers C1, C2 and K( 0≤K≤1000), which means for camp C1, C2 and all camps on the path from C1 to C2, decrease K soldiers to these camps.

'Q', followed by one integer C, which is a query and means Aragorn wants to know the number of enemies in camp C at that time.
 
Output
For each query, you need to output the actually number of enemies in the specified camp.
 
Sample Input
3 2 5 1 2 3 2 1 2 3 I 1 3 5 Q 2 D 1 2 2 Q 1 Q 3
 
Sample Output
7 4 8

题目大意就是给你t组样例,每个样例第一行是n,m,p,其中n的意思是有n个节点,m=n-1,表示节点间的边数,p表示询问次数,下一行是n个整数,表示每个节点的权值.在下面q行分别是对树进行操作.每行开头都有一个字符,如果字符是'I'的话,后面跟三个数字u,v,k,表示在u到v的路径上所有点权值加k,字符'D'则是在u,v间路径上所有的点的权值减k.字符'Q'则不同,在后面只输入一个数u,表示要询问u点当前的权值.

这类问题就要涉及到之前的那些概念:size,top,son等.整体思路和上文lca差不多,但是,由于可能对路径上每个节点都要进行操作,那么数据量一旦增大,就很有可能超时.那么在从两点向上走直到相遇时,我们考虑可不可以直接一整条一整条的重链进行跳跃,然后对每条重链区间进行处理,这样就会节省很多时间.之前也说过,根据节点的子树size大小来划分重儿子,找到重链,那我们可以尝试在从两点出发向上搜索时直接跳跃到其所在的重儿子的父节点上,这样就做到了直接从这条重链跳跃到另外一条重链上.而需要求出树上各个点的top值(也就是该节点所在重链的最顶端的点).那么在一开始dfs中可以这样操作:

void dfs(int x,int fx)
{
    f[x]=fx,size[x]=1,son[x]=0,deep[x]=deep[fx]+1;
    //size在刚开始搜索时没有搜索到子树,初始化为1,此时没有找到重儿子,初始化
    for(int i=0;i<g[x].size();i++)//遍历
    {
        int to=g[x][i];
        if(to==f[x])continue;
        dfs(to,x);
        size[x]+=size[to];
        //节点x的子树大小size是它所有子树的size的和,前面的递归可以逐层算出子树大小
        if(size[son[x]]<size[to])son[x]=to;
        //将x节点当前记录的重儿子的size值与遍历到的点size值相比,取size值大的最为新的重儿子
    }
    return;
}
void dfs1(int x,int tx)
{
    top[x]=tx;//先初始化topx
    if(son[x]!=0)dfs1(son[x],tx);
//当当前的节点有重儿子的时候,那么进行递归,直接将重儿子所在的链的top值初始化为该点的重儿子.
    for(int i=0;i<g[x].size();i++)
    {
        int to=g[x][i];
        if(to!=f[x]&&to!=son[x])dfs1(to,to);
//如果遍历到的点不是目前节点的父节点和重儿子,毫无疑问两者以轻边相连
//那么我们把遍历到的那个节点作为新的重链的开头,开始标记一条新的重链
    }
    return;
}

重链也进行了划分,可以在重链间来回跳跃了,但是,如何把跳跃过的这些区间进行区间处理呢?很容易想到的就是线段树进行区间处理,可是这里我们没有可以进行维护的数组,因为重链原数组中不一定是连续的,而线段树却要求是连续的区间顺序,难搞.

但仔细观察第二个dfs,会发现它递归是按照重链的顺序连续递归的,它递归完一整条重链后会去递归下一条重链,那么就可以联想到把他遍历的顺序用一个新数组记录下来,再用线段树去加以维护,就可以进行区间操作了.以下是修改后的第二次dfs:

void dfs1(int x,int tx)
{
    top[x]=tx;
    nid[x]=++dfn;
//dfn初始化为0,nid里面存的就是dfs的序,x的遍历顺序就是其中存的值
    oid[dfn]=x;//dfs序
    if(son[x]!=0)dfs1(son[x],tx);
    for(int i=0;i<g[x].size();i++)
    {
        int to=g[x][i];
        if(to!=f[x]&&to!=son[x])dfs1(to,to);
    }
    return;
}

上ac代码:

#include<iostream>
#include<vector>
#define N 50007
using namespace std;
int dfn,n,arr[N],deep[N],size[N],f[N],son[N],top[N],oid[N],nid[N];
char ch[N];
struct node
{
    int val,lazy;
    int l,r;
}tree[N<<2];
vector<int>g[60007];
void init()//初始化
{
    dfn=0;
    for(int i=0;i<=n;i++)
    {
        son[i]=0;
        deep[i]=0;
        size[i]=0;
        f[i]=0;
        top[i]=0;
        oid[i]=0;
        nid[i]=0;
        g[i].clear();
    }
}
void dfs(int x,int fx)
{
    f[x]=fx,size[x]=1,son[x]=0,deep[x]=deep[fx]+1;
    for(int i=0;i<g[x].size();i++)
    {
        int to=g[x][i];
        if(to==f[x])continue;
        dfs(to,x);
        size[x]+=size[to];
        if(size[son[x]]<size[to])son[x]=to;
    }
    return;
}
void dfs1(int x,int tx)
{
    top[x]=tx;
    nid[x]=++dfn;
    oid[dfn]=x;//dfs序
    if(son[x]!=0)dfs1(son[x],tx);
    for(int i=0;i<g[x].size();i++)
    {
        int to=g[x][i];
        if(to!=f[x]&&to!=son[x])dfs1(to,to);
    }
    return;
}
void push_down(int node,int llen,int rlen)//懒人标记
{
    tree[node<<1].lazy+=tree[node].lazy;
    tree[node<<1|1].lazy+=tree[node].lazy;
    
    tree[node<<1].val+=tree[node].lazy*llen;
    tree[node<<1|1].val+=tree[node].lazy*rlen;
    tree[node].lazy=0;
}
void build_tree(int l,int r,int node)//建树
{
    tree[node].l=l,tree[node].r=r,tree[node].lazy=0;
    if(l==r)
    {
        tree[node].val=arr[oid[l]];
        return ;
    }
    int mid=(l+r)>>1;
    build_tree(l,mid,node<<1);
    build_tree(mid+1,r,node<<1|1);
    tree[node].val=tree[node<<1].val+tree[node<<1|1].val;
    return;
}
void update(int l,int r,int node,int val)//区间更新
{
    if(r>=tree[node].r&&l<=tree[node].l)
    {
        tree[node].val+=val*(tree[node].r-tree[node].l+1);
        tree[node].lazy+=val;
        return;
    }
    int mid=(tree[node].l+tree[node].r)>>1;
    push_down(node,mid-tree[node].l+1,tree[node].r-mid);
    if(l<=mid)update(l,r,node<<1,val);
    if(r>mid)update(l,r,node<<1|1,val);
    tree[node].val=tree[node<<1].val+tree[node<<1|1].val;
    return;
}
int query(int L,int R,int node)//区间查询
{
    int ans=0;
    if(L<=tree[node].l&&tree[node].r<=R)
    {
        return tree[node].val;
    }
    int mid=(tree[node].l+tree[node].r)>>1;
    push_down(node,mid-tree[node].l+1,tree[node].r-mid);
    if(L<=mid)ans+= query(L,R,node<<1);
    else if(R>=mid)ans+= query(L,R,node<<1|1);
    return ans;
}
void chain(int x,int y,int k)
{
    while(top[x]!=top[y])
    {
        if(deep[top[x]]<deep[top[y]])swap(x,y);
        update(nid[top[x]],nid[x],1,k);
        //更新该点到其top的重链
        x=f[top[x]];
    }
    if(deep[x]>deep[y])swap(x,y);
    update(nid[x],nid[y],1,k);
    //此时两点在同一重链上,对两者之间的重链进行更新
    return;
}
int main()
{
    int m,p,u,v,k;
    while(scanf("%d%d%d",&n,&m,&p)!=EOF)
    {
        for(int i=1;i<=n;i++)scanf("%d",&arr[i]);
        init();
        while(m--)//邻接表存图
        {
            scanf("%d%d",&u,&v);
            g[u].push_back(v);
            g[v].push_back(u);
        }
        dfs(1,1);
        dfs1(1,1);
        build_tree(1,n,1);
        getchar();
        for(int i=0;i<p;i++)
        {
            scanf("%s",ch);
            if(ch[0]=='I')
            {
                scanf("%d%d%d",&u,&v,&k);
                chain(u,v,k);
            }
            else if(ch[0]=='D')
            {
                scanf("%d%d%d",&u,&v,&k);
                chain(u,v,-k);
            }
            else if(ch[0]=='Q')
            {
                scanf("%d",&u);
                printf("%d\n",query(nid[u],nid[u],1));
            }
        }
    }
    return 0;
}

感谢观看qwq.

(后续还有对子树的处理,会对该文进行更新)

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值