2020杭电多校9-Game——无旋treap

题目链接:

http://acm.hdu.edu.cn/showproblem.php?pid=6873

前置知识:

无旋treap和线段树

无旋treap推荐博客:https://www.luogu.com.cn/blog/85514/fhq-treap-xue-xi-bi-ji

线段树推荐博客:https://www.cnblogs.com/TheRoadToTheGold/p/6254255.html

题目大意:

 一共有n个位置,每个位置上都有一些方块。每次有两种操作:
1 x 询问位置x有多少个方块
2 x y 将位置x y的方块向左移一格,并问你有多少个方块被推动
所有方块会受力学的影响,即推动时,下方没有方块时会下降。

题解:

牛客多校10有一道差不多的题,不过那个题是个签到题,而这个题就不是这么好写了,而且还不好调!!!!

首先考虑怎么能够维护这两个操作,并且时间复杂度要是O(nlogn)。

比赛时一眼想到了可以用splay或者fhq treap来维护,但是因为时间原因就没来的及写。。。

我们可以用排名来表示位置,权值来表示方块个数,这样看起来就可以套无旋 treap的模板了。。。

第一个操作比较好处理,就是查询排名为x的方块数。

对于第二个操作我们可以这样维护:

假设第一个比高度yy小的位置为pos

那么这个操作就等同于以下操作:

①把pos位置的高度 加上 (pos+1的高度减去(yy-1))

②把x位置的高度 变成 (yy-1)

③把[pos+2,x]的高度平移到[pos+1,x-1]

怎么用无旋treap维护呢?

我们可以先求出pos位置和pos+1位置,并同时分割出[1,pos-1]区间和[pos+2,x]区间.

然后我们就可以直接合并[1,pos-1]区间和[pos-2,x]区间,这一步就实现了平移的操作(即③操作)

最后直接插入pos位置的高度和x位置的高度(即①操作和②操作)就行了。

因此我们就需要按权值分裂和按排名分裂,以及合并三个基础操作。

而且按权值分裂还需要稍微修改一下。

代码如下:

