链分治学习笔记

你说的对,但是……

前言

知周所众,树分治包括淀粉质和涟粉质……

边分治:mmp

可以说,链分治是相当强大的一种树分治,用于处理一类全局带修最优性问题。

所以十分抽象,但对应得应用也很多。在学习前,务必保证对树链剖分的思想完全掌握。

那么,在膜拜树链老祖宗 @sublimetext orz 后获取 rp,开始吧!

在这里就没人看得到我是芙宁娜的狗了

重链分治

\(DDP\) 开始的伟大思想

链接

DDP?什么东西?

理论上是“动态动态规划”,也就是带修 dp。

在本题中,即“没有上司的舞会”带修 promax 版。

带上修改操作后,原本静态的 dp 就无法使用了。

我们尝试从 \(0\) 开始,逐步发掘链分治的全过程。

\(DP\) 方程

易得:

\[f_{u,0}=\sum_{v \in son_u} \max(f_{v,0},f_{v,1}) \]

\[f_{u,1}=s_u + \sum_{v \in son_u} f_{v,0} \]

观察性质

当对其进行单点修改时,修改的一定是该点到根的路径。

如果考虑暴力跳,可以做到 \(O(n)\)

考虑优化

祖先链考虑差分,但是发现作为 dp 方程只能一步步网上跳,那么链上修改就没多少方法了:淀粉质或树链剖分

在这里,为了保证修改的次数,我们选择重链剖分

发掘优势

重链剖分有一点很重要的性质就是路径拆链不超过 \(O(\log n)\)

因此考虑对每个重链维护 dp 方程。

发现有这样几点优势:

  1. 数量少,可以保证转移。

  2. 起点为叶子,保证初始状态。

  3. 很明显可以数据结构维护。

也就是说,我们相当于把树重构成了一棵重链树,一切操作都是在这棵树上暴力跳,但是高度固定 \(O(\log n)\)

于是,难点也就在重链间的互相更新上了。

继续发掘

如果要把 dp 方程整体放在链上,还需要再发掘性质。

我们考虑从重链剖分的表亲——树上启发式合并获得灵感。

树上启发式合并是如何优化时间复杂度的?对于大小较少的轻儿子反复遍历,对重儿子单独维护。

而在重链剖分上,轻儿子意味着新的重链,更是有特殊的性质。

所以,我们考虑将轻重儿子分离开讨论:定义数组 \(g\) 为轻子树的 dp 方程合并,即 \(0\) 表示可取可不取,\(1\) 表示必不取并算上自己的权,可以转换:

\[f_{u,0}=g_{u,0} + \max(f_{dson_u,0},f_{dson_u,1}) \]

\[f_{u,1}=g_{u,1} + f_{dson_u,0} \]

对于叶子,有 \(g_{u,0}=0,g_{u,1} = s_u\)

至此,我们已经将她表示成了重链剖分所适应的形式,接下来的转移就是重头戏了。

矩阵乘法

没错,当式子与上几项皆有关联,难以得出通项公式时,矩阵乘法无疑是很好的优化方式。

在这道题里,转移出现了 \(\max\) 操作,但并不妨碍我们使用可以支持所有有结合律的运算的广义矩阵乘法

定义运输 \(\ast\)

\(C = A \ast B\),有

\[C_{i,j}=\max_{k}(A_{i,k}+B_{k,j}) \]

笔者不会证明此操作具有结合律,但感谢理解下两个符合结合律的运算的合成应该也符合结合律。

这样,转换一下 dp 方程的形式:

\[f_{u,0}=\max(f_{dson_u,0} + g_{u,0},f_{dson_u,1} + g_{u,0}) \]

\[f_{u,1}=\max(f_{dson_u,0} + g_{u,1},-\infty ) \]

定义转移矩阵 \(T\)

即:

\[\begin{bmatrix} f_{dson_u,0}&f_{dson_u,1} \end{bmatrix} \ast T = \begin{bmatrix} f_{u,0}&f_{u,1} \end{bmatrix} \]

发现 \(T\) 显然是 \(2 \times 2\) 的。设

\[T = \begin{bmatrix} E_1&E_2 \\ E_3&E_4 \end{bmatrix}\]

\[\begin{bmatrix} f_{dson_u,0}&f_{dson_u,1} \end{bmatrix} \ast \begin{bmatrix} E_1&E_2 \\ E_3&E_4 \end{bmatrix} = \begin{bmatrix} \max(f_{dson_u,0}+E_1,f_{dson_u,1}+E_3)& \max(f_{dson_u,0}+E_2,f_{dson_u,1}+E_4)\end{bmatrix}\]

稍微解一下有

\[T = \begin{bmatrix} g_{u,0}&g_{u,1} \\ g_{u,0}&-\infty \end{bmatrix}\]

维护树链

终于,我们开始探讨如何维护每一条重链。容易发现 \(T\) 本身和重链内部的转移没有联系(即内部没有 \(f\) 存在)。那么,我们在节点上维护一个矩阵,对每条树链维护一个维护矩阵连 \(\ast\) 的线段树。由于要合并树链,并且我们只关心最初的 \(f\) 值,我们转换一下式子:

