1. 程式人生 > 實用技巧 >[Noip2017]列隊

[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\n",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\n",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);
			//撕開成兩半,塞進去
        }
	}