分块
其实就是把讲课的课件整理一下
算是对于分块的详解(?)了吧
首先,我们来考虑这样一个模型:有一段连续的老板的垃圾桶塞人序列a[1]~a[n],然后现在我们需要执行几类操作:
1.求出其中一段垃圾桶内人数的和
鸭我会!前缀和!
2.区间垃圾桶里塞人
Emmmm线段树!树状数组!
3. 查询一段区间上有多少个垃圾桶里人数<k (k>0且给定)
。。。平。。平衡树?
4. k每次都不一样,并极其的多。另外,为了防止装*不让写平衡树+线段树,占用空间不能超过****Mb
告辞
友情提示:由于老板往垃圾桶里塞了太多人所以会爆long long
并且lbg和yxm一定在同一个垃圾桶里(似乎并没啥用【滑稽
啊对,yfl和gjm也在同一个垃圾桶里www
何谓分块
分块,顾名思义,就是把一段序列分成一小块一小块得来处理,维护。
我们把每一块当成一个整体,只记录维护整体的有关信息,就是分块。
(对没错就是暴力
(而且一点不优雅
首先,回到之前那道题,很朴素的做法就是:
- 从询问区间的l到r扫过去,每回加上扫到的值
- 直接把a[i]重新赋值不就得了
- 从询问区间的l到r扫过去,每回遇到<k的位置,答案+1
(嗯你没看错,红果果的暴力
但是,分块就是在这个基础上优化的!!!
假设我们总共的序列长度为n,然后我们把它切成多块,然后把每一块里的东西当成一个整体来看
我们先考虑前两个问题
区间加和区间求和
1.对于完整的块,我们希望有个东西能直接找出这整个块的和,于是每个块要维护这个块的所有元素的和。
对于不完整块,因为元素比较少,我们可以直接暴力扫这个小块统计答案,
2.这里,我们换种思路,记录一个lazy标记表示整个块被加上过多少了,
对于完整块,我们直接lazy += 加上的数x,块内的和ans += x * 元素个数(因为每个元素都被加上了x)
对于不完整块,直接暴力修改就好了,顺便可以把lazy标记清了
至于分块具体怎么操作嘛。。。
(我想您们这么强肯定都会orz
但是在此我还是叨叨一遍分块中要用到的几个名词和对每个块的提前处理
(您们大可以不看跳过
- 区间:数列中连续一段的元素
- 区间操作:将某个区间[a,b]的所有元素进行某种改动的操作
- 块:我们将数列划分成若干个不相交的区间,每个区间称为一个块
- 整块:在一个区间操作时,完整包含于区间的块
- 不完整的块:在一个区间操作时,只有部分包含于区间的块,即区间左右端点所在的两个块
首先我们既然用到了分块
就要提前把块分出来
(这不是废话嘛
代码如下
int n; scanf("%d",&n); for(int i = 1; i <= n; i++) scanf("%d",&a[i]); sq = sqrt(n); for(int i = 1; i <= n; i++) block[i] = (i - 1) / sq + 1;
单点查询的话可以只用一个block数组记录每个数所在的块
一个f作为每一块加法标记
如果是区间查询的话,再加上一个sum数组记录区间和
还有的题是求小于k的值的个数或前驱
这种时候需要用vector和set等stl来排序便于查找k的位置
下面这份代码是让大家看一下处理区间的流程
首先是暴力修稿l到l所在块的右端点,或者是r(如果r也在l所在块中
然后若l与r不在同一块中
再去暴力修改r所在块的左端点到r
(以上的暴力修改就是对前文所提到过的不完整的块的操作
最后将完整的块打上标记
void add(int l,int r,int c) { for(int i = l; i <= min(block[l] * sq,r); i++) a[i] += c; if(block[l] != block[r]) for(int i = (block[r] - 1) * sq + 1; i <= r; i++) a[i] += c; for(int i = block[l] + 1; i <= block[r] - 1; i++) f[i] += c; }
了解了分块的基础之后,我们就可以用分块帮助老板统计往垃圾桶里塞的人了!
我们看一哈第一道题(老板对于往垃圾桶里塞人有各种各样的问题)
有一段连续的老板的垃圾桶塞人序列a[1]~a[n],然后现在我们需要执行几类操作:
- 求出其中一个垃圾桶内的人数
- 区间垃圾桶里塞人
第一行输入一个数字n。
第二行输入n个数字,第i个数字为a[i],以空格隔开。
接下来输入n行询问,每行输入四个数字opt、l、r、c,以空格隔开。
若opt=0,表示将位于[l,r]的之间的数字都加c。
若opt=1,表示询问a[r]的值(l和c忽略)
这就是一道简单的不能再简单的分块题了
我们给每个块设置一个加法标记(就是记录这个块中元素一起加了多少),每次操作对每个整块直接O(1)标记,而不完整的块由于元素比较少,暴力修改元素的值。
每次询问时返回元素的值加上其所在块的加法标记。
这样每次操作的复杂度是O(n/m)+O(m),根据均值不等式,当m取√n时总复杂度最低,为了方便,我们都默认下文的分块大小为√n。
#include<cstdio> #include<algorithm> #include<cmath> #define sev en using namespace std; #define N 50010 int a[N],block[N],f[N]; int sq; void add(int l,int r,int c) { for(int i = l; i <= min(block[l] * sq,r); i++) a[i] += c; if(block[l] != block[r]) for(int i = (block[r] - 1) * sq + 1; i <= r; i++) a[i] += c; for(int i = block[l] + 1; i <= block[r] - 1; i++) f[i] += c; } int main() { int n; scanf("%d",&n); for(int i = 1; i <= n; i++) scanf("%d",&a[i]); sq = sqrt(n); for(int i = 1; i <= n; i++) block[i] = (i - 1) / sq + 1; for(int i = 1; i <= n; i++) { int op,l,r,c; scanf("%d%d%d%d",&op,&l,&r,&c); if(op == 0) add(l,r,c); if(op == 1) printf("%d\n",a[r] + f[block[r]]); } return 0; }
既然懂了第一题
那么我们来看看老板提出的第二个问题
有一段连续的老板的垃圾桶塞人序列a[1]~a[n],然后现在我们需要执行几类操作:
- 区间垃圾桶里塞人
- 老板想看看区间[l,r]中有几个垃圾桶人数小于x,以便于下次塞人方便(尽管他知道了也依旧很我行我素爱往哪个里塞就往哪个里塞
输入格式同上
不过若opt=1,表示询问[l,r]中,小于c^2的数字的个数
我们先来思考只有询问操作的情况,不完整的块枚举统计即可
而在每个整块内寻找小于一个值的元素数,我们就要求块内元素是有序的,这样就能使用二分法对块内查询
需要预处理时每块做一遍排序,复杂度O(nlogn),每次查询在√n个块内二分,以及暴力2√n个元素,总复杂度O(nlogn + n√nlog√n)。
那么区间加怎么办呢?
套用第一题的方法,维护一个加法标记,略有区别的地方在于,不完整的块修改后可能会使得该块内数字乱序,所以头尾两个不完整块需要重新排序
在加法标记下的询问操作,块外还是暴力,查询小于(x – 加法标记)的元素个数
块内用(x – 加法标记)作为二分的值即可。
#include<cstdio> #include<algorithm> #include<cmath> #include<vector> #define sev en using namespace std; #define N 50010 int a[N],block[N],f[N]; int sq,n; vector<int>v[510]; void reset(int x) { v[x].clear(); for(int i = (x - 1) * sq + 1; i <= min(x * sq,n); i++) v[x].push_back(a[i]); sort(v[x].begin(),v[x].end()); } void add(int l,int r,int c) { for(int i = l; i <= min(block[l] * sq,r); i++) a[i] += c; reset(block[l]); if(block[l] != block[r]) { for(int i = (block[r] - 1) * sq + 1; i <= r; i++) a[i] += c; reset(block[r]); } for(int i = block[l] + 1; i <= block[r] - 1; i++) f[i] += c; } int query(int l,int r,int c) { int ans = 0; for(int i = l; i <= min(block[l] * sq,r); i++) if(a[i] + f[block[l]] < c) ans++; if(block[l] != block[r]) for(int i = (block[r] - 1) * sq + 1; i <= r; i++) if(a[i] + f[block[r]] < c) ans++; for(int i = block[l] + 1; i <= block[r] - 1; i++) { int x = c - f[i]; ans += lower_bound(v[i].begin(),v[i].end(),x) - v[i].begin(); } return ans; } int main() { scanf("%d",&n); for(int i = 1; i <= n; i++) scanf("%d",&a[i]); sq = sqrt(n); for(int i = 1; i <= n; i++) { block[i] = (i - 1) / sq + 1; v[block[i]].push_back(a[i]); } for(int i = 1;i <= block[n];i++) sort(v[i].begin(),v[i].end()); for(int i = 1; i <= n; i++) { int op,l,r,c; scanf("%d%d%d%d",&op,&l,&r,&c); if(op == 0) add(l,r,c); if(op == 1) printf("%d\n",query(l,r,c * c)); } return 0; }
第三道题
有一段连续的老板的垃圾桶塞人序列a[1]~a[n],然后现在我们需要执行几类操作:
- 区间垃圾桶里塞人
- 老板想看看区间[l,r]的垃圾桶中的总人数
(显然这道题很简单了
这题的询问变成了区间上的询问,不完整的块还是暴力
而要想快速统计完整块的答案,需要维护每个块的元素和sum,先要预处理一下。
区间修改的话,不完整的块直接改,顺便更新块的元素和
完整的块标记一下就可以了
#include<cstdio> #include<algorithm> #include<cmath> #define sev en using namespace std; #define N 50010 #define ll long long int block[N]; ll sum[N],f[N],a[N]; int sq; void add(int l,int r,int c) { for(int i = l; i <= min(block[l] * sq,r); i++) a[i] += c,sum[block[l]] += c; if(block[l] != block[r]) for(int i = (block[r] - 1) * sq + 1; i <= r; i++) a[i] += c,sum[block[r]] += c; for(int i = block[l] + 1; i <= block[r] - 1; i++) f[i] += c; } ll query(int l,int r) { ll ans = 0; for(int i = l; i <= min(block[l] * sq,r); i++) ans += a[i] + f[block[l]]; if(block[l] != block[r]) for(int i = (block[r] - 1) * sq + 1; i <= r; i++) ans += a[i] + f[block[r]]; for(int i = block[l] + 1; i <= block[r] - 1; i++) ans += sum[i] + sq * f[i]; return ans; } int main() { int n; scanf("%d",&n); for(int i = 1; i <= n; i++) scanf("%d",&a[i]); sq = sqrt(n); for(int i = 1; i <= n; i++) { block[i] = (i - 1) / sq + 1; sum[block[i]] += a[i]; } for(int i = 1; i <= n; i++) { int op,l,r,c; scanf("%d%d%d%d",&op,&l,&r,&c); if(op == 0) add(l,r,c); if(op == 1) printf("%d\n",query(l,r) % (c + 1)); } return 0; }
第四道题
有一段连续的垃圾桶的塞老板序列a[1]~a[n],然后现在我们需要执行几类操作:
垃圾桶觉得区间[l,r]中老板太多了,于是它准备扔一些老板出来,但是老板身为老板,又有一些怪癖,比如他想从这段区间开方地跳出来
垃圾桶想看看区间[l,r]中老板数
(因为老板太别扭所以我们就有了这些艰巨的任务,请对老板口诛笔伐
(在此声明,这道膜改是某wjh干的,不是我
(这道题的意思就是区间开方和区间求和
这题的修改只有下取整开方,而一个数经过几次开方之后,它的值就会变成0 或者1。
如果每次区间开方只不涉及完整的块,意味着不超过2√n个元素,直接暴力即可。
如果涉及了一些完整的块,这些块经过几次操作以后就会都变成0 / 1,于是我们采取一种分块优化的暴力做法,只要每个整块暴力开方后,记录一下元素是否都变成了0 / 1,区间修改时跳过那些全为0 / 1 的块即可。
这样每个元素至多被开方不超过4次
#include<cstdio> #include<cmath> #include<algorithm> #define sev en using namespace std; #define N 50010 int sq,n; int a[N],f[N],sum[N]; int block[N]; void sqr(int x) { if(f[x]) return ; f[x] = 1; sum[x] = 0; for(int i = (x - 1) * sq + 1; i <= x * sq; i++) { a[i] = sqrt(a[i]); sum[x] += a[i]; if(a[i] > 1) f[x] = 0; } } void add(int l,int r,int c) { for(int i = l; i <= min(block[l] * sq,r); i++) { sum[block[l]] -= a[i]; a[i] = sqrt(a[i]); sum[block[l]] += a[i]; } if(block[l] != block[r]) for(int i = (block[r] - 1) * sq + 1; i <= r; i++) { sum[block[r]] -= a[i]; a[i] = sqrt(a[i]); sum[block[r]] += a[i]; } for(int i = block[l] + 1; i <= block[r] - 1; i++) sqr(i); } int query(int l,int r) { int ans = 0; for(int i = l; i <= min(block[l] * sq,r); i++) ans += a[i]; if(block[l] != block[r]) for(int i = (block[r] - 1) * sq + 1; i <= r; i++) ans += a[i]; for(int i = block[l] + 1; i <= block[r] - 1; i++) ans += sum[i]; return ans; } int main() { scanf("%d",&n); for(int i = 1; i <= n; i++) scanf("%d",&a[i]); sq = sqrt(n); for(int i = 1; i <= n; i++) { block[i] = (i - 1) / sq + 1; sum[block[i]] += a[i]; } for(int i = 1; i <= n; i++) { int op,l,r,c; scanf("%d%d%d%d",&op,&l,&r,&c); if(op == 0) add(l,r,c); if(op == 1) printf("%d\n",query(l,r)); } return 0; }
Emmmm因为一些不可预料的事情
本来准备做的6-9题还有老板作诗和蒲公英就。。。咕咕咕
so
—————————————— 一周的分界线(gg强行阻挠咕咕咕)———————————————
但是
有一些东西吧。。
人算不如天算,世事无常
于是我鸽不了了。。。
所以我很苦【哔——】的码了作诗。。。
来看题面吧
题目描述
神犇yfl虐完IOI之后给dalao wjh出了一题:
gjm是yfl的小公主【大雾】,平时的一大爱好是作诗。
由于时间紧迫,gjm作完诗之后还要虐OI,于是gjm找来一篇长度为N的文章,阅读M次,每次只阅读其中连续的一段[l,r],从这一段中选出一些汉字构成诗。因为gjm喜欢对偶(yfl),所以gjm规定最后选出的每个汉字都必须在[l,r]里出现了正偶数次。而且gjm认为选出的汉字的种类数(两个一样的汉字称为同一种)越多越好(为了拿到更多的素材!)。于是gjm请wjh安排选法。
wjh这种dalao当然不想做这么简单的题了,于是把问题扔给了你……
问题简述:N个数,M组询问,每次问[l,r]中有多少个数出现正偶数次。
输入格式:
输入第一行三个整数n、c以及m。表示文章字数、汉字的种类数、要选择M次。
第二行有n个整数,每个数Ai在[1, c]间,代表一个编码为Ai的汉字。
接下来m行每行两个整数l和r,设上一个询问的答案为ans(第一个询问时ans=0),令L=(l+ans)mod n+1, R=(r+ans)mod n+1,若L>R,交换L和R,则本次询问为[L,R]。
输出格式:
输出共m行,每行一个整数,第i个数表示gjm第i次能选出的汉字的最多种类数。
其实这题就是预处理难想一点
cnt[i][j] 表示第i块开始到结尾,j的出现次数;
f[i][j]表示i块开头到j块末尾的答案;
#include<cstdio> #include<algorithm> #include<cmath> #define sev en using namespace std; #define N 100010 #define L 320 int n,m,c; int a[N],cnt[L][N]; int block[N],num; int ans,l,r; int f[L][L],vis[N]; int st[L],tmp[N]; void solve(int l,int r) { if(block[l] == block[r] || block[l] + 1 == block[r]){ for(int i = l; i <= r; i++) { vis[a[i]]++; if(vis[a[i]] % 2 == 0) ans++; else if(vis[a[i]] != 1) ans--; } for(int i = l; i <= r; i++) vis[a[i]] = 0; return ; } if(l == st[block[l]] && r == st[block[r] + 1] - 1){ ans = f[block[l]][block[r]]; return ; } else if(l == st[block[l]]) { ans = f[block[l]][block[r] - 1]; for(int i = st[block[r]]; i <= r; i++) vis[a[i]] = cnt[block[r] - 1][a[i]] - cnt[block[l] - 1][a[i]]; for(int i = st[block[r]]; i <= r; i++) { vis[a[i]]++; if(vis[a[i]] % 2 == 0) ans++; else if(vis[a[i]] != 1) ans--; } for(int i = st[block[r]]; i <= r; i++) vis[a[i]] = 0; return ; } else if(r == st[block[r] + 1] - 1) { ans = f[block[l] + 1][block[r]]; for(int i = l; i < st[block[l] + 1]; i++) vis[a[i]] = cnt[block[r]][a[i]] - cnt[block[l]][a[i]]; for(int i = l;i < st[block[l] + 1];i++){ vis[a[i]]++; if(vis[a[i]] % 2 == 0) ans++; else if(vis[a[i]] != 1) ans--; } for(int i = l;i < st[block[l] + 1];i++) vis[a[i]] = 0; return ; } else{ int x = block[l] + 1; int y = block[r] - 1; ans = f[x][y]; for(int i = l;i < st[x];i++) vis[a[i]] = cnt[y][a[i]] - cnt[x - 1][a[i]]; for(int i = st[y + 1];i <= r;i++) vis[a[i]] = cnt[y][a[i]] - cnt[x - 1][a[i]]; for(int i = l;i < st[x];i++){ vis[a[i]]++; if(vis[a[i]] % 2 == 0) ans++; else if(vis[a[i]] != 1) ans--; } for(int i = st[y + 1];i <= r;i++){ vis[a[i]]++; if(vis[a[i]] % 2 == 0) ans++; else if(vis[a[i]] != 1) ans--; } for(int i = l;i < st[x];i++) vis[a[i]] = 0; for(int i = st[y + 1];i <= r;i++) vis[a[i]] = 0; return ; } } int main() { scanf("%d%d%d",&n,&c,&m); int sq = sqrt(n); for(int i = 1; i <= n; i++) { scanf("%d",&a[i]); block[i] = (i - 1) / sq + 1; cnt[block[i]][a[i]]++; if(block[i] != block[i - 1]) st[block[i]] = i; } num = block[n]; for(int i = 1; i <= num; i++) for(int j = 1; j <= c; j++) cnt[i][j] += cnt[i - 1][j]; for(int i = 1; i <= num; i++) { int t = 0; for(int j = 1; j <= c; j++) tmp[j] = 0; for(int j = st[i]; j <= n; j++) { tmp[a[j]]++; if(tmp[a[j]] % 2 == 0) t++; else if(tmp[a[j]] != 1) t--; f[i][block[j]] = t; } } ans = 0; for(int i = 1; i <= m; i++) { scanf("%d%d",&l,&r); l = (l + ans) % n + 1; r = (r + ans) % n + 1; ans = 0; if(l > r) swap(l,r); solve(l,r); printf("%d\n",ans); } return 0; }
码完以上操作
我已经心力憔悴了。。。
但是,还有一道题叫做蒲公英。。。【怨念
[膜改]蒲公英
在乡下的小路旁长着很多dalao,而我们的问题正和这些dalao有关
为了简化起见,我们把所有的dalao看成一个长度为n的序列(a1,a2,…,an),其中ai为一个正整数,表示第i个dalao的名字
每个dalao都会出现很多次,比如dtx,clqq,hy,wjh,yfl,lbg…他们tql所以生长了很多(在小路旁)
每次询问区间[l,r],你需要回答区间里出现次数最多的是哪位dalao,如果有若干位dalao出现次数相同,则输出姓名编号最小的那位
注意,你的算法必须是在线的
再注意⚠️
以下并非标程,是吸氧才过的。。。
// luogu-judger-enable-o2 #include<cstdio> #include<map> #include<vector> #include<algorithm> #include<cstring> #include<cmath> #define sev en using namespace std; #define N 50010 int n,m,sq,id; map<int,int>mp; vector<int>v[N]; int cnt[N],f[510][510],val[N]; int a[N],block[N]; void pre(int x) { memset(cnt,0,sizeof(cnt)); int mx = 0,ans = 0; for(int i = (x - 1) * sq + 1; i <= n; i++) { cnt[a[i]]++; int t = block[i]; if(cnt[a[i]] > mx || (cnt[a[i]] == mx && val[a[i]] < val[ans])) ans = a[i],mx = cnt[a[i]]; f[x][t] = ans; } } int query(int l,int r,int x) { int t = upper_bound(v[x].begin(),v[x].end(),r) - lower_bound(v[x].begin(),v[x].end(),l); return t; } int query(int l,int r) { int ans,mx; ans = f[block[l] + 1][block[r] - 1]; mx = query(l,r,ans); for(int i = l; i <= min(block[l] * sq,r); i++) { int t = query(l,r,a[i]); if(t > mx || (t == mx && val[a[i]] < val[ans])) ans = a[i],mx = t; } if(block[l] != block[r]) { for(int i = (block[r] - 1) * sq + 1; i <= r; i++) { int t = query(l,r,a[i]); if(t > mx || (t == mx && val[a[i]] < val[ans])) ans = a[i],mx = t; } } return ans; } int main() { scanf("%d%d",&n,&m); sq = sqrt(n); int ans = 0; for(int i = 1; i <= n; i++) { scanf("%d",&a[i]); if(!mp[a[i]]) { mp[a[i]] = ++id; val[id] = a[i]; } a[i] = mp[a[i]]; v[a[i]].push_back(i); } for(int i = 1; i <= n; i++) block[i] = (i - 1) / sq + 1; for(int i = 1; i <= block[n]; i++) pre(i); for(int i = 1; i <= m; i++) { int l,r; scanf("%d%d",&l,&r); l = (l + ans - 1) % n + 1; r = (r + ans - 1) % n + 1; if(l > r) swap(l,r); ans = val[query(l,r)]; printf("%d\n",ans); } return 0; }
分块部分题目:
◦ Loj 6282-6285数列分块入门6-9(没有讲解,酌情AC
◦ Luogu P4135 作诗&& P4168[Violet]蒲公英 (码量巨大,酌情尝试
参考博客:
hzwer dalao的分块教程 orz
END.