笛卡爾樹


 

聽上去有丶厲害,實際也很巧妙

學習了這兩篇:ReMoon - 單調棧的應用 --- 笛卡爾樹與虛樹

       ACM算法日常 - 算法合集 | 神奇的笛卡爾樹 - HDU 1506

 

板子:

struct Cartesian
{
    int root;
    int ls[N],rs[N];
    vector<int> v;
    
    void clear()
    {
        root=0;
        v.clear();
        for(int i=1;i<n;i++)
            ls[i]=rs[i]=0;
    }
    
    void build(int *a)
    {
        for(int i=1;i<n;i++)
        {
            int j=0;
            
            //a[v.back()]<a[i] 大根
            //a[v.back()]>a[i] 小根 
            while(v.size() && a[v.back()]<a[i])
                j=v.back(),v.pop_back();
            
            if(!v.size()) root=i;
            else rs[v.back()]=i;
            
            ls[i]=j;
            v.push_back(i);
        }
    }
};
View Code

 


 

~ 簡介 ~

雖然名字中帶有“樹”,但是笛卡爾樹其實是對於一個序列的轉化,並通過這個轉化獲得更多此序列的信息

對於一個簡單的序列:$2,8,5,7,1,4$,我們可以建立如下的笛卡爾樹($pos$表示原序列中的位置,$val$表示該位置的值)

 

笛卡爾樹有這樣的基本性質:

   對於樹上的任意一點$x$和左右兒子$left,right$,有:

   1. $pos[left]<pos[x]<pos[right]$

   2. $val[x]<val[left],val[right]$

即一般講解所說的$pos$滿足二叉查找樹,$val$滿足堆

 

直觀點說,就是這兩條延伸性質:

   以樹上任意一點$x$為根構成的子樹中,

   1. 各節點的$pos$是連續的,且對$pos$的中序遍歷即為原序列順序(由$pos$滿足二叉查找樹可得)

   2. $x$點的$val$為全子樹最小(由$val$滿足堆可得)

 


 

~ 建樹 ~

 

有了對笛卡爾樹結構的了解,現在考慮怎么建立這棵樹

 

【方法一】優先滿足$val$

要想優先滿足$val$的條件,那就必須從頂向下建樹了

利用上面的延伸性質2,每次選取當前區間$[l,r]$中$val$的最小值所在的$pos$(記$pos=i$)作為子樹的根節點

然后對於$[l,i-1],[i+1,r]$遞歸地不斷重復上述過程

其中選取區間$val$最小值所在的$pos$可以使用線段樹優化

總復雜度$O(nlogn)$

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

const int N=100005;
const int INF=1<<30;

int n;
int val[N];

int sz;
int t[N<<2];

inline void Add(int i)
{
    int k=i+sz-1;
    t[k]=i;
    k>>=1;
    while(k)
    {
        int left=t[k<<1],right=t[k<<1|1];
        t[k]=(val[left]<val[right]?left:right);
        k>>=1;
    }
}

inline int Query(int k,int l,int r,int a,int b)
{
    if(a>r || b<l)
        return 0;
    if(a>=l && b<=r)
        return t[k];
    
    int mid=(a+b)>>1;
    int left=Query(k<<1,l,r,a,mid),right=Query(k<<1|1,l,r,mid+1,b);
    return (val[left]<val[right]?left:right);
}

void Init()
{
    sz=1;
    while(sz<n)
        sz<<=1;
    
    val[0]=INF;
    for(int i=1;i<(sz<<1);i++)
        t[i]=0;
    for(int i=1;i<=n;i++)
        Add(i);
}

int ls[N],rs[N];

inline int Build(int l,int r)
{
    if(l>r)
        return 0;
    
    int pos=Query(1,l,r,1,sz);
    ls[pos]=Build(l,pos-1);
    rs[pos]=Build(pos+1,r);
    return pos;
}

int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        scanf("%d",&val[i]);
    
    Init();
    int root=Build(1,n);
    
