析合樹學習筆記
考慮這樣一類問題:
給定一個排列 \(p_i\),定義一個區間 \([l,r]\) 是好的,當且僅當 \(\cup{p_{l\cdots r}}\) 恰好為一段連續的數字。
多次詢問一個區間的子區間中好的區間有幾個。
這類問題應該有不止一種解法。而其中比較通用的一種就是析合樹。
考慮上面那個問題。其實“好的區間”有一個專業點的詞叫“連續段”。
比如 \(p=\{2,3,1,4,5\}\),連續段有 \([1,1],[2,2],[3,3],[4,4],[5,5],[1,2],[1,3],[1,4],[4,5],[1,5]\)。
但是這樣的連續段有 \(O(n^2)\) 個,不可能一個個求出來。
但是我們發現一些奇特的性質:比如上述 \([1,4],[4,5],[1,5]\) 是所有由端點在 \(\{1,4,5\}\) 構成的區間。而這些都是連續段。
所以我們考慮能不能建出一顆樹。首先它應該是類似於線段樹的樣子,即每個節點代表一個線段。
但考慮上述的性質,我們希望它不一定是一個二叉樹,而是希望它的某個節點的所有兒子恰好構成上述的“連續段集合”。
所以我們需要引入一個新的定義:本原段。
這個定義的數學表達可以去看 OI-Wiki。簡單描述一下就是一類連續段,任何連續段與它們要麼無交,要麼包含。
比如 \(p=\{3,2,1,4\}\),那麼 \([1,3]\) 就是一個本原段,因為連續段 \([1,4]\)
但是 \([1,2]\) 就不是一個本原段。雖然它是一個連續段,但是連續段 \([2,3]\) 與它有交且不是包含關係。
顯然可以證明,任意一個長度大於1的本原段都可以分割成若干長度更小的本原段,兩個本原段不存在交。所以本原段的個數是 \(O(n)\) 的。
然後我們規定:析合樹上的所有節點都代表一個本原段。而且某個大於1的本原段是由其子節點合併而成。
我們規定一個子節點集合是:某個節點的所有子節點的區間構成的集合。比如 \(p=\{2,4,1,3\}\),\([1,4]\) 的子節點集合就是 \(\{[1,1],[2,2],[3,3],[4,4]\}\)
而我們想要的“連續段集合”就是任取節點集合排序後的一個區間,它都是一個連續段。
但是我們發現,這裡的“本原段”與我們上述希望的“連續段集合”有些時候不一定成立。
比如 \(p=\{2,4,1,3\}\),長度大於1的連續段只有 \([1,4]\),同樣長度大於1的本原段也只有 \([1,4]\)。所以 \([1,1],[2,2],[3,3],[4,4]\) 都是 \([1,4]\) 的子節點。
但是顯然並不存在所謂的“連續段集合”。所以我們需要修改一下定義。
我們發現,雖然上述的 \([1,4]\) 的子節點不能構成“連續段集合”,但是它有另一個性質:取其子節點集合中的任意一個子區間,只要區間裡不止一個元素,所得到的的區間都不是連續段。
比如上述 \([1,4]\),可以發現大小不為 1 和 4 的所有區間都不是連續段。
這樣我們定義析合樹上有兩種節點:析點和合點(對應析合樹)。
析點的性質:任取其子節點集合中的一個不止一個元素的子區間,所得到的的區間都不是連續段。
合點的性質:任取其子節點集合中的一個區間,所得到的的區間都是連續段。
可以證明,任何一個本原段不是合點就是析點。
特別的為了方便,我們認為所有長度為1的區間都對應析點。
那麼接下來就是如何構造了。
考慮貪心構造。用一個棧,每次加入時首先把加入節點看做長度為1的區間。考慮有以下有幾種情況:
- 加入的節點可以直接塞入棧頂的子節點。直接處理
- 加入的節點可以合併從棧頂起的若干個節點,形成一個合點。
- 加入的節點可以合併從棧頂起的若干個節點(可以是0),形成一個析點。
對於 1 可以直接比較得出結果。但是 2 就比較麻煩了。
事實上,只要我們能判斷 2,剩下的情況就是 3。
首先先引入一個很明顯的用於判定的結論:一個區間是連續段當且僅當 \(\max\{p_{l\cdots r}\}-\min\{p_{l\cdots r}\}=r-l\)。
然後對於當前位置 \(i\)。我們希望維護這樣一個數組 \(Q_j=\max\{p_{j\cdots i}\}-\min\{p_{j\cdots i}\}-(i-j)\)。
那麼前面兩個玩意用單調棧維護,後面那個用線段樹維護。每次單調棧改變時順便改變整體的值即可。
求出了這個東西,我們會發現:很明顯當 \(Q_j=0\) 時一定存在一個區間滿足條件。
那麼我們找出最左端的那個位置 \(p'\),不斷合併上去。可以發現,無論是情況 2 還是情況 3,合併的最終位置都是 \(p'\)。
這樣就可以做到均攤 \(O(1)\) 的優秀複雜度。
至於統計 0 那就是常見套路了:處理區間最小值。由於這是一個排列,所以一定有 \(Q_j\geq 0\)。
[CERC2017]Intrinsic Interval
題目大意:多次詢問求包含某段區間的最小長度連續段。
首先建出析合樹,找到兩個位置的LCA。如果這是一個析點,那麼最後答案就是該點的區間。因為不存在一個更小的子區間是連續段了。
否則答案應該是對應兩個子樹的區間最大並。
即考慮定義:如果這是一個合點,任何一個子區間均是連續段,所以取包含 \(l\) 區間左端點和包含 \(r\) 的區間的右端點一定最優。
複雜度 \(O(n\log n)\)。
#include<iostream>
#include<cstdio>
#include<cstring>
#define N 200010
using namespace std;
int num[N];
struct ST{
int lg[N],al[N][18],ar[N][18];
void init(int n)
{
lg[1]=0;
for(int i=2;i<=n;i++) lg[i]=lg[i>>1]+1;
for(int i=1;i<=n;i++) al[i][0]=ar[i][0]=num[i];
for(int i=1;i<=16;i++)
for(int j=1;j+(1<<i)-1<=n;j++) al[j][i]=min(al[j][i-1],al[j+(1<<(i-1))][i-1]),ar[j][i]=max(ar[j][i-1],ar[j+(1<<(i-1))][i-1]);
}
int get_min(int l,int r){int t=lg[r-l+1];return min(al[l][t],al[r-(1<<t)+1][t]);}
int get_max(int l,int r){int t=lg[r-l+1];return max(ar[l][t],ar[r-(1<<t)+1][t]);}
}st;
struct seg_tree{
int val[N<<2],tag[N<<2];
void set_tag(int u,int v){val[u]+=v;tag[u]+=v;}
void push_down(int u)
{
if(!tag[u]) return;
set_tag(u<<1,tag[u]),set_tag(u<<1|1,tag[u]),tag[u]=0;
}
void insert(int u,int l,int r,int L,int R,int v)
{
if(L<=l && r<=R){set_tag(u,v);return;}
push_down(u);
int mid=(l+r)>>1;
if(L<=mid) insert(u<<1,l,mid,L,R,v);
if(R>mid) insert(u<<1|1,mid+1,r,L,R,v);
val[u]=min(val[u<<1],val[u<<1|1]);
}
int answer(int u,int l,int r)
{
if(l==r) return l;
push_down(u);
int mid=(l+r)>>1;
if(!val[u<<1]) return answer(u<<1,l,mid);
else return answer(u<<1|1,mid+1,r);
}
}t;
int nxt[N<<1],to[N<<1],head[N],cnt;
void add(int u,int v)
{
nxt[++cnt]=head[u];
to[cnt]=v;
head[u]=cnt;
}
int fa[N][18],dep[N];
bool a_line(int l,int r){return st.get_max(l,r)-st.get_min(l,r)==r-l;}
int t1[N],tt1,t2[N],tt2;
int tn[N],tp;
int mn[N],id[N],lf[N],rf[N],typ[N],tot;
int build(int n)
{
for(int i=1;i<=n;i++)
{
for(;tt1 && num[i]<=num[t1[tt1]];tt1--) t.insert(1,1,n,t1[tt1-1]+1,t1[tt1],num[t1[tt1]]);
for(;tt2 && num[i]>=num[t2[tt2]];tt2--) t.insert(1,1,n,t2[tt2-1]+1,t2[tt2],-num[t2[tt2]]);
t.insert(1,1,n,t1[tt1]+1,i,-num[i]);
t.insert(1,1,n,t2[tt2]+1,i,num[i]);
t1[++tt1]=t2[++tt2]=i;
id[i]=++tot;lf[tot]=rf[tot]=i;
int p=t.answer(1,1,n),u=tot;
while(tp && lf[tn[tp]]>=p)
{
if(typ[tn[tp]] && a_line(mn[tn[tp]],i)){rf[tn[tp]]=i;add(tn[tp],u);u=tn[tp--];continue;}
if(a_line(lf[tn[tp]],i))
{
typ[++tot]=1;
lf[tot]=lf[tn[tp]],rf[tot]=i;mn[tot]=lf[u];
add(tot,tn[tp--]),add(tot,u);
}
else
{
add(++tot,u);
do add(tot,tn[tp--]); while(tp && !a_line(lf[tn[tp]],i));
lf[tot]=lf[tn[tp]],rf[tot]=i;
add(tot,tn[tp--]);
}
u=tot;
}
tn[++tp]=u;
t.insert(1,1,n,1,i,-1);
}
return tn[1];
}
void dfs(int u,int p)
{
fa[u][0]=p;
dep[u]=dep[p]+1;
for(int i=1;fa[u][i-1];i++) fa[u][i]=fa[fa[u][i-1]][i-1];
for(int i=head[u];i;i=nxt[i]) dfs(to[i],u);
}
int up(int x,int k)
{
for(int i=17;i>=0;i--)
if(k&(1<<i)) x=fa[x][i];
return x;
}
int lca(int x,int y)
{
if(dep[x]<dep[y]) swap(x,y);x=up(x,dep[x]-dep[y]);
if(x==y) return x;
for(int i=17;i>=0;i--)
if(fa[x][i]!=fa[y][i]) x=fa[x][i],y=fa[y][i];
return fa[x][0];
}
int main()
{
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&num[i]);
st.init(n);
int rt=build(n);
dfs(rt,0);
int m;
scanf("%d",&m);
while(m --> 0)
{
int l,r;
scanf("%d%d",&l,&r);
int x=id[l],y=id[r];
int c=lca(x,y);
if(!x || !y || !c) throw;
if(typ[c]) printf("%d %d\n",lf[up(x,dep[x]-dep[c]-1)],rf[up(y,dep[y]-dep[c]-1)]);
else printf("%d %d\n",lf[c],rf[c]);
}
return 0;
}