1. 程式人生 > 實用技巧 >析合樹學習筆記

析合樹學習筆記

OI-Wiki

考慮這樣一類問題:

給定一個排列 \(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,3]\)\([4,4]\)\([1,3]\) 無交,其餘都被 \([1,3]\) 包含。

但是 \([1,2]\) 就不是一個本原段。雖然它是一個連續段,但是連續段 \([2,3]\) 與它有交且不是包含關係。

顯然可以證明,任意一個長度大於1的本原段都可以分割成若干長度更小的本原段,兩個本原段不存在交。所以本原段的個數是 \(O(n)\) 的。

然後我們規定:析合樹上的所有節點都代表一個本原段。而且某個大於1的本原段是由其子節點合併而成。

我們規定一個子節點集合是:某個節點的所有子節點的區間構成的集合。比如 \(p=\{2,4,1,3\}\)\([1,4]\) 的子節點集合就是 \(\{[1,1],[2,2],[3,3],[4,4]\}\)

。其中 \([[3,3],[4,4]],[[1,1],[3,3]]\) 都是其中的子區間,對應的區間分別是 \([3,4],[1,3]\)

而我們想要的“連續段集合”就是任取節點集合排序後的一個區間,它都是一個連續段。

但是我們發現,這裡的“本原段”與我們上述希望的“連續段集合”有些時候不一定成立。

比如 \(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的區間。考慮有以下有幾種情況:

  1. 加入的節點可以直接塞入棧頂的子節點。直接處理
  2. 加入的節點可以合併從棧頂起的若干個節點,形成一個合點。
  3. 加入的節點可以合併從棧頂起的若干個節點(可以是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;
}