淺談分塊——入門
前言
在學會分塊之前,覺得分塊是一個很深奧的東西,很玄學。但其實分塊的作用也很廣泛,也非常簡單,在這裡分享一下。
分塊的定義與分塊的基本性質
分塊,顧名思義,就是將一個數組分成一些小塊。
而分塊有一個基本性質,就是塊的大小不會影響答案,只對時間有一定影響。
一般有以下三種分塊方式:
No.1: 固定長度分塊。
No.2 : 長度為sqrt(n)
No.3 : 每塊隨機長度。
常用的話一般選擇No.1或No.2。
分塊
如果選擇No.2的分塊方式的話,我們定義以下幾個變數:
1 block = (int)sqrt(n) //塊的長度 2 3 sum = n / block; 4 if (n % block) sum++;//塊數 5 6 p[i] = (i - 1) / block + 1;//第i個元素在第幾塊中
這幾個變數十分有用,在後文中會講到。
以上是分塊的初始化。
下面我們來講對於查詢怎麼對付,
對於區間查詢,有三種情況:
1:詢問區間[x,y]中x不是塊的左邊界,y也不是塊的右邊界。
2:詢問區間[x,y]中x不是塊的左邊界,y也是塊的右邊界。
3:詢問區間[x,y]中x是塊的左邊界,y也不是塊的右邊界。
我們發現,對於比較普通的1情況可以把詢問區間分成3個部分:
左邊多出來的、中間的k塊、右邊多出來的。
可以發現,對於一塊來說查詢是O(1)的(因為我可以預處理每一塊的值)
對於多出來的邊角料部分總和不足2*sqrt(n),基於這個時間複雜度,我們可以進行暴力求解。
所以可以發現對於1操作查詢的總複雜度(一共有n次操作)為O(n*sqrt(n))。
對於2、3情況,我們可以將這些情況當成1情況來處理。
程式碼展示:
1 for (int i = x; i <= min(y,p[x] * block); i++)//處理左邊角料 2 //do something 3 4 for (int i = (p[y] - 1) * block + 1; i <= y; i++)//處理右邊角料 5 //do something 6 7 for(int i = p[x] + 1; i <= p[y] - 1; i++)//處理中間k塊 8 //do something 9 10 //這裡特別注意一下,對於一個塊的左邊界,我們可以求出上一個塊的右邊界(p[x] - 1)*block,+1後就是這個塊的左邊界。
現在我們來講一下修改操作。
跟查詢操作一樣,我們也可以分成3部分,對於邊角料暴力修改a[i],對於一塊,我們修改這一塊的值A[p[i]]
總時間複雜度為O(n*sqrt(n))。
程式碼展示:
for (int i = x; i <= min(y,p[x] * block); i++)//處理左邊角料 change(a[i]); for (int i = (p[y] - 1) * block + 1; i <= y; i++)//處理右邊角料 change(a[i]) for(int i = p[x] + 1; i <= p[y] - 1; i++)//處理中間k塊 change(A[i]);//這裡的i已經是塊號了,所以修改的是A[i],與講解的不太一樣
對於預處理,我們發現即使每個塊暴力進行對於A[i]的預處理,時間複雜度也只有O(n),
所以預處理就塊內暴力求解。
程式碼展示:
void work(int x,int y,int t) { int st = 0; for (int i = x; i <= y; i++) //do something with st A[t] = st; } for (int i = 1; i <= sum; i++)//一共sum塊 work((p[i] - 1) * block + 1,p[i] * block,i);//傳入塊的左右邊界和編號
幾種常用的(入門)模板
1:區間加法,單點求和。(loj6277)
預處理時求得每個塊內的和,修改時對於邊角料暴力修改值。
對於中間的k塊,修改加法標記addtag[i],表示第i個塊加上了addtag[i]。
對於詢問,我們要記下對於一個塊一共加上了多少(記在addtag裡),最後輸出a[i] + addtag[p[i]]
完整程式碼展示:
1 #include<bits/stdc++.h> 2 #define int long long 3 using namespace std; 4 int block,sum,Ans[50005],p[50005],n,m,Add[50005]; 5 int a[50005]; 6 inline void work(int as,int bs,int x) 7 { 8 int st = 0; 9 for (int i = as; i <= bs; i++) 10 st += a[i]; 11 Ans[x] = st; 12 } 13 inline int read(){ 14 int s=0,w=1; 15 char ch=getchar(); 16 while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();} 17 while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar(); 18 return s*w; 19 } 20 signed main(){ 21 scanf("%lld",&n); 22 block = (int)sqrt(n); 23 sum = n / block; 24 if (n % block) sum++; 25 for (int i = 1; i <= n; i++) 26 { 27 //scanf("%lld",&a[i]); 28 a[i] = read(); 29 p[i] = (i - 1) / block + 1; 30 } 31 for (int i = 1; i <= sum; i++) 32 work((i - 1) * block + 1,i * block,i);//預處理塊內和 33 for (int i = 1; i <= n; i++) 34 { 35 int x,y,st = 0,op,k,mp,io; 36 op = read(); 37 if (op == 1) 38 { 39 mp = read(); 40 x = read(); 41 io = read(); 42 printf("%lld\n",a[x] + Add[p[x]]);//輸出對於a[i]加上了多少 43 } else 44 { 45 x = read(); 46 y = read(); 47 k = read(); 48 for (int j = x; j <= min(p[x] * block,y); j++) 49 a[j] += k; 50 if (p[x] != p[y]) 51 for (int j = (p[y] - 1) * block + 1; j <= y; j++) 52 a[j] += k; 53 for (int j = p[x] + 1; j <= p[y] - 1; j++) 54 Add[j] += k;//這裡直接修改加法標記 55 } 56 } 57 return 0; 58 }
2:區間加法,區間求和。(loj6280)
預處理時求得每個塊內的和,修改時對於邊角料暴力修改值,修改了a[i]的值,也要把a[i]所在塊的值修改。
對於中間的k塊,修改加法標記addtag[i],表示第i個塊加上了addtag[i]。
對於詢問,我們要記下對於一個塊一共加上了多少(記在addtag裡),對於邊角料同上一個模板一樣,暴力+a[i]+addtag[p[i]]
對於中間的k塊加上A[i],因為其中有block個數,所以要+block個addtag[i]
完整程式碼展示:
1 #include<bits/stdc++.h> 2 #define int long long 3 using namespace std; 4 const long long inf = 66666666233; 5 int block,sum,Ans[3000005],p[3000005],n,m,Add[3000005]; 6 int a[3000005]; 7 void work(int as,int bs,int x) 8 { 9 int st = 0; 10 for (int i = as; i <= bs; i++) 11 st += a[i]; 12 Ans[x] = st; 13 } 14 signed main(){ 15 scanf("%lld",&n); 16 block = (int)sqrt(n); 17 sum = n / block; 18 if (n % block) sum++; 19 for (int i = 1; i <= n; i++) 20 { 21 scanf("%lld",&a[i]); 22 p[i] = (i - 1) / block + 1; 23 } 24 for (int i = 1; i <= sum; i++) 25 work((i - 1) * block + 1,i * block,i); 26 for (int i = 1; i <= n; i++) 27 { 28 int x,y,st = 0,op,k; 29 scanf("%lld",&op); 30 if (op == 1) 31 { 32 scanf("%lld%lld%lld",&x,&y,&k); 33 for (int j = x; j <= y; j) 34 if ((j - 1) % block || (p[j] * block > y)) 35 { 36 st += Add[p[j]] + a[j]; 37 j++; 38 } else 39 { 40 st += Ans[p[j]] + Add[p[j]] * ((p[j] * block) - ((p[j] - 1) * block + 1) + 1);//加上block個Add[p[j]] 41 j += block; 42 } 43 printf("%lld\n",st % (k + 1)); 44 } else 45 { 46 scanf("%lld%lld%lld",&x,&y,&k); 47 for (int j = x; j <= y; j) 48 if ((j - 1) % block || (p[j] * block > y)) 49 { 50 a[j] += k; 51 Ans[p[j]] += k; 52 j++; 53 } else 54 { 55 Add[p[j]] += k; 56 j += block; 57 } 58 } 59 } 60 return 0; 61 }
3:區間開方,區間求和。(loj6281)
預處理時求得每個塊內的和,修改時對於邊角料暴力修改值,修改了a[i]的值,也要把a[i]所在塊的值修改。
對於中間的k塊,修改總和A[i],表示第i個塊減少了。
對於詢問,我們要記下對於一個塊一共加上了多少(記在addtag裡),邊角料暴力+a[i]
對於中間的k塊加上A[i],輸出答案。
完整程式碼展示:
1 #include<bits/stdc++.h> 2 #define int long long 3 using namespace std; 4 int block,sum,Ans[50005],p[50005],n,m,Add[50005],tag[500005]; 5 int a[50005]; 6 inline void work(int as,int bs,int x) 7 { 8 int st = 0; 9 for (int i = as; i <= bs; i++) 10 st += a[i]; 11 Add[x] += st; 12 //COp[x] = st; 13 } 14 inline int read(){ 15 int s=0,w=1; 16 char ch=getchar(); 17 while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();} 18 while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar(); 19 return s*w; 20 } 21 void push_down(int x){ 22 Add[x]=0; 23 int flag=1; 24 for(int i=(x-1)*block+1;i<=min(n,x*block);i++){ 25 a[i]=(int)sqrt(a[i]*1.0); 26 Add[x]+=a[i]; 27 if(a[i]!=1) flag=0; 28 } 29 tag[x]=flag; 30 }//這裡注意一下,對於中間的塊要標記下傳,重新更新 31 signed main(){ 32 scanf("%lld",&n); 33 block = (int)sqrt(n); 34 sum = n / block; 35 if (n % block) sum++; 36 for (int i = 1; i <= n; i++) 37 { 38 a[i] = read(); 39 p[i] = (i - 1) / block + 1; 40 } 41 for (int i = 1; i <= sum; i++) 42 work((i - 1) * block + 1,i * block,i); 43 for (int i = 1; i <= n; i++) 44 { 45 int x,y,st = 0,op,k,mp,io; 46 op = read(); 47 if (op == 1) 48 { 49 x = read(); 50 y = read(); 51 mp = read(); 52 for (int j = x; j <= min(p[x] * block,y); j++) 53 st += a[j]; 54 if (p[x] != p[y]) 55 for (int j = (p[y] - 1) * block + 1; j <= y; j++) 56 st += a[j]; 57 for (int j = p[x] + 1; j <= p[y] - 1; j++) 58 st += Add[j]; 59 printf("%d\n",st); 60 } else 61 { 62 x = read(); 63 y = read(); 64 k = read(); 65 for (int j = x; j <= min(p[x] * block,y); j++) 66 { 67 int uy = a[j]; 68 a[j] = (int)sqrt(a[j]); 69 Add[p[j]] -= uy - a[j];//開方就等於減少了sqrt(a[i]) 70 } 71 if (p[x] != p[y]) 72 for (int j = (p[y] - 1) * block + 1; j <= y; j++) 73 { 74 int uy = a[j]; 75 a[j] = (int)sqrt(a[j]); 76 Add[p[j]] -= uy - a[j]; 77 } 78 for (int j = p[x] + 1; j <= p[y] - 1; j++) 79 if (tag[j] == 0) push_down(j); 80 } 81 } 82 return 0; 83 }
4:區間加法,區間詢問小於c2的數。(loj6278、luoguP4145)
預處理時對於所有塊進行快內排序,將未排序的a陣列賦值給tlps,修改時對於邊角料暴力修改值,修改tlps[i]的值(如果修改的是a的話,因為排過序,會發現位置不對)。
對於中間的k塊,修改A[i]。
對於詢問,邊角料暴力判斷
對於中間的k塊(對於a陣列)lower_bound c^2 - addtag(因為詢問所有a + addtag 等價於 c^2 - addtag),得出這個位置,可以發現,這個位置的前面都是符合要求的。
完整程式碼展示:
1 #include<bits/stdc++.h> 2 #define int long long 3 using namespace std; 4 int block,sum,Ans[50005],p[50005],n,m,Add[50005]; 5 int a[50005],tlps[50005]; 6 inline void work(int as,int bs) 7 { 8 for (int i = as; i <= bs; i++) 9 a[i] = tlps[i]; 10 sort(a + as,a + bs + 1); 11 } 12 signed main(){ 13 //freopen("a1.in","r",stdin); 14 //freopen("ate.out","w",stdout); 15 memset(Add,0,sizeof(Add)); 16 scanf("%lld",&n); 17 block = (int)sqrt(n); 18 sum = n / block; 19 if (n % block) sum++; 20 for (int i = 1; i <= n; i++) 21 { 22 scanf("%lld",&a[i]); 23 tlps[i] = a[i];//注意要複製一份 24 p[i] = (i - 1) / block + 1; 25 } 26 for (int i = 1; i <= sum; i++) 27 work((i - 1) * block + 1,min(i * block,n)); 28 for (int i = 1; i <= n; i++) 29 { 30 int x,y,op,k; 31 scanf("%lld",&op); 32 if (op == 1) 33 { 34 scanf("%lld%lld%lld",&x,&y,&k); 35 k = k * k; 36 int v = 0; 37 for (int j = x; j <= min(p[x] * block,y); j++) 38 if (tlps[j] + Add[p[x]] < k) 39 v++; 40 if (p[x] != p[y]) 41 for (int j = (p[y] - 1) * block + 1; j <= y; j++) 42 if (tlps[j] + Add[p[y]] < k) 43 v++; 44 for (int j = p[x] + 1; j <= p[y] - 1; j++) 45 v += lower_bound(a + (j - 1) * block + 1,min(a + n,a + j * block + 1),k - Add[j]) - (a + (j - 1) * block + 1);//二分查詢 46 printf("%lld\n",v); 47 } else 48 { 49 scanf("%lld%lld%lld",&x,&y,&k); 50 for (int j = x; j <= min(p[x] * block,y); j++) 51 { 52 tlps[j] += k;//修改的是原陣列 53 //a[j] += k; 54 } 55 work((p[x] - 1) * block + 1,min(n,p[x] * block)); 56 if (p[x] != p[y]) 57 { 58 for (int j = (p[y] - 1) * block + 1; j <= y; j++) 59 { 60 tlps[j] += k; 61 //a[j] += k; 62 } 63 work((p[y] - 1) * block + 1,min(n,p[y] * block)); 64 } 65 for (int j = p[x] + 1; j <= p[y] - 1; j++) 66 Add[j] += k; 67 } 68 } 69 return 0; 70 }