专题·树链剖分【including 洛谷·【模板】树链剖分

初见安~~~终于学会了树剖~~~

【兴奋】当初机房的大佬在学树剖的时候我反复强调过:“学树剖没有前途的!!!”

恩。真香。

一、重链与重儿子

所谓树剖——树链剖分,就是赋予一个链的概念来优化一些或者说是应对一些操作的,所以相应的会有一些专用的概念定义。

 

一个节点的重儿子,为其更大的一颗子树的根节点。从这个点连向重儿子的边我们称为重边。

这里我们定义子树的大小是取决于节点的数量,而和点权没有任何关系。

就比如在下图中:

红色的边就是重边。

我们一眼就可以发现——这些重边多多少少连成了一条链。而这些由重边连续连起来的点和边就组成了重链,也就是树链。

我们可以发现树链的一些性质——比如,一条树链一定是一个轻儿子或者根节点开头,通过重边串起来一些重儿子;一个非叶子节点只有一个重儿子;一个单独的叶子节点也是一条树链,【满足以轻儿子开头】,所以一棵树一定可以被划分为几条链等等。

那么树剖有什么用呢——处理树上的一些相关问题。比如——维护树上区间,树上路径等等。

区间我们想到了线段树,树上路径想到了LCA,但是它们都有一个特点——连续。线段树只能维护连续区间,LCA路径也是不间断的。所以为了便于处理,我们要对这个图重新标号,以便查找。怎么标呢?我们可以想到——在树链上操作LCA路径,那么路径也是要连贯的,也就是说重链上的编号要连贯,所以我们重新编号的时候是在dfs序的基础上遵循先遍历重儿子的原则。

二、例题

我们还是以一个模板题为背景吧——洛谷P3384 【模板】树链剖分

这个题有4个操作:

操作1: 格式: 1 x y z 表示将树从x到y结点最短路径上所有节点的值都加上z

操作2: 格式: 2 x y 表示求树从x到y结点最短路径上所有节点的值之和

操作3: 格式: 3 x z 表示将以x为根节点的子树内所有节点值都加上z

操作4: 格式: 4 x 表示求以x为根节点的子树内所有节点值之和

可以很容易发现——操作1、2都需要走一遍x到y的路径,操作3、4都需要操作以x为根的子树。所以我们先思考怎么遍历这些区间——

首先遍历x到y的路径,我们亦容易想到LCA——两个点同时往上跳,直到某个值相同,可以一起操作。所以我们的思路就是:两个点不在同一条链就往链头的父亲节点跳,在同一条链上就直接处理。而处理方法也很简单——因为全程都在链上以连续的新节点编号来操作,所以线段树维护区间距离就很方便了,完全不受树剖影响地敲一个基本的建树、查询、区间修改+延迟标记的代码就可以了。【不清楚没关系,后面会有代码和详解】

而对于操作3和4,以x为根的子树,显然编号也是连续的——毕竟编号时的最基本原则还是dfs遍历。但是有一个小问题——我们知道以x为根的子树最小的编号是x的编号,但是最大的编号我们并不知道,如果遍历一遍来找的话复杂度就会比较高了。所以——这是我们在初始化树剖的时候要存储下来的一个变量——以x为根的子树的最大的节点编号。也就是代码中的cnt[ ]。

大致操作就如上啦~下面是代码及详解——

#include<bits/stdc++.h>
#define maxn 500010
using namespace std;
typedef long long ll;
struct graph//关于存图的操作简单打个包
{
	struct edge
	{
		ll to, nxt;
		edge(){}
		edge(ll tt, ll nn)
		{
			to = tt;
			nxt = nn;
		}
	}e[maxn << 1];
	ll head[maxn], k = 0;
	void init()
	{
		memset(head, -1, sizeof head);
	}
	
	void add(ll u, ll v)
	{
		e[k] = edge(v, head[u]);
		head[u] = k++;
	}
}g;

struct node
{
	ll c, f;//c是区间和,f是延迟标记
}t[maxn << 2];

ll fa[maxn], dep[maxn], size[maxn], son[maxn];
ll dfn[maxn], top[maxn], cnt[maxn], tot;
//cnt——该子树最大节点编号(线段树上)dfn映射原标号与新标号
ll n, m, root, val[maxn], mod;
//val为节点上的值 
//*****************************************************
//关于初始化 
//to find the heavy son
void dfs_getson(ll u)//深优遍历初始化并得出重儿子
{
	size[u] = 1;
	for(ll i = g.head[u]; ~i; i = g.e[i].nxt)
	{
		ll v = g.e[i].to;
		if(v == fa[u]) continue;
		fa[v] = u;
		dep[v] = dep[u] + 1;
		dfs_getson(v);
		size[u] += size[v];
		if(size[v] > size[son[u]]) son[u] = v;//这个儿子更大,这才是重儿子
	}
}

