前天的多校題。朝鮮就是毒瘤,簽到題都這么難/kk
HDOJ題目頁面傳送門
給定一個無向圖\(G=(V,E),|V|=n,|E|=m\),每個點有點權\(a_i\),設\(S_i=\{a_j\mid (i,j)\in E\}\),支持\(2\)種\(q\)次操作:
- \(\texttt1\ x\ y\):令\(a_x=y\);
- \(\texttt2\ x\):求\(\mathrm{mex}(S_x)\)。
\(n,m\in\left[1,10^5\right],a_i\in\left[0,10^9\right]\)。本題多測,最多有\(10\)組數據。\(\mathrm{TL}=3\mathrm s\)。
以下認為\(n,m\)同階,復雜度里用\(n\)表示。
對於無向圖上的詢問題,一般有一個套路:根號分治(還是聽yjz講課才會的)。考慮設一個界限\(lim\),將度數\(\leq lim\)的都設為小點,其他都是大點。不難發現,小點度數上限是\(\mathrm O(lim)\),大點個數上限是\(\mathrm O\!\left(\dfrac n{lim}\right)\)(因為\(\sum\limits_{i=1}^n|\delta(i)|=2m\)),這是兩個很好的性質。
於是分成四個需要思考的問題:小點修改,小點查詢,大點修改,大點查詢。
首先有一條引理:\(\mathrm{mex}(S)=\mathrm O(|S|)\)。證明很容易,當\(S=\{0,1,2,\cdots\}\)時會把\(\mathrm{mex}(S)\)卡最大。注意到對於小點查詢,小點\(x\)的度數不會太大,那么\(\mathrm{mex}(S_x)\)也不會太大,是\(\mathrm O(lim)\)級別的,考慮不用對它維護任何東西,直接暴力查。至於咋暴力,在外面開一個全局的\([0,n]\)上的bool
數組(點權超過\(n\)可以直接當作\(n\)看,正確性顯然),每次小點查詢將鄰居點權賦上true
,然后從頭遍歷,查完撤銷影響。
那么對應的,大點查詢的話我們就需要維護一些東西來實現快速查詢;而無論是小點修改還是大點修改,都可以修改所有鄰居大點(這樣的點數量是\(\mathrm O\!\left(\dfrac m{lim}\right)\)級別的)所維護的數據結構。
看到在集合里查東西,很容易想到平衡樹,這里使用fhq-Treap。這樣,改點權的時候是平衡樹正常操作(單點插入+單點刪除),\(\mathrm O(\log n)\);查\(\mathrm{mex}\)的時候,可以用類似二分的思想,在當前節點時,如果左子樹是滿的(即開頭到內部到當前節點之間沒有一點空隙),就往右子樹走,否則往左子樹走,時間復雜度與樹高成正比,\(\mathrm O(\log n)\)。
下面來整理一下各操作的復雜度:
- 小點修改:平衡樹插入+刪除,\(\mathrm O\!\left(\dfrac n{lim}\log n\right)\);
- 小點查詢:暴力,\(\mathrm O(lim)\);
- 大點修改:平衡樹插入+刪除,\(\mathrm O\!\left(\dfrac n{lim}\log n\right)\);
- 大點查詢:平衡樹查詢,\(\mathrm O(\log n)\)。
時間復雜度就是\(\mathrm O\!\left(q\left(\dfrac n{lim}\log n+lim\right)\right)\)。根據均值不等式,當\(lim\)取\(\sqrt{m\log n}\)的時候最優,為\(\mathrm O\!\left(n\sqrt{n\log n}\right)\)。
現場覺得這個復雜度有點懸,常數又大,寫好之后交上去,果然T了!這比WA還要狠。后來事實證明不是復雜度/常數的問題,是寫掛了。之后TLE->RE->WA->AC,太不容易了……
代碼(現場rush的,還有sjc在旁邊不停啰嗦,有點丑,見諒):
#include<bits/stdc++.h>
using namespace std;
#define mp make_pair
#define X first
#define Y second
#define pb push_back
void read(int &x){
x=0;char c=getchar();
while(!isdigit(c))c=getchar();
while(isdigit(c))x=(x<<1)+(x<<3)+(c^48),c=getchar();
}
void prt(int x){
if(x>9)prt(x/10);
putchar(x%10^48);
}
mt19937 rng(20060617);
const int N=100000;
int n,m,qu;
int a[N+1];
vector<int> nei[N+1];
vector<int> nei_big[N+1];
struct fhq_treap{//平衡樹
int sz,root;
struct node{unsigned key;int lson,rson,sz,v,mx;}nd[2*N+1];
#define key(p) nd[p].key
#define lson(p) nd[p].lson
#define rson(p) nd[p].rson
#define sz(p) nd[p].sz
#define v(p) nd[p].v
#define mx(p) nd[p].mx
void init(){
sz=root=0;
nd[0]=node({0,0,0,0,0,0});
}
void sprup(int p){sz(p)=sz(lson(p))+1+sz(rson(p));mx(p)=max(mx(lson(p)),max(v(p),mx(rson(p))));}
pair<int,int> split(int x,int p=-1){~p||(p=root);
if(!x)return mp(0,p);
pair<int,int> sp;
if(x<=sz(lson(p)))return sp=split(x,lson(p)),lson(p)=sp.Y,sprup(p),mp(sp.X,p);
return sp=split(x-1-sz(lson(p)),rson(p)),rson(p)=sp.X,sprup(p),mp(p,sp.Y);
}
int mrg(int p,int q){
if(!p||!q)return p|q;
if(key(p)<key(q))return rson(p)=mrg(rson(p),q),sprup(p),p;
return lson(q)=mrg(p,lson(q)),sprup(q),q;
}
int lss(int v,int p=-1){~p||(p=root);
if(!p)return 0;
if(v(p)<v)return sz(lson(p))+1+lss(v,rson(p));
return lss(v,lson(p));
}
int nwnd(int v){return nd[++sz]=node({rng(),0,0,1,v,v}),sz;}
void insert(int v){
// cout<<"insert "<<v<<"\n";
pair<int,int> sp=split(lss(v));
root=mrg(mrg(sp.X,nwnd(v)),sp.Y);
}
void del(int v){
// cout<<"del "<<v<<"\n";
pair<int,int> sp=split(lss(v)),sp0=split(1,sp.Y);
root=mrg(sp.X,sp0.Y);
}
int mex(int now=0,int p=-1){~p||(p=root);
// printf("mex %d %d\n",now,v(p));
if(!p)return now;
if(v(p)-now==sz(lson(p)))return mex(v(p)+1,rson(p));
return mex(now,lson(p));
}
}trp[100];
int hav[100][N+1];
int big_id[N+1];
bool tmp[N+1];
void mian(){
read(n);read(m);
for(int i=1;i<=n;i++)nei[i].clear();
for(int i=1;i<=n;i++)read(a[i]),a[i]=min(n,a[i]);
for(int i=1;i<=m;i++){
int x,y;
read(x);read(y);
nei[x].pb(y);nei[y].pb(x);
}
int lim=sqrt(m*log2(n));
for(int i=1;i<=n;i++){
nei_big[i].clear();
for(int j=0;j<nei[i].size();j++)
if(nei[nei[i][j]].size()>lim)nei_big[i].pb(nei[i][j]);
}
int now=0;
for(int i=1;i<=n;i++)if(nei[i].size()>lim){
trp[big_id[i]=now++].init();
for(int j=0;j<=n;j++)hav[big_id[i]][j]=0;
for(int j=0;j<nei[i].size();j++){
if(!hav[big_id[i]][a[nei[i][j]]])trp[big_id[i]].insert(a[nei[i][j]]);
hav[big_id[i]][a[nei[i][j]]]++;
// cout<<big_id[i]<<"!\n";
}
}
read(qu);
while(qu--){
int tp,x,y;
read(tp);read(x);
if(tp==1){//修改
read(y);
y=min(n,y);
for(int i=0;i<nei_big[x].size();i++){
int z=nei_big[x][i];
hav[big_id[z]][a[x]]--;
// cout<<big_id[z]<<"!\n";
if(!hav[big_id[z]][a[x]])trp[big_id[z]].del(a[x]);
if(!hav[big_id[z]][y])trp[big_id[z]].insert(y);
hav[big_id[z]][y]++;
}
a[x]=y;
}
else{
if(nei[x].size()<=lim){//小點查詢
for(int i=0;i<nei[x].size();i++)tmp[a[nei[x][i]]]=true;
for(int i=0;;i++)if(!tmp[i]){prt(i);putchar('\n');break;}
for(int i=0;i<nei[x].size();i++)tmp[a[nei[x][i]]]=false;
}
else prt(trp[big_id[x]].mex()),putchar('\n');//大點查詢
}
}
}
int main(){
int testnum;
cin>>testnum;
while(testnum--)mian();
return 0;
}
后來看到隔壁dls的題解,發現竟然還有\(\mathrm O(n\sqrt n)\)的算法………………
回顧上面整理的各操作的復雜度,發現一個現象,平衡樹查詢的次數只有\(\mathrm O(q)\),而平衡樹插入+刪除的次數有\(\mathrm O(q\dfrac n{lim})\),這兩個操作復雜度卻一樣。那我們是不是可以犧牲一下查詢的復雜度,來實現插入+刪除復雜度的更優呢?飢渴的人們想要把插入+刪除弄成\(\mathrm O(1)\),而能有操作復雜度為\(\mathrm O(1)\)的數據結構並不多,比較典型的就是萬能的分塊。
考慮基於值域分塊,每塊\(sz1\)個數,既然要\(\mathrm O(1)\),插入+刪除的時候只能直接改一下就得走人;而對於查詢,考慮找到第一個不滿的塊,再在里面暴力找,這樣看來,我們得維護每一塊內有的元素的個數,為了實現這個的維護,還得維護一個bool
數組,這些都是可以\(\mathrm O(1)\)修改的,於是查詢復雜度\(\mathrm O\!\left(sz1+\dfrac n{sz1}\right)\)。為了最優,\(sz1\)取\(\sqrt n\)。此時不難發現,總復雜度已經優化到\(\mathrm O(n\sqrt n)\)了……
又是根號分治又是分塊,這不是lxl的作風么/jk
代碼(在原來代碼上魔改的):
#include<bits/stdc++.h>
using namespace std;
#define mp make_pair
#define X first
#define Y second
#define pb push_back
void read(int &x){
x=0;char c=getchar();
while(!isdigit(c))c=getchar();
while(isdigit(c))x=(x<<1)+(x<<3)+(c^48),c=getchar();
}
void prt(int x){
if(x>9)prt(x/10);
putchar(x%10^48);
}
mt19937 rng(20060617);
const int N=100000,DB_SZ1=333,DB_SZ=DB_SZ1;
int n,m,qu;
int a[N+1];
vector<int> nei[N+1];
vector<int> nei_big[N+1];
struct dvdblk{//分塊
int sz,sz1;
struct block{int l,r,sz;bool hav[DB_SZ1];}blk[DB_SZ];
#define l(p) blk[p].l
#define r(p) blk[p].r
#define sz(p) blk[p].sz
#define hav(p) blk[p].hav
void bldblk(int p,int l,int r){
l(p)=l;r(p)=r;sz(p)=0;memset(hav(p),0,sizeof(hav(p)));
}
void init(){
sz1=sqrt(n);
sz=(n+sz1)/sz1;
for(int i=1;i<=sz;i++)bldblk(i,(i-1)*sz1,min(n,i*sz1-1));
}
void insert(int v){
int p=(v+sz1)/sz1;
hav(p)[v-l(p)]=true;
sz(p)++;
}
void del(int v){
int p=(v+sz1)/sz1;
hav(p)[v-l(p)]=false;
sz(p)--;
}
int mex(){
for(int i=1;i<=sz1;i++)if(sz(i)<r(i)-l(i)+1){
for(int j=l(i);;j++)if(!hav(i)[j-l(i)])return j;
}
}
}db[333];
int hav[333][N+1];
int big_id[N+1];
bool tmp[N+1];
void mian(){
read(n);read(m);
for(int i=1;i<=n;i++)nei[i].clear();
for(int i=1;i<=n;i++)read(a[i]),a[i]=min(n,a[i]);
for(int i=1;i<=m;i++){
int x,y;
read(x);read(y);
nei[x].pb(y);nei[y].pb(x);
}
int lim=sqrt(m);
for(int i=1;i<=n;i++){
nei_big[i].clear();
for(int j=0;j<nei[i].size();j++)
if(nei[nei[i][j]].size()>lim)nei_big[i].pb(nei[i][j]);
}
int now=0;
for(int i=1;i<=n;i++)if(nei[i].size()>lim){
db[big_id[i]=now++].init();
for(int j=0;j<=n;j++)hav[big_id[i]][j]=0;
for(int j=0;j<nei[i].size();j++){
if(!hav[big_id[i]][a[nei[i][j]]])db[big_id[i]].insert(a[nei[i][j]]);
hav[big_id[i]][a[nei[i][j]]]++;
// cout<<big_id[i]<<"!\n";
}
}
read(qu);
while(qu--){
int tp,x,y;
read(tp);read(x);
if(tp==1){//修改
read(y);
y=min(n,y);
for(int i=0;i<nei_big[x].size();i++){
int z=nei_big[x][i];
hav[big_id[z]][a[x]]--;
// cout<<big_id[z]<<"!\n";
if(!hav[big_id[z]][a[x]])db[big_id[z]].del(a[x]);
if(!hav[big_id[z]][y])db[big_id[z]].insert(y);
hav[big_id[z]][y]++;
}
a[x]=y;
}
else{
if(nei[x].size()<=lim){//小點查詢
for(int i=0;i<nei[x].size();i++)tmp[a[nei[x][i]]]=true;
for(int i=0;;i++)if(!tmp[i]){prt(i);putchar('\n');break;}
for(int i=0;i<nei[x].size();i++)tmp[a[nei[x][i]]]=false;
}
else prt(db[big_id[x]].mex()),putchar('\n');//大點查詢
}
}
}
int main(){
int testnum;
cin>>testnum;
while(testnum--)mian();
return 0;
}