树链剖分学习笔记

树链剖分学习笔记

树链剖分这个算法是我在 NOIP2015 DAY2T3 中学会的一个优化暴力的算法,突然感觉这个算法很吊,然后思路好想,代码精简(bushi,十分符合我的口味,于是就搞来学了一下。

前置芝士:线段树,倍增

我们知道,对于这样一个问题:

在一棵树上,查询两点之间的距离。

这个题目比较简单,用一遍搜索去找到每一个点关于原点那一条链的前缀,然后最后的答案就是 \(f_u + f_v - 2 \times f_{LCA(u,v)}\) 这个结论是比较显然的,时间复杂度是预处理的 \(O(n\log_2 n+m \log_2 n)\) 。

那么再引入一个问题:

在一棵树上,进行m次操作,每一次操作使两个节点之间的节点(包括两个节点)的权值全部加 k ,最后求某两个节点之间的距离

这个问题也比较简单,就是用树上差分跑一遍,然后再扫一遍这个链就可以了,时间复杂度是 \(O(n)\) 的,非常的迅猛。

但是如果将这两个问题融合在一起,那么整个思路就变得不好想了,要不就是 \(O(\log_2 n)\) 查询,\(O(n)\) 修改,要不就是 \(O(1)\) 修改,\(O(n)\) 查询,两个时间复杂度都会烂掉,所以说引入了这样一个算法:树链剖分 。

算法讲解

树链剖分,顾名思义,就是将一棵树分成若干个链,然后进行操作的意思,那么怎么样子才能用一个比较优秀的时间复杂度完成上面说到的两个操作呢?

我们从浅入深讲解树链剖分的实现。

首先,对于一个树而言,如果说我们将其进行重轻链剖分……

等会,这个重轻链是啥?

看看百度百科怎么说

定义size(X)为以X为根的子树的节点个数。 ž令V为U的儿子节点中size值最大的节点,那么边(U,V)被称为重边,树中重边之外的边被称为轻边。

啊,感觉讲的还是很清楚的,但是为了体现我写学习笔记的意义,所以我打算再讲一遍。

重轻链剖分,是指对于任意一个节点而言,如果说他的某一个儿子节点中的子节点最大,那么这个节点就是其重儿子,其余的都是轻儿子,将该点和其连续重儿子相连所得到的一条链叫做重链,重链中的每一条边都是重边。

这种概念的东西不是很好懂,于是我画了……于是我没有画图,大家感性理解一下就可以了,看看代码就很容易理解了。

那么在重轻链分好之后我们根据重轻链的编号来进行线段树上的操作就可以了!但是问题来了,为什么这样是可以的呢?因为在重轻链剖分时,我们会有一个操作,使一整条重链上的序号都是连续的,这个操作就是标注dfs序啦,跟tarjan是一样的。

算法实现

首先根据模板题来看一下我们代码的实现部分

首先是线段树:

struct Sgiment_Tree{
    #define ls o << 1
    #define rs o << 1 | 1
    ll val[N << 2],tag[N << 2],l[N << 2],r[N << 2];
    inline void pushup(const int o) {
        val[o] = val[ls] + val[rs];
    }
    inline void pushdown(const int o) {
        if(tag[o]) {
            val[ls] += tag[o] * (r[ls] - l[ls] + 1);
            val[rs] += tag[o] * (r[rs] - l[rs] + 1);//这个地方卡了自己三天,后来发现这里的l写成了r导致没有ac,但是luogu测完之后竟然有30分,就离谱。
            tag[ls] += tag[o],tag[rs] += tag[o];
            tag[o] = 0;
        }
    }
    void build(const int o,const int lef,const int rig) {
        l[o] = lef,r[o] = rig;
        if(l == r) return;
        int mid = lef + rig >> 1;
        build(ls,lef,mid);
        build(rs,mid+1,rig);
    }
    void update(const int o,const int lef,const int rig,const int x,const int y,const int k) {
        if(x <= lef && y >= rig) {
            val[o] += (r[o] - l[o] + 1) * k;
            tag[o] += k;
            return ;
        }
        pushdown(o);
        int mid = lef + rig >> 1;
        if(x <= mid) update(ls,lef,mid,x,y,k);
        if(y > mid) update(rs,mid+1,rig,x,y,k);
        pushup(o);
    }
    ll query(const int o,const int lef,const int rig,const int x,const int y) {
        if(x <= lef && y >= rig) return val[o];
        pushdown(o);
        ll ret = 0;
        int mid = lef + rig >> 1;
        if(x <= mid) ret += query(ls,lef,mid,x,y);
        if(y > mid) ret += query(rs,mid+1,rig,x,y);
        return ret;
    }
}SGT;

某人曾说过,以上的东西最好在5分钟之内可以打完。

然后我们就再来看一下树链剖分怎么操作。

看一下变量定义:

fa[i]:表示该节点的父亲节点。

top[i]:表示该节点所在重链的最顶端。

id[i]:表示该节点的dfs序。//当然用dfn也是可以的。

siz[i]:表示该点下面的子树的大小。

dep[i]:表示该点的深度。

son[i]:表示重儿子所在的点。

wt[i]:表示当前id点的值。

有了以上的定义差不多就可以开始做了。

有以下两个dfs来实现整体的预处理。

dfs1:找出每个点的深度,子树的大小,重儿子所在的点。

dfs2:找出每一条重链并且进行update操作建树。

以下是代码实现:

void dfs1(const int now,const int pre) {
	fa[now] = pre,siz[now] = 1,dep[now] = dep[pre] + 1;
	for(int eg = hd[now];eg;eg = nxt[eg]) {
		if(to[eg] == pre) continue;
		dfs1(to[eg]);
		siz[now] += eg[to][siz];//这里解释一下,这个eg[to][siz]的意思就是siz[to[eg]],但是我写的这个很明显好看一些,下面的也是一样的,now[son][siz]表示siz[son[now]]。
		if(eg[to][siz] > now[son][siz]) son[now] = to[eg];
	}
	return;
}
void dfs2(const int now,const int tp) {
	top[now] = tp,id[now] = ++cnt,now[id][wt] = num[now];
	if(!son[now]) return;
	dfs2(son[now],tp);//先搞重儿子,使得整一条链是连续的。
	for(int eg = hd[eg];eg;eg = nxt[eg]) {
		if(to[eg] == son[now] || to[eg] == fa[now]) continue;
		dfs2(to[eg],to[eg]);//其余的所有轻儿子由自己为重链的顶端再找一条链。
	}
	return;
}

以上就是预处理的过程,过程的时间复杂度是很显然的 \(O(n)\) 。所以说预处理的过程的时间复杂度基本上不用算,而且这种操作也很简单。

看一下我们要解决的问题:

  1. 使 \(x\to y\) 之间的最短路径加上值 \(z\) 。
  2. 求 \(x\to y\) 之间的最短路径上的权值和。
  3. 使以 \(x\) 为根的子树上所有的点加上 \(z\) 。
  4. 求以 \(x\) 为根的子树上所有点的权值和。

最终结果皆模p。

用树链剖分来看看吧。

inline void solve1() {
	int x,y,z;
	x = rd,y = rd,z = rd;
	while(top[x] != top[y]) {//如果不在一条链上
		if(x[top][dep] < y[top][dep]) swap(x,y);//先找深的
		SGT.update(1,1,n,x[top][id],id[x],z % p);//一整条链的序号是连续的
		x = x[top][fa];//找到该条链的最顶端的父亲节点
	}
	if(dep[x] > dep[y]) swap(x,y);
	SGT.update(1,1,n,id[x],id[y],z % p);
	return ;
}
int ans;
inline void solve2() {
	ans = 0;
	int x = rd, y = rd;
	while(top[x] != top[y]) {
		if(x[top][dep] < y[top][dep]) swap(x,y);
		int tem = SGT.query(1,1,n,x[top][id],id[x]);
		(ans += tem) %= p;
		x = x[top][fa];
	}
	if(dep[x] > dep[y]) swap(x,y);
	(ans += SGT.query(1,1,n,id[x],id[y])) %= p;
	printf("%lld\n",ans);
	return;
}
inline void solve3() {
	int x = rd,z = rd;
	SGT.update(1,1,n,id[x],id[x] + siz[x] - 1,z);//由于dfs序的特性,所以说一个子树上的编号都是连续的,这个很好证明。
}
inline void solve4() {
	int x = rd;
	ans = SGT.query(1,1,n,id[x],id[x] + siz[x] - 1);
	printf("%lld\n",ans);
}

以上就是树链剖分的全部内容。

posted @ 2021-03-10 16:10  Kamiya-Kina  阅读(157)  评论(0)    收藏  举报