void dfs_rewrite(ll u, ll tp)//找到链头
{
	top[u] = tp;
	dfn[u] = ++tot; id[tot] = u;//映射到线段树上 
	if(son[u]) dfs_rewrite(son[u], tp);//先遍历重链
	for(ll i = g.head[u]; ~i; i = g.e[i].nxt)
	{
		ll v = g.e[i].to;
		if(v != son[u] && v != fa[u]) dfs_rewrite(v, v);其他轻儿子的链
	}
	cnt[u] = tot; //回溯标记该子树最大编号
}

//*************************************************
//关于线段树 【基本和线段树模板1一模一样
void build(ll p, ll l, ll r)
{
	if(l == r) 
	{
		t[p].c = val[id[l]];//线段树上用的是新的编号
		return; 
	}
	
	ll mid = l + r >> 1;
	build(p << 1, l, mid);
	build(p << 1 | 1, mid + 1, r);
	t[p].c = t[p << 1].c + t[p << 1 | 1].c;
}

void down(ll p, ll l, ll r)
{
	t[p << 1].f += t[p].f;
	t[p << 1 | 1].f += t[p].f;
	ll mid = l + r >> 1;
	t[p << 1].c += t[p].f * (mid - l + 1);
	t[p << 1 | 1].c += t[p].f * (r - mid);
	t[p].f = 0;
}

void change(ll p, ll l, ll r, ll ls, ll rs, ll x)
{
	if(ls <= l && r <= rs)
	{
		t[p].c += (r - l + 1) * x;
		t[p].f += x;
		return;
	}
	if(t[p].f) down(p, l, r);
	ll mid = l + r >> 1;
	if(ls <= mid) change(p << 1, l, mid, ls, rs, x);
	if(rs > mid) change(p << 1 | 1, mid + 1, r, ls, rs, x);
	t[p].c = t[p << 1].c + t[p << 1 | 1].c;
}

ll getsum(ll p, ll l, ll r, ll ls, ll rs)
{
	if(ls <= l && r <= rs) 
	{
		
		return t[p].c;
	}
	if(t[p].f) down(p, l, r);
	ll mid = l + r >> 1;
	ll ans = 0;
	if(ls <= mid) ans += getsum(p << 1, l, mid, ls, rs), ans %= mod;
	if(rs > mid) ans += getsum(p << 1 | 1, mid + 1, r, ls, rs), ans %= mod;
	return ans;
}
//******************************************************
//关于操作入口
void change_xtoy()//1
{
	ll x, y, z;
	scanf("%lld%lld%lld", &x, &y, &z);
	while(top[x] != top[y])
	{
		if(dep[top[x]] > dep[top[y]]) swap(x, y);
		change(1, 1, tot, dfn[top[y]], dfn[y], z);范围从y到y的链头
		y = fa[top[y]];//深度更深的一个往上跳
	}
	if(dep[x] > dep[y]) swap(x, y);//on the same line
	change(1, 1, tot, dfn[x], dfn[y], z);
}

void getson_xtoy()//2
{
	ll x, y;
	scanf("%lld%lld", &x, &y);
	ll ans = 0;
	while(top[x] != top[y])
	{
		if(dep[top[x]] > dep[top[y]]) swap(x, y);
		ans = (ans + getsum(1, 1, tot, dfn[top[y]], dfn[y])) % mod;		
		y = fa[top[y]];
	}
	if(dep[x] > dep[y]) swap(x, y);
	ans += getsum(1, 1, tot, dfn[x], dfn[y]);
	prllf("%lld\n", ans % mod);
}

void change_sontree()//3
{
	ll x, y;
	scanf("%lld%lld", &x, &y);
	change(1, 1, tot, dfn[x], cnt[x], y);//直接操作
}

void getsum_sontree()//4
{
	ll x;
	scanf("%lld", &x);
	prllf("%lld\n", getsum(1, 1, tot, dfn[x], cnt[x]) % mod);
} 

ll main()
{
	g.init();
	scanf("%lld%lld%lld%lld", &n, &m, &root, &mod);
	for(ll i = 1; i <= n; i++) scanf("%lld", &val[i]);
	for(ll i = 1; i < n; i++)
	{
		ll u, v;
		scanf("%lld%lld", &u, &v);
		g.add(u, v);
		g.add(v, u);
	}
	
	dfs_getson(root);
	dfs_rewrite(root, root);//初始化
	
	build(1, 1, tot);
	
	for(ll i = 1; i <= m; i++)
	{
		ll op;
		scanf("%lld", &op);
		if(op == 1) change_xtoy();
		if(op == 2) getson_xtoy();
		if(op == 3) change_sontree();
		if(op == 4) getsum_sontree();
		
	}
	return 0;
}

也许这份代码看起来很长,(有很多丑陋的注释)但是核心的部分也就只有初始化的两个dfs函数和操作1、2的入口函数xtoy,加起来50行左右,所以树剖只要理解到了倒也挺简单的:)而在往后的各大知识点的学习中(什么LCT,DDP……反正我都不会)树剖更是作为了一个基础知识,所以一定要掌握好哇~~

三、边转点

最后,树剖+线段树可以维护的是树上的点权区间,如果是边权区间的话就要边转点。

接下来特别讲一下关于树剖+线段树的边化点问题【清楚的自行跳过】

