笛卡尔树学习笔记

听上去有丶厉害,实际也很巧妙

学习了这两篇:ReMoon - 单调栈的应用 --- 笛卡尔树与虚树

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


~ 简介 ~

虽然名字中带有“树”,但是笛卡尔树其实是对于一个序列的转化,并通过这个转化获得更多此序列的信息

对于一个简单的序列:$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

(待续)

猜你喜欢

转载自www.cnblogs.com/LiuRunky/p/Cartesian_Tree.html