/*    for(int i=1;i<=n;i++)
        printf("i=%d: ls=%d rs=%d\n",i,ls[i],rs[i]);*/
    return 0;
}
View Code

 

【方法二】優先滿足$pos$

由於對於子樹的先序遍歷是原序列順序,所以考慮按$i=1\text{ ~ }n$的順序依次加入節點並調整樹的結構,使得當前的樹為子序列$[1,i]$所構成的笛卡爾樹

由於$pos$滿足二叉排序樹,而$i$在區間$[1,i]$中$pos$最大,所以$i$插入的位置為 序列$[1,i-1]$所構成的笛卡爾樹的根節點 一直向右兒子走、直到走到了空節點

這樣插入后,$i$的$pos$已經滿足要求了,但是$val$卻不一定滿足堆

於是考慮怎么調整當前的樹

若$i$的$val$不滿足要求,即存在某(些)祖先$j$,使得$j$為根的子樹中$val$全大於$val[i]$;顯然我們需要通過調整$i$的位置,使得$i$成為$j$的祖先

是這樣操作的:

   0. 剛剛插入完成后,可能樹是這樣的

    

   1. 將$i$向上一層移動;這時由於$pos[k]<pos[i]$,所以$k$成為$i$的左兒子,$k'$依然是$k$的左兒子

   

   2. 繼續將$i$向上一層移動,相似的,$j$也應當屬於$i$的左子樹;不妨讓$j$為$i$的左兒子,$k$為$j$的右兒子(使用這種調整方法,$j,k,k'$相互間與原來的連邊相同

   

以上的調整操作都是在$[1,i-1]$序列構成的笛卡爾樹的最右鏈(即從根節點一直向右兒子走的這條路徑)上進行的

在處理完后,我們對比一下調整前后的樹結構,發現只有很少的地方出現了變化:

   1. $k$的右兒子變成了空節點

   2. $j$的父親變成了$i$,且$j$是$i$的左兒子

   3. $i$繼承了原來$j$的父親

事實上,即使$i$到$j$的路徑很長很長,一共也只有這三個地方發生了變化,所以我們的調整不是很復雜

現在最大的問題變成,如何找到$j$

目光回到最右鏈上,由於$val$滿足堆,於是最右鏈上的各節點$val$是單調遞增的;可以考慮用單調棧維護,棧中裝的是最右鏈上節點的$pos$

而我們要找的$j$,就是$val[j]<val[i]$、且最靠近棧底的元素

 

原理理解了之后,重新整理一下思路,盡量簡單清楚地建笛卡爾樹:

   1. 用單調棧維護最右鏈

   2. 每次插入當前的$i$,在單調棧中不停彈出棧頂,直到棧頂$fa$滿足$val[fa]<val[i]$,則最后一次彈出的就是$j$

   3. 將$i$作為$fa$的右兒子,$j$作為$i$的左兒子

是不是很簡單owo

 

復雜度$O(n)$,是相當優秀的一種方法

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;

const int N=100005;

int n;
int a[N];

int root;
int ls[N],rs[N];
vector<int> v;

void Build()
{
    for(int i=1;i<=n;i++)
    {
        int j=0;
        while(v.size() && a[v.back()]>a[i])
        {
            j=v.back();
            v.pop_back();
        }
        
        if(!v.size())
            root=i;
        else
            rs[v.back()]=i;
        
        ls[i]=j;
        v.push_back(i);
    }
}

int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    
    Build();
    
/*    for(int i=1;i<=n;i++)
        printf("i=%d ls=%d rs=%d\n",i,ls[i],rs[i]);*/
    return 0;
}
View Code

所以在一些情況下,笛卡爾樹的題目可以不用建樹,直接用單調棧就夠了

 


 

~應用~

最簡單的一個應用是求元素的左右延伸區間

具體點說,就是對於一個數列$a$,詢問以$a[i]$為區間最大(小)值的最長區間

使用笛卡爾樹,就可以通過$O(n)$的預處理做到$O(1)$查詢:進行中序遍歷,每個節點$x$的子樹的$pos$最小、最大值就是答案

