閑話
莫隊算法似乎還是需要一點分塊思想的。。。。。。於是我就先來搞分塊啦!
膜拜hzwer學長神犇%%%Orz
這九道題,每一道都堪稱經典,強力打Call!點這里進入
算法簡述
每一次考試被炸得體無完膚之后,又聽到講題目的Dalao們爆出一句
數據不大,用分塊暴力搞一下就AC了
的時候,我就會五體投地,不得翻身。。。。。。
分塊?暴力?真的有如此玄學?
直到現在我還是覺得它很笨拙,但要熟練運用(尤其是打大暴力部分分的時候),絕非易事。
沒錯,分塊是優化的暴力,更輕松地資瓷在線,是一種算法思想,主要用來維護區間信息,因此也可以當成數據結構。
當絞盡腦汁也無法發現能用自帶log的數據結構(線段樹,樹狀數組,平衡樹······)維護的時候,一定不要拋棄它。
一般的分塊題目形式,都是給你若干個區間處理操作。這些區間有長有短,直接暴力操作復雜度是\(O(n^2)\)的。
分塊就是為了解決區間過長所導致的低效而出現的。
具體做法,是把數列分成若干個小塊,每個小塊內部,每個元素的值仍然需要維護,然后再維護整個小塊的信息。
對於一個很長的操作區間,其內部一定會包含若干連續的小塊對吧。
對這些小塊,直接對小塊信息操作,不用一個個操作每個元素啦,從而省去了大量時間。
當然,區間兩頭會有不包含一個完整小塊的部分,暴力修改。然而這兩頭長度加起來不會超過塊的長度的兩倍,復雜度有保證。
常見題目類型&套路&難度分析(持續更新中)
-
維護塊信息總和★★☆☆☆
最裸的一類啦。。。。。。
例題有下面hzwer九題中的1、4 -
維護塊標記並進行懶處理★★★☆☆
需要分析復雜度的思路,巧妙地利用題目特點,維護好懶標記並正確釋放,但實現難度不高
例題有下面hzwer九題中的5、7和8 -
塊內維護其它結構★★★☆☆
思路較清晰,但實現難度加大,常見的類型(我只見過的)有
—— 有序表,以便資磁塊內二分答案等操作,如下面hzwer九題中的2、3
—— 鏈表,可以方便地插入和刪除,更可以高效完成塊與塊的合並或分裂(重構),如下面hzwer九題中的6
—— 平衡樹,更多高級操作,題目仍在發現中(當然,可以把hzwer九題中的3、6兩題的操作合在一起)(手動滑稽)
—— 。。。。。。 -
維護塊區間的信息★★★★☆
解釋一下,這里的“塊區間”指的是第\(l\)個塊到第\(r\)個塊的信息。如果需要這樣維護的話,通常思維難度和實現難度都上了一個檔次。
題目仍在發現並落實中。。。。。。(hzwer九題中的9當然是個火題辣,不過利用了離線我用常數更優的莫隊水了一下)
題目列表
hzwer數列分塊入門九題
hzwer的題解都很詳細了,我再寫點自己的東西吧。
分塊入門 1 by hzwer
LOJ題目傳送門
題面等於洛谷模板樹狀數組2。。。。。。
不過既然學分塊就好好學嘛。
這里的小塊信息就是一個加法標記,表示對整個區間的所有數加上標記值。
於是做法就很簡單了。
對於這個分塊長度(以下設為\(m\))的問題,我不是很清楚。對於每個具體的題目,到底該取多少呢?
方便的話,默認\(m=\sqrt n\)吧。
分析一下單次修改的復雜度。在本題中,整塊修改復雜度取決於塊的數量,復雜度\(O({n\over m})\);
不完整塊暴力修改復雜度取決於塊的長度,復雜度\(O(m)\)。
根據基本不等式,\({n\over m}+m≥2\sqrt n\)當且僅當\({n\over m}=m\)時等號成立。
於是\(m=\sqrt n\)。
當然,如果數據是隨機的,那么平均情況下的系數考慮一下會得到更好的\(m\)。
首先,詢問區間的平均長度是\(n\over 3\)(試出來的,我太弱了不會證,歡迎Dalao指教)
然后,暴力修改的部分平均長度肯定是\(m\)(\({m\over 2}×2\)),得到帶系數的復雜度\(O({n\over {3m}}+m)\)。
再次用基本不等式得到更優的\(m\)是\(\sqrt{{n\over 3}}\)。
目測hzwer的數據是rand的,所以改了以后確實快不少。
貼下代碼吧。貌似我的代碼永遠是最短最丑的。。。。。。
#include<cstdio>
#include<cmath>
int a[50009],b[233];//b為小塊加法標記
int main()
{
register int n,L,i,op,l,r,c;
scanf("%d",&n);
L=sqrt(n/3);
for(i=0;i<n;++i)scanf("%d",&a[i]);
for(i=0;i<n;++i)
{
scanf("%d%d%d%d",&op,&l,&r,&c);
--l;--r;//為了方便,數組從0下標開始存了
if(op)printf("%d\n",a[r]+b[r/L]);
else
{
for(;l<=r&&l%L;++l)a[l]+=c;//暴力(左)
for(;l+L-1<=r;l+=L)b[l/L]+=c;//改塊
for(;l<=r;++l)a[l]+=c;//暴力(右)
}
}
return 0;
}
分塊入門 2 by hzwer
LOJ題目傳送門
這時候,分塊成為了最優解法。
排序預處理,詢問操作塊內二分,塊外暴力。
修改操作塊內放加法標記,塊外暴力加排序重構。
平均情況下復雜度大致為\(O(n\log m+n({n\over m}\log m+m\log m))\)
利用平均系數\(m\)同樣可取\(\sqrt{{n\over 3}}\)
實際上我嘗試了一下,\(\sqrt{{n\over 4}}\)即\(\sqrt n\over 2\)更快。
因為預處理中帶了個\(m\),加上修改操作中也帶有\(m\)而在上式中為了簡便被省略了。
所以\(m\)適當的更小一點。
還有一點,其實修改操作時對兩個不完整部分的重新排序,可以不用寫sort。
原塊已經排好序,修改相當於給這個有序序列部分元素加上一個相等的值。
那么現在這個序列可以分成兩個集合——被加的和沒被加的。可以看出這兩個集合按在原序列的位置中,仍然是各自有序的。
對兩個有序的序列再排序,最高效的不就是二路歸並嗎?
這樣做的話,要在有序序列中額外維護每個元素在原序列中的位置。
掃一遍這個不完整塊的時候,根據位置判斷是否被加,然后放入對應集合。最后對兩個集合歸並排序放回有序序列即可。
僅僅對單次修改操作而言,這樣做使得復雜度降至\(O({n\over m}+m)\),去掉了\(\log\)。
然而這種寫法也有一點常數,也比較麻煩,我就偷了點懶QvQ
上代碼
#include<cstdio>
#include<cmath>
#include<cstring>
#include<algorithm>
using namespace std;
#define R register
char s[1314520];
int a[54250],b[54250],t[2333];//請忽略丑陋的數組長度定義
#define resort(P) {memcpy(b+P,a+P,L<<2);sort(b+P,b+P+L);}//重構排序
#define in(z) {while(*++p<'-');z=*p&15;while(*++p>'-')z*=10,z+=*p&15;}
int main(){
fread(s,1,sizeof(s),stdin);
R char*p=s-1;
R int n,L,i,op,l,r,c,ans;
in(n);
L=sqrt(n)/2;
for(i=0;i<n;++i)in(a[i]);
for(;i%L;++i)a[i]=1e9;//人工在最末塊尾部插入inf,避免vector帶來的常數
for(i=0;i<n;i+=L)resort(i);//初始化排序
for(i=0;i<n;++i){
in(op);in(l);in(r);in(c);
--l;--r;
if(op){
ans=0;c*=c;
for(;l<=r&&l%L;++l)ans+=a[l]+t[l/L]<c;//暴力(左)
for(;l+L-1<=r;l+=L)ans+=lower_bound(b+l,b+l+L,c-t[l/L])-b-l;//塊內二分統計答案
for(;l<=r;++l)ans+=a[l]+t[l/L]<c;//暴力(右)
printf("%d\n",ans);
}
else{
if(l/L==r/L){//在同一個塊中,暴力改完走人
for(;l<=r;++l)a[l]+=c;
resort(r/L*L);
continue;
}
if(l%L){//暴力(左)
for(;l%L;++l)a[l]+=c;
resort(l-L);
}
for(;l+L-1<=r;l+=L)t[l/L]+=c;//直接改
if(r>=l){//暴力(右)
for(;r>=l;--r)a[r]+=c;
resort(l);
}
}
}
return 0;
}
分塊入門 3 by hzwer
LOJ題目傳送門
基本思路與2差不多,除了二分統計答案的方式。
因此就沒什么好說的啦,代碼跟上面一樣丑,就不貼了
分塊入門 4 by hzwer
LOJ題目傳送門
題面等於洛谷模板線段樹1。。。。。。
相比1,再加上塊內總和的維護就好啦,代碼也不貼了
分塊入門 5 by hzwer
LOJ題目傳送門
其實分塊不是正解,我還是慣性思維想到了線段樹。
反正開方不會超過四次,那可以用帶log的數據結構維護。
考慮線段樹,弄個標記表示當前節點區間已全部變為0或1,假如兩個子區間都打標記了,那么當前區間也打上,以后區間開方碰到有標記就結束掉。
這樣復雜度是對的,\(O(N\log N)\)帶上一個不小的常數,但總比分塊好點。。。。。。
分塊的話也就是在塊內搞同樣意義的標記嘛!似乎還要寫鏈表,不是很輕松的暴力。。。。。。我還是太懶了
線段樹代碼(放在這里極其不和諧)
#include<cstdio>
#include<cmath>
#define R register
#define G c=getchar()
inline void in(R int&z){
R char G;
while(c<'-')G;
z=c&15;G;
while(c>'-')z*=10,z+=c&15,G;
}
const int M=3000000;
int le[M],mi[M],ri[M],s[M],t[M];
#define lc u<<1
#define rc u<<1|1
#define pushup s[u]=s[lc]+s[rc],t[u]=t[lc]&t[rc]//上傳和以及標記
void build(R int u,R int l,R int r){//建樹
le[u]=l;ri[u]=r;mi[u]=(l+r)>>1;
if(l==r){
in(s[u]);
if(s[u]<2)t[u]=1;
return;
}
build(lc,l,mi[u]);
build(rc,mi[u]+1,r);
pushup;
}
void update(R int u,R int l,R int r){//更新,略超出模板范圍
if(t[u])return;
if(le[u]==ri[u]){
s[u]=sqrt(s[u]);
if(s[u]<2)t[u]=1;
return;
}
if(r<=mi[u])update(lc,l,r);
else if(l>mi[u])update(rc,l,r);
else update(lc,l,mi[u]),update(rc,mi[u]+1,r);
pushup;
}
int ask(R int u,R int l,R int r){//查詢完全是模板
if(l==le[u]&&r==ri[u])return s[u];
if(r<=mi[u])return ask(lc,l,r);
else if(l>mi[u])return ask(rc,l,r);
else return ask(lc,l,mi[u])+ask(rc,mi[u]+1,r);
}
int main(){
R int n,op,l,r,c;
in(n);
build(1,1,n);
while(n--){
in(op);in(l);in(r);in(c);
if(op)printf("%d\n",ask(1,l,r));
else update(1,l,r);
}
return 0;
}
分塊入門 6 by hzwer
LOJ題目傳送門
總覺得像平衡樹基本操作。。。。。。
這里引入了一個新的概念——重構。
為了方便,我手寫了一下鏈表套鏈表(滑稽)
就是塊與塊之間用鏈表相連,塊中的每個元素也用鏈表連起來。
我采用了區間過大重構的方法。
這樣的話當區間過大時,直接新開一個塊插入在塊的鏈表中,再讓其指向原塊的中點位置元素,省了挺多事的。
查詢的話就維護每個塊的大小,從前往后掃,以此確定某點在哪一個塊中,再在塊內挨個找。
還有一個小小的idea,可以讓靠前的塊更大,靠后的塊略小,復雜度被平均了,會降低一些(只是貌似帶來了更大的常數。。。。。。)
代碼
#include<cstdio>
#include<cmath>
#define G c=getchar()
#define in(z) G;\
while(c<'-')G;\
z=c&15;G;\
while(c>'-')z*=10,z+=c&15,G
const int B=1009,N=200009;
int s[B],neb[B],he[B],ne[N],v[N];
//neb塊鏈表,he塊首元素,ne元素鏈表,注意區分
int main()
{
register int n,L,i,j,b,p,pb,op,l,r;
register char c;
in(n);L=sqrt(n);
for(b=i=j=0;i<n;++i,++j){
in(v[i]);
if(j==L){
j=0;
neb[b]=b+1;
he[++b]=i;
}
ne[i]=i+1;
++s[b];
}
p=n-1;pb=b;//p新點,pb新塊
while(n--){
in(op);in(l);in(r);in(j);
if(op){//查詢,先掃塊,再掃元素
for(b=0;s[b]<r;r-=s[b],b=neb[b]);
for(i=he[b],--r;r;i=ne[i],--r);
printf("%d\n",v[i]);
}
else{
for(b=0;s[b]<l;l-=s[b],b=neb[b]);
if(--l){
for(i=he[b],--l;l;i=ne[i],--l);
ne[++p]=ne[i],ne[i]=p;
}//找到插入位置並插入,注意特判塊頭
else ne[++p]=he[b],he[b]=p;
v[p]=r;
if(++s[b]>=L<<1){//重構,把塊砍兩半
s[++pb]=s[b]>>=1;
neb[pb]=neb[b];neb[b]=pb;//神奇的鏈表指來指去
for(i=he[b],l=L-1;l;i=ne[i],--l);
he[pb]=ne[i];
}
}
}
return 0;
}
分塊入門 7 by hzwer
LOJ題目傳送門
題面等於洛谷模板線段樹2(不是模板,是大火題!)
已經敲完線段樹2和Tree II,對此題失去了興趣(其實是因為我太弱了,真的怕放標記又寫掛。。。。。。)
分塊入門 8 by hzwer
LOJ題目傳送門
思路像5,但代碼不易,尤其是每次暴力左右不完整部分、破壞了塊的同一個值的時候,還要釋放標記,寫起來是真心累。。。。。。
長長的代碼
#include<cstdio>
#include<cmath>
#include<iostream>
using namespace std;
#define G ch=getchar()
#define in(z) G;\
while(ch<'-')G;\
z=ch&15;G;\
while(ch>'-')z*=10,z+=ch&15,G
int a[100009],t[1009];
int main(){
register int n,L,i,j,l,r,c,bel,ber,ans;
register char ch;
in(n);L=sqrt(n);
for(i=0;i<n;++i){in(a[i]);}
for(j=0;j<n;++j){
in(l);in(r);in(c);--l;//所有下標減了1,並強行轉化成左閉右開
bel=l/L;ber=r/L;ans=0;
//下面真的是一大堆討論,為了減少代碼量,把很多情況合並到一起處理了
if(bel!=ber){
if(l%L){//不完整(左)
if(t[bel]){
if(c==t[bel]){//與標記相等
ans+=(bel+1)*L-l;l=(bel+1)*L;
goto M;
}
for(i=bel*L;i<l;++i)a[i]=t[bel];//不相等,放標記
t[bel]=0;
for(i=l,l=(bel+1)*L;i<l;++i)a[i]=c;//暴力改
}
else for(i=l,l=(bel+1)*L;i<l;++i)
a[i]==c?++ans:a[i]=c;//暴力
}
else --bel;
M:while(++bel<ber){//完整
l+=L;
if(t[bel]){
if(c==t[bel]){
ans+=L;
continue;
}
}
else for(i=l-L;i<l;++i)
a[i]==c?++ans:a[i]=c;
t[bel]=c;//printf("bel%d ans%d\n",bel,ans);
}
}
if(l==r)goto N;//無不完整(右)
if(t[bel]){//在不完整的同一塊中,或者不完整(右)
if(c==t[bel]){
ans+=r-l;
goto N;
}
for(i=bel*L;i<l;++i)a[i]=t[bel];//兩邊都要放
for(i=l;i<r;++i)a[i]=c;
for(i=min((bel+1)*L,n)-1;i>=r;--i)a[i]=t[bel];
t[bel]=0;
}
else for(i=l;i<r;++i)
a[i]==c?++ans:a[i]=c;
N:printf("%d\n",ans);
}
return 0;
}
持續更新中。。。。。。