前序
分块是一种基于暴力的解决在线区间查询和区间修改等常见区间操作的算法,可以说是一种优雅的暴力,而且莫队等其他算法也是在这个算法的基础上进行的改进,所以说是很经典有用的技巧和思想。
知识
分块顾名思义就是把区间分成一块一块的处理,然后维护的是一个块里面的值,以达到快速解决题目给出的问题。这类算法思想不难理解,难的在于怎样运用,具体思想和代码实现可以参考UESTC的算法讲堂。
这个算法的时间复杂度通常是O(nsqrt(n))的,当然这个也取决于我们分的块的大小和块的数量。
通常情况下根据不等式默认把块的大小定为O(sqrt(n)),这样一次操作的时间复杂就是O(sqrt(n))。
注意分块是在线的,因此对于一些奇怪的区间查询和区间修改操作,或者线段树解决不了的问题,这个算法通常都可以解决。
不过要注意时间复杂度,保证分块能过!
习题
其实分块的重点应该在于维护怎样的值,所以就需要大量的习题练习。
当然,最经典的入门分块习题就是黄学长的数列分块入门(「分块」数列分块入门1 – 9 by hzwer )
所以下面的习题和解析都是来自黄学长,我这里只是给出我自己的代码实现。
一些涉及到的词语解释:
区间:数列中连续一段的元素
区间操作:将某个区间[a,b]的所有元素进行某种改动的操作
块:我们将数列划分成若干个不相交的区间,每个区间称为一个块
整块:在一个区间操作时,完整包含于区间的块
不完整的块:在一个区间操作时,只有部分包含于区间的块,即区间左右端点所在的两个块
分块入门 1 by hzwer
给出一个长为n的数列,以及n个操作,操作涉及区间加法,单点查值。
这是一道能用许多数据结构优化的经典题,可以用于不同数据结构训练。
数列分块就是把数列中每m个元素打包起来,达到优化算法的目的。
以此题为例,如果我们把每m个元素分为一块,共有n/m块,每次区间加的操作会涉及O(n/m)个整块,以及区间两侧两个不完整的块中至多2m个元素。
我们给每个块设置一个加法标记(就是记录这个块中元素一起加了多少),每次操作对每个整块直接O(1)标记,而不完整的块由于元素比较少,暴力修改元素的值。
每次询问时返回元素的值加上其所在块的加法标记。
这样每次操作的复杂度是O(n/m)+O(m),根据均值不等式,当m取√n时总复杂度最低,为了方便,我们都默认下文的分块大小为√n。
代码实现:
#include <iostream> #include <cmath> #include <algorithm> #include <cstdio> using namespace std; const int N = 1e5 + 7; int block, num, belong[N], l[N], r[N], n, a[N], tag[N]; void build() { block = sqrt(n); num = n / block; if (n % block) num++; for (int i = 1; i <= num; i++) l[i] = (i - 1) * block + 1, r[i] = i * block; r[num] = n; for (int i = 1; i <= n; i++) belong[i] = (i - 1) / block + 1; } void add(int x, int y, int val) { if (belong[x] == belong[y]) { for (int i = x; i <= y; i++) a[i] += val; return; } for (int i = x; i <= r[belong[x]]; i++) a[i] += val; for (int i = belong[x] + 1; i <= belong[y] - 1; i++) tag[i] += val; for (int i = l[belong[y]]; i <= y; i++) a[i] += val; } int main() { scanf("%d", &n); build(); for (int i = 1; i <= n; i++) scanf("%d", &a[i]); for (int i = 1; i <= n; i++) { int opt, x, y, val; scanf("%d%d%d%d", &opt, &x, &y, &val); if (opt == 0) add(x, y, val); else printf("%lld\n", a[y] + tag[belong[y]]); } return 0; }
分块入门 2 by hzwer
给出一个长为n的数列,以及n个操作,操作涉及区间加法,询问区间内小于某个值x的元素个数。
有了上一题的经验,我们可以发现,数列简单分块问题实际上有三项东西要我们思考:
对于每次区间操作:
1.不完整的块 的O(√n)个元素怎么处理?
2.O(√n)个 整块 怎么处理?
3.要预处理什么信息(复杂度不能超过后面的操作)?
我们先来思考只有询问操作的情况,不完整的块枚举统计即可;而要在每个整块内寻找小于一个值的元素数,于是我们不得不要求块内元素是有序的,这样就能使用二分法对块内查询,需要预处理时每块做一遍排序,复杂度O(nlogn),每次查询在√n个块内二分,以及暴力2√n个元素,总复杂度O(nlogn + n√nlog√n)。
可以通过均值不等式计算出更优的分块大小,就不展开讨论了
那么区间加怎么办呢?
套用第一题的方法,维护一个加法标记,略有区别的地方在于,不完整的块修改后可能会使得该块内数字乱序,所以头尾两个不完整块需要重新排序,复杂度分析略。
在加法标记下的询问操作,块外还是暴力,查询小于(x – 加法标记)的元素个数,块内用(x – 加法标记)作为二分的值即可。
代码实现:
#include<iostream> #include<algorithm> #include<cmath> #include<cstring> #include<cstdio> #include<cstdlib> #include<vector> #include<map> #include<set> #include<stack> #include<queue> #define PI atan(1.0)*4 #define e 2.718281828 #define rp(i,s,t) for (i = (s); i <= (t); i++) #define RP(i,s,t) for (i = (t); i >= (s); i--) #define ll long long #define ull unsigned long long #define mst(a,b) memset(a,b,sizeof(a)) // #define push_back() pb() #define pair<int,int> pii; #define fastIn \ ios_base::sync_with_stdio(0); \ cin.tie(0); using namespace std; inline int read() { int a=0,b=1; char c=getchar(); while(c<'0'||c>'9') { if(c=='-') b=-1; c=getchar(); } while(c>='0'&&c<='9') { a=(a<<3)+(a<<1)+c-'0'; c=getchar(); } return a*b; } inline void write(int n) { if(n<0) { putchar('-'); n=-n; } if(n>=10) write(n/10); putchar(n%10+'0'); } const int N=5e4+7; int n,l[N],r[N],num,block,a[N],tag[N],belong[N]; vector<int>ve[N]; void build(){ block=sqrt(n); num=n/block;if(n%block) num++; int i; rp(i,1,num) l[i]=(i-1)*block+1,r[i]=block*i; r[num]=n; rp(i,1,n){ belong[i]=(i-1)/block+1; ve[belong[i]].push_back(a[i]); } rp(i,1,num) sort(ve[i].begin(),ve[i].end()); } void reset(int x){ int i; ve[x].clear(); rp(i,l[x],r[x]) ve[x].push_back(a[i]); sort(ve[x].begin(),ve[x].end()); } void add(int x,int y,int val){ int i; if(belong[x]==belong[y]){ rp(i,x,y) a[i]+=val; reset(belong[x]); return ; } rp(i,x,r[belong[x]]) a[i]+=val; reset(belong[x]); rp(i,belong[x]+1,belong[y]-1) tag[i]+=val; rp(i,l[belong[y]],y) a[i]+=val; reset(belong[y]); } int query(int x,int y,int val){ int ans=0,i; if(belong[x]==belong[y]){ rp(i,x,y) if(a[i]+tag[belong[x]]<val) ans++; return ans; } rp(i,x,r[belong[x]]) if(a[i]+tag[belong[x]]<val) ans++; rp(i,l[belong[y]],y) if(a[i]+tag[belong[y]]<val) ans++; rp(i,belong[x]+1,belong[y]-1) ans+=lower_bound(ve[i].begin(),ve[i].end(),val-tag[i])-ve[i].begin(); return ans; } int main(){ n=read(); int i; rp(i,1,n) a[i]=read(); build(); rp(i,1,n){ int opt=read(),x=read(),y=read(),val=read(); if(opt==0) add(x,y,val); else printf("%d\n",query(x,y,val*val)); } return 0; }
分块入门 3 by hzwer
给出一个长为n的数列,以及n个操作,操作涉及区间加法,询问区间内小于某个值x的前驱(比其小的最大元素)。
n<=100000其实是为了区分暴力和一些常数较大的写法。
接着第二题的解法,其实只要把块内查询的二分稍作修改即可。
不过这题其实想表达:可以在块内维护其它结构使其更具有拓展性,比如放一个 set ,这样如果还有插入、删除元素的操作,会更加的方便。
分块的调试检测技巧:
可以生成一些大数据,然后用两份分块大小不同的代码来对拍,还可以根据运行时间尝试调整分块大小,减小常数。
代码实现
#include<iostream> #include<algorithm> #include<cmath> #include<cstring> #include<cstdio> #include<cstdlib> #include<vector> #include<map> #include<set> #include<stack> #include<queue> #define PI atan(1.0)*4 #define e 2.718281828 #define rp(i,s,t) for (i = (s); i <= (t); i++) #define RP(i,s,t) for (i = (t); i >= (s); i--) #define ll long long #define ull unsigned long long #define mst(a,b) memset(a,b,sizeof(a)) // #define push_back() pb() #define pair<int,int> pii; #define fastIn \ ios_base::sync_with_stdio(0); \ cin.tie(0); using namespace std; inline int read() { int a=0,b=1; char c=getchar(); while(c<'0'||c>'9') { if(c=='-') b=-1; c=getchar(); } while(c>='0'&&c<='9') { a=(a<<3)+(a<<1)+c-'0'; c=getchar(); } return a*b; } inline void write(int n) { if(n<0) { putchar('-'); n=-n; } if(n>=10) write(n/10); putchar(n%10+'0'); } const int N=1e5+7; int n,l[N],r[N],num,block,a[N],tag[N],belong[N]; set<int>ve[N]; void build(){ block=sqrt(n); num=n/block;if(n%block) num++; int i; rp(i,1,num) l[i]=(i-1)*block+1,r[i]=block*i; r[num]=n; rp(i,1,n){ belong[i]=(i-1)/block+1; ve[belong[i]].insert(a[i]); } } void add(int x,int y,int val){ int i; if(belong[x]==belong[y]){ rp(i,x,y){ ve[belong[x]].erase(a[i]); a[i]+=val; ve[belong[x]].insert(a[i]); } return ; } rp(i,x,r[belong[x]]){ ve[belong[x]].erase(a[i]); a[i]+=val; ve[belong[x]].insert(a[i]); } rp(i,belong[x]+1,belong[y]-1) tag[i]+=val; rp(i,l[belong[y]],y){ ve[belong[y]].erase(a[i]); a[i]+=val; ve[belong[y]].insert(a[i]); } } int query(int x,int y,int val){ int ans=-1,i; if(belong[x]==belong[y]){ rp(i,x,y) if(a[i]+tag[belong[x]]<val) ans=max(ans,a[i]+tag[belong[x]]); return ans; } rp(i,x,r[belong[x]]) if(a[i]+tag[belong[x]]<val) ans=max(ans,a[i]+tag[belong[x]]); rp(i,l[belong[y]],y) if(a[i]+tag[belong[y]]<val) ans=max(ans,a[i]+tag[belong[y]]); rp(i,belong[x]+1,belong[y]-1){ set<int>:: iterator it=ve[i].lower_bound(val-tag[i]); if(it==ve[i].begin()) continue;//没找到 it--; ans=max(ans,*it+tag[i]); } return ans; } int main(){ n=read(); int i; rp(i,1,n) a[i]=read(); build(); rp(i,1,n){ int opt=read(),x=read(),y=read(),val=read(); if(opt==0) add(x,y,val); else printf("%d\n",query(x,y,val)); } return 0; }
分块入门 4 by hzwer
给出一个长为n的数列,以及n个操作,操作涉及区间加法,区间求和。
这题的询问变成了区间上的询问,不完整的块还是暴力;而要想快速统计完整块的答案,需要维护每个块的元素和,先要预处理一下。
考虑区间修改操作,不完整的块直接改,顺便更新块的元素和;完整的块类似之前标记的做法,直接根据块的元素和所加的值计算元素和的增量。
代码实现
#include<iostream> #include<algorithm> #include<cmath> #include<cstring> #include<cstdio> #include<cstdlib> #include<vector> #include<map> #include<set> #include<stack> #include<queue> #define PI atan(1.0)*4 #define e 2.718281828 #define rp(i,s,t) for (i = (s); i <= (t); i++) #define RP(i,s,t) for (i = (t); i >= (s); i--) #define ll long long #define ull unsigned long long #define mst(a,b) memset(a,b,sizeof(a)) #define push_back() pb() #define pair<int,int> pii; #define fastIn \ ios_base::sync_with_stdio(0); \ cin.tie(0); using namespace std; inline int read() { int a=0,b=1; char c=getchar(); while(c<'0'||c>'9') { if(c=='-') b=-1; c=getchar(); } while(c>='0'&&c<='9') { a=(a<<3)+(a<<1)+c-'0'; c=getchar(); } return a*b; } inline void write(int n) { if(n<0) { putchar('-'); n=-n; } if(n>=10) write(n/10); putchar(n%10+'0'); } const int N=5e4+7; int num,block,l[N],r[N],belong[N],n; ll sum[N],a[N],tag[N]; void build(){ int i; block=sqrt(n); num=n/block;if(n%block) num++; rp(i,1,num) l[i]=(i-1)*block+1,r[i]=i*block; r[num]=n; rp(i,1,n){ belong[i]=(i-1)/block+1; sum[belong[i]]+=a[i]; } } void add(int x,int y,int val){ int i; if(belong[x]==belong[y]){ rp(i,x,y){ sum[belong[x]]-=a[i]; a[i]+=val; sum[belong[x]]+=a[i]; } return ; } rp(i,x,r[belong[x]]){ sum[belong[x]]-=a[i]; a[i]+=val; sum[belong[x]]+=a[i]; } rp(i,l[belong[y]],y){ sum[belong[y]]-=a[i]; a[i]+=val; sum[belong[y]]+=a[i]; } rp(i,belong[x]+1,belong[y]-1) tag[i]+=val; } ll query(int x,int y,int val){ ll ans=0,i; if(belong[x]==belong[y]){ rp(i,x,y) ans=(ans+a[i]+tag[belong[x]])%(val+1); return ans; } rp(i,x,r[belong[x]]) ans=(ans+a[i]+tag[belong[x]])%(val+1); rp(i,l[belong[y]],y) ans=(ans+a[i]+tag[belong[y]])%(val+1); rp(i,belong[x]+1,belong[y]-1) ans=(ans+sum[i]+block*tag[i])%(val+1);//sum数组维护的整块的和,而且加的是整个块修改后的值。 return ans%(val+1); } int main(){ n=read(); int i; rp(i,1,n) a[i]=read(); build(); rp(i,1,n){ int opt=read(),x=read(),y=read(),val=read(); if(opt==0) add(x,y,val); if(opt==1) printf("%d\n",query(x,y,val)%(val+1)); } return 0; }
分块入门 5 by hzwer
给出一个长为n的数列,以及n个操作,操作涉及区间开方,区间求和。
稍作思考可以发现,开方操作比较棘手,主要是对于整块开方时,必须要知道每一个元素,才能知道他们开方后的和,也就是说,难以快速对一个块信息进行更新。
看来我们要另辟蹊径。不难发现,这题的修改就只有下取整开方,而一个数经过几次开方之后,它的值就会变成 0 或者 1。
如果每次区间开方只不涉及完整的块,意味着不超过2√n个元素,直接暴力即可。
如果涉及了一些完整的块,这些块经过几次操作以后就会都变成 0 / 1,于是我们采取一种分块优化的暴力做法,只要每个整块暴力开方后,记录一下元素是否都变成了 0 / 1,区间修改时跳过那些全为 0 / 1 的块即可。
这样每个元素至多被开方不超过4次,显然复杂度没有问题。
代码实现
#include<iostream> #include<algorithm> #include<cmath> #include<cstring> #include<cstdio> #include<cstdlib> #include<vector> #include<map> #include<set> #include<stack> #include<queue> #define PI atan(1.0)*4 #define e 2.718281828 #define rp(i,s,t) for (i = (s); i <= (t); i++) #define RP(i,s,t) for (i = (t); i >= (s); i--) #define ll long long #define ull unsigned long long #define mst(a,b) memset(a,b,sizeof(a)) #define push_back() pb() #define pair<int,int> pii; #define fastIn \ ios_base::sync_with_stdio(0); \ cin.tie(0); using namespace std; inline int read() { int a=0,b=1; char c=getchar(); while(c<'0'||c>'9') { if(c=='-') b=-1; c=getchar(); } while(c>='0'&&c<='9') { a=(a<<3)+(a<<1)+c-'0'; c=getchar(); } return a*b; } inline void write(int n) { if(n<0) { putchar('-'); n=-n; } if(n>=10) write(n/10); putchar(n%10+'0'); } const int N = 1e5 + 7; int block, num, belong[N], l[N], r[N], n, a[N], tag[N],sum[N]; void build() { block = sqrt(n); num = n / block; if (n % block) num++; for (int i = 1; i <= num; i++) l[i] = (i - 1) * block + 1, r[i] = i * block; r[num] = n; for (int i = 1; i <= n; i++){ belong[i] = (i - 1) / block + 1; sum[belong[i]]+=a[i]; } } void solve_sqrt(int x){ int i; if(tag[x]) return ; tag[x]=1; sum[x]=0; rp(i,l[x],r[x]){ a[i]=sqrt(a[i]); sum[x]+=a[i]; if(a[i]>1) tag[x]=0; } } void Sqrt(int x, int y) { if (belong[x] == belong[y]) { for (int i = x; i <= y; i++){ sum[belong[x]]-=a[i]; a[i] = sqrt(a[i]); sum[belong[x]]+=a[i]; } return; } for (int i = x; i <= r[belong[x]]; i++){ sum[belong[x]]-=a[i]; a[i] = sqrt(a[i]); sum[belong[x]]+=a[i]; } for (int i = belong[x] + 1; i <= belong[y] - 1; i++) solve_sqrt(i); for (int i = l[belong[y]]; i <= y; i++){ sum[belong[y]]-=a[i]; a[i] = sqrt(a[i]); sum[belong[y]]+=a[i]; } } int query(int x,int y){ int ans=0,i; if(belong[x]==belong[y]){ rp(i,x,y) ans+=a[i]; return ans; } rp(i,x,r[belong[x]]) ans+=a[i]; rp(i,l[belong[y]],y) ans+=a[i]; rp(i,belong[x]+1,belong[y]-1) ans+=sum[i]; return ans; } int main(){ n=read(); int i; rp(i,1,n) a[i]=read(); build(); rp(i,1,n){ int opt=read(),x=read(),y=read(),val=read(); if(opt==0) Sqrt(x,y); else printf("%d\n",query(x,y)); } return 0; }
分块入门 6 by hzwer
给出一个长为n的数列,以及n个操作,操作涉及单点插入,单点询问,数据随机生成。
先说随机数据的情况
之前提到过,如果我们块内用数组以外的数据结构,能够支持其它不一样的操作,比如此题每块内可以放一个动态的数组,每次插入时先找到位置所在的块,再暴力插入,把块内的其它元素直接向后移动一位,当然用链表也是可以的。
查询的时候类似,复杂度分析略。
但是这样做有个问题,如果数据不随机怎么办?
如果先在一个块有大量单点插入,这个块的大小会大大超过√n,那块内的暴力就没有复杂度保证了。
还需要引入一个操作:重新分块(重构)
每根号n次插入后,重新把数列平均分一下块,重构需要的复杂度为O(n),重构的次数为√n,所以重构的复杂度没有问题,而且保证了每个块的大小相对均衡。
当然,也可以当某个块过大时重构,或者只把这个块分成两半。代码实现
#include<iostream> #include<algorithm> #include<cmath> #include<cstring> #include<cstdio> #include<cstdlib> #include<vector> #include<map> #include<set> #include<stack> #include<queue> #define PI atan(1.0)*4 #define e 2.718281828 #define rp(i,s,t) for (i = (s); i <= (t); i++) #define RP(i,s,t) for (i = (t); i >= (s); i--) #define ll long long #define ull unsigned long long #define mst(a,b) memset(a,b,sizeof(a)) // #define push_back() pb() // #define pair<int,int> pii // #define make_pair(a,b) mk(a,b) #define fastIn \ ios_base::sync_with_stdio(0); \ cin.tie(0); using namespace std; inline int read() { int a=0,b=1; char c=getchar(); while(c<'0'||c>'9') { if(c=='-') b=-1; c=getchar(); } while(c>='0'&&c<='9') { a=(a<<3)+(a<<1)+c-'0'; c=getchar(); } return a*b; } inline void write(int n) { if(n<0) { putchar('-'); n=-n; } if(n>=10) write(n/10); putchar(n%10+'0'); } int n,num,block; const int N=2e5+7; vector<int> ve[N]; int v[N],a[N],top; pair<int,int> query(int xx){ int x=1; while(xx>ve[x].size()) xx-=ve[x].size(),x++; return make_pair(x,xx-1); } void rebuild() { top=0; int i; rp(i,1,num){ for(vector<int>::iterator it=ve[i].begin();it!=ve[i].end();it++) v[++top]=*it; ve[i].clear(); } block=sqrt(top); rp(i,1,top) ve[(i-1)/block+1].push_back(v[i]); num=(top-1)/block+1; } void insert(int x,int y){ pair<int,int> nn=query(x); ve[nn.first].insert(ve[nn.first].begin()+nn.second,y); if(ve[nn.first].size()>20*block) rebuild(); } int main(){ n=read();block=sqrt(n); int i; rp(i,1,n) a[i]=read(); rp(i,1,n) ve[(i-1)/block+1].push_back(a[i]); num=(n-1)/block+1; rp(i,1,n){ int opt=read(),x=read(),y=read(),val=read(); if(opt==0) insert(x,y); else{ pair<int,int> nn=query(y); printf("%d\n",ve[nn.first][nn.second]); } } return 0; }
分块入门 7 by hzwer
给出一个长为n的数列,以及n个操作,操作涉及区间乘法,区间加法,单点询问。
很显然,如果只有区间乘法,和分块入门 1 的做法没有本质区别,但要思考如何同时维护两种标记。
我们让乘法标记的优先级高于加法(如果反过来的话,新的加法标记无法处理)
若当前的一个块乘以m1后加上a1,这时进行一个乘m2的操作,则原来的标记变成m1*m2,a1*m2
若当前的一个块乘以m1后加上a1,这时进行一个加a2的操作,则原来的标记变成m1,a1+a2
代码实现:
#include<iostream> #include<algorithm> #include<cmath> #include<cstring> #include<cstdio> #include<cstdlib> #include<vector> #include<map> #include<set> #include<stack> #include<queue> #define PI atan(1.0)*4 #define e 2.718281828 #define rp(i,s,t) for (i = (s); i <= (t); i++) #define RP(i,s,t) for (i = (t); i >= (s); i--) #define ll long long #define ull unsigned long long #define mst(a,b) memset(a,b,sizeof(a)) #define push_back() pb() #define pair<int,int> pii; #define fastIn \ ios_base::sync_with_stdio(0); \ cin.tie(0); using namespace std; inline int read() { int a=0,b=1; char c=getchar(); while(c<'0'||c>'9') { if(c=='-') b=-1; c=getchar(); } while(c>='0'&&c<='9') { a=(a<<3)+(a<<1)+c-'0'; c=getchar(); } return a*b; } inline void write(int n) { if(n<0) { putchar('-'); n=-n; } if(n>=10) write(n/10); putchar(n%10+'0'); } const int N=1e5+7; const int mod=1e4+7; int n,num,block,l[N],r[N],belong[N],a[N]; int tag1[N],tag2[N]; void build() { int i; block=sqrt(n); num=n/block;if(n%block) num++; rp(i,1,num) l[i]=(i-1)*block+1,r[i]=i*block; r[num]=n; rp(i,1,n) belong[i]=(i-1)/block+1; rp(i,1,num) tag2[i]=1; } void reset(int x){ int i; rp(i,l[x],r[x]) a[i]=(a[i]*tag2[x]+tag1[x])%mod; tag1[x]=0,tag2[x]=1; } void solve(int opt,int x,int y,int val) { reset(belong[x]); int i; if(belong[x]==belong[y]){ rp(i,x,y){ if(opt==0) a[i]+=val; else a[i]*=val; a[i]%=mod; } return ; } rp(i,x,r[belong[x]]){ if(opt==0) a[i]+=val; else a[i]*=val; a[i]%=mod; } reset(belong[y]); rp(i,l[belong[y]],y){ if(opt==0) a[i]+=val; else a[i]*=val; a[i]%=mod; } rp(i,belong[x]+1,belong[y]-1){ if(opt==0) tag1[i]=(tag1[i]+val)%mod; else tag1[i]=(tag1[i]*val)%mod,tag2[i]=(tag2[i]*val)%mod; } } int main(){ n=read(); int i; rp(i,1,n) a[i]=read(); build(); rp(i,1,n){ int opt=read(),x=read(),y=read(),val=read(); if(opt==2) printf("%d\n",(a[y]*tag2[belong[y]]+tag1[belong[y]])%mod); else solve(opt,x,y,val); } return 0; }
分块入门 8 by hzwer
给出一个长为n的数列,以及n个操作,操作涉及区间询问等于一个数c的元素,并将这个区间的所有元素改为c。
区间修改没有什么难度,这题难在区间查询比较奇怪,因为权值种类比较多,似乎没有什么好的维护方法。
模拟一些数据可以发现,询问后一整段都会被修改,几次询问后数列可能只剩下几段不同的区间了。
我们思考这样一个暴力,还是分块,维护每个分块是否只有一种权值,区间操作的时候,对于同权值的一个块就O(1)统计答案,否则暴力统计答案,并修改标记,不完整的块也暴力。
这样看似最差情况每次都会耗费O(n)的时间,但其实可以这样分析:
假设初始序列都是同一个值,那么查询是O(√n),如果这时进行一个区间操作,它最多破坏首尾2个块的标记,所以只能使后面的询问至多多2个块的暴力时间,所以均摊每次操作复杂度还是O(√n)。
换句话说,要想让一个操作耗费O(n)的时间,要先花费√n个操作对数列进行修改。
初始序列不同值,经过类似分析后,就可以放心的暴力啦。
代码实现
#include<iostream> #include<algorithm> #include<cmath> #include<cstring> #include<cstdio> #include<cstdlib> #include<vector> #include<map> #include<set> #include<stack> #include<queue> #define PI atan(1.0)*4 #define e 2.718281828 #define rp(i,s,t) for (i = (s); i <= (t); i++) #define RP(i,s,t) for (i = (t); i >= (s); i--) #define ll long long #define ull unsigned long long #define mst(a,b) memset(a,b,sizeof(a)) #define push_back() pb() #define pair<int,int> pii; #define fastIn \ ios_base::sync_with_stdio(0); \ cin.tie(0); using namespace std; inline int read() { int a=0,b=1; char c=getchar(); while(c<'0'||c>'9') { if(c=='-') b=-1; c=getchar(); } while(c>='0'&&c<='9') { a=(a<<3)+(a<<1)+c-'0'; c=getchar(); } return a*b; } inline void write(int n) { if(n<0) { putchar('-'); n=-n; } if(n>=10) write(n/10); putchar(n%10+'0'); } const int N=1e5+7; int n,num,block,l[N],r[N],belong[N]; int a[N],tag[N]; void build() { memset(tag,-1,sizeof(tag)); int i; block=sqrt(n); num=n/block;if(n%block) num++; rp(i,1,num) l[i]=(i-1)*block+1,r[i]=block*i; r[num]=n; rp(i,1,n) belong[i]=(i-1)/block+1; } void reset(int x) { int i; if(tag[x]==-1) return ; rp(i,l[x],r[x]) a[i]=tag[x]; tag[x]=-1; } int query(int x,int y,int val){ int res=0,i,j; reset(belong[x]); if(belong[x]==belong[y]){ rp(i,x,y){ if(a[i]==val) res++; else a[i]=val; } return res; } rp(i,x,r[belong[x]]){ if(a[i]==val) res++; else a[i]=val; } reset(belong[y]); rp(i,l[belong[y]],y){ if(a[i]==val) res++; else a[i]=val; } rp(i,belong[x]+1,belong[y]-1){ if(tag[i]!=-1){ if(tag[i]!=val) tag[i]=val; else res+=block; } else{ rp(j,l[i],r[i]){ if(a[j]!=val) a[j]=val; else res++; } tag[i]=val; } } return res; } int main(){ n=read(); int i; build(); rp(i,1,n) a[i]=read(); rp(i,1,n){ int x=read(),y=read(),val=read(); printf("%d\n",query(x,y,val)); } return 0; }
分块入门 9 by hzwer
给出一个长为n的数列,以及n个操作,操作涉及询问区间的最小众数
这是一个经典难题,我们通常维护数据结构来解决这个问题,但是这也是一类很经典的分块习题。
如果离线的话,莫队和树状数组都可以解决,但是强制在线的话就能用分块来解决。
如果不涉及修改操作的话有两种解法,一种是二分,另一种是预处理。
很明显第一个时间复杂度为O(),第二个为。
关于这类问题,陈立杰的论文里面讲的特别好:区间众数解题报告
第一种算法的代码实现(时间复杂度为)
#include<iostream> #include<algorithm> #include<cmath> #include<cstring> #include<cstdio> #include<cstdlib> #include<vector> #include<map> #include<set> #include<stack> #include<queue> #define PI atan(1.0)*4 #define e 2.718281828 #define rp(i,s,t) for (i = (s); i <= (t); i++) #define RP(i,s,t) for (i = (t); i >= (s); i--) #define ll long long #define ull unsigned long long #define mst(a,b) memset(a,b,sizeof(a)) // #define push_back() pb() // #define pair<int,int> pii; #define inf 0x7fffffff #define fastIn \ ios_base::sync_with_stdio(0); \ cin.tie(0); using namespace std; inline int read() { int a=0,b=1; char c=getchar(); while(c<'0'||c>'9') { if(c=='-') b=-1; c=getchar(); } while(c>='0'&&c<='9') { a=(a<<3)+(a<<1)+c-'0'; c=getchar(); } return a*b; } inline void write(int n) { if(n<0) { putchar('-'); n=-n; } if(n>=10) write(n/10); putchar(n%10+'0'); } const int N=1e5+7; //belong用来存储属于哪一个块,l和r数组表示每一块的左右边界,num表示分的块数,block表示块的大小 int belong[N],l[N],r[N],num,block,n,a[N],id=0; //id表示有多少种数 int val[N],cnt[N]; //val[i]用来存储离散化前对应的真实值 //cnt[i]表示第i种数的个数 int f[10005][10005]; //f[i][j]存储从第i个块开始到第j个块的所有数的最小众数(离散化后的) map<int,int> mp; //mp用来离散化 vector<int>ve[N]; //ve用来存储每一种数(离散后)包含的数的坐标 void build(){//初始化 block=30;//玄学过 num=n/block;if(n%block) num++; int i; rp(i,1,num) l[i]=(i-1)*block+1,r[i]=i*block; r[num]=n; rp(i,1,n) belong[i]=(i-1)/block+1; } void init(int x) { mst(cnt,0); int ans=0,mx=0,i; rp(i,l[x],n){ cnt[a[i]]++; int t=belong[i]; if(cnt[a[i]]>mx||(cnt[a[i]]==mx&&val[a[i]]<val[ans])) mx=cnt[a[i]],ans=a[i]; f[x][t]=ans; } } int query(int x,int y,int v){//二分去找[x,y]中有多少值为v的数 int t=upper_bound(ve[v].begin(),ve[v].end(),y)-lower_bound(ve[v].begin(),ve[v].end(),x); return t; } int query(int x,int y){//查询[x,y]中的最小众数的离散化后的下标 int ans=f[belong[x]+1][belong[y]-1];//ans初始化为整块之间的最小众数(离散化后) int mx=query(x,y,ans),i;//mx表示区间内ans的个数 if(belong[x]==belong[y]){//属于同一块,直接暴力查询,并更新值 rp(i,x,y){ int t=query(x,y,a[i]); if(t>mx||(t==mx&&val[a[i]]<val[ans])) mx=t,ans=a[i]; } return ans; } rp(i,x,r[belong[x]]){//暴力查询左边不完整块并更新值 int t=query(x,y,a[i]); if(t>mx||(t==mx&&val[a[i]]<val[ans])) mx=t,ans=a[i]; } rp(i,l[belong[y]],y){//暴力查询右边不完整块并更新值 int t=query(x,y,a[i]); if(t>mx||(t==mx&&val[a[i]]<val[ans])) mx=t,ans=a[i]; } return ans; } int main(){ int i; n=read(); rp(i,1,n){ a[i]=read(); /*离散化*/ if(mp[a[i]]==0){ mp[a[i]]=++id; val[id]=a[i]; } a[i]=mp[a[i]]; /*离散化*/ ve[a[i]].push_back(i); } build(); rp(i,1,num) init(i);//预处理求出f数组 rp(i,1,n){ int x=read(),y=read(); if(x>y) swap(x,y); printf("%d\n",val[query(x,y)]); } return 0; }
第二种算法的代码实现(时间复杂度为O(),题目来源:luoguP4135 作诗)
记得开O2优化
/* Luogu P4135 记得要开O2优化,这里的做法时间复杂度是O((n+q)*sqrt(n)),即预处理优化了一下 常规做法为二分,时间复杂度为O((n+q)*sqrt(n)*log(n)) */ #include<iostream> #include<algorithm> #include<cmath> #include<cstring> #include<cstdio> #include<cstdlib> #include<vector> #include<map> #include<set> #include<stack> #include<queue> #define PI atan(1.0)*4 #define e 2.718281828 #define rp(i,s,t) for (i = (s); i <= (t); i++) #define RP(i,s,t) for (i = (t); i >= (s); i--) #define ll long long #define ull unsigned long long #define mst(a,b) memset(a,b,sizeof(a)) #define push_back() pb() #define pair<int,int> pii; #define fastIn \ ios_base::sync_with_stdio(0); \ cin.tie(0); using namespace std; inline int read() { int a=0,b=1; char c=getchar(); while(c<'0'||c>'9') { if(c=='-') b=-1; c=getchar(); } while(c>='0'&&c<='9') { a=(a<<3)+(a<<1)+c-'0'; c=getchar(); } return a*b; } inline void write(int n) { if(n<0) { putchar('-'); n=-n; } if(n>=10) write(n/10); putchar(n%10+'0'); } const int N = 1e5 + 7; const int B = 320; int n, m, c,block, a[N], l[B], r[B], belong[N],num; int sum[B][N], ans[B][B], cnt[N]; //sum[i][j]表示从第一块到第i块中j的出现次数(即前缀和) //ans[i][j]表示从第i块到第j块中出现次数为正偶数的数的个数(查询时用来快速求区间内整块的出现次数为正偶数的个数 //cnt[i]用来统计i的出现次数 inline void init() { int i,j; block = sqrt(n); num=n/block;if(n%block) num++; rp(i,1,num) l[i]=(i-1)*block+1,r[i]=i*block; r[num]=n; rp(i,1,n) belong[i]=(i-1)/block+1; //先预处理求出每一块内每个数的出现次数 rp(i,1,num) rp(j,l[i],r[i]) sum[i][a[j]]++; //利用前缀和求出1~i区间内每个数的出现次数 rp(i,1,c) rp(j,1,num) sum[j][i] += sum[j - 1][i]; //预处理求出ans数组 rp(i,1,num) {//枚举每一块 int res = 0; rp(j,l[i],n){//枚举后面每一块中的每一个数并用res记录 cnt[a[j]]++; if(cnt[a[j]] > 0 && cnt[a[j]] % 2 == 0) res++; else if(cnt[a[j]] > 2) res--; ans[i][belong[j]] = res; } rp(j,l[i],n) cnt[a[j]]--;//统计后要记得消除原先的影响 } } int query(int x, int y) { int res = 0,i; if(belong[x]==belong[y]) {//同一块内直接暴力 rp(i,x,y) { cnt[a[i]]++; if(cnt[a[i]] > 0 && cnt[a[i]] % 2 == 0) res++; else if(cnt[a[i]] > 2) res--; } rp(i,x,y) cnt[a[i]]--;//消除影响 return res; } else { res = ans[belong[x] + 1][belong[y] - 1]; rp(i,x,r[belong[x]]) { cnt[a[i]]++; int tmp = sum[belong[y] - 1][a[i]] - sum[belong[x]][a[i]] + cnt[a[i]]; //先求出区间内的整块里面a[i]出现的个数,再加上当前的个数,判断是否为正偶数就行了 if(tmp > 0 && tmp % 2 == 0) res++; else if(tmp > 2) res--; } rp(i,l[belong[y]],y) { cnt[a[i]]++; int tmp = cnt[a[i]] + sum[belong[y] - 1][a[i]] - sum[belong[x]][a[i]]; if(tmp > 0 && tmp % 2 == 0) res++; else if(tmp > 2) res--; } rp(i,x,r[belong[x]]) cnt[a[i]]--;//消除影响 rp(i,l[belong[y]],y) cnt[a[i]]--; return res; } } int main() { int i; n=read(),c=read(),m=read(); rp(i,1,n) a[i]=read(); init(); int ans=0; while(m--){ int x=read(),y=read(); x=(x+ans)%n+1, y=(y+ans)%n+1; if(x>y) swap(x,y); ans=query(x, y);//用来记录上一次查询的结果 printf("%d\n",ans); } return 0; }
待修改的求区间众数的代码实现:
#include <bits/stdc++.h> using namespace std; const int maxn = 20005, N = 28; int sum[N][maxn], a[maxn], cc[N][N]; int tmp[maxn], tim[N][N][maxn], bb[N][N][maxn]; //第i~j块数字x出现的次数(记bb[i][j][x]) //第i~j块出现次数为x的个数(记tim[i][j][x] //前i块数字j出现的次数(定义为sum[i][j]) //cc[i][j]计算众数的出现次数 void solve() { int n, Q; scanf("%d%d", &n, &Q); for (int i = 0; i < n; i++) scanf("%d", &a[i]); int block=(int)pow(n, 1.0 / 3) + 1; int num =n/block;if(n%block) num++; for (int i = 1; i <= num; i++) { for (int j = 0; j < n; j++) sum[i][j] = sum[i - 1][j]; for (int j = (i - 1) * block; j < n && j < i * block; j++) sum[i][a[j]]++; } for (int i = 1; i <= num; i++) { for (int j = i; j <= num; j++) { cc[i][j] = cc[i][j - 1]; for (int k = 0; k < n; k++) //初始化 { bb[i][j][k] = bb[i][j - 1][k]; tim[i][j][k] = tim[i][j - 1][k]; } for (int k = (j - 1) * block; k < n && k < j * block; k++) { int p = bb[i][j][a[k]]++; tim[i][j][p]--; tim[i][j][p + 1]++; int x = sum[j][a[k]] - sum[i - 1][a[k]]; if (x > cc[i][j]) cc[i][j] = x; } } } int op, L, R, ans; while (Q--) { scanf("%d%d%d", &op, &L, &R); if (op)//查询众数的出现次数 { int x = L / block + 1, y = R / block + 1; ans = cc[x + 1][y - 1]; for (int i = L; i <= R && i < x * block; i++) tmp[a[i]] = max(0, sum[y - 1][a[i]] - sum[x][a[i]]); for (int i = (y - 1) * block; i <= R; i++) tmp[a[i]] = max(0, sum[y - 1][a[i]] - sum[x][a[i]]); for (int i = L; i < x * block && i <= R; i++) { tmp[a[i]]++; if (tmp[a[i]] > ans) ans = tmp[a[i]]; } if (x != y) { for (int i = (y - 1) * block; i <= R; i++) { tmp[a[i]]++; if (tmp[a[i]] > ans) ans = tmp[a[i]]; } } printf("%d\n", ans); } else { int p = a[L]; a[L] = R; int x = L / block + 1; for (int i = x; i <= num; i++) { sum[i][p]--; sum[i][R]++; } for (int i = 1; i <= num; i++) { for (int j = i; j <= num; j++) { if (i <= x && x <= j) //包含x块 { int t = bb[i][j][p]--; tim[i][j][t]--; //次数和个数变化 tim[i][j][t - 1]++; t = bb[i][j][R]++; tim[i][j][t]--; tim[i][j][t + 1]++; t = cc[i][j]; if (tim[i][j][t - 1]) //判断是否存在个数 cc[i][j] = t - 1; if (tim[i][j][t]) cc[i][j] = t; if (tim[i][j][t + 1]) cc[i][j] = t + 1; } } } } } } int main() { solve(); return 0; } /* 5 3 1 1 2 2 2 1 0 4 0 1 2 1 0 4 */