筆記:樹鏈剖分 之 輕重鏈剖分


Step 0-介紹

在之前線段樹的學習中,我們知道了如何對一個區間進行快速修改。

同樣我們可以在樹上進行快速修改(什么腦回路),完成以下幾個操作:

  • 修改樹上兩點之間的路徑上所有點的值。

  • 查詢樹上兩點之間的路徑上節點權值的和/極值/其它(在序列上可以用數據結構維護,便於合並的信息)

  • 此外,樹剖還可以用來 \(O(\log n)\)(且常數較小)地求 \(LCA\)。在某些題目中,還可以利用其性質來靈活地運用樹剖。

Q:那么在一棵樹上要如何進行修改呢?

沒錯!就是樹鏈剖分

Q:要怎么做呢?

就是把一顆樹 分屍(大霧 分成幾條鏈,再拼成一個線段樹...

那么接下來要解決的就是如何不重不漏的分屍了。

Step 1-幾種分屍方法

  1. 重鏈剖分
  2. 長鏈剖分不會
  3. 用於 Link/cut Tree 的剖分(有時被稱作“實鏈剖分”)(不會

ps:大多數情況下(沒有特別說明時), “樹鏈剖分” 都指 “重鏈剖分” 。

Step 2-詳解

1.重鏈剖分

(1)一些定義:

定義 重子節點 表示其子節點中子樹最大的子結點。如果有多個子樹最大的子結點,取其一。如果沒有子節點,就無重子節點。

定義 輕子節點 表示剩余的所有子結點。

從這個結點到重子節點的邊為 重邊

到其他輕子節點的邊為 輕邊

若干條首尾銜接的重邊構成 重鏈

把落單的結點也當作重鏈,那么整棵樹就被剖分成若干條重鏈。

如圖:

(2)重鏈剖分的性質

  • 樹上每個節點都屬於且僅屬於一條重鏈

  • 重鏈開頭的結點不一定是重子節點。

  • 所有的重鏈將整棵樹 完全剖分

  • 在剖分時 優先遍歷重兒子 ,最后重鏈的 DFS 序就會是連續的。

  • 在剖分時 重邊優先遍歷 ,最后樹的 DFN 序上,重鏈內的 DFN 序是連續的。按 DFN 排序后的序列即為剖分后的鏈。

  • 一顆子樹內的 DFN 序是連續的。

  • 可以發現,當我們向下經過一條 輕邊 時,所在子樹的大小至少會除以二。

  • 對於樹上的任意一條路徑,把它拆分成從 \(lca\) 分別向兩邊往下走,分別最多走 \(O(\log n)\) 次,因此,樹上的每條路徑都可以被拆分成不超過 \(O(\log n)\) 條重鏈。

(3)實現思路

樹剖的實現分兩個 DFS 的過程。偽代碼如下:

第一個 DFS 記錄每個結點的父節點(father)、深度(deep)、子樹大小(size)、重子節點(hson)。

\[\begin{array}{l} \text{TREE-BUILD }(u,dep) \\ \begin{array}{ll} 1 & u.hson\gets 0 \\ 2 & u.hson.size\gets 0 \\ 3 & u.deep\gets dep \\ 4 & u.size\gets 1 \\ 5 & \textbf{for }\text{each }u\text{'s son }v \\ 6 & \qquad u.size\gets u.size + \text{TREE-BUILD }(v,dep+1) \\ 7 & \qquad v.father\gets u \\ 8 & \qquad \textbf{if }v.size> u.hson.size \\ 9 & \qquad \qquad u.hson\gets v \\ 10 & \textbf{return } u.size \end{array} \end{array} \]

第二個 DFS 記錄所在鏈的鏈頂(top,應初始化為結點本身)、重邊優先遍歷時的 DFS 序(dfn)、DFS 序對應的節點編號(rank)。

\[\begin{array}{l} \text{TREE-DECOMPOSITION }(u,top) \\ \begin{array}{ll} 1 & u.top\gets top \\ 2 & tot\gets tot+1\\ 3 & u.dfn\gets tot \\ 4 & rank(tot)\gets u \\ 5 & \textbf{if }u.hson\text{ is not }0 \\ 6 & \qquad \text{TREE-DECOMPOSITION }(u.hson,top) \\ 7 & \qquad \textbf{for }\text{each }u\text{'s son }v \\ 8 & \qquad \qquad \textbf{if }v\text{ is not }u.hson \\ 9 & \qquad \qquad \qquad \text{TREE-DECOMPOSITION }(v,v) \end{array} \end{array} \]

(4)代碼實現

定義幾個數組:

  • \(fa(x)\) 表示節點 \(x\) 在樹上的父親。
  • \(dep(x)\) 表示節點 \(x\) 在樹上的深度。
  • \(siz(x)\) 表示節點 \(x\) 的子樹的節點個數。
  • \(son(x)\) 表示節點 \(x\)重兒子
  • \(top(x)\) 表示節點 \(x\) 所在 重鏈 的頂部節點(深度最小)。
  • \(dfn(x)\) 表示節點 \(x\)DFS 序 ,也是其在線段樹中的編號。
  • \(rnk(x)\) 表示 DFS 序所對應的節點編號,有 \(rnk(dfn(x))=x\)

我們進行兩遍 DFS 預處理出這些值,其中第一次 DFS 求出 \(fa(x)\) , \(dep(x)\) , \(siz(x)\) , \(son(x)\) ,第二次 DFS 求出 \(top(x)\) , \(dfn(x)\) , \(rnk(x)\)

void dfs1(int u) //第一次 DFS 求出 fa(x), dep(x), siz(x), son(x);
{
  siz[u]=1;
  for(int i=head[u];i;i=next[i])
  {
  	int v=to[i];
  	if(v==fa[u]) continue;
  	dep[v]=dep[u]+1;
  	fa[v]=u;
  	dfs1(v);
  	siz[u]+=siz[v];
  	if(siz[v]>siz[son[u]]) son[u]=v;
  }
}
void dfs2(int u, int tt) //第二次 DFS 求出 top(x), dfn(x), rank(x);
{
  top[u]=tt;
  dfn[u]=++cnt;
  rank[cnt]=u;
  if(!son[u]) return;
  dfs2(son[u],tt);  //優先對重兒子進行 DFS,可以保證同一條重鏈上的點 DFS 序連續
  for(int i=head[u];i;i=next[i])
  {
  	int v=to[i];
  	if(v!=son[u]&&v!=fa[u]) 
  	 dfs2(v,v);
  }
}

Q:具體操作呢?

請進-->新人食用之樹剖

2.長鏈剖分

請進-->樹鏈剖分

3.用於 Link/cut Tree 的剖分

我也不知道在哪里...

Step 3-常見應用

1.路徑上維護

用樹鏈剖分求樹上兩點路徑權值和,偽代碼如下:

\[\begin{array}{l} \text{TREE-PATH-SUM }(u,v) \\ \begin{array}{ll} 1 & tot\gets 0 \\ 2 & \textbf{while }u.top\text{ is not }v.top \\ 3 & \qquad \textbf{if }u.top.deep< v.top.deep \\ 4 & \qquad \qquad \text{SWAP}(u, v) \\ 5 & \qquad tot\gets tot + \text{sum of values between }u\text{ and }u.top \\ 6 & \qquad u\gets u.top.father \\ 7 & tot\gets tot + \text{sum of values between }u\text{ and }v \\ 8 & \textbf{return } tot \end{array} \end{array} \]

鏈上的 DFS 序是連續的,可以使用線段樹、樹狀數組維護。

每次選擇深度較大的鏈往上跳,直到兩點在同一條鏈上。

同樣的跳鏈結構適用於維護、統計路徑上的其他信息。

2.子樹維護

有時會要求,維護子樹上的信息,譬如將以 \(x\) 為根的子樹的所有結點的權值增加 \(v\)

在 DFS 搜索的時候,子樹中的結點的 DFS 序是連續的。

每一個結點記錄 bottom 表示所在子樹連續區間末端的結點。

這樣就把子樹信息轉化為連續的一段區間信息。

3.求最近公共祖先

不斷向上跳重鏈,當跳到同一條重鏈上時,深度較小的結點即為 LCA。

向上跳重鏈時需要先跳所在重鏈頂端深度較大的那個。

參考代碼:

int lca(int u, int v) {
  while (top[u] != top[v]) {
    if (dep[top[u]] > dep[top[v]])
      u = fa[top[u]];
    else
      v = fa[top[v]];
  }
  return dep[u] > dep[v] ? v : u;
}

Step 4-例題

1.P2590「ZJOI2008」樹的統計

My Code
//「ZJOI2008」樹的統計
#include <iostream>
#include <cstdio>
#define INF 10000005
using namespace std;
const int N=1e5+10;
struct TREE{
	int l,r,v,maxx;
}t[N*4];
int n,q,a[N];
int head[N*2],to[N*2],next[N*2],tot;
int fa[N],siz[N],dfn[N],rank[N],son[N],top[N],dep[N],cnt;

void add(int u,int v)
{
	next[++tot]=head[u];
	head[u]=tot;
	to[tot]=v;
}

void dfs1(int u)
{
	siz[u]=1;
	for(int i=head[u];i;i=next[i])
	{
		int v=to[i];
		if(v==fa[u]) continue;
		dep[v]=dep[u]+1;
		fa[v]=u;
		dfs1(v);
		siz[u]+=siz[v];
		if(siz[v]>siz[son[u]]) son[u]=v;
	}
}

void dfs2(int u,int tt)
{
	top[u]=tt;
	dfn[u]=++cnt;
	rank[cnt]=u;
	if(!son[u]) return ;
	dfs2(son[u],tt);
	for(int i=head[u];i;i=next[i])
	{
		int v=to[i];
		if(v!=son[u]&&v!=fa[u])
		 dfs2(v,v);
	}
}

void build(int k,int l,int r)
{
	t[k].l=l,t[k].r=r;
	if(l==r)
	{
		t[k].v=t[k].maxx=a[rank[l]];
		return ;
	}
	int mid=(l+r)>>1;
	build(k<<1,l,mid);
	build(k<<1|1,mid+1,r);
	t[k].v=t[k<<1].v+t[k<<1|1].v;
	t[k].maxx=max(t[k<<1].maxx,t[k<<1|1].maxx);
}

void change(int k,int x,int w)
{
	int l=t[k].l,r=t[k].r;
	if(r<x||l>x) return ; 
	if(l==r&&l==x)
	{
		t[k].v=t[k].maxx=w;
		return ;
	}
	int mid=(l+r)>>1;
	if(x<=mid) change(k<<1,x,w);
	else change(k<<1|1,x,w);
	t[k].v=t[k<<1].v+t[k<<1|1].v;
	t[k].maxx=max(t[k<<1].maxx,t[k<<1|1].maxx);
}

int qmax(int k,int x,int y)
{
	int l=t[k].l,r=t[k].r;
	if(l>=x&&r<=y) return t[k].maxx;
	int mid=(l+r)>>1;
	int maxx=-INF;
	if(x<=mid) maxx=max(maxx,qmax(k<<1,x,y));
	if(y>mid) maxx=max(maxx,qmax(k<<1|1,x,y));
	return maxx;
}

int qsum(int k,int x,int y)
{
	int l=t[k].l,r=t[k].r;
	if(l>=x&&r<=y) return t[k].v;
	int mid=(l+r)>>1;
	int res=0;
	if(x<=mid) res+=qsum(k<<1,x,y);
	if(y>mid) res+=qsum(k<<1|1,x,y);
	return res;
}

int QSUM(int x,int y)
{
	int res=0;
	while(top[x]!=top[y])
	{
		if(dep[top[x]]<dep[top[y]]) swap(x,y);
		res+=qsum(1,dfn[top[x]],dfn[x]);
		x=fa[top[x]];
	}
	if(dep[x]>dep[y]) swap(x,y);
	return res+=qsum(1,dfn[x],dfn[y]);
}

int QMAX(int x,int y)
{
	int res=-INF;
	while(top[x]!=top[y])
	{
		if(dep[top[x]]<dep[top[y]]) swap(x,y);
		res=max(res,qmax(1,dfn[top[x]],dfn[x]));
		x=fa[top[x]];
	}
	if(dep[x]>dep[y]) swap(x,y);
	return res=max(res,qmax(1,dfn[x],dfn[y]));
}

int main()
{
	scanf("%d",&n);
	int x,y;
	for(int i=1;i<n;i++)
	{
		scanf("%d%d",&x,&y);
		add(x,y),add(y,x);
	}
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	dfs1(1);
	dfs2(1,1);
	build(1,1,n);
	scanf("%d",&q);
	char k[8];
	while(q--)
	{
		cin>>k;
		scanf("%d%d",&x,&y);
		if(k[0]=='C') change(1,dfn[x],y);
		else if(k[1]=='M') printf("%d\n",QMAX(x,y));
		else printf("%d\n",QSUM(x,y));
	}
	return 0;
}

2.P3384 【模板】輕重鏈剖分

My Code
//P3384 【模板】輕重鏈剖分
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int N=1e6+100;
struct TREE{
	int v,l,r,add;
}t[N];
int n,m,p,root;
int cnt,tot,head[N],to[N],next[N];
int a[N];
int fa[N],dep[N],siz[N],son[N],top[N],dfn[N],rank[N];

inline int read()
{
	char ch=getchar();int x=0,f=1;
	while(ch<'0'||ch>'9')
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9')
	 x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
	return x*f;
}

inline void add(int u,int v)
{
	next[++tot]=head[u];
	head[u]=tot;
	to[tot]=v;
}

/*
我們先給出一些定義:
	1.fa(x) 表示節點 x 在樹上的父親;
	2.dep(x) 表示節點 x 在樹上的深度;
	3.siz(x) 表示節點 x 的子樹的節點個數;
	4.son(x) 表示節點 x 的重兒子 ;
	5.top(x) 表示節點 x 所在重鏈的頂部節點(深度最小);
	6.dfn(x) 表示節點 x 的 DFS 序 ,也是其在線段樹中的編號;
	7.rank(x) 表示 DFS 序所對應的節點編號,有 rank(dfn(x))=x;
*/

void dfs1(int u) //第一次 DFS 求出 fa(x), dep(x), siz(x), son(x);
{
	siz[u]=1;
	for(int i=head[u];i;i=next[i])
	{
		int v=to[i];
		if(v==fa[u]) continue;
		dep[v]=dep[u]+1;
		fa[v]=u;
		dfs1(v);
		siz[u]+=siz[v];
		if(siz[v]>siz[son[u]]) son[u]=v;
	}
}
void dfs2(int u, int tt) //第二次 DFS 求出 top(x), dfn(x), rank(x);
{
	top[u]=tt;
	dfn[u]=++cnt;
	rank[cnt]=u;
	if(!son[u]) return;
	dfs2(son[u],tt);  //優先對重兒子進行 DFS,可以保證同一條重鏈上的點 DFS 序連續
	for(int i=head[u];i;i=next[i])
	{
		int v=to[i];
		if(v!=son[u]&&v!=fa[u]) 
		 dfs2(v,v);
	}
}

/*
性質:
	1.樹上每個節點都屬於且僅屬於一條重鏈 。
	2.重鏈開頭的結點不一定是重子節點(因為重邊是對於每一個結點都有定義
	  的)。
	3.所有的重鏈將整棵樹 完全剖分 。
	4.在剖分時 優先遍歷重兒子 ,最后重鏈的 DFS 序就會是連續的。
	5.在剖分時 重邊優先遍歷 ,最后樹的 DFN 序上,重鏈內的 DFN 序是連續
	  的。按 DFN 排序后的序列即為剖分后的鏈。
	6.一顆子樹內的 DFN 序是連續的。
	7.可以發現,當我們向下經過一條 輕邊 時,所在子樹的大小至少會除以二。
	8.因此,對於樹上的任意一條路徑,把它拆分成從 lca 分別向兩邊往下走,
	  分別最多走 O(log n)次,因此,樹上的每條路徑都可以被拆分成不超過 
	  O(log n)條重鏈。
*/ 

void build(int k,int l,int r)
{
	t[k].l=l,t[k].r=r,t[k].add=0,t[k].v=0;
	if(l==r)
	{
		t[k].v=a[rank[l]];               
		return ;
	}
	int mid=(l+r)>>1;
	build(k<<1,l,mid);
	build(k<<1|1,mid+1,r);
	t[k].v=(t[k<<1].v+t[k<<1|1].v)%p;
}

void pushdown(int k)
{
	if(t[k].add)
	{
		t[k<<1].add+=t[k].add;
		t[k<<1|1].add+=t[k].add;
		int l=t[k<<1].l,r=t[k<<1].r;
		t[k<<1].v+=(r-l+1)*t[k].add;
		t[k<<1].v%=p;
		l=t[k<<1|1].l,r=t[k<<1|1].r;
		t[k<<1|1].v+=(r-l+1)*t[k].add;
		t[k<<1|1].v%=p;
		t[k].add=0;
	}
}

void qadd(int k,int x,int y,int v)
{
	int l=t[k].l,r=t[k].r;
	if(l>=x&&r<=y)                   
	{
		t[k].add+=v;
		t[k].v+=(r-l+1)*v;
		return ;
	}
	pushdown(k);
	int mid=(l+r)>>1;
	if(x<=mid) qadd(k<<1,x,y,v);
	if(y>mid) qadd(k<<1|1,x,y,v);
	t[k].v=(t[k<<1].v+t[k<<1|1].v)%p;
}

int query(int k,int x,int y)
{
	int l=t[k].l,r=t[k].r;
	if(l>=x&&r<=y) return t[k].v;
	int mid=(l+r)>>1;
	pushdown(k);
	int res=0;
	if(x<=mid) res+=query(k<<1,x,y);
	if(y>mid) res+=query(k<<1|1,x,y);
	return res;
}

void qsol(int x,int y,int v)
{
	while(top[x]!=top[y])
	{
		if(dep[top[x]]<dep[top[y]]) swap(x,y);
		qadd(1,dfn[top[x]],dfn[x],v);
		x=fa[top[x]];
	}
	if(dep[x]>dep[y]) swap(x,y);
	qadd(1,dfn[x],dfn[y],v);
	return ;
}

int query2(int x,int y)
{
	int sum=0;
	while(top[x]!=top[y])
	{
		if(dep[top[x]]<dep[top[y]]) swap(x,y);
		sum+=query(1,dfn[top[x]],dfn[x]);
		sum%=p;
		x=fa[top[x]];
	}
	if(dep[x]>dep[y]) swap(x,y);
	sum+=query(1,dfn[x],dfn[y]);
	sum%=p;
	return sum;
}

int main()
{
	int u,v,x,y,w,k;
	n=read(),m=read(),root=read(),p=read();
	for(int i=1;i<=n;i++) a[i]=read();
	for(int i=1;i<n;i++)
	{
		u=read(),v=read();
		add(u,v),add(v,u);
	}
	dfs1(root);
	dfs2(root,root);
	build(1,1,n);
	while(m--)
	{
		k=read();
		if(k==1)
		{
			x=read(),y=read(),w=read();
			qsol(x,y,w%p);
		}
		if(k==2)
		{
			x=read(),y=read();
			cout<<query2(x,y)<<endl;
		}
		if(k==3)
		{
			x=read(),w=read();
			qadd(1,dfn[x],dfn[x]+siz[x]-1,w);
		}
		if(k==4)
		{
			x=read();
			cout<<query(1,dfn[x],dfn[x]+siz[x]-1)%p<<endl;
		}
	}
	return 0;
} 

Step 5-寫在后面

參考資料

OI Wiki

燭木的博客

都看到這里了,不打算點個贊嘛


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM