笛卡尔树

 

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

学习了这两篇: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,i1],[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 ~ n的顺序依次加入节点并调整树的结构,使得当前的树为子序列[1,i]所构成的笛卡尔树

由于pos满足二叉排序树,而i在区间[1,i]pos最大,所以i插入的位置为 序列[1,i1]所构成的笛卡尔树的根节点 一直向右儿子走、直到走到了空节点

这样插入后,ipos已经满足要求了,但是val却不一定满足堆

于是考虑怎么调整当前的树

ival不满足要求,即存在某(些)祖先j,使得j为根的子树中val全大于val[i];显然我们需要通过调整i的位置,使得i成为j的祖先

是这样操作的:

   0. 刚刚插入完成后,可能树是这样的

    

   1. 将i向上一层移动;这时由于pos[k]<pos[i],所以k成为i的左儿子,k依然是k的左儿子

   

   2. 继续将i向上一层移动,相似的,j也应当属于i的左子树;不妨让ji的左儿子,kj的右儿子(此时有多种调整方法;但当我们使用这种调整方法时,j,k,k相互间与原来的连边相同

   

以上的调整操作都是在[1,i1]序列构成的笛卡尔树的最右链(即从根节点一直向右儿子走的这条路径)上进行的

在处理完后,我们对比一下调整前后的树结构,发现只有很少的地方出现了变化:

   1. k的右儿子变成了空节点

   2. j的父亲变成了i,且ji的左儿子

   3. i继承了原来j的父亲

事实上,即使ij的路径很长很长,一共也只有这三个地方发生了变化,所以我们的调整不是很复杂

现在最大的问题变成,如何找到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]的最大值ai,那么i即为笛卡尔树中 此区间对应子树的根节点

于是将区间分成两部分[l,i1],[i+1,r]的操作,就可以转化成笛卡尔树上的分治

同时,这个题解将“统计[l,r]aix的数量”这个主席树问题,离线后通过拆分转化为树状数组问题,设计十分巧妙

复制代码
#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)

由于需要对al,...,ar求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上海)

首先可以根据隔板的高度,对于n1个隔板建立一个大根笛卡尔树

有了这棵笛卡尔树,我们可以考虑利用它来划分出分治区间

比如,对于笛卡尔树根节点对应原序列的位置posroot,相当于将1~n的区间划分成两部分[1,posroot],[posroot+1,n],且每部分的水位最高都不超过h[posroot];其余节点的划分同理

我们先将每个查询分配到划分树上,具体方法是,先倍增出每个划分树节点的父亲关系,然后对于每个查询{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牛客暑期多校第四场)

 

Yandex Contest 24597H  (Longest Loose Segment,2021 PTZ Camp Day4)

复制代码
Time limit: 2 seconds
Memory limit: 256Mb

A list A is called loose if max(A) + min(A) > len(A).
Today Rikka got a list A of length n. She wants to find the longest segment [l,r] in A
such that list [A_l,A_{l+1},...,A_r] is loose.
Rikka will make m turns with list A. On each turn, Rikka will perform one or more given
operations in sequence. Each operation is swapping two elements in list A. Your task is
to calculate the length of the longest loose segment of A and the resulting list
after each turn.
Note that the operations on turn i are performed on the list that was the result of
turn (i-1).

Input
The first line contains two integers n and m (1<=n<=10^6 and 1<=m<=30).
The second line contains n integers A_i(-10^6<=A_i<=10^6) that consitute the
initial list A.
Then follow m descriptions of the turns. For each turn, the first line contains a single
integer k (1<=k<=10^6), the number of swaps. Then k lines follow: each of them
contains two integers u_i and v_i (1<=u_i,v_i<=n and u_i≠v_i) such that Rikka
will swap A_{u_i} and A_{v_i} in this operation.
It is guaranteed that Sum_{k}<=10^6.

Output
On the first line, output a single integer: the length of the longest loose segment of A.
Then output m lines. On each of them, print a single integer: the length of the longest
loose segment of the resulting list after each turn.

Example
standard input
5 2
1 2 -2 3 4
1
2 3
1
1 2

standard output
2
3
4
题面(原链接非公开)
复制代码

在笛卡尔树上用类似线段树的方法在每个节点维护多个值、父节点的值通过子节点合并得到确实是未曾设想的道路...不过想来也很有道理:反正每个节点最多只有两个儿子,并且只要保证每次都完整建树并完整计算一遍(即不能像线段树一样支持修改),那么该做法就是线性的。

m轮中,每一轮都重新建立一棵笛卡尔树并进行dp。在每个笛卡尔树的节点x上,除了建树得到的ls[x],rs[x],再额外维护len[x](最长Loose Segment的长度)、sz[x](子树大小,即所包含的区间长度)、mx[x](子树中最大的a[i]值)。其中sz[x],mx[x]的维护是显然的,而len[x]的维护需要一点对于Loose Segment性质的简化。

若在当前区间中最长Loose Segment不经过x,那么一定为max(len[ls],len[rs]);否则最长Loose Segment的最大a[i]的值一定为max(mx[ls],mx[rs]),因为区间最小值已经确定为a[x],而区间最大值显然越大越好(从而使得理论长度mx+mn1最大)。设mx[ls]>mx[rs],那么左区间能够延伸到x的位置相当于要求a[x]+mx[ls]1sz[ls],这是因为若a[x]+mx[ls]1<sz[ls],那么sz[ls]len[ls]>a[x]+mx[ls]1,则最长Loose Segment一定不如左区间中的长。若左区间能延伸到x,则有len[x]=max(a[x]+mx[ls]1,sz[x])

复制代码
#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;

const int oo=1<<30;
const int N=1000005;

//a的下标从1到n 
struct Cartesian
{
    int root;
    int ls[N],rs[N];
    vector<int> v;
    
    void clear(int n)
    {
        root=0;
        v.clear();
        for(int i=1;i<=n;i++)
            ls[i]=rs[i]=0;
    }
    
    void build(int *a,int n)
    {
        clear(n);
        
        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);
        }
    }
}t;

int n,m;
int a[N];

int ans;
int len[N],sz[N],mx[N];

inline void dfs(int x)
{
    int ls=t.ls[x],rs=t.rs[x];
    if(ls)
        dfs(ls);
    if(rs)
        dfs(rs);
    
    sz[x]=sz[ls]+sz[rs]+1;
    mx[x]=a[x];
    len[x]=max(a[x]>0?1:0,max(len[ls],len[rs]));
    
    if(mx[ls]<mx[rs])
        swap(ls,rs);
    if(ls)
    {
        mx[x]=max(mx[x],mx[ls]);
        if(a[x]+mx[x]-1>sz[ls])
            len[x]=min(a[x]+mx[x]-1,sz[x]);
    }
    
    ans=max(ans,len[x]);
}

int solve()
{
    t.build(a,n);
    
    ans=0;
    dfs(t.root);
    return ans;
}

int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    
    mx[0]=-oo;
    printf("%d\n",solve());
    
    while(m--)
    {
        int k,x,y;
        scanf("%d",&k);
        while(k--)
        {
            scanf("%d%d",&x,&y);
            swap(a[x],a[y]);
        }
        
        printf("%d\n",solve());
    }
    return 0;
}
View Code
复制代码

 

ICPC 2020 EC-Final B

 

(完)

posted @   LiuRunky  阅读(5750)  评论(0编辑  收藏  举报
编辑推荐:
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示