1. 程式人生 > >解題:由乃OI 2018 五彩斑斕的世界

解題:由乃OI 2018 五彩斑斕的世界

題面

寫在前面的扯淡:

分塊的總體學習告一段落,這算是分塊集中學習的最後一題麼;以後當然也可能會寫,就是零零散散的題了=。=

在洛谷上搜ynoi發現好像只有這道題和 由乃OI 2018 未來日記 是分塊,久聞由乃OI之大名,就想試著寫一寫這道題(那道太毒了,寫不了);結果一開始差點被嚇跑了,不過最後還是硬著頭皮寫完了QAQ

“分塊最重要的就是常數” — —shadowice1984

好像挺有道理的,分塊從誕生之日起就是一個依靠你的決策而決定複雜度的演算法(或思想),它不像什麼高階的資料結構每塊的寫法都是基本固定的,複雜度有著嚴格的證明— —分塊是個相對靈活的演算法(所以也說它是一種思想)。舉個例子:最簡單的那種序列分塊基本上覆雜度都是$O(\frac{n^2}{size})$,$size$就是你自己決定的塊大小,大家可能都知道塊大小開$sqrt(n)$(當然這是均值不等式算出來的),但是有時候可能某個操作的常數非常大,把塊大小適當地調整可能會跑的更快......但是當卡常超過一定的地步,演算法就又失去了本身的意義和美感......算了我語言表達能力太差了,還是趕快寫題解吧=。=

考慮到區間一個個查詢修改非常慢,先用並查集把每塊裡相同的數字連到第一次出現的位置上,然後考慮修改操作$(l,r,v)$:

對於一個最大值$maxx$不超過$2*v$的塊,我們直接修改,即把大於$v$而小於最大值的部分的每個數$x$用並查集連到$x-v$

對於一個最大值$maxx$大於$2*v$的塊,我們反過來修改,把小於$v$的部分的每個數$x$連到$x+v$,同時在區間上打標記

為什麼要這樣做?

首先我們發現這兩種修改方法本質是一樣的

然後我們發現對於第一種修改我們每次動的數是$v$級別的,同時我們把最大值縮小了$v$

而對於第二種修改我們每次動的數是$maxx-v$級別的,在那個限制下其實還是$v$級別的,和上面的效率是一樣的(注意我們是根據$maxx$分出兩種情況的,這也是用到了分塊的思想)

然後均攤複雜度就有保證了,當然你還需要卡常(其實卡常不是很厲害,我就用了爛大街的快讀+快輸+register跑過去問題不大,還有這題當年在CF上好像因為CF太快+優化+3s實現把暴力放過去了=。=)

  1 #include<cmath>
  2 #include<cstdio>
  3 #include<cctype>
  4 #include<cstring>
  5 #include<algorithm>
  6 using namespace std;
  7 const int N=100005,Sq=320