#include<bits/stdc++.h>
#define rp(i,s,t) for(int i=(s);i<=(t);i++)
using namespace std;
typedef long long ll;
inline int read(){
    int s=0,f=1;
    char ch=getchar();
    while(ch<'0'||ch>'9'){
        if(ch=='-') f=-1;
        ch=getchar();
    }
    while(ch>='0'&&ch<='9'){
        s=s*10+ch-'0';
        ch=getchar();
    }
    return s*f;
}
mt19937 rnd(time(0));
const int INF = 0x3f3f3f3f;
const int N = 600000 + 5;
struct Tree{
    int ls,rs;
    int sz,rnd,val;
    int min;
    ll sum;
}tree[N];
int cnt;
int x,y,z,root;
int l,r,l1,r1,l2,r2;
inline void init(){
    cnt=root=0;
    memset(tree,0,sizeof tree);
    tree[0].min=INF;
    tree[0].sum=0;
}
inline void pushup(int now){//线段树上传操作
    tree[now].sz=tree[tree[now].ls].sz+tree[tree[now].rs].sz+1;
    tree[now].min=min(min(tree[tree[now].ls].min,tree[tree[now].rs].min),tree[now].val);
    tree[now].sum=tree[tree[now].ls].sum+tree[tree[now].rs].sum+tree[now].val;
}
inline void split_val(int now,int v,int &x,int &y){
    if(!now) {
        x=y=0;
        return ;
    }
    if(tree[now].val<=v||tree[tree[now].rs].min<=v)//注意当右儿子的最小值小于等于v时,也可以把当前点以及左子树加到左区间虚拟树
        x=now,split_val(tree[now].rs,v,tree[now].rs,y);
    else 
        y=now,split_val(tree[now].ls,v,x,tree[now].ls);
    pushup(now);
}
inline void split_rank(int now,int k,int &x,int &y){
    if(!now){
        x=y=0;
        return ;
    }
    if(k<=tree[tree[now].ls].sz) y=now,split_rank(tree[now].ls,k,x,tree[now].ls);
    else x=now,split_rank(tree[now].rs,k-tree[tree[now].ls].sz-1,tree[now].rs,y);
    pushup(now);
}
inline int merge(int u,int v){
    if(!u||!v)return u|v;//如果有任意一个树为空就返回非空的一棵的根
    if(tree[u].rnd<tree[v].rnd){//如果u为当前根
        tree[u].rs=merge(tree[u].rs,v);//继续合并
        pushup(u);//上传
        return u;//返回根
    }
    else{//如果v为当前根
        tree[v].ls=merge(u,tree[v].ls);
        pushup(v);
        return v;
    }
}
inline int new_node(int a){//新建节点
    tree[++cnt].sz=1;
    tree[cnt].val=tree[cnt].min=tree[cnt].sum=a;tree[cnt].rnd=rand();
    return cnt;//此时的cnt代表的是新建节点
}
inline void ins(int p,int a){//插入值为a,排名为p的节点
    split_rank(root,p-1,x,y);
    root=merge(merge(x,new_node(a)),y);
}
inline int kth(int k){//求第k排名的值
    split_rank(root,k,x,y);
    split_rank(x,k-1,l,r);
    int val=tree[r].val;
    root=merge(merge(l,r),y);
    return val;
}
inline ll calc(int xx,int yy){//计算答案,这道题排名等同与位置
    /*
    假设第一个比高度yy小的位置为pos
    那么这个操作就等同于以下操作:
    ①把pos位置的高度 加上 (pos+1的高度减去(yy-1))
    ②把x位置的高度 变成 (yy-1)
    ③把[pos+2,x]的高度平移到[pos+1,x-1]

    怎么用无旋treap维护呢?
    我们可以先求出pos位置和pos+1位置,并同时分割出[1,pos-1]区间和[pos+2,x]区间.
    然后我们就可以直接合并[1,pos-1]区间和[pos-2,x]区间,这一步就实现了平移的操作(即③操作)
    最后直接插入pos位置的高度和x位置的高度就行了。
    */
    if(kth(xx)<yy) return 0*1ll;//当第x排名的值小于yy时(即第x位置的高度小于yy)
    int ls,rs;split_rank(root,xx,ls,rs);//通过排名分裂把总区间分成排名为[1,x](即 ls)和[x+1,y](即 rs)
    if(tree[ls].min>=yy){//当分裂后的左区间树的最小值也大于等于yy,即[1,x]区间没有比yy小的
        root=merge(ls,rs);
        return 0*1ll;
    }
    split_val(ls,yy-1,l,r);//通过权值排名再把[1,x]区间按照权值(即高度)分裂为[min,yy-1](即 l 子树)和[yy,max](即 r 子树)
    ll ans=tree[r].sum-1ll*tree[r].sz*(yy-1);//移动的方块树为r子树的方块和减去r子树的大小乘以(yy-1),这个不难想到
    int pos=tree[l].sz;//pos用来第一个比高度yy小的位置
    split_rank(r,1,l2,r2);//把r子树分裂成[pos+1,pos+1](l2)区间和[pos+2,x]区间(r2)
    split_rank(l,pos-1,l1,r1);//把r子树分裂成[1,pos-1](l1)区间和[pos,pos]区间(r1)
    tmp=tree[r1].val+tree[l2].val-yy+1;//预先求出①操作的pos位置的最终权值
    root=merge(merge(l1,r2),rs);//对应上面的③操作
    ins(pos,tmp);ins(xx,yy-1);//对应上面的①和②操作操作
    return ans;
}
vector<int> v;
void dfs(int now){
    if(tree[now].ls) dfs(tree[now].ls);
    v.push_back(tree[now].val);
    if(tree[now].rs) dfs(tree[now].rs);
}
int main() {
    int T=read();
    while(T--){
        int n=read(),q=read();
        init();v.clear();
        rp(i,1,n){
            int vv=read();
            ins(i,vv);
        }
        while(q--){
            int opt=read();
            if(opt==1) {
                x=read(),y=read();
                printf("%lld\n",calc(x,y));
            }
            else{
                x=read();
                printf("%d\n",kth(x));
            }
        }
        dfs(root);
        rp(i,0,n-1) printf("%d%c",v[i],i==n-1?'\n':' ');
    }
    return 0;
}
/*
1
8 4
2 1 1 4 4 6 2 3
1 6 4
2 5
1 1 1
1 8 2
*/

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值