链分治学习笔记
你说的对,但是……
前言
知周所众,树分治包括淀粉质和涟粉质……
边分治:mmp
可以说,链分治是相当强大的一种树分治,用于处理一类全局带修最优性问题。
所以十分抽象,但对应得应用也很多。在学习前,务必保证对树链剖分的思想完全掌握。
那么,在膜拜树链老祖宗 @sublimetext orz 后获取 rp,开始吧!
在这里就没人看得到我是芙宁娜的狗了
重链分治
从 \(DDP\) 开始的伟大思想
DDP?什么东西?
理论上是“动态动态规划”,也就是带修 dp。
在本题中,即“没有上司的舞会”带修 promax 版。
带上修改操作后,原本静态的 dp 就无法使用了。
我们尝试从 \(0\) 开始,逐步发掘链分治的全过程。
\(DP\) 方程
易得:
观察性质
当对其进行单点修改时,修改的一定是该点到根的路径。
如果考虑暴力跳,可以做到 \(O(n)\)。
考虑优化
祖先链考虑差分,但是发现作为 dp 方程只能一步步网上跳,那么链上修改就没多少方法了:淀粉质或树链剖分。
在这里,为了保证修改的次数,我们选择重链剖分。
发掘优势
重链剖分有一点很重要的性质就是路径拆链不超过 \(O(\log n)\) 条。
因此考虑对每个重链维护 dp 方程。
发现有这样几点优势:
-
数量少,可以保证转移。
-
起点为叶子,保证初始状态。
-
很明显可以数据结构维护。
也就是说,我们相当于把树重构成了一棵重链树,一切操作都是在这棵树上暴力跳,但是高度固定 \(O(\log n)\)。
于是,难点也就在重链间的互相更新上了。
继续发掘
如果要把 dp 方程整体放在链上,还需要再发掘性质。
我们考虑从重链剖分的表亲——树上启发式合并获得灵感。
树上启发式合并是如何优化时间复杂度的?对于大小较少的轻儿子反复遍历,对重儿子单独维护。
而在重链剖分上,轻儿子意味着新的重链,更是有特殊的性质。
所以,我们考虑将轻重儿子分离开讨论:定义数组 \(g\) 为轻子树的 dp 方程合并,即 \(0\) 表示可取可不取,\(1\) 表示必不取并算上自己的权,可以转换:
对于叶子,有 \(g_{u,0}=0,g_{u,1} = s_u\)。
至此,我们已经将她表示成了重链剖分所适应的形式,接下来的转移就是重头戏了。
矩阵乘法
没错,当式子与上几项皆有关联,难以得出通项公式时,矩阵乘法无疑是很好的优化方式。
在这道题里,转移出现了 \(\max\) 操作,但并不妨碍我们使用可以支持所有有结合律的运算的广义矩阵乘法。
定义运输 \(\ast\):
令 \(C = A \ast B\),有
笔者不会证明此操作具有结合律,但感谢理解下两个符合结合律的运算的合成应该也符合结合律。
这样,转换一下 dp 方程的形式:
定义转移矩阵 \(T\)。
即:
发现 \(T\) 显然是 \(2 \times 2\) 的。设
则
稍微解一下有
维护树链
终于,我们开始探讨如何维护每一条重链。容易发现 \(T\) 本身和重链内部的转移没有联系(即内部没有 \(f\) 存在)。那么,我们在节点上维护一个矩阵,对每条树链维护一个维护矩阵连 \(\ast\) 的线段树。由于要合并树链,并且我们只关心最初的 \(f\) 值,我们转换一下式子:
以上,本题有关链分治的思路已经完结,但我们稍微再深入一点:
番外:全局平衡二叉树
上面的算法是 \(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),即对原树重链剖分,然后把每个重链压缩成一个点。
这里着重发掘一下重链压缩树的性质:
-
高度不超过 \(O(\log n)\),一切暴力跳祖先的问题可以直接跳。
-
每个节点在原树皆包含叶子,看似很无用,实际上这告诉我们每个节点可以直接 build。
-
每个节点的儿子都是接在她中间的轻儿子,也就是说,儿子的总量是可接受的,用全局平衡数据结构维护可以 \(O(\log n)\)。
-
一切原树上的单点修改在重链压缩树上依然是单点修改。
当然不用真的建出重链压缩树,但用她来画图理解可以更好的理解链分治。
下面引入几道例题。
由 \(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,甚至更多离谱的东西。
本文目前只更完了重链分治,至于长链分治、实链分治……
就接着咕吧……

浙公网安备 33010602011771号