Update:原來的洛谷U21715已成坑qwq
已經被某位管理員巨佬放進公共題庫啦!又可以多一個AC記錄啦!
其實也可以到這里交啦
思路分析
動態維護樹的重心
題目中說到國家的首都會選在某個使得其他城市到它距離之和最小的城市,那不就是樹的重心了嘛。樹的重心性質真的很好,看看wuhulala巨佬的這篇博客。
網上大多數解法都是啟發式合並。利用了以重心為根的子樹大小不超過原樹的一半,每次合並兩顆樹的時候,小的往大的上面合並,而且是一個點一個點地link上去,每次link完檢查一下這個子樹如果超過了當前整個樹大小的一半,就把重心向當前點移動一下。這樣做,合並次數上限\(O(N\log N)\),每次更新重心也需要\(O(\log N)\)的復雜度,所以復雜度極限是\(O(N\log^2N)\),還不夠優秀。
因此我就想再優化一下算法。復雜度瓶頸就在於一個一個link,能不能考慮直接連接兩顆樹對重心的影響呢?
答案是肯定的。又要用到一個性質——連接兩顆樹后,新的重心一定在原來的兩個重心的路徑上。這個證明不難,反證一下就好了:對於在路徑上的點來說,它的最大子樹一定是往原來兩個重心方向上的那兩個子樹的其中一個,它的其它子樹肯定比這個最大子樹要小,不然重心就不會是那兩個點了;而對於不在路徑上的點,它的最大子樹是往原來兩個重心方向上的那個子樹,這顯然劣於在路徑上的點。
那就可以直接link然后把鏈提出來去找了。我們當然不能把整條鏈全找遍了,畢竟重心的性質那么多,也要好好利用嘛。
我們可以這樣形象地理解:提出來的鏈就是一個序列,每個點都帶了一些若干大小的虛子樹。把子樹大小和看成序列的權值,而最接近中位數的地方就是重心了。
(下面s和si的定義和蒟蒻的LCT總結中是一樣的)
具體找法:類似樹上二分,我們需要不斷逼近樹的重心的位置。記下lsum表示當前鏈中搜索區間左端點以左的子樹大小,rsum表示右端點以右的。x的整個子樹就表示了當前搜索區間,在中序遍歷中x把搜索區間分成了左右兩塊(在Splay中對應x的左子樹和右子樹)。
如果x左子樹的s加上lsum和x右子樹的s加上rsum(對應原樹中x往原來兩個重心方向上的兩個子樹大小)都不超過新樹總大小的一半,那么x當然就是重心啦!之前已經提到x其它的子樹大小就不用考慮了。當然,如果總大小是奇數,重心只會有一個,那就找到了。否則,因為必須編號最小,所以還要繼續找下去。
當我們沒有確定答案時,還要繼續找下去,那么就要跳兒子了。x把整個鏈分成了左右兩個部分,而重心顯然會在大小更大的一部分中,這個也應該好證明。如果左邊比右邊小,那就跳右兒子繼續找。這時候當前搜索區間減小了,搜索區間以外的部分增大了,lsum應該加上si[x]+1。反之亦然。如果跳進了空兒子,那肯定所有情況都考慮完了,直接結束查找。
當然,重心找到了就還是要伸展一下,保證復雜度。(蒟蒻造數據的時候懶得去卡了qwq)
這一部分套用Splay的復雜度,是均攤\(O(\log N)\)的,總復雜度也就降到了\(O(N\log N)\)。
findroot實在很慢,於是可以寫個並查集來維護每個點所在樹的重心。
#include<cstdio>
#include<cstdlib>
#define R register int
#define I inline void
const int N=100009,INF=2147483647;
int f[N],c[N][2],si[N],s[N],h[N];
bool r[N];
#define lc c[x][0]
#define rc c[x][1]
inline bool nroot(R x){return c[f[x]][0]==x||c[f[x]][1]==x;}
I pushup(R x){
s[x]=s[lc]+s[rc]+si[x]+1;
}
I pushdown(R x){
if(r[x]){
R t=lc;lc=rc;rc=t;
r[lc]^=1;r[rc]^=1;r[x]=0;
}
}
I pushall(R x){
if(nroot(x))pushall(f[x]);
pushdown(x);
}
I rotate(R x){
R y=f[x],z=f[y],k=c[y][1]==x,w=c[x][!k];
if(nroot(y))c[z][c[z][1]==y]=x;
f[f[f[c[c[x][!k]=y][k]=w]=y]=x]=z;pushup(y);//為三行rotate打call
}
I splay(R x){
pushall(x);
R y;
while(nroot(x)){
if(nroot(y=f[x]))rotate((c[f[y]][0]==y)^(c[y][0]==x)?x:y);
rotate(x);
}
pushup(x);
}
I access(R x){
for(R y=0;x;x=f[y=x]){
splay(x);
si[x]+=s[rc];
si[x]-=s[rc=y];
pushup(x);
}
}
I makeroot(R x){
access(x);splay(x);
r[x]^=1;
}
I split(R x,R y){
makeroot(x);
access(y);splay(y);
}
I link(R x,R y){
split(x,y);
si[f[x]=y]+=s[x];
pushup(y);
}
int geth(R x){
if(h[x]==x)return x;
return h[x]=geth(h[x]);
}
inline int update(R x){
R l,r,ji=s[x]&1,sum=s[x]>>1,lsum=0,rsum=0,newp=INF,nowl,nowr;
while(x){
pushdown(x);//注意pushdown
nowl=s[l=lc]+lsum;nowr=s[r=rc]+rsum;
if(nowl<=sum&&nowr<=sum){
if(ji){newp=x;break;}//剪枝,確定已經直接找到
else if(newp>x)newp=x;//選編號最小的
}
if(nowl<nowr)lsum+=s[l]+si[x]+1,x=r;
else rsum+=s[r]+si[x]+1,x=l;//縮小搜索區間
}
splay(newp);//保證復雜度
return newp;
}
#define G ch=getchar()
#define gc G;while(ch<'-')G
#define in(z) gc;z=ch&15;G;while(ch>'-')z*=10,z+=ch&15,G;
int main(){
register char ch;
R n,m,x,y,z,Xor=0;
in(n);in(m);
for(R i=1;i<=n;++i)s[i]=1,h[i]=i,Xor^=i;
while(m--){
gc;
switch(ch){
case 'A':in(x);in(y);link(x,y);
split(x=geth(x),y=geth(y));//提出原重心路徑
z=update(y);
Xor=Xor^x^y^z;
h[x]=h[y]=h[z]=z;//並查集維護好
break;
case 'Q':in(x);printf("%d\n",geth(x));break;
case 'X':gc;gc;printf("%d\n",Xor);
}
}
return 0;
}