[Noip2017]列隊
[Noip2017]列隊
一.前言
咱高一是不是也要軍訓啊,害怕 qwq……題目連結
二.思路
這篇題解使用的是平衡樹(FHQ),樹狀陣列不會寫也……
首先通過簡單的 模擬/手玩 可以大概的知道,對於位置 \((x,y)\),移動它會影響的只有 \((x,y+1)\) 到 \((x,m)\) 全部左移,以及 \((x+1,m)\) 到 \((n,m)\) 全部上移,最後在右下角插回去。
觀察到 11~16 的測試點給的是全部 \(x=1\),從區域性入手獲得靈感。在這種情況下,只需要維護第一行和最後一列就行。對於第一行,操作一共有兩個:刪除第 k 個數,在末尾插入一個數 u。具體的來說,若是 \((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);
//撕開成兩半,塞進去
}
}