\[\begin{bmatrix} g_{u,0}&g_{u,0} \\ g_{u,1}&-\infty \end{bmatrix} \ast \begin{bmatrix} f_{dson_u,0}\\f_{dson_u,1} \end{bmatrix} = \begin{bmatrix} f_{u,0}\\f_{u,1} \end{bmatrix}\]

以上,本题有关链分治的思路已经完结,但我们稍微再深入一点:

番外:全局平衡二叉树

上面的算法是 \(O(\log^2 n)\) 的,原因在重链剖分+线段树。

当然,如果换成实链剖分+ splay 就可以 \(O(\log n)\)

为什么呢?

因为 splay 相较于整棵树是平衡的(当然得益于她的摊还分析),虽然每个实链的 splay 不一定均衡,但从整棵 LCT 上看就是 \(O(\log n)\) 的。

当然,这道题我们没必要上实链剖分这样的动态结构,我们考虑对于重链剖分建一棵全局平衡的线段树。

一个方法是对于线段树的划分不是绝对中点而是带权中点:以每个点的轻子树 sz 为权。

为什么这么做就可以均衡了呢?因为在 DDP 中,我们只会从一个节点直接跳到根,这使得除了最开始的点,其她都是完整的重链,并且都是从轻子树跳上去的,并且都是单点修改。

将每条重链的线段树连起来,会发现是一棵均衡的多叉线段树,复杂度可以感性理解,为 \(O(\log n)\)

