析合树小记
定义
-
排列:由 \(1\sim n\) 打乱组成的序列。
-
连续段:\([l,r]\) 被称为连续段,当且仅当排列 \(a\) 中 \(a_{l...r}\) 在排序后值域也连续。
构想
析合树是一种处理排列连续段问题的有力数据结构。
但是一个排列的连续段数可能达到 \(O(n^2)\),我们该如何存储?
一些连续段可能会与其他连续段严格相交,我们称这些连续段为非本原连续段。
其他连续段称为本原连续段,简称本原段。
我们可以证明,一个排列的本原段个数不超过 \(O(n)\),于是我们考虑只存储本原段。
一个本原段可能由多个本原段组成,所以我们采用树形结构存储。
每个结点表示一个本原段。若他不是叶子,那么他是由儿子对应的本原段从左到右连接而成的。
析合树
析合树是一种符合我们构想的一种树,他将上面的结点分为了两类:析点、合点。在这里,我们不讨论叶子的分类。
-
合点:一个结点是合点,当且仅当他的儿子本原段也是有序的,可以是从小到大,也可以是从大到小。例如:\([1,10]=[1,3]+[4,5]+[6,9]+[10,10]\)。
-
析点:不是合点的点就是析点。例如:\([1,10]=[6,9]+[1,3]+[10,10]+[4,5]\)。
定义子连续段为一个点的连续几个儿子组成的非本原连续段。
-
定理:析点的儿子无法组成子连续段。
-
证明:反证法,假设有一段儿子组成了子连续段,那么不存在其他连续段与这个子连续段严格相交。根据定义,这些儿子会合并成一个合点,矛盾。
构建
这里使用增量法。
维护一个栈,存储一棵析合树森林。
加入 \(a_i\),我们令 \(now\) 为新的结点,分情况:
- 栈顶不是叶子,且 \(now\) 可以作为栈顶的儿子。
此时栈顶一定不是析点,否则会存在子连续段,所以一定是合点。
我们还需要判断接为栈顶儿子后栈顶是否仍然是合点,额外记录 \(M_u\) 表示点 \(u\) 最右边的儿子的左端点,判断 \([M_u,i]\) 是否为连续段即可。
最后把栈顶作为新的 \(now\)。
- 与栈顶并列,形成一个合点,作为父亲。
判断两者是否形成一个连续段即可,然后把合点作为 \(now\)。
- 与栈顶部若干个点一同并列,形成一个析点,作为父亲。
我们不断弹出栈顶,并判断是否已经形成一个连续段,若已经形成则退出。然后把析点作为 \(now\)。
每次还需要判断 \(now\) 是否还能与先前本原段合并,我们需要支持查找可以合并到的最早的位置,可以用 这题 的线段树完成。
这里放一下 CF 的图。
我们要用一个 ST 表、一个线段树、三个栈,时间复杂度 \(O(n\log n)\)。当然你愿意的话,可以试试学 \(O(n)\) 的析合树。
点击查看代码
#define ll long long
#define pb push_back
using namespace std;
const ll maxn=2e5+10;
ll n,a[maxn];
struct RMQ{
ll st_min[20][maxn], st_max[20][maxn], Log[maxn];
void build(){
for(ll i=2;i<=n;i++) Log[i]=Log[i>>1]+1;
for(ll i=1;i<=n;i++) st_min[0][i]=st_max[0][i]=a[i];
for(ll i=1;(1<<i)<=n;i++)
for(ll j=1;j+(1<<i)-1<=n;j++)
st_min[i][j]=min(st_min[i-1][j],st_min[i-1][j+(1<<i-1)]),
st_max[i][j]=max(st_max[i-1][j],st_max[i-1][j+(1<<i-1)]);
}
ll qry_min(ll l,ll r){
ll k=Log[r-l];
return min(st_min[k][l],st_min[k][r-(1<<k)+1]);
}
ll qry_max(ll l,ll r){
ll k=Log[r-l];
return max(st_max[k][l],st_max[k][r-(1<<k)+1]);
}
}D;
struct SGT{
ll tag[maxn<<2], mn[maxn<<2], pos[maxn<<2];
void addtag(ll p,ll v){
tag[p]+=v, mn[p]+=v;
}
void pushdown(ll p){
addtag(p<<1,tag[p]), addtag(p<<1|1,tag[p]);
tag[p]=0;
}
void modify(ll p,ll l,ll r,ll ql,ll qr,ll v){
if(ql<=l&&r<=qr) {addtag(p,v); return;}
pushdown(p); ll mid=l+r>>1;
if(ql<=mid) modify(p<<1,l,mid,ql,qr,v);
if(mid<qr) modify(p<<1|1,mid+1,r,ql,qr,v);
mn[p]=min(mn[p<<1],mn[p<<1|1]);
if(mn[p]==mn[p<<1]) pos[p]=pos[p<<1];
else pos[p]=pos[p<<1|1];
}
void build(ll p,ll l,ll r){
pos[p]=l;
if(l==r) return;
ll mid=l+r>>1;
build(p<<1,l,mid), build(p<<1|1,mid+1,r);
}
}T;
ll chk(ll l,ll r) {return D.qry_max(l,r)-D.qry_min(l,r)-(r-l)==0;}
ll stk_min[maxn],top_min,stk_max[maxn],top_max;
ll stk[maxn],top,typ[maxn<<1],L[maxn<<1],R[maxn<<1],M[maxn<<1],cnt,id[maxn];
vector<ll>to[maxn<<1];
void build(){
D.build();
T.build(1,1,n);
for(ll i=1;i<=n;i++){
while(top_min&&a[stk_min[top_min]]>a[i])
T.modify(1,1,n,stk_min[top_min-1]+1,stk_min[top_min],a[stk_min[top_min]]), --top_min;
while(top_max&&a[stk_max[top_max]]<a[i])
T.modify(1,1,n,stk_max[top_max-1]+1,stk_max[top_max],-a[stk_max[top_max]]), --top_max;
T.modify(1,1,n,stk_min[top_min]+1,i,-a[i]);
T.modify(1,1,n,stk_max[top_max]+1,i,a[i]);
stk_min[++top_min]=i, stk_max[++top_max]=i;
id[i]=++cnt, L[cnt]=R[cnt]=i;
ll now=cnt;
while(top&&L[stk[top]]>=T.pos[1]){
if(typ[stk[top]]&&chk(M[stk[top]],i)){
to[stk[top]].pb(now);
R[stk[top]]=i, M[stk[top]]=L[now], now=stk[top];
--top;
} else if(chk(L[stk[top]],i)){
++cnt, typ[cnt]=1;
L[cnt]=L[stk[top]], R[cnt]=i, M[cnt]=L[now];
to[cnt].pb(stk[top]), to[cnt].pb(now);
now=cnt, --top;
} else{
++cnt, to[cnt].pb(now);
do to[cnt].pb(stk[top--]);
while (top&&!chk(L[stk[top]],i));
L[cnt]=L[stk[top]], R[cnt]=i, to[cnt].pb(stk[top]);
now=cnt, --top;
}
} stk[++top]=now;
T.modify(1,1,n,1,i,-1);
}
}
例题
先建立析合树,我们容易知道本原连续段的个数。
考虑非本原连续段,一定是合点的若干个连续的儿子组成的。设合点 \(u\) 有 \(S_u\) 个儿子,贡献为 \({S_u\choose 2}\)。
建立析合树,考虑求两个位置 \(l,r\) 在析合树上的 LCA,设对应连续段为 \([L,R]\),那么 \([L,R]\) 是同时包含 \(l,r\) 两个位置的最小本原段。
考虑非本原连续段,当 LCA 是合点时,我们在位置 \(l,r\) 往上跳到 LCA 的儿子上,容易得到包含 \(l,r\) 的最小非本原连续段。
建立析合树,考虑位置 \(l-1,r+1\),我们要求的是夹在中间的连续段个数。
考虑 \(l-1,r+1\) 的 LCA,以及两个点到 LCA 的路径。设 \(s_1[u]\) 表示 dfn 比 \(u\) 小的所有点的连续段个数,\(s_2[u]\) 则为 dfn 比 \(u\) 大的连续段个数,容易通过前缀和 + 差分计算出 LCA 以外的贡献。
对于 LCA 即其儿子,我们设 \(rk_u\) 表示 \(u\) 在其父亲的儿子中的第几个,\(pre_u\) 表示 \(u\) 的父亲中直到 \(u\) 的一段儿子的贡献,即可计算 LCA 的夹在中间的儿子的贡献。