析合树学习笔记
析合树学习笔记
基本介绍
对于一个 \(n\) 阶排列 \(p\),我们定义一个区间 \([l,r]\) 是一个「连续段」,当且仅当 \(p_{[l,r]}\) 值域连续。
一个基础且重要的结论是,两个相交连续段的交/并/差仍是一个连续段。
我们定义一个排列的本源连续段为和其它连续段仅有包含或不交关系的连续段。这样的本源段形成了一个树形结构,称之为析合树。
析合树的定义
我们定义析合树上节点的儿子排列为其儿子值域区间的相对顺序。
我们称儿子排列为顺序或者逆序的析合树节点为「合点」。非合点称为「析点」。
对于合点来说,显然其儿子序列的任意一个子区间都是连续段。
一个很好的性质是析点的儿子序列的任意一个大于 1 的子区间都不是连续段。
使用反证法,析点儿子序列中最长的子区间构成连续段一定是一个本源段,其需要包含在析合树中,故矛盾。
一个有趣的结论是任意一个 \(n\) 个叶子,析点儿子个数不少于 4,合点儿子个数不少于 2 的析合树都能至少对应一个 \(n\) 阶排列。
析合树的构造
我们增量的构建析合树,假设我们已经得到了前 \(i-1\) 个元素构成的析合森林。
我们称一个本源段被打断当且仅当新的一个连续段和其相交且没有包含关系。
我们现在需要考虑的是,假如新的元素 \(p_i\) 后,哪些原本的本源段将被打断,会新增哪些新的以 \(i\) 为右端点的本源段。
我们给出一些观察:
-
若在原本的析合森林中结点 \(u\) 已经有了父亲,那么本源段 \(u\) 不会被打断。
考虑和 \(u\) 相交一定和 \(u\) 的父亲相交,那么和 \(u\) 父亲的交也和 \(u\) 相交,不符合 \(u\) 是本源段的前提,矛盾。
-
新形成的连续段的左端点一定是原本的析合森林的根的左端点。
如果左端点不是根的左端点,其一定被某个根打断。
-
若新形成了一个以析合森林根的左端点为左端点的连续段,其一定为本源段。
没有其它连续段与其有相交且不包含关系。
我们给出具体的一次增量的过程。
维护析合森林根节点的栈 \(st\),当前增量节点 \(cur\),初始时 \(L(cur)=R(cur)=i\)。
- 若 \([st_{tp},cur]\) 为一个连续段。
- 若 \(cur\) 往左延伸能打断 \(st_{tp}\),这等价于 \(st_{tp}\) 为合点且儿子排列顺序,\(cur\) 接在 \(st_{tp}\) 下面,令新的 \(cur'=st_{tp}\),继续新一轮插入即可。
- 否则,建立一个新点 \(k\),令 \(st_{tp}\) 和 \(cur\) 为其儿子,新点 \(k\) 为合点,令新的 \(cur'=k\)。
- 若 \([st_{tp},cur]\) 不为一个连续段。
- 找到包含 \(cur\) 的往左延伸的最小连续段,记其为 \([st_{p},st_{p+1},\cdots,st_{tp},cur]\)。
建立新点 \(rt\) 表示连续段 \([L(st_p),R(cur)]\),将 \(st_p,st_{p+1},\cdots,st_{tp},cur\) 接在 \(rt\) 下面。
新点 \(rt\) 为析点,令新的 \(cur'=rt\)。 - 若找不到这样的连续段,插入结束,将 \(cur\) 压入栈中即可。
- 找到包含 \(cur\) 的往左延伸的最小连续段,记其为 \([st_{p},st_{p+1},\cdots,st_{tp},cur]\)。
容易发现我们需要快速的得知一个以 \(i\) 为右端点的区间是否是连续段,还可能需要找到最小的一个以 \(i\) 为右端点的连续段,我们可以使用线段树和两个单调栈维护 \(mx-mn-(r-l)\),因为排列 \(mx-mn\ge r-l\) 的性质,为连续段的左端点将是最小值 0。
时间复杂度 \(O(n\log n)\)。
线性构造就鸽了,也没去找 LCA 的课件。
代码实现:
struct node{
int l,r;
bool op;
}t[2*maxn];
int tot,tp,st[maxn];
int id[maxn],dep[2*maxn];
int f[2*maxn][18];
int mn[4*maxn],laz[4*maxn];
void pushup(int k){mn[k]=min(mn[k<<1],mn[k<<1|1]);return ;}
void add_laz(int k,int v){mn[k]+=v,laz[k]+=v;return ;}
void pushdown(int k){
if(laz[k]==0)return ;
add_laz(k<<1,laz[k]),add_laz(k<<1|1,laz[k]);
laz[k]=0;
return ;
}
void modify(int k,int l,int r,int x,int y,int v){
if(l>=x&&r<=y){add_laz(k,v);return ;}
int mid=l+((r-l)>>1);
pushdown(k);
if(x<=mid)modify(k<<1,l,mid,x,y,v);
if(y>mid)modify(k<<1|1,mid+1,r,x,y,v);
pushup(k);
return ;
}
void modify(int x,int y,int v){modify(1,1,n,x,y,v);return ;}
int query(int k,int l,int r,int x,int y){
if(l>=x&&r<=y)return mn[k];
int mid=l+((r-l)>>1);
pushdown(k);
if(y<=mid)return query(k<<1,l,mid,x,y);
if(x>mid)return query(k<<1|1,mid+1,r,x,y);
return min(query(k<<1,l,mid,x,y),query(k<<1|1,mid+1,r,x,y));
}
int query(int x){return query(1,1,n,x,x);}
int query(int x,int y){return (x<=y)?query(1,1,n,x,y):1e9;}
int find_pos(int k,int l,int r,int x,int y){
if(l>y||r<x||mn[k])return -1;
if(l==r)return l;
int mid=l+((r-l)>>1);
pushdown(k);
int v=find_pos(k<<1|1,mid+1,r,x,y);
if(v!=-1)return v;
return find_pos(k<<1,l,mid,x,y);
}
int tp1,tp2,st1[maxn],st2[maxn];
void build(){
for(int i=1;i<=n;i++){
if(i>1)modify(1,i-1,-1);
while(tp1&&p[i]>p[st1[tp1]])modify(st1[tp1-1]+1,st1[tp1],p[i]-p[st1[tp1]]),tp1--;
while(tp2&&p[i]<p[st2[tp2]])modify(st2[tp2-1]+1,st2[tp2],p[st2[tp2]]-p[i]),tp2--;
st1[++tp1]=i,st2[++tp2]=i;
int cur=++tot;t[cur]=(node){i,i,0};
id[i]=cur;
while(tp){
if(query(t[st[tp]].l)==0){
if(query(t[st[tp]].l+1,t[cur].l-1)==0)
f[cur][0]=st[tp],t[st[tp]].r=i,cur=st[tp];
else
f[cur][0]=f[st[tp]][0]=++tot,cur=tot,t[cur]=(node){t[st[tp]].l,i,1};
tp--;
}
else{
int pos=find_pos(1,1,n,1,t[cur].l-1);
if(pos==-1)break;
int k=++tot;t[k]=(node){pos,i,0};f[cur][0]=k;
while(t[st[tp]].l>=pos)f[st[tp]][0]=k,tp--;
cur=k;
}
}
st[++tp]=cur;
}
return ;
}