对每一个重链单独维护,在重链间相互影响,不就是链分治的核心思想吗(笑

另一种实现

知周所众,重链剖分不一定要套线段树。

使用 BST 可以达到同样的效果。这个 BST 比较鸡肋,不能维护区间,不能旋转。但是在 DDP 中,你只需要维护单点即可。对每条重链建带权 BST,相较上面的写法常数减半。

代码

#include<bits/stdc++.h>
//#define int long long
using namespace std;
namespace fast_IO {
#define IOSIZE 100000
int precision=3,POW[10]={1,10,100,1000,10000,100000,1000000,10000000,100000000,1000000000};char ibuf[IOSIZE],obuf[IOSIZE],*p1=ibuf,*p2=ibuf,*p3=obuf;
#ifdef ONLINE_JUDGE
#define getchar() ((p1==p2)and(p2=(p1=ibuf)+fread(ibuf,1,IOSIZE,stdin),p1==p2)?(EOF):(*p1++))
#define putchar(x) ((p3==obuf+IOSIZE)&&(fwrite(obuf,p3-obuf,1,stdout),p3=obuf),*p3++=x)
#define isdigit(ch) (ch>47&&ch<58)
#define isspace(ch) (ch<33)
#endif
template<typename T>inline T read(){T s=0;int w=1;char ch;while(ch=getchar(),!isdigit(ch)&&(ch!=EOF))if(ch==45)w=-1;if(ch==EOF)return 0;while(isdigit(ch))s=s*10+ch-48,ch=getchar();return s*w;}template<typename T>inline bool read(T&s){s=0;int w=1;char ch;while(ch=getchar(),!isdigit(ch)&&(ch!=EOF))if(ch==45)w=-1;if(ch==EOF)return 0;while(isdigit(ch))s=s*10+ch-48,ch=getchar();return s*=w,1;}inline bool read(char&s){while(s=getchar(),isspace(s));return 1;}inline bool read(char*s){char ch;while(ch=getchar(),isspace(ch));if(ch==EOF)return 0;while(!isspace(ch))*s++=ch,ch=getchar();*s='\000';return 1;}template<typename T>inline void print(T x){if(x<0)putchar(45),x=-x;if(x>9)print(x/10);putchar(x%10+48);}inline void print(char x){putchar(x);}inline void print(char*x){while(*x)putchar(*x++);}inline void print(const char*x){for(int i=0;x[i];i++)putchar(x[i]);}inline bool read(std::string&s){s="";char ch;while(ch=getchar(),isspace(ch));if(ch==EOF)return 0;while(!isspace(ch))s+=ch,ch=getchar();return 1;}inline void print(std::string x){for(int i=0,n=x.size();i<n;i++)putchar(x[i]);}inline bool read(bool&b){char ch;while(ch=getchar(),isspace(ch));b=ch^48;return 1;}inline void print(bool b){putchar(b+48);}inline bool read(double&x){int a=0,b=0;char ch=getchar();bool f=0;while(ch<48||ch>57){if(ch==45)f=1;ch=getchar();}while(47<ch&&ch<58){a=(a<<1)+(a<<3)+(ch^48);ch=getchar();}if(ch!=46){x=f?-a:a;return 1;}ch=getchar();while(47<ch&&ch<58){b=(b<<1)+(b<<3)+(ch^48),ch=getchar();}x=b;while(x>1)x/=10;x=f?-a-x:a+x;return 1;}inline void print(double x){if(x<0){putchar(45),x=-x;}x=round((long double)x*POW[precision])/POW[precision],print((long long)x),putchar(46),x-=(long long)(x);for(int i=1;i<=precision;i++)x*=10,putchar(x+48),x-=(int)x;}template<typename T,typename...T1>inline int read(T& a,T1&...other){return read(a)+read(other...);}template<typename T,typename...T1>inline void print(T a,T1...other){print(a),print(other...);}struct Fast_IO{~Fast_IO(){fwrite(obuf,p3-obuf,1,stdout);}}io;template<typename T>Fast_IO& operator>>(Fast_IO&io,T&b){return read(b),io;}template<typename T>Fast_IO& operator<<(Fast_IO&io,T b){return print(b),io;}
#define cout io
#define cin io
#define endl '\n'
}using namespace fast_IO;
namespace TYX_YNXK{
	#define il inline
	#define bl bool
	#define ll long long
	#define vd void
	#define N 1000005
	#define INF 0x3f3f3f3f
	#define pb push_back
	#define pii pair<int,int>
	#define fi first
	#define se second
	#define MP make_pair
	#define DEBUG cout<<"You are right,but you are wrong"<<'\n'
	#define END cout<<"You are right,but you are right now"<<'\n'
	int n,m,s[N],pre[N],tfa[N],rt,ans;
	struct matrix{
		int e[2][2];
		matrix(int a1=-INF,int a2=-INF,int a3=-INF,int a4=-INF){e[0][0]=a1,e[0][1]=a2,e[1][0]=a3,e[1][1]=a4;}
		il vd init(){e[0][0]=e[1][1]=0,e[0][1]=e[1][0]=-INF;}
		il int Max(){return max(max(e[0][0],e[0][1]),max(e[1][0],e[1][1]));}
	};
	matrix operator*(const matrix&a,const matrix&b){
		matrix c;
		if(a.e[0][0]+b.e[0][0]>c.e[0][0])c.e[0][0]=a.e[0][0]+b.e[0][0];
		if(a.e[0][1]+b.e[1][0]>c.e[0][0])c.e[0][0]=a.e[0][1]+b.e[1][0];
		if(a.e[0][0]+b.e[0][1]>c.e[0][1])c.e[0][1]=a.e[0][0]+b.e[0][1];
		if(a.e[0][1]+b.e[1][1]>c.e[0][1])c.e[0][1]=a.e[0][1]+b.e[1][1];
		if(a.e[1][0]+b.e[0][0]>c.e[1][0])c.e[1][0]=a.e[1][0]+b.e[0][0];
		if(a.e[1][1]+b.e[1][0]>c.e[1][0])c.e[1][0]=a.e[1][1]+b.e[1][0];
		if(a.e[1][0]+b.e[0][1]>c.e[1][1])c.e[1][1]=a.e[1][0]+b.e[0][1];
		if(a.e[1][1]+b.e[1][1]>c.e[1][1])c.e[1][1]=a.e[1][1]+b.e[1][1];
		return c;
	}
	int dfn[N],dcnt,DFS[N],fa[N],sz[N],dson[N];
	vector<int>G[N];
	vd dfs1(int u){
		sz[u]=1;
		for(int v:G[u])if(v^fa[u]){
			fa[v]=u,dfs1(v),sz[u]+=sz[v];
			if(sz[dson[u]]<sz[v])dson[u]=v;
		}
	}
	vd dfs2(int u){
		DFS[dfn[u]=++dcnt]=u;
		if(dson[u])dfs2(dson[u]);
		for(int v:G[u])if(v^fa[u]&&v^dson[u])dfs2(v);
	}
	struct node{int son[2];matrix sum,val;}t[N];
	il vd pushup(int k){t[k].sum=t[t[k].son[0]].sum*t[k].val*t[t[k].son[1]].sum;}
	il int ave(int L,int R){
		int mid,res=R,over=pre[R]-pre[L-1]>>1,l=L,r=R;
		while(l<=r){
			mid=l+r>>1;
			if(pre[mid]-pre[L-1]>=over)res=mid,r=mid-1;
			else l=mid+1;
		}
		return res;
	}
	int build(int l,int r){
		if(l>r)return 0;
		int mid=ave(l,r),k=DFS[mid];
		t[k].son[0]=build(l,mid-1),t[k].son[1]=build(mid+1,r);
		if(t[k].son[0])tfa[t[k].son[0]]=k;if(t[k].son[1])tfa[t[k].son[1]]=k;
		return pushup(k),k;
	}
	il vd add(int u,int v){
		t[u].val.e[0][0]=(t[u].val.e[0][1]+=t[v].sum.Max());
		t[u].val.e[1][0]+=max(t[v].sum.e[0][0],t[v].sum.e[0][1]);
	}
	il vd del(int u,int v){
		t[u].val.e[0][0]=(t[u].val.e[0][1]-=t[v].sum.Max());
		t[u].val.e[1][0]-=max(t[v].sum.e[0][0],t[v].sum.e[0][1]);
	}
	il vd link(int u,int v){add(u,v),tfa[v]=u;}
	int tbuild(int u){
		int l=dfn[u],r;
		for(;u;u=dson[u]){
			r=dfn[u];
			for(int v:G[u])if(v^fa[u]&&v^dson[u])link(u,tbuild(v));
		}return build(l,r);
	}
	vd update(int u,int v){
		t[u].val.e[1][0]+=v-s[u],s[u]=v;
		for(;u;u=tfa[u]){
			if(tfa[u]&&t[tfa[u]].son[0]!=u&&t[tfa[u]].son[1]!=u)del(tfa[u],u),pushup(u),add(tfa[u],u);
			else pushup(u);
		}
	}
	signed main(){
		cin>>n>>m;t[0].val.init(),t[0].sum.init();
		for(int i=1;i<=n;i++)cin>>s[i];
		for(int i=1,u,v;i<n;i++)cin>>u>>v,G[u].pb(v),G[v].pb(u);
		dfs1(1),dfs2(1);
		for(int i=1;i<=n;i++)pre[i]=pre[i-1]+sz[DFS[i]]-sz[dson[DFS[i]]];
		for(int i=1;i<=n;i++)t[i].val.e[1][0]=s[i],t[i].val.e[0][0]=t[i].val.e[0][1]=0;
		rt=tbuild(1);
		while(m--){
			int u,v;cin>>u>>v,update(u,v);
			cout<<t[rt].sum.Max()<<'\n';
		}
		return 0;
	}
}
signed main(){
	TYX_YNXK::main();
	return 0;
}

总结

终于写完了这道题,可以说,从这题可以看出整个链分治的思想。在处理一类不能对每个结点都查询/修改的问题时,可以使用重链压缩树(我自己编的名词\kk),即对原树重链剖分,然后把每个重链压缩成一个点。

这里着重发掘一下重链压缩树的性质:

  1. 高度不超过 \(O(\log n)\),一切暴力跳祖先的问题可以直接跳。

  2. 每个节点在原树皆包含叶子,看似很无用,实际上这告诉我们每个节点可以直接 build。

  3. 每个节点的儿子都是接在她中间的轻儿子,也就是说,儿子的总量是可接受的,用全局平衡数据结构维护可以 \(O(\log n)\)

  4. 一切原树上的单点修改在重链压缩树上依然是单点修改。

当然不用真的建出重链压缩树,但用她来画图理解可以更好的理解链分治。

下面引入几道例题。

\(Qtree4\) 领导的永恒维护

链接

我们将由刚才的思路,一步步推出本题的解法。

题目分析

带修最远同色点。

两点问题,如果不考虑淀粉质(事实上本题可以淀粉树),可以想到在 LCA 统一维护子树。但修改会导致需要一直向上跳,复杂度最坏 \(O(n)\)。如此典型的情况,用重链压缩树优化深度。

重链压缩

建出树后,我们可以暴力上跳到对应节点。接下来考虑的问题就是如何收集答案和如何修改。对于收集答案,因为涉及修改,我们要支持插入删除查询最值,显然堆。然后是修改,我们考虑用线段树维护重链压缩树上的节点,直接单点修改即可。而向上跳的修改,我们考虑对每个节点开一个堆,维护轻链的同色长。

全局平衡优化

使用全局平衡二叉树优化。

对于答案堆,可以直接用全局+轻子树和。

对于距离堆,其实就是轻儿子个数,采用边分治转二叉树。

至此,我们把她优化成了 \(O(n \log n)\) 的优秀算法。

代码

因为有边分治,就先跳过全局平衡二叉树写法了/kk

#include<bits/stdc++.h>
//#define int long long
using namespace std;
namespace fast_IO {
#define IOSIZE 100000
int precision=3,POW[10]={1,10,100,1000,10000,100000,1000000,10000000,100000000,1000000000};char ibuf[IOSIZE],obuf[IOSIZE],*p1=ibuf,*p2=ibuf,*p3=obuf;
#ifdef ONLINE_JUDGE
#define getchar() ((p1==p2)and(p2=(p1=ibuf)+fread(ibuf,1,IOSIZE,stdin),p1==p2)?(EOF):(*p1++))
#define putchar(x) ((p3==obuf+IOSIZE)&&(fwrite(obuf,p3-obuf,1,stdout),p3=obuf),*p3++=x)
#define isdigit(ch) (ch>47&&ch<58)
#define isspace(ch) (ch<33)
#endif
template<typename T>inline T read(){T s=0;int w=1;char ch;while(ch=getchar(),!isdigit(ch)&&(ch!=EOF))if(ch==45)w=-1;if(ch==EOF)return 0;while(isdigit(ch))s=s*10+ch-48,ch=getchar();return s*w;}template<typename T>inline bool read(T&s){s=0;int w=1;char ch;while(ch=getchar(),!isdigit(ch)&&(ch!=EOF))if(ch==45)w=-1;if(ch==EOF)return 0;while(isdigit(ch))s=s*10+ch-48,ch=getchar();return s*=w,1;}inline bool read(char&s){while(s=getchar(),isspace(s));return 1;}inline bool read(char*s){char ch;while(ch=getchar(),isspace(ch));if(ch==EOF)return 0;while(!isspace(ch))*s++=ch,ch=getchar();*s='\000';return 1;}template<typename T>inline void print(T x){if(x<0)putchar(45),x=-x;if(x>9)print(x/10);putchar(x%10+48);}inline void print(char x){putchar(x);}inline void print(char*x){while(*x)putchar(*x++);}inline void print(const char*x){for(int i=0;x[i];i++)putchar(x[i]);}inline bool read(std::string&s){s="";char ch;while(ch=getchar(),isspace(ch));if(ch==EOF)return 0;while(!isspace(ch))s+=ch,ch=getchar();return 1;}inline void print(std::string x){for(int i=0,n=x.size();i<n;i++)putchar(x[i]);}inline bool read(bool&b){char ch;while(ch=getchar(),isspace(ch));b=ch^48;return 1;}inline void print(bool b){putchar(b+48);}inline bool read(double&x){int a=0,b=0;char ch=getchar();bool f=0;while(ch<48||ch>57){if(ch==45)f=1;ch=getchar();}while(47<ch&&ch<58){a=(a<<1)+(a<<3)+(ch^48);ch=getchar();}if(ch!=46){x=f?-a:a;return 1;}ch=getchar();while(47<ch&&ch<58){b=(b<<1)+(b<<3)+(ch^48),ch=getchar();}x=b;while(x>1)x/=10;x=f?-a-x:a+x;return 1;}inline void print(double x){if(x<0){putchar(45),x=-x;}x=round((long double)x*POW[precision])/POW[precision],print((long long)x),putchar(46),x-=(long long)(x);for(int i=1;i<=precision;i++)x*=10,putchar(x+48),x-=(int)x;}template<typename T,typename...T1>inline int read(T& a,T1&...other){return read(a)+read(other...);}template<typename T,typename...T1>inline void print(T a,T1...other){print(a),print(other...);}struct Fast_IO{~Fast_IO(){fwrite(obuf,p3-obuf,1,stdout);}}io;template<typename T>Fast_IO& operator>>(Fast_IO&io,T&b){return read(b),io;}template<typename T>Fast_IO& operator<<(Fast_IO&io,T b){return print(b),io;}
#define cout io
#define cin io
#define endl '\n'
}using namespace fast_IO;
namespace TYX_YNXK{
	#define il inline
	#define bl bool
	#define ll long long
	#define vd void
	#define N 100005
	#define INF 0x3f3f3f3f
	#define pb push_back
	#define pii pair<int,int>
	#define fi first
	#define se second
	#define MP make_pair
	#define DEBUG cout<<"You are right,but you are wrong"<<'\n'
	#define END cout<<"You are right,but you are right now"<<'\n'
	int n,m,rt[N],col[N];
	struct Heap{
		multiset<int,greater<int> > a;
		il vd push(int v){a.insert(v);}
		il vd pop(int v){multiset<int,greater<int> >::iterator it=a.lower_bound(v);if(it!=a.end()) a.erase(it);}
		il int top(){if(a.empty()) return -INF;return *a.begin();}
	}ans,st[N];
	namespace Edge{
		int h[N],etot,dfn[N],dcnt,DFS[N],fa[N],dep[N],sz[N],dson[N],top[N],end[N];
		struct edge{int to,nxt,val;}e[N<<1];
		il vd lj(int u,int v,int w){e[++etot]=(edge){v,h[u],w},h[u]=etot;}
		vd dfs1(int u){
			sz[u]=1;
			for(int i=h[u];i;i=e[i].nxt){
				int v=e[i].to;
				if(v==fa[u]) continue;
				fa[v]=u,dep[v]=dep[u]+e[i].val,dfs1(v),sz[u]+=sz[v];
				if(sz[dson[u]]<sz[v]) dson[u]=v;
			}
		}
		vd dfs2(int u,int tp){
			DFS[dfn[u]=++dcnt]=u,top[u]=tp;end[tp]=u;
			if(dson[u]) dfs2(dson[u],tp);
			for(int i=h[u];i;i=e[i].nxt){
				int v=e[i].to;
				if(v==fa[u]||v==dson[u]) continue;
				dfs2(v,v);
			}
		}
	}using namespace Edge;
	namespace Segment_Tree{
		#define Z t[k].l
		#define Y t[k].r
		#define ls Z,l,mid
		#define rs Y,mid+1,r
		int Node;
		struct node{int l,r,mv,lv,rv;}t[N<<2];
		il vd pushup(int k,int l,int r){
			int mid=l+r>>1;
			t[k].lv=max(t[Z].lv,dep[DFS[mid+1]]-dep[DFS[l]]+t[Y].lv);
			t[k].rv=max(t[Y].rv,dep[DFS[r]]-dep[DFS[mid]]+t[Z].rv);
			t[k].mv=max(max(t[Z].mv,t[Y].mv),t[Z].rv+t[Y].lv+dep[DFS[mid+1]]-dep[DFS[mid]]);
		}
		vd build(int k,int l,int r){
			if(l==r){
				int u=DFS[l];
				for(int i=h[u];i;i=e[i].nxt){
					int v=e[i].to;
					if(v==fa[u]||v==dson[u]) continue;
					st[u].push(t[rt[v]].lv+e[i].val);
				}
				int x=st[u].top();st[u].pop(x);
				int y=st[u].top();st[u].push(x);
				t[k].lv=t[k].rv=max(x,0);
				t[k].mv=max(0,max(x,x+y));
				return;
			}
			int mid=l+r>>1;
			if(!t[k].l) t[k].l=++Node;build(ls);
			if(!t[k].r) t[k].r=++Node;build(rs);
			pushup(k,l,r);
		}
		vd update(int k,int l,int r,int L,int R){
			if(l==r){
				if(L!=R) st[L].push(t[rt[R]].lv+dep[R]-dep[L]);
				int x=st[L].top();st[L].pop(x);
				int y=st[L].top();st[L].push(x);
				if(col[L]) t[k].lv=t[k].rv=x,t[k].mv=x+y;
				else t[k].lv=t[k].rv=max(x,0),t[k].mv=max(0,max(x,x+y));
				return;
			}
			int mid=l+r>>1;
			if(dfn[L]<=mid) update(ls,L,R);
			else update(rs,L,R);
			pushup(k,l,r);
		}
		il vd modify(int u){
			int lst=u;
			while(u){
				int x=t[rt[top[u]]].mv;
				if(fa[top[u]]) st[fa[top[u]]].pop(t[rt[top[u]]].lv+dep[top[u]]-dep[fa[top[u]]]);
				update(rt[top[u]],dfn[top[u]],dfn[end[top[u]]],u,lst);
				int y=t[rt[top[u]]].mv;
				if(x!=y) ans.pop(x),ans.push(y);
				lst=top[u],u=fa[top[u]];
			}
		}
	}using namespace Segment_Tree;
	signed main(){
		cin>>n;
		for(int i=1,u,v,w;i<n;i++) cin>>u>>v>>w,lj(u,v,w),lj(v,u,w);
		dfs1(1),dfs2(1,1);
		for(int i=n;i;--i){
			int u=DFS[i];
			if(u==top[u]){
				if(!rt[u]) rt[u]=++Node;
				build(rt[u],i,dfn[end[top[u]]]);
				ans.push(t[rt[u]].mv);
			}
		}
		cin>>m;int count=n;
		while(m--){
			char opt;cin>>opt;
			switch(opt){
				case 'C':{
					int u;cin>>u;
					col[u]^=1;
					if(!col[u]) ++count;else --count;
					modify(u);
					break;
				}
				case 'A':{
					if(count==0) cout<<"They have disappeared.\n";
					else cout<<ans.top()<<'\n';
					break;
				}
			}
		}
		return 0;
	}
}
signed main(){
	TYX_YNXK::main();
	return 0;
}

总结

做完后,我们来梳理一下什么题可以用重链压缩树优化。

其实就一句话:修改是祖先链。

这和淀粉质是不同的:淀粉质处理的是全局路径的查询,而重链压缩树处理的是祖先路径的查询。

根据我们前面推导的,任何重链压缩树都可以用全局平衡二叉树优化。

也就是说,我们获得了一个恒定的处理祖先链问题的 \(O(n \log n)\) 算法。

\(Qtree6\) 统治下的闪电突袭

链接

待修连通块大小。

一个奇妙的邻域问题?只能上 LCT?

题目转换

纯粹邻域问题一定不能用树剖这类维护子树、路径的结构,哪怕是 LCT 也是把她转到根上来消除邻域。

一定有没有发掘到的性质。

我们发现,既然是连通块大小,那么选哪个点都无所谓。考虑跳到最顶上的点,将邻域转子树。

那么问题就变得很清晰了。

我们对每个点维护黑白连通块的大小,修改暴力跳改即可。

重链压缩

又到了熟悉的深度相关祖先链优化环境♪(*)

直接建重链压缩树,考虑如何修改。

容易发现修改的就是自己到祖先链第一个黑/白点。

树剖维护即可。

代码

#include<bits/stdc++.h>
//#define int long long
using namespace std;
namespace fast_IO {
#define IOSIZE 100000
int precision=3,POW[10]={1,10,100,1000,10000,100000,1000000,10000000,100000000,1000000000};char ibuf[IOSIZE],obuf[IOSIZE],*p1=ibuf,*p2=ibuf,*p3=obuf;
#ifdef ONLINE_JUDGE
#define getchar() ((p1==p2)and(p2=(p1=ibuf)+fread(ibuf,1,IOSIZE,stdin),p1==p2)?(EOF):(*p1++))
#define putchar(x) ((p3==obuf+IOSIZE)&&(fwrite(obuf,p3-obuf,1,stdout),p3=obuf),*p3++=x)
#define isdigit(ch) (ch>47&&ch<58)
#define isspace(ch) (ch<33)
#endif
template<typename T>inline T read(){T s=0;int w=1;char ch;while(ch=getchar(),!isdigit(ch)&&(ch!=EOF))if(ch==45)w=-1;if(ch==EOF)return 0;while(isdigit(ch))s=s*10+ch-48,ch=getchar();return s*w;}template<typename T>inline bool read(T&s){s=0;int w=1;char ch;while(ch=getchar(),!isdigit(ch)&&(ch!=EOF))if(ch==45)w=-1;if(ch==EOF)return 0;while(isdigit(ch))s=s*10+ch-48,ch=getchar();return s*=w,1;}inline bool read(char&s){while(s=getchar(),isspace(s));return 1;}inline bool read(char*s){char ch;while(ch=getchar(),isspace(ch));if(ch==EOF)return 0;while(!isspace(ch))*s++=ch,ch=getchar();*s='\000';return 1;}template<typename T>inline void print(T x){if(x<0)putchar(45),x=-x;if(x>9)print(x/10);putchar(x%10+48);}inline void print(char x){putchar(x);}inline void print(char*x){while(*x)putchar(*x++);}inline void print(const char*x){for(int i=0;x[i];i++)putchar(x[i]);}inline bool read(std::string&s){s="";char ch;while(ch=getchar(),isspace(ch));if(ch==EOF)return 0;while(!isspace(ch))s+=ch,ch=getchar();return 1;}inline void print(std::string x){for(int i=0,n=x.size();i<n;i++)putchar(x[i]);}inline bool read(bool&b){char ch;while(ch=getchar(),isspace(ch));b=ch^48;return 1;}inline void print(bool b){putchar(b+48);}inline bool read(double&x){int a=0,b=0;char ch=getchar();bool f=0;while(ch<48||ch>57){if(ch==45)f=1;ch=getchar();}while(47<ch&&ch<58){a=(a<<1)+(a<<3)+(ch^48);ch=getchar();}if(ch!=46){x=f?-a:a;return 1;}ch=getchar();while(47<ch&&ch<58){b=(b<<1)+(b<<3)+(ch^48),ch=getchar();}x=b;while(x>1)x/=10;x=f?-a-x:a+x;return 1;}inline void print(double x){if(x<0){putchar(45),x=-x;}x=round((long double)x*POW[precision])/POW[precision],print((long long)x),putchar(46),x-=(long long)(x);for(int i=1;i<=precision;i++)x*=10,putchar(x+48),x-=(int)x;}template<typename T,typename...T1>inline int read(T& a,T1&...other){return read(a)+read(other...);}template<typename T,typename...T1>inline void print(T a,T1...other){print(a),print(other...);}struct Fast_IO{~Fast_IO(){fwrite(obuf,p3-obuf,1,stdout);}}io;template<typename T>Fast_IO& operator>>(Fast_IO&io,T&b){return read(b),io;}template<typename T>Fast_IO& operator<<(Fast_IO&io,T b){return print(b),io;}
#define cout io
#define cin io
#define endl '\n'
}using namespace fast_IO;
namespace TYX_YNXK{
	#define il inline
	#define bl bool
	#define ll long long
	#define vd void
	#define N 100005
	#define INF 0x3f3f3f3f
	#define pb push_back
	#define pii pair<int,int>
	#define fi first
	#define se second
	#define MP make_pair
	#define DEBUG cout<<"You are right,but you are wrong"<<'\n'
	#define END cout<<"You are right,but you are right now"<<'\n'
	int n,m;bl col[N];
	namespace Edge{
		int h[N],etot,dfn[N],dcnt,DFS[N],fa[N],dep[N],sz[N],dson[N],top[N];
		struct edge{int to,nxt;}e[N<<1];
		struct list{int l,r;}b[N];int bcnt;
		il vd lj(int u,int v){e[++etot]=(edge){v,h[u]},h[u]=etot;}
		vd dfs1(int u){
			sz[u]=1;
			for(int i=h[u];i;i=e[i].nxt){
				int v=e[i].to;
				if(v==fa[u]) continue;
				fa[v]=u,dep[v]=dep[u]+1,dfs1(v),sz[u]+=sz[v];
				if(sz[dson[u]]<sz[v]) dson[u]=v;
			}
		}
		vd dfs2(int u,int tp){
			DFS[dfn[u]=++dcnt]=u,top[u]=tp;
			if(dson[u]) dfs2(dson[u],tp);
			for(int i=h[u];i;i=e[i].nxt){
				int v=e[i].to;
				if(v==fa[u]||v==dson[u]) continue;
				dfs2(v,v);
			}
		}
		il vd sublist(int u,int v){
			bcnt=0;
			while(top[u]!=top[v]){
				if(dep[top[u]]<dep[top[v]]) swap(u,v);
				b[++bcnt]=(list){dfn[top[u]],dfn[u]};
				u=fa[top[u]];
			}
			if(dep[u]>dep[v]) swap(u,v);
			b[++bcnt]=(list){dfn[u],dfn[v]};
		}
		il int lca(int u,int v){
			while(top[u]!=top[v]){
				if(dep[top[u]]<dep[top[v]]) swap(u,v);
				u=fa[top[u]];
			}
			if(dep[u]>dep[v]) swap(u,v);
			return u;
		}
	}using namespace Edge;
	#define Z k<<1
	#define Y k<<1|1
	#define ls Z,l,mid
	#define rs Y,mid+1,r
	namespace Segment_Tree{
		int t[N<<2][2];
		il vd pushup(int k){t[k][0]=max(t[Z][0],t[Y][0]),t[k][1]=max(t[Z][1],t[Y][1]);}
		vd build(int k,int l,int r){
			t[k][1]=r,t[k][0]=-INF;
			if(l==r) return;
			int mid=l+r>>1;
			build(ls),build(rs);
		}
		vd update(int k,int l,int r,int pos){
			if(l==r) return swap(t[k][0],t[k][1]);
			int mid=l+r>>1;
			if(pos<=mid) update(ls,pos);
			else update(rs,pos);
			pushup(k);
		}
		int query(int k,int l,int r,int L,int R,int opt){
			if(L<=l&&r<=R) return t[k][opt];
			int mid=l+r>>1;
			if(L<=mid&&R>mid) return max(query(ls,L,R,opt),query(rs,L,R,opt));
			if(L<=mid) return query(ls,L,R,opt);
			return query(rs,L,R,opt);
		}
	}using namespace Segment_Tree;
	struct Seg{
		int add[N<<2],s[N];
		il vd adder(int k,int l,int r,int v){
			if(l==r) s[l]+=v;
			else add[k]+=v;
		}
		il vd pushdown(int k,int l,int r){
			if(!add[k]) return;
			int mid=l+r>>1;
			adder(ls,add[k]),adder(rs,add[k]);
			add[k]=0;
		}
		il vd update(int k,int l,int r,int L,int R,int v){
			if(L<=l&&r<=R) return adder(k,l,r,v);
			int mid=l+r>>1;pushdown(k,l,r);
			if(L<=mid) update(ls,L,R,v);
			if(R>mid) update(rs,L,R,v);
		}
		int query(int k,int l,int r,int pos){
			if(l==r) return s[l];
			int mid=l+r>>1;pushdown(k,l,r);
			if(pos<=mid) return query(ls,pos);
			return query(rs,pos);
		}
	}T[2];
	il int get_fa(int u,int v){
		while(dep[fa[top[u]]]>dep[v]) u=fa[top[u]];
		if(dep[top[u]]>dep[v]) u=top[u];
		if(dep[u]==dep[v]+1) return dfn[u];
		return dfn[v]+1;
	}
	signed main(){
		cin>>n;for(int i=1;i<=n;i++) col[i]=1;
		for(int i=1,u,v;i<n;i++) cin>>u>>v,lj(u,v),lj(v,u);
		dep[1]=1,dfs1(1),dfs2(1,1);
		for(int i=1;i<=n;i++){
			T[1].s[dfn[i]]=sz[i];
			T[0].s[dfn[i]]=1;
		}
		build(1,1,n);
		cin>>m;
		while(m--){
			int opt,u;cin>>opt>>u;
			switch(opt){
				case 0:{
					sublist(1,u);int v;
					for(int i=1;i<=bcnt&&(v=query(1,1,n,b[i].l,b[i].r,col[u]^1))<1;i++);
					if(v<1) v=1;else v=DFS[v];
					if(col[v]==col[u]) v=dfn[v];
					else v=get_fa(u,v);
					cout<<T[col[u]].query(1,1,n,v)<<'\n';
					break;
				}
				case 1:{
					if(fa[u]) sublist(1,fa[u]);
					int x[2],f[2];
					x[0]=T[0].query(1,1,n,dfn[u]);
					x[1]=T[1].query(1,1,n,dfn[u]);
					if(fa[u]){
						for(int i=1;i<=bcnt&&(f[0]=query(1,1,n,b[i].l,b[i].r,0))<1;i++);if(f[0]<1) f[0]=1;else f[0]=DFS[f[0]];
						for(int i=1;i<=bcnt&&(f[1]=query(1,1,n,b[i].l,b[i].r,1))<1;i++);if(f[1]<1) f[1]=1;else f[1]=DFS[f[1]];
					}
					if(fa[u]){
						sublist(fa[u],f[col[u]^1]);
						for(int i=1;i<=bcnt;i++) T[col[u]].update(1,1,n,b[i].l,b[i].r,-x[col[u]]);
						sublist(fa[u],f[col[u]]);
						for(int i=1;i<=bcnt;i++) T[col[u]^1].update(1,1,n,b[i].l,b[i].r,x[col[u]^1]);
					}
					update(1,1,n,dfn[u]);
					col[u]^=1;
					break;
				}
			}
		}
		return 0;
	}
}
signed main(){
	TYX_YNXK::main();
	return 0;
}

总结

写了这么多道题,你会发现一些神奇的事情:在同一条重链时,数据结构使单点修改的高度不超过 \(O(\log n)\),重链性质使高度不超过 \(O(\log n)\),中和下来便是 \(O(\log^2 n)\)。(是不是很像根号分治?)

而全局平衡二叉树,中和了数据结构和重链数量的高度。如果你是 BST 写法,每条重链刚好重构成一棵二叉树,再将每棵二叉树的根节点连上原本轻边连上的点,原本的树就重构成了一棵树,她基于重链压缩树,我们称之为重链重构树

由于这种写法的优越性,以后我们将继续使用重链重构树分析题目。

而这棵树最重要的性质是:高度 \(O(\log n)\)

BST 上暴力跳深度,轻边上暴力改单点信息,重链重构树并没有改变两条链间的相对顺序,这使得她在某些题目当中优于更泛用、但要求不关注树的形态、复杂度较高的淀粉树。

后记

到这里,这篇学习笔记都没更完。

链分治绝对算一门艺术,你难以想象她可以处理子树、dp,甚至更多离谱的东西。

本文目前只更完了重链分治,至于长链分治、实链分治……

就接着咕吧……

posted @ 2025-02-11 10:39  一念行空  阅读(78)  评论(0)    收藏  举报