,M=100000; 8 int a[N],ori[N],blo[N],ll[N],rr[N],aset[N]; 9 int siz[N],maxx[Sq],laz[Sq],firs[Sq][N]; 10 int n,m,t1,t2,t3,t4,sqr,cnt; 11 inline int read() 12 { 13 int ret=0; 14 char ch=getchar(); 15 while(!isdigit(ch)) 16 ch=getchar(); 17 while(isdigit(ch)) 18 ret=(ret<<3)+(ret<<1)+(ch^48),ch=getchar(); 19 return ret; 20 } 21 void write(int x) 22 { 23 if(x>9) write(x/10); 24 putchar(x%10+48); 25 } 26 int finda(int x) 27 { 28 return (x==aset[x])?x:aset[x]=finda(aset[x]); 29 } 30 inline void rebuild(int b)//塊重構 31 { 32 register int i; 33 for(i=ll[b];i<=rr[b];i++) 34 { 35 maxx[b]=max(maxx[b],a[i]); 36 firs[b][a[i]]=0,aset[i]=i,siz[i]=1; 37 } 38 for(i=ll[b];i<=rr[b];i++)//把每個數都併到第一次出現的位置 39 { 40 int &fir=firs[b][a[i]]; 41 if(fir) siz[fir]+=siz[i],aset[i]=fir; 42 else fir=i,ori[i]=a[i]; 43 } 44 } 45 inline void force(int b,int l,int r,int v)//大力重構 46 { 47 register int i; 48 for(i=ll[b];i<=rr[b];i++) 49 firs[b][a[i]=ori[finda(i)]]=0; 50 for(i=l;i<=r;i++) 51 if(a[i]-laz[b]>v) a[i]-=v; 52 rebuild(b); 53 } 54 void change(int l,int r,int v) 55 { 56 register int i,j; 57 int b1=blo[l],b2=blo[r]; 58 if(b1!=b2) 59 { 60 force(b1,l,rr[b1],v); 61 force(b2,ll[b2],r,v); 62 for(i=b1+1;i<=b2-1;i++) 63 if(maxx[i]-laz[i]<2*v)//當最大值不超過2*v時,正常地修改 64 { 65 for(j=laz[i]+v+1;j<=maxx[i];j++) 66 { 67 int &pos1=firs[i][j]; 68 int &pos2=firs[i][j-v]; 69 if(pos1) 70 { 71 if(pos2) siz[pos2]+=siz[pos1],aset[pos1]=pos2; 72 else pos2=pos1,ori[pos2]=j-v; pos1=0; 73 } 74 } 75 maxx[i]=min(maxx[i],laz[i]+v); 76 } 77 else//否則反過來,把其餘的數加上這個值並打標記 78 { 79 for(j=laz[i]+1;j<=laz[i]+v;j++) 80 { 81 int &pos1=firs[i][j]; 82 int &pos2=firs[i][j+v]; 83 if(pos1) 84 { 85 if(pos2) siz[pos2]+=siz[pos1],aset[pos1]=pos2; 86 else pos2=pos1,ori[pos2]=j+v; pos1=0; 87 } 88 } 89 laz[i]+=v; 90 } 91 } 92 else force(b1,l,r,v); 93 //修改的理論依據:對於第一種情況最大值在O(v)時間內減小了v,對於第二種情況最大值在O(max-v)減少了max-v,所以最終的均攤複雜度是O(1)的 94 //(雖然CF的官方題解這裡寫的是極差,但我覺得不太對,例如對233,235兩個數來說,將大於234的數減去234後極差反而在增大) 95 } 96 int query(int l,int r,int v)//普通的查詢 97 { 98 register int i; 99 int b1=blo[l],b2=blo[r],ret=0; 100 if(b1!=b2) 101 { 102 for(i=l;i<=rr[b1];i++) 103 ret+=(ori[finda(i)]-laz[b1]==v); 104 for(i=ll[b2];i<=r;i++) 105 ret+=(ori[finda(i)]-laz[b2]==v); 106 for(i=b1+1;i<=b2-1;i++) 107 if(laz[i]+v<=M) ret+=siz[firs[i][laz[i]+v]]; 108 } 109 else 110 for(i=l;i<=r;i++) 111 ret+=(ori[finda(i)]-laz[b1]==v); 112 return ret; 113 } 114 int main () 115 { 116 register int i; 117 n=read(),m=read(); 118 sqr=sqrt(n)+5,ll[cnt=1]=1; 119 for(i=1;i<=n;i++) 120 { 121 a[i]=read(),ori[i]=a[i]; 122 aset[i]=i,blo[i]=(i-1)/sqr+1; 123 maxx[blo[i]]=max(maxx[blo[i]],a[i]); 124 if(i%sqr==0) rr[cnt++]=i,ll[cnt]=i+1; 125 } 126 rr[cnt]=n; 127 for(i=1;i<=cnt;i++) rebuild(i); 128 for(i=1;i<=m;i++) 129 { 130 t1=read(),t2=read(),t3=read(),t4=read(); 131 if(t1==1) change(t2,t3,t4); else printf("%d\n",query(t2,t3,t4)); 132 } 133 return 0; 134 }
View Code