• [Noip2017]列队


    [Noip2017]列队

    一.前言

    ​ 咱高一是不是也要军训啊,害怕 qwq……题目链接

    二.思路

    ​ 这篇题解使用的是平衡树(FHQ),树状数组不会写也……

    ​ 首先通过简单的 模拟/手玩 可以大概的知道,对于位置 ((x,y)),移动它会影响的只有 ((x,y+1))((x,m)) 全部左移,以及 ((x+1,m))((n,m)) 全部上移,最后在右下角插回去。

    ​ 观察到 11~16 的测试点给的是全部 (x=1),从局部入手获得灵感。在这种情况下,只需要维护第一行和最后一列就行。对于第一行,操作一共有两个:删除第 k 个数,在末尾插入一个数 u。具体的来说,若是 ((1,i)) 离开,那么对于第一行会删掉第 i 个数,在最后插入 ((2,m)),对于最后一列会删掉第 1 个数,在末尾插入 ((1,i))

    ​ 从中我们可以找到几个共同点:

    • 都是删掉处在位置 k 的一个数
    • 都是在末尾插入一个数

    这里若是魔改一下,改成对于行只维护 1~m-1 就会方便很多。此时同时对第一行与最后一列进行维护就可以了。不失一般性,将 1 改成 x ,对于每一行都维护就行。

    这里的维护我们需要用到平衡树(杀鸡用牛刀qwq),接下来只是按我笨拙的想法讲一下原理,不是真正的算法,有点套用替罪羊的建树思想,手中已有一个数列,把他拉成一个平衡树……见图

    此时(虽然不怎么像树),但是通过中序遍历是可以将整个数组都找到。在这个时候,给每个点一个随机值,使得在不违背遍历顺序的基础上以这个随机值进行旋转。见图。

    (彩色的是随机值)这样中序遍历的顺序还是一样的(大概)但是很有效的将树给"压扁"了,使得每次的查询时间大幅缩减。对于每个点维护一个 (size) 表示子树大小,就能快速找到第 k 个数了。

    ​ 然后再说如何基于 FHQ 来操作,FHQ 的基操 merge 与 split 是重点,但是也不难(不会的先学了来)。merge的时候就按照随机值merge就好,给出代码。

    int merge(int x,int y){//以 y 为根的子树要比 x 后访问
    	if(!x||!y)return x+y;
    	if(rad[x]<rad[y]){//按随机值,这里规则可以随便搞其实hhhh都可以A
    		ch[x][1]=merge(ch[x][1],y);
    		update(x);//维护size
    		return x;
    	}
    	else {
    		ch[y][0]=merge(x,ch[y][0]);
    		update(y);
    		return y;
    	}
    }
    

    然后这题的split稍稍有点复杂……首先由数据规模可以得出两个结论:

    • 每个点都开,空间开不起
    • 由于操作数远小于点数,会有一大片是连续的

    于是我们可以选择将连续的看作是一个点,记录这个点的长度(总共包括几个点),以及开头的值(就可以计算出所有值),当这个点中的其中一个位置需要操作时,把这个点撕开成该位置前与后再插回去就行。

    ​ 于是 split 要分出三棵树:在 k 位置之前的,(包含)k 位置(可能是一个点,也可能是连续一大片),k位置之后。给出代码。

    void split(int x,int k,int &a,int &b,int &c){
    	if(!x)a=b=c=0;
    	else{
    		if(size[ch[x][0]]>=k)c=x,split(ch[x][0],k,a,b,ch[x][0]);
            //就在左子树里面,于是进左子树找,归还一个只留>k的部分的左子树
    		else{
    			k-=size[ch[x][0]];//直接排除左子树,减去消耗
    			if(k<=len[x]){//正好就是该节点本身
    				b=x;
    				a=ch[x][0];
    				c=ch[x][1];
    				ch[x][0]=ch[x][1]=0;
    			}
    			else{//进右子树寻找,还一个只有<k 的右子树
    				a=x;
    				k-=len[x];//减去消耗
    				split(ch[x][1],k,ch[x][1],b,c);
    			}
    		}
    		update(x);
    	}
    }
    

    剩下就是主体操作,要注意操作第二维为 m 的情况,特判一下。代码会详细解释

    for(int i=1;i<=n;++i){
    		root[i]=add(1LL*(i-1)*m+1,m-1);//直接插入一行
    		root[0]=merge(root[0],add(1LL*i*m,1));//维护一下最后一列
    	}
    	for(int i=1,x,y;i<=q;++i){
    		x=read();y=read();
    		if(y==m){//特判
    			int a,b,c;
    			split(root[0],x,a,b,c);//取出来
    			printf("%lld
    ",start[b]);
    			root[0]=merge(merge(a,c),b);//塞在最后
    		}
    		else{
    			int a,b,c;
    			split(root[0],x,a,b,c);//在最后一列取出来
    			root[0]=merge(a,c);//合并了
    			root[x]=merge(root[x],b);//反手先塞进去
    			split(root[x],y,a,b,c);//取出要输出的那个
    			y-=size[a];//因为有可能取出来连续的一串,需要知道是这一串的第几个
    			printf("%lld
    ",start[b]+y-1);//输出了
    			root[0]=merge(root[0],add(start[b]+y-1,1));//塞进最后一列末尾
    			root[x]=
    			merge(merge(a,merge(add(start[b],y-1),add(start[b]+y,len[b]-y))),c);
    			//撕开成两半,塞进去
            }
    	}
    
  • 相关阅读:
    2014 12 27 bestcoder 第一题
    大数阶乘问题!
    学校acm比赛题
    hdu1002 大数相加问题
    写于创建之初
    Linux指令 vi编辑,保存及退出
    Java基础-反射
    Java基础-接口与多态
    00023-layui表单有check、radio、switch,未选中时值不提交的解决
    00022-layui 显示表单,iframe父子页面传值
  • 原文地址:https://www.cnblogs.com/clockwhite/p/13505532.html
Copyright © 2020-2023  润新知