分块
分块,顾名思义,就是将序列分成一小块一小块的来处理和维护。
我们把一段当成一个整体,只记录整体有关的信息,这就是分块。
首先,我们考虑一道题。
1.区间修改,查询区间中一段区间有多少数 < k;
一种朴素算法就是,暴力从区间 L 到 R 扫过去,每遇到小于k的位置加1
没错,是不是这种方法很傻。但分块就是从这里优化过来的。
假设,我们有一个长度为 n 的序列,然后我们把他切成 sqrt(n)块,把每一块中的东西当成一个整体。(至于为什么块的大小是sqrt(n),自己可以百度一下QAQ)
几个术语: 完整块 被操作区间完全覆盖的块
不完整块 不完全覆盖的块
然后我们考虑怎么得出答案
- 首先我们要预处理每个点在哪个块,以及每个块的左右端点。
- 然后, 对于不完整的块,我们可以考虑直接暴力修改,因为元素比较少。
- 那么, 对于完整的块,我们就可以考虑打上lazy标记。
总而言之,分块就是将小块暴力,大块处理的一种方法。
然后这就可以用分块——这种看似高大上的方法做完。。。。。。比之前傻傻的暴力是不是好了很多。
下面就是预处理代码
1 for(int i = 1; i <= n; i++){ 2 scanf("%d",&a[i]); 3 pos[i] = (i-1)/len +1;//每个点所在的块 4 L[pos[i]] = 2333333; 5 } 6 for(int i = 1; i <= n; i++){ 7 L[pos[i]] = min(L[pos[i]],i);//每个块的左右端点 8 R[pos[i]] = max(R[pos[i]],i); 9 }
一个比较重要的问题就是,块的数量不一定是len+1
他正确的是n/len+1
思想讲完了,下面就该看几道题了。
题意大概是给出一个长为n的序列,以及n个操作,操作有区间开方和区间求和。
我们会发现开方操作比较难,主要是维护整块时,必须一个个的去修改,那这样岂不是和不同的暴力复杂度不就差不多了吗.
然后我们接着往深里想,不难发现,一个数经过几次开方后,就会变成0或1.
如果每次区间开方只要修改不完整的块,直接暴力修改就可以了。
如果修改一些整块,我们可以只要在每个整块暴力开方后,记录一下元素是否变成了0或1,修改时直接跳过全为0或1的块
就okk了 ,这样每个元素至多开方不超过4次,复杂度刚好可以。
下面是代码;
1 #include<iostream> 2 #include<cstdio> 3 #include<algorithm> 4 #include<cmath> 5 #include<cstring> 6 using namespace std; 7 const int N = 1e5+100; 8 long long n,m,k,l,r; 9 long long a[N],pos[N],L[N],R[N],sum[N]; 10 bool vis[N]; 11 void check(long long x){//检查每个块是否全为0或1 12 if(vis[x]) return; 13 vis[x] = 1; 14 sum[x] = 0; 15 for(int i = L[x]; i <= R[x]; i++){ 16 a[i] = sqrt(a[i]); 17 sum[x] += a[i]; 18 if(a[i] > 1) vis[x] = 0; 19 } 20 } 21 void chenge(long long l,long long r){ 22 int p = pos[l],q = pos[r]; 23 if(p == q){ 24 for(int i = l; i <= r; i++){//同一个块直接暴力改 25 sum[p] -=a[i]; 26 a[i] = sqrt(a[i]); 27 sum[p] += a[i]; 28 } 29 } 30 else { 31 for(int i = p+1; i <= q-1; i++) check(i); 32 for(int i = l; i <= R[p]; i++){//左边的散块 33 sum[p] -=a[i]; 34 a[i] = sqrt(a[i]); 35 sum[p] += a[i]; 36 } 37 for(int i = L[q]; i <= r; i++){ 38 sum[q] -= a[i]; 39 a[i] = sqrt(a[i]); 40 sum[q] += a[i]; 41 } 42 } 43 } 44 long long ask(long long l,long long r){ 45 long long ans = 0; 46 int p = pos[l],q = pos[r]; 47 if(p == q) for(int i = l; i <= r; i++) ans += a[i]; 48 else { 49 for(long long i = p+1; i <= q-1; i++) ans += sum[i]; 50 for(int i =l; i <= R[p]; i++) ans+= a[i]; 51 for(long long i = L[q]; i <= r; i++) ans += a[i]; 52 } 53 return ans; 54 } 55 int main(){ 56 cin>>n; 57 memset(vis, false, sizeof(vis)); 58 int len = sqrt(n); 59 for(long long i = 1; i <= n; i++) cin>>a[i]; 60 for(long long i = 1; i <= n; i++){ 61 pos[i] = (i-1)/len+1; 62 L[pos[i]] = 23333333; 63 } 64 for(long long i = 1; i <= n; i++){ 65 L[pos[i]] = min(L[pos[i]],i); 66 R[pos[i]] = max(R[pos[i]],i); 67 } 68 for(int i = 1; i <= n/len+1; i++){//预处理每个块的和 69 for(int j = L[i]; j <= R[i]; j++){ 70 sum[i]+=a[j]; 71 } 72 } 73 cin>>m; 74 for(long long i = 1; i <= m; i++){ 75 cin>>k>>l>>r; 76 if(l > r) swap(l,r); 77 if( k == 0) chenge(l,r); 78 else cout<<ask(l,r)<<endl; 79 } 80 return 0; 81 }
关于这道题,你还可以用线段树做,并且要比分块快QAQ。
1 #include<iostream> 2 #include<cstdio> 3 #include<algorithm> 4 #include<cmath> 5 #include<cstring> 6 using namespace std; 7 const int N = 1e5+100; 8 long long n,m,k,l,r; 9 long long a[N],pos[N],L[N],R[N],sum[N]; 10 bool vis[N]; 11 void check(long long x){ 12 if(vis[x]) return; 13 vis[x] = 1; 14 sum[x] = 0; 15 for(int i = L[x]; i <= R[x]; i++){ 16 a[i] = sqrt(a[i]); 17 sum[x] += a[i]; 18 if(a[i] > 1) vis[x] = 0; 19 } 20 } 21 void chenge(long long l,long long r){ 22 int p = pos[l],q = pos[r]; 23 if(p == q){ 24 for(int i = l; i <= r; i++){ 25 sum[p] -=a[i]; 26 a[i] = sqrt(a[i]); 27 sum[p] += a[i]; 28 } 29 } 30 else { 31 for(int i = p+1; i <= q-1; i++) check(i); 32 for(int i = l; i <= R[p]; i++){ 33 sum[p] -=a[i]; 34 a[i] = sqrt(a[i]); 35 sum[p] += a[i]; 36 } 37 for(int i = L[q]; i <= r; i++){ 38 sum[q] -= a[i]; 39 a[i] = sqrt(a[i]); 40 sum[q] += a[i]; 41 } 42 } 43 } 44 long long ask(long long l,long long r){ 45 long long ans = 0; 46 int p = pos[l],q = pos[r]; 47 if(p == q) for(int i = l; i <= r; i++) ans += a[i]; 48 else { 49 for(long long i = p+1; i <= q-1; i++) ans += sum[i]; 50 for(int i =l; i <= R[p]; i++) ans+= a[i]; 51 for(long long i = L[q]; i <= r; i++) ans += a[i]; 52 } 53 return ans; 54 } 55 int main(){ 56 cin>>n; 57 memset(vis, false, sizeof(vis)); 58 int len = sqrt(n); 59 for(long long i = 1; i <= n; i++) cin>>a[i]; 60 for(long long i = 1; i <= n; i++){ 61 pos[i] = (i-1)/len+1; 62 L[pos[i]] = 23333333; 63 } 64 for(long long i = 1; i <= n; i++){ 65 L[pos[i]] = min(L[pos[i]],i); 66 R[pos[i]] = max(R[pos[i]],i); 67 } 68 for(int i = 1; i <= n/len+1; i++){ 69 for(int j = L[i]; j <= R[i]; j++){ 70 sum[i]+=a[j]; 71 } 72 } 73 cin>>m; 74 for(long long i = 1; i <= m; i++){ 75 cin>>k>>l>>r; 76 if(l > r) swap(l,r); 77 if( k == 0) chenge(l,r); 78 else cout<<ask(l,r)<<endl;
第二题 P2801 教主的魔法
这个题大致就是区间加和区间询问小于k的数。
对于每个询问,可以先将这个块排好序,然后二分查找k的值就行了。散块直接暴力查询没有排序的原序列。
怎么修改呢???
对于整块,我们可以打上 lazy 标记,查询时直接查 >= k - lazy 的值;
对于散块,直接暴力修改,在直接排个序就okk了。
例三 zcl的妹
例三 ZCL的妹子
1 #include<iostream> 2 #include<cstdio> 3 #include<algorithm> 4 #include<cmath> 5 using namespace std; 6 const int N = 1e6+10; 7 #define LL long long 8 char opt; 9 int n,m,x,y,len; 10 int L[N],R[N]; 11 LL sum[N],size[N],a[N],pos[N],cnt[N]; 12 inline int read() 13 { 14 int s = 0, w = 1; char ch = getchar(); 15 while(ch < '0' || ch > '9'){if(ch == '-') w = -1;ch = getchar();} 16 while(ch >= '0'&&ch <= '9'){s = s * 10 +ch -'0'; ch = getchar();} 17 return s * w; 18 } 19 void chenge1(int x,int y) 20 { 21 if(!cnt[x]) return ; 22 int xx = pos[x]; 23 sum[xx] -= y; 24 a[x] -= y; 25 } 26 void chenge2(int x,int y) 27 { 28 int xx = pos[x]; 29 if(cnt[x] == 1) 30 { 31 sum[xx] -= a[x]; 32 a[x] = y; 33 sum[xx] += a[x]; 34 } 35 else{ 36 cnt[x] = 1; 37 sum[x] -= a[x]; 38 a[x] = y; 39 size[xx]++; 40 sum[xx] += a[x]; 41 } 42 } 43 void chenge3(int x) 44 { 45 int p = 1; 46 while(x - size[p] > 0) x -= size[p], p++; 47 for(int i = L[p]; i <= R[p]; i++) 48 { 49 x -= cnt[i]; 50 if(x == 0) 51 { 52 size[p]--; 53 sum[p] -= a[i]; 54 cnt[i] = a[i] = 0; 55 return ; 56 } 57 } 58 } 59 LL ask() 60 { 61 LL ans = 0; 62 for(int i = 1; i <= pos[500000]; i++) ans += sum[i]; 63 return ans; 64 } 65 int main() 66 { 67 n = read(); m = read(); 68 len = sqrt(500000); 69 for(int i = 1; i <= n; i++) a[i] = read(),cnt[i] = 1; 70 for(int i = 1; i <= 500000; i++) 71 { 72 pos[i] = (i-1)/len+1; 73 L[pos[i]] = 23333333; 74 } 75 for(int i = 1; i <= 500000; i++) 76 { 77 size[pos[i]] += cnt[i]; 78 L[pos[i]] = min(L[pos[i]] , i); 79 R[pos[i]] = max(R[pos[i]] , i); 80 sum[pos[i]] += a[i]; 81 } 82 for(int i = 1; i <= m; i++) 83 { 84 cin>>opt; 85 if(opt == 'C') 86 { 87 x = read(); y = read(); 88 chenge1(x,y); 89 } 90 if(opt == 'I') 91 { 92 x = read(); y = read(); 93 chenge2(x,y); 94 } 95 if(opt == 'D') 96 { 97 x = read(); 98 chenge3(x); 99 } 100 if(opt == 'Q') printf("%lld\n",ask()); 101 } 102 return 0; 103 }
这道题题意就是给你一段区间,然后询问区间的最小众数。
首先 a < 1e9先离散化;
然后预处理出两个数组 p[i][j] 表示第i个块到第j个块的最小众数。
s[i][j] 前i个块中 j出现的次数
如何处理呢
- 对于s, 直接每一个块扫一遍 。
- 对于 p ,双重枚举i,j开个数组暴力统计每个数出现多少次就行了。
- 对于询问操作,残块暴力搞,整块骚操作。所以对于posr - posl <= 2 的情况直接可以暴力搞出来
4.对于其他的情况,答案要么是整块中的众数,或者残块中的数。对于残块中的数,我们已经预处理了之前数字出现的前缀和,
所以能很快的求出这个数出现的次数。
5.对于整块中的众数,我们也可以很快的求出ta在残块中的次数。这样答案就可以出来了。
void Treaker()//分块预处理
{
for(int i=1;i<=n;++i)
for(int j=pos[i];j<=pos[n];++j)s[j][a[i]]++;
for(int i=1;i<=pos[n];++i)
{
memset(tong,0,sizeof(tong));
for(int j=i;j<=pos[n];++j)
{
p[i][j]=p[i][j-1];
for(int k=L[j];k<=R[j];++k)
{
tong[a[k]]++;
if((tong[a[k]]>tong[p[i][j]])||(tong[a[k]]==tong[p[i][j]]&&a[k]<p[i][j]))p[i][j]=a[k];
}
}
}
memset(tong,0,sizeof(tong));
}
比较重要的是
p[i][j]=p[i][j-1]
因为我们需要先继承先前的区间众数,然后才能更新,否则的话就会听取 WA 声一片(据说我们学长神犇就卡在这里好几回)
if(pos[r]-pos[l]<=2)
{
int res=0;
for(int i=l;i<=r;++i)
{
++tong[a[i]];
if(tong[a[i]]>tong[res]||(tong[a[i]]==tong[res]&&a[i]<res))res=a[i];
}
for(int i=l;i<=r;++i)tong[a[i]]=0;
return res;
}
int res=0;
for(int i=l;i<=R[pos[l]];++i)if(!tong[a[i]])tong[a[i]]+=s[pos[r]-1][a[i]]-s[pos[l]][a[i]];
for(int i=L[pos[r]];i<=r;++i)if(!tong[a[i]])tong[a[i]]+=s[pos[r]-1][a[i]]-s[pos[l]][a[i]];
for(int i=l;i<=R[pos[l]];++i)
{
++tong[a[i]];
if(tong[a[i]]>tong[res]||(tong[a[i]]==tong[res]&&a[i]<res))res=a[i];
}
for(int i=L[pos[r]];i<=r;++i)
{
++tong[a[i]];
if(tong[a[i]]>tong[res]||(tong[a[i]]==tong[res]&&a[i]<res))res=a[i];
}
int k=p[pos[l]+1][pos[r]-1];
int lin=s[pos[r]-1][k]-s[pos[l]][k];
for(int i=l;i<=R[pos[l]];++i)lin+=(a[i]==k);
for(int i=L[pos[r]];i<=r;++i)lin+=(a[i]==k);
if(lin>tong[res]||(lin==tong[res]&&k<res))res=k;
for(int i=l;i<=R[pos[l]];++i)tong[a[i]]=0;
for(int i=L[pos[r]];i<=r;++i)tong[a[i]]=0;
return res;
最后有几点要注意
- 注意离散化,否则直接RE到原地起飞。
- 注意 p 数组的转移,否则 WA 到怀疑人生。
- 注意分块的边界,否则TLE 到直接原地爆炸。
由于本蒟蒻水平太差,此篇题解参考了神犇(wjlss)的博客,如果有人看不懂的话,这里有链接QAQ wljss
最后本人代码一直炸,所以附上神犇的代码
#include<algorithm>
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
using namespace std;
int n,m,tot,len,l,r,ans;
const int N=40010,M=50010,K=510;
int a[N],b[N],pos[N],s[K][N],L[K],R[K],p[K][K],tong[N];
int read()
{
char ch;int x=0,f=1;
while(!isdigit(ch=getchar()))
{(ch=='-')&&(f=-f);}
while(isdigit(ch))
{x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return x*f;
}
int cnt=0;
void yych()//离散化
{
len=sqrt(n);
memset(L,0x3f,sizeof(L));
for(int i=1;i<=n;++i)
{
pos[i]=(i-1)/len+1;
L[pos[i]]=min(L[pos[i]],i);
R[pos[i]]=max(R[pos[i]],i);
a[i]=read();
b[i]=a[i];
}
sort(b+1,b+1+n);
tot=unique(b+1,b+1+n)-b-1;
for(int i=1;i<=n;++i)a[i]=lower_bound(b+1,b+1+tot,a[i])-b;
}
void Treaker()//分块预处理
{
for(int i=1;i<=n;++i)
for(int j=pos[i];j<=pos[n];++j)s[j][a[i]]++;
for(int i=1;i<=pos[n];++i)
{
memset(tong,0,sizeof(tong));
for(int j=i;j<=pos[n];++j)
{
p[i][j]=p[i][j-1];
for(int k=L[j];k<=R[j];++k)
{
tong[a[k]]++;
if((tong[a[k]]>tong[p[i][j]])||(tong[a[k]]==tong[p[i][j]]&&a[k]<p[i][j]))p[i][j]=a[k];
}
}
}
memset(tong,0,sizeof(tong));
}
int ask(int l,int r)
{
if(pos[r]-pos[l]<=2)
{
int res=0;
for(int i=l;i<=r;++i)
{
++tong[a[i]];
if(tong[a[i]]>tong[res]||(tong[a[i]]==tong[res]&&a[i]<res))res=a[i];
}
for(int i=l;i<=r;++i)tong[a[i]]=0;
return res;
}
int res=0;
for(int i=l;i<=R[pos[l]];++i)if(!tong[a[i]])tong[a[i]]+=s[pos[r]-1][a[i]]-s[pos[l]][a[i]];
for(int i=L[pos[r]];i<=r;++i)if(!tong[a[i]])tong[a[i]]+=s[pos[r]-1][a[i]]-s[pos[l]][a[i]];
for(int i=l;i<=R[pos[l]];++i)
{
++tong[a[i]];
if(tong[a[i]]>tong[res]||(tong[a[i]]==tong[res]&&a[i]<res))res=a[i];
}
for(int i=L[pos[r]];i<=r;++i)
{
++tong[a[i]];
if(tong[a[i]]>tong[res]||(tong[a[i]]==tong[res]&&a[i]<res))res=a[i];
}
int k=p[pos[l]+1][pos[r]-1];
int lin=s[pos[r]-1][k]-s[pos[l]][k];
for(int i=l;i<=R[pos[l]];++i)lin+=(a[i]==k);
for(int i=L[pos[r]];i<=r;++i)lin+=(a[i]==k);
if(lin>tong[res]||(lin==tong[res]&&k<res))res=k;
for(int i=l;i<=R[pos[l]];++i)tong[a[i]]=0;
for(int i=L[pos[r]];i<=r;++i)tong[a[i]]=0;
return res;
}
int main()
{
cin>>n>>m;
yych();
Treaker();
for(int i=1;i<=m;++i)
{
l=read();r=read();
l=(l+ans-1)%n+1;r=(r+ans-1)%n+1;
if(l>r)swap(l,r);
ans=b[ask(l,r)];
printf("%d\n",ans);
}
return 0;
}
然后你就可以AC 一道紫题(据说前几年这个还是一道黑题)
最后,分块的东西也就这么多,主要理解小块暴力,整块瞎搞就可以了。
最后附上习题。