模板題:HDU 1506 ($Largest\ Rectangle\ in\ a\ Histogram$)

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;

typedef long long ll;
const int N=100005;

int n;
int a[N];

int root;
int ls[N],rs[N];
vector<int> v;

void Build()
{
    v.clear();
    memset(ls,0,sizeof(ls));
    memset(rs,0,sizeof(rs));
    
    for(int i=1;i<=n;i++)
    {
        int j=0;
        while(v.size() && a[v.back()]>a[i])
        {
            j=v.back();
            v.pop_back();
        }
        
        if(!v.size())
             root=i;
        else
            rs[v.back()]=i;
        
        ls[i]=j;
        v.push_back(i);
    }
}

int l[N],r[N];

void dfs(int x)
{
    l[x]=r[x]=x;
    
    if(ls[x])
    {
        dfs(ls[x]);
        l[x]=l[ls[x]];
    }
    if(rs[x])
    {
        dfs(rs[x]);
        r[x]=r[rs[x]];
    }
}

int main()
{
    scanf("%d",&n);
    while(n)
    {
        for(int i=1;i<=n;i++)
            scanf("%d",&a[i]);
        
        Build();
        
        dfs(root);
        
        ll ans=0;
        for(int i=1;i<=n;i++)
            ans=max(ans,ll(a[i])*(r[i]-l[i]+1));
        printf("%lld\n",ans);
        
        scanf("%d",&n);
    }
    return 0;
}
View Code

 

一個稍微高級一點的應用,就是給出分治的邊界

一道不錯的題:Luogu P4755 ($Beautiful\ Pair$)

官方題解已經很完善了:FlierKing - 題解 P4755 【Beautiful Pair】

簡單點說,就是每次取當前區間$[l,r]$的最大值$a_i$,那么$i$即為笛卡爾樹中 此區間對應子樹的根節點

於是將區間分成兩部分$[l,i-1],[i+1,r]$的操作,就可以轉化成笛卡爾樹上的分治

同時,這個題解將“統計$[l,r]$中$a_i\leq x$的數量”這個主席樹問題,離線后通過拆分轉化為樹狀數組問題,設計十分巧妙

#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;

typedef long long ll;
typedef pair<int,int> pii;
const int N=100005;

int n;
int a[N];

int root;
int ls[N],rs[N];
vector<int> v;

void Build()
{
    for(int i=1;i<=n;i++)
    {
         int j=0;
        while(v.size() && a[v.back()]<a[i])
        {
            j=v.back();
            v.pop_back();
        }
        
        if(!v.size())
            root=i;
        else
            rs[v.back()]=i;
        
        ls[i]=j;
        v.push_back(i);
    }
}

int l[N],r[N];

inline void dfs(int x)
{
    if(ls[x])
    {
        dfs(ls[x]);
        l[x]=l[ls[x]];
    }
    else
        l[x]=x;
    if(rs[x])
    {
        dfs(rs[x]);
        r[x]=r[rs[x]];
    }
    else
        r[x]=x;
}

vector<pii> add[N];

inline void Solve(int x)
{
    int lp=x-l[x],rp=r[x]-x;
    if(lp<rp)
        for(int i=l[x];i<=x;i++)
        {
            add[r[x]].push_back(pii(a[x]/a[i],1));
            add[x-1].push_back(pii(a[x]/a[i],-1));
        }
    else
        for(int i=x;i<=r[x];i++)
        {
            add[x].push_back(pii(a[x]/a[i],1));
            add[l[x]-1].push_back(pii(a[x]/a[i],-1));
        }
    
    if(ls[x])
        Solve(ls[x]);
    if(rs[x])
        Solve(rs[x]);
}

vector<int> pos;

int t[N];

inline int lowbit(int x)
{
    return x&(-x);
}

inline void Add(int k,int x)
{
    for(int i=k;i<=n;i+=lowbit(i))
        t[i]+=x;
}

inline int Query(int k)
{
    int res=0;
    for(int i=k;i;i-=lowbit(i))
        res+=t[i];
    return res;
}