边化点,顾名思义就是把边权转化为点权来用。而又因为每个点的父亲是唯一的,所以我们都是把边权给了深度更大的那个点。图上表示大概就是这样的:

而后我们边化点,各个点的点权就是这样的:

很明显,第一个点是没有值的。所以在边化点线段树维护操作完毕过后查询的时候要注意,搜索范围要么从2开始,要么赋值第一个点为极大值。还有就是,因为每个点所对应的边都是它到它父亲节点的边,所以最后我们查询区间或者修改区间的时候LCA那个点不能考虑进来,区间应为dfn[son[lca]]到dfn[]。

可以看一个例题来强化一下:洛谷P1967 货车运输

迎评:)
——End——

  • 13
    点赞
  • 49
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
#include <cstdio> #include <iostream> #include <vector> #define N 30003 #define INF 2147483647 using namespace std; int n,f[N][20],dep[N],siz[N],son[N],top[N],tot,pos[N],w[N]; int Max[N*4],Sum[N*4]; vector <int> to[N]; void dfs1(int x){ siz[x]=1; int sz=to[x].size(); for(int i=0;i<sz;++i){ int y=to[x][i]; if(y==f[x][0])continue; f[y][0]=x; dep[y]=dep[x]+1; dfs1(y); siz[x]+=siz[y]; if(siz[y]>siz[son[x]])son[x]=y; } } void dfs2(int x,int root){ top[x]=root; pos[x]=++tot; if(son[x])dfs2(son[x],root); int sz=to[x].size(); for(int i=0;i<sz;++i){ int y=to[x][i]; if(y==f[x][0] || y==son[x])continue; dfs2(y,y); } } void update(int k,int l,int r,int P,int V){ if(l==r){ Max[k]=Sum[k]=V; return; } int mid=(l+r)>>1; if(P<=mid)update(k*2,l,mid,P,V); else update(k*2+1,mid+1,r,P,V); Max[k]=max(Max[k*2],Max[k*2+1]); Sum[k]=Sum[k*2]+Sum[k*2+1]; } void up(int &x,int goal){ for(int i=15;i>=0;--i) if(dep[f[x][i]]>=goal)x=f[x][i]; } int lca(int x,int y){ if(dep[x]>dep[y])up(x,dep[y]); if(dep[x]<dep[y])up(y,dep[x]); if(x==y)return x; for(int i=15;i>=0;--i) if(f[x][i]!=f[y][i])x=f[x][i],y=f[y][i]; return f[x][0]; } int getm(int k,int l,int r,int L,int R){ if(L<=l && r<=R)return Max[k]; int res=-INF,mid=(l+r)>>1; if(L<=mid)res=max(res,getm(k*2,l,mid,L,R)); if(R>mid)res=max(res,getm(k*2+1,mid+1,r,L,R)); return res; } int gets(int k,int l,int r,int L,int R){ if(L<=l && r<=R)return Sum[k]; int res=0,mid=(l+r)>>1; if(L<=mid)res+=gets(k*2,l,mid,L,R); if(R>mid)res+=gets(k*2+1,mid+1,r,L,R); return res; } int main(){ scanf("%d",&n); for(int i=1,a,b;i<n;++i){ scanf("%d%d",&a,&b); to[a].push_back(b); to[b].push_back(a); } dep[1]=1; dfs1(1); dfs2(1,1); for(int i=1;i<=15;++i) for(int j=1;j<=n;++j)f[j][i]=f[f[j][i-1]][i-1]; for(int i=1;i<=n;++i){ scanf("%d",&w[i]); update(1,1,n,pos[i],w[i]); } int q; scanf("%d",&q); while(q--){ char s[10]; int u,v,t; scanf("%s",s); if(s[1]=='H'){ scanf("%d%d",&u,&t); w[u]=t; update(1,1,n,pos[u],t); } if(s[1]=='M'){ scanf("%d%d",&u,&v); int ans=-INF,t=lca(u,v); for(int i=u;i;i=f[top[i]][0]) if(dep[t]<dep[top[i]]) ans=max(ans,getm(1,1,n,pos[top[i]],pos[i])); else{ ans=max(ans,getm(1,1,n,pos[t],pos[i])); break; } for(int i=v;i;i=f[top[i]][0]) if(dep[t]<dep[top[i]]) ans=max(ans,getm(1,1,n,pos[top[i]],pos[i])); else{ ans=max(ans,getm(1,1,n,pos[t],pos[i])); break; } printf("%d\n",ans); } if(s[1]=='S'){ scanf("%d%d",&u,&v); int ans=0,t=lca(u,v); for(int i=u;i;i=f[top[i]][0]) if(dep[t]<dep[top[i]]) ans+=gets(1,1,n,pos[top[i]],pos[i]); else{ ans+=gets(1,1,n,pos[t],pos[i]); break; } for(int i=v;i;i=f[top[i]][0]) if(dep[t]<dep[top[i]]) ans+=gets(1,1,n,pos[top[i]],pos[i]); else{ ans+=gets(1,1,n,pos[t],pos[i]); break; } printf("%d\n",ans-w[t]); } } }

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值