int main()
{
    scanf("%d",&n);
    pos.push_back(0);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]),pos.push_back(a[i]);
    
    sort(pos.begin(),pos.end());
    pos.resize(unique(pos.begin(),pos.end())-pos.begin());
    
    Build();
    dfs(root);
    
    Solve(root);
    
    ll ans=0;
    for(int i=1;i<=n;i++)
    {
        int p=lower_bound(pos.begin(),pos.end(),a[i])-pos.begin();
        Add(p,1);
        
        for(int j=0;j<add[i].size();j++)
        {
            int lim=lower_bound(pos.begin(),pos.end(),add[i][j].first)-pos.begin();
            if(pos[lim]>add[i][j].first)
                lim--;
            
            ans=ans+add[i][j].second*Query(lim);
        }
    }
    printf("%lld\n",ans);
    return 0;
}
View Code

 

學完之后立馬就現場碰到基本一樣的題...

牛客ACM 883G ($Removing\ Stones$,2019牛客暑期多校訓練營(第三場))

只不過對於當前區間,是遍歷較小的那一半、並在另一半二分

#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;

typedef long long ll;
const int N=300005;

int n;
int a[N];
ll p[N];

int root;
int ls[N],rs[N];
vector<int> v;

void Build()
{
    for(int i=1;i<=n;i++)
    {
        int j=0;
        while(v.size() && a[v.back()]<a[i])
        {
            j=v.back();
            v.pop_back();
        }
        
        if(!v.size())
            root=i;
        else
            rs[v.back()]=i;
        
        ls[i]=j;
        v.push_back(i);
    }
}

int l[N],r[N];

inline void dfs(int x)
{
    if(ls[x])
    {
        dfs(ls[x]);
        l[x]=l[ls[x]];
    }
    else
        l[x]=x;
    if(rs[x])
    {
        dfs(rs[x]);
        r[x]=r[rs[x]];
    }
    else
        r[x]=x;
}

ll ans=0;

void Solve(int x)
{
    ll sum=0;
    int lp=x-l[x]+1,rp=r[x]-x+1;
    
    int left,right,mid;
    if(lp<rp)
        for(int i=x;i>=l[x];i--)
        {
            sum+=a[i];
            left=x,right=r[x]+1,mid;
            while(left<right)
            {
                mid=(left+right)>>1;
                if(sum+p[mid]-p[x]<2LL*a[x])
                    left=mid+1;
                else
                    right=mid;
            }
            
            ans+=r[x]-left+1;
        }
    else
        for(int i=x;i<=r[x];i++)
        {
            sum+=a[i];
            left=l[x],right=x,mid;
            while(left<right)
            {
                mid=(left+right)>>1;
                if(sum+p[x-1]-p[mid-1]>=2LL*a[x])
                    left=mid+1;
                else
                    right=mid;
            }
            if(sum+p[x-1]-p[left-1]<2LL*a[x])
                left--;
            
            ans+=left-l[x]+1;
        }
    
    if(ls[x])
        Solve(ls[x]);
    if(rs[x])
        Solve(rs[x]);
}

void Clear()
{
    ans=0;
    v.clear();
    for(int i=1;i<=n;i++)
        ls[i]=rs[i]=0;
}

int main()
{
    int T;
    scanf("%d",&T);
    while(T--)
    {
        Clear();
        
        scanf("%d",&n);
        for(int i=1;i<=n;i++)
            scanf("%d",&a[i]),p[i]=p[i-1]+a[i];
        
        Build();
        dfs(root);
        
        Solve(root);
        printf("%lld\n",ans);
    }
    return 0;
}
View Code

 

一道比較明顯的題:HDU 6701 ($Make\ Rounddog\ Happy$,$2019\ Multi-University\ Training\ Contest\ 10$)

由於需要對$a_l,...,a_r$求max,所以能比較自然地想到笛卡爾樹上分治

然后就是處理區間內數字不同的限制;不過也並不困難,只要正向、反向各掃一遍就能預處理出來$i$向左、向右不出現重復數字的最大延伸長度了

#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;

typedef long long ll;
const int N=300005;

int n,k;
int a[N];

int root;
int ls[N],rs[N];
vector<int> v;

void Build()
{
    v.clear();
    for(int i=1;i<=n;i++)
    {
        int j=0;
        while(v.size() && a[v.back()]<a[i])
        {
            j=v.back();
            v.pop_back();
        }
        
        if(!v.size())
            root=i;
        else
            rs[v.back()]=i;
        
        ls[i]=j;
        v.push_back(i);
    }
}

int l[N],r[N];

void dfs(int x)
{
    l[x]=r[x]=x;
    if(ls[x])
        dfs(ls[x]),l[x]=l[ls[x]];
    if(rs[x])
        dfs(rs[x]),r[x]=r[rs[x]];
}

int cnt[N];
int L[N],R[N];

ll ans=0;

void Solve(int x)
{
    int lp=x-l[x],rp=r[x]-x;
    if(lp<rp)
    {
        for(int i=l[x];i<=x;i++)
            ans+=max(0,min(R[i],r[x])-max(a[x]-k+i-1,x)+1);
    }
    else
    {
        for(int i=x;i<=r[x];i++)
            ans+=max(0,min(k-a[x]+i+1,x)-max(L[i],l[x])+1);
    }
    
    if(ls[x])
        Solve(ls[x]);
    if(rs[x])
        Solve(rs[x]);
}

int main()
{
    int T;
    scanf("%d",&T);
    while(T--)
    {
        scanf("%d%d",&n,&k);
        ans=0;
        for(int i=1;i<=n;i++)
            ls[i]=rs[i]=0;
        
        for(int i=1;i<=n;i++)
            scanf("%d",&a[i]);
        
        Build();
        dfs(root);
        
        int j=1;
        for(int i=1;i<=n;i++)
        {
            cnt[a[i]]++;
            while(cnt[a[i]]>1)
            {
                R[j]=i-1;
                cnt[a[j]]--;
                j++;
            }
        }
        for(int i=j;i<=n;i++)
            R[i]=n,cnt[a[i]]--;
        
        j=n;
        for(int i=n;i>=1;i--)
        {
            cnt[a[i]]++;
            while(cnt[a[i]]>1)
            {
                L[j]=i+1;
                cnt[a[j]]--;
                j--;
            }
        }
        for(int i=j;i>=1;i--)
            L[i]=1,cnt[a[i]]--;
        
        Solve(root);
        
        printf("%lld\n",ans);
    }
    return 0;
}
View Code

 

標算是左偏樹,不過用笛卡爾樹+倍增也能搞過去:HDU 5575 ($Discover\ Water\ Tank$,$2015\ ACM/ICPC$上海)

首先可以根據隔板的高度,對於$n-1$個隔板建立一個大根笛卡爾樹

有了這棵笛卡爾樹,我們可以考慮利用它來划分出分治區間

比如,對於笛卡爾樹根節點對應原序列的位置$pos_{root}$,相當於將$1\text{~}n$的區間划分成兩部分$[1,pos_{root}],[pos_{root}+1,n]$,且每部分的水位最高都不超過$h[pos_{root}]$;其余節點的划分同理

我們先將每個查詢分配到划分樹上,具體方法是,先倍增出每個划分樹節點的父親關系,然后對於每個查詢$\{x,y,w\}$,從$[x,x]$對應的區間,向上找到樹上最深的 水位限制大於等於$y$的祖先,並將這個查詢扔到那個節點的vector中

於是考慮樹形dp

對於一個區間,要不將它整體灌滿,要不不灌滿、並向下遞歸;所以對於每個划分樹上的節點,分別記錄灌滿和不灌滿的 最多正確詢問數

注意$y$的邊界即可(我的處理是將每個$y++$)

#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;

struct Query
{
    int x,y,w;
    Query(int a,int b,int c)
    {
        x=a,y=b,w=c;
    }
};
inline bool operator <(Query A,Query B)
{
    return A.y<B.y;
}
inline bool operator >(Query A,Query B)
{
    return A.y>B.y;
}

const int INF=1<<30;
const int N=200005;
const int LOG=20;

int n,m;
int h[N];

struct Cartesian
{
    int root;
    int ls[N],rs[N];
    vector<int> v;
    
    void clear()
    {
        root=0;
        v.clear();
        for(int i=1;i<n;i++)
            ls[i]=rs[i]=0;
    }
    
    void build()
    {
        for(int i=1;i<n;i++)
        {
            int j=0;
            while(v.size() && h[v.back()]<h[i])
            {
                j=v.back();
                v.pop_back();
            }
            
            if(!v.size())
                root=i;
            else
                rs[v.back()]=i;
            
            ls[i]=j;
            v.push_back(i);
        }
    }
}tree;

int tot;
int lb[N],rb[N],lim[N];
int ls[N],rs[N];
int fa[N][LOG];

int place[N];

void Build(int x,int y,int l,int r,int f)
{
    lb[x]=l,rb[x]=r;
    fa[x][0]=f;
    if(l==r)
    {
        place[l]=x;
        return;
    }
    
    ls[x]=++tot;
    lim[tot]=h[y];
    Build(tot,tree.ls[y],l,y,x);
    
    rs[x]=++tot;
    lim[tot]=h[y];
    Build(tot,tree.rs[y],y+1,r,x);
}

int sum[N],sub[N];
vector<Query> v[N];

void dfs(int x)
{
    if(ls[x])
        dfs(ls[x]);
    if(rs[x])
        dfs(rs[x]);
    
    int empty=0,full=0;
    for(int i=0;i<v[x].size();i++)
    {
        Query tmp=v[x][i];
        if(tmp.w==0)
            empty++;
    }
    
    sub[x]=sub[ls[x]]+sub[rs[x]]+empty;
    for(int i=0;i<v[x].size();)
    {
        int j=i;
        while(j<v[x].size() && v[x][i].y==v[x][j].y)
        {
            Query tmp=v[x][j];
            if(tmp.w==0)
                empty--;
            if(tmp.w==1)
                full++;
            j++;
        }
        i=j;
        
        sub[x]=max(sub[x],sum[ls[x]]+sum[rs[x]]+full+empty);
    }
    sum[x]=sum[ls[x]]+sum[rs[x]]+full;
}

int main()
{
    int T;
    scanf("%d",&T);
    for(int kase=1;kase<=T;kase++)
    {
        scanf("%d%d",&n,&m);
        for(int i=1;i<n;i++)
            scanf("%d",&h[i]);
        
        tree.clear();
        tree.build();
        
        for(int i=1;i<=tot;i++)
        {
            v[i].clear();
            ls[i]=rs[i]=fa[i][0]=0;
            sum[i]=sub[i]=0;
        }
        tot=1;
        
        lim[1]=INF;
        Build(1,tree.root,1,n,0);
        
        for(int i=1;i<LOG;i++)
            for(int j=1;j<=tot;j++)
                fa[j][i]=fa[fa[j][i-1]][i-1];
        
        for(int i=1;i<=m;i++)
        {
            int x,y,w;
            scanf("%d%d%d",&x,&y,&w);
            Query tmp(x,++y,w);
            
            int p=place[x];
            for(int j=LOG-1;j>=0;j--)
                if(fa[p][j] && y>lim[fa[p][j]])
                    p=fa[p][j];
            if(y>lim[p])
                p=fa[p][0];
    
            v[p].push_back(tmp);
        }
        
        for(int i=1;i<=tot;i++)
            sort(v[i].begin(),v[i].end());
        
        dfs(1);
        printf("Case #%d: %d",kase,sub[1]);
        putchar('\n');
    }
    return 0;
}
View Code

 


 

一般都是銀牌題難度的樣子吧,平常見的不多,遇到再補充

Nowcoder 209390  (Sort the String Revision,2020牛客暑期多校第四場)

 

(完)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM