分块入门 by hzwer
文章内容来自于「分块」数列分块入门1 – 9 by hzwer -
分块 - hzwer.com ,如有侵权,请联系作者删除
入门1
给出一个长为n的数列,以及n个操作,操作涉及区间加法,单点查值。
这是一道能用许多数据结构优化的经典题,可以用于不同数据结构训练。
数列分块就是把数列中每m个元素打包起来,达到优化算法的目的。
以此题为例,如果我们把每m个元素分为一块,共有\(O(\frac{n}{m})\) 块,每次区间加的操作会涉及\(O(\frac{n}{m})\) 个整块,以及区间两侧两个不完整的块中至多2m个元素。
我们给每个块设置一个加法标记(就是记录这个块中元素一起加了多少),每次操作对每个整块直接O(1)标记,而不完整的块由于元素比较少,暴力修改元素的值。
每次询问时返回元素的值加上其所在块的加法标记。
这样每次操作的复杂度是\(O(\frac{n}{m})+O(m)\) ,根据均值不等式,当\(m\) 取\(\sqrt{n}\) 时总复杂度最低,为了方便,我们都默认下文的分块大小为\(\sqrt{n}\)
#include <map> #include <set> #include <cmath> #include <stack> #include <queue> #include <cstdio> #include <vector> #include <cstring> #include <cstdlib> #include <iostream> #include <algorithm> #define mod 998244353 #define pi acos(-1) #define inf 0x7fffffff #define ll long long using namespace std;ll read () { ll x=0 ,f=1 ;char ch=getchar (); while (ch<'0' ||ch>'9' ){if (ch=='-' )f=-1 ;ch=getchar ();} while (ch>='0' &&ch<='9' ){x=x*10 +ch-'0' ;ch=getchar ();} return x*f; } int n,blo;int v[50005 ],bl[50005 ],atag[50005 ];void add (int a,int b,int c) { for (int i=a;i<=min (bl[a]*blo,b);i++) v[i]+=c; if (bl[a]!=bl[b]) for (int i=(bl[b]-1 )*blo+1 ;i<=b;i++) v[i]+=c; for (int i=bl[a]+1 ;i<=bl[b]-1 ;i++) atag[i]+=c; } int main () { n=read ();blo=sqrt (n); for (int i=1 ;i<=n;i++)v[i]=read (); for (int i=1 ;i<=n;i++)bl[i]=(i-1 )/blo+1 ; for (int i=1 ;i<=n;i++) { int f=read (),a=read (),b=read (),c=read (); if (f==0 )add (a,b,c); if (f==1 ){ printf ("%d\n" ,v[b]+atag[bl[b]]); cout<<v[b]<<" " <<atag[bl[b]]; } } return 0 ; }
入门2
给出一个长为n的数列,以及n个操作,操作涉及区间加法,询问区间内小于某个值x的元素个数。
有了上一题的经验,我们可以发现,数列简单分块问题实际上有三项东西要我们思考:
对于每次区间操作:
1.不完整的块 的\(O(\sqrt{n})\) 个元素怎么处理?
2.\(O(\sqrt{n})\) 个
整块 怎么处理?
3.要预处理什么信息(复杂度不能超过后面的操作)?
我们先来思考只有询问操作的情况,不完整的块枚举统计即可;而要在每个整块内寻找小于一个值的元素数,于是我们不得不要求块内元素是有序的,这样就能使用二分法对块内查询,需要预处理时每块做一遍排序,复杂度\(O(nlog\ n)\) ,每次查询在\(\sqrt{n}\) 个块内二分,以及暴力\(2\sqrt{n}\) 个元素,总复杂度\(O(nlog\ n+n\sqrt{n}log\sqrt{n})\) 。
可以通过均值不等式计算出更优的分块大小,就不展开讨论了
那么区间加怎么办呢?
套用第一题的方法,维护一个加法标记,略有区别的地方在于,不完整的块修改后可能会使得该块内数字乱序,所以头尾两个不完整块需要重新排序,复杂度分析略。
在加法标记下的询问操作,块外还是暴力,查询小于(x –
加法标记)的元素个数,块内用(x – 加法标记)作为二分的值即可。
#include <bits/stdc++.h> using namespace std;using ll=long long ;const int MAXN=1e5 +10 ;int aa[MAXN],be[MAXN],tag[MAXN];vector<int > v[MAXN]; int n,block;void reset (int x) { v[x].clear (); for (int i=(x-1 )*block+1 ;i<=min (n,x*block);i++){ v[x].push_back (aa[i]); } sort (v[x].begin (),v[x].end ()); } void add (int a,int b,int c) { for (int i=a;i<=min (b,be[a]*block);i++){ aa[i]+=c; } reset (be[a]); if (be[a]!=be[b]){ for (int i=(be[b]-1 )*block+1 ;i<=b;i++){ aa[i]+=c; } reset (be[b]); } for (int i=be[a]+1 ;i<=be[b]-1 ;i++){ tag[i]+=c; } } int query (int a,int b,int c) { int ans=0 ; for (int i=a;i<=min (be[a]*block,b);i++){ if (aa[i]+tag[be[i]]<c)ans++; } if (be[a]!=be[b]){ for (int i=block*(be[b]-1 )+1 ;i<=b;i++){ if (aa[i]+tag[be[i]]<c)ans++; } } for (int i=be[a]+1 ;i<=be[b]-1 ;i++){ int x=c-tag[i]; ans+=lower_bound (v[i].begin (),v[i].end (),x)-v[i].begin (); } return ans; } int main () { ios::sync_with_stdio (false ),cin.tie (nullptr ); n;cin>>n; for (int i=1 ;i<=n;i++)cin>>aa[i]; block=sqrt (n); for (int i=1 ;i<=n;i++){ be[i]=(i-1 )/block+1 ; v[be[i]].push_back (aa[i]); } for (int i=1 ;i<=be[n];i++){ sort (v[i].begin (),v[i].end ()); } for (int i=1 ;i<=n;i++){ int f,a,b,c;cin>>f>>a>>b>>c; if (f==0 )add (a,b,c); if (f==1 )cout<<query (a,b,c*c)<<"\n" ; } return 0 ; }
入门3
给出一个长为n的数列,以及n个操作,操作涉及区间加法,询问区间内小于某个值x的前驱(比其小的最大元素)。
n<=100000其实是为了区分暴力和一些常数较大的写法。
接着第二题的解法,其实只要把块内查询的二分稍作修改即可。
不过这题其实想表达:可以在块内维护其它结构使其更具有拓展性,比如放一个
set
,这样如果还有插入、删除元素的操作,会更加的方便。
分块的调试检测技巧:
可以生成一些大数据,然后用两份分块大小不同的代码来对拍,还可以根据运行时间尝试调整分块大小,减小常数。
#include <bits/stdc++.h> using namespace std;using ll = long long ;const int MAXN = 1e5 + 10 ;int a[MAXN], be[MAXN], tag[MAXN];int n, block;set<int > s[1050 ]; void add (int x, int y, int z) { for (int i = x; i <= min (y, be[x]*block); i++) { s[be[i]].erase (a[i]); a[i] += z; s[be[i]].insert (a[i]); } if (be[x] != be[y]) { for (int i = block * (be[y] - 1 ) + 1 ; i <= y; i++) { s[be[i]].erase (a[i]); a[i] += z; s[be[i]].insert (a[i]); } } for (int i = be[x] + 1 ; i <= be[y] - 1 ; i++) { tag[i] += z; } } int query (int x, int y, int z) { int ans = -1 ; for (int i = x; i <= min (y, be[x]*block); i++) { int val = a[i] + tag[be[i]]; if (val < z) ans = max (ans, val); } if (be[x] != be[y]) { for (int i = block * (be[y] - 1 ) + 1 ; i <= y; i++) { int val = a[i] + tag[be[i]]; if (val < z) ans = max (ans, val); } } for (int i = be[x] + 1 ; i <= be[y] - 1 ; i++) { int num = z - tag[i]; set<int >::iterator it = s[i].lower_bound (num); if (it == s[i].begin ()) continue ; --it; ans = max (ans, *it + tag[i]); } return ans; } int main () { ios::sync_with_stdio (false ), cin.tie (nullptr ); n; cin >> n; block = sqrt (n); for (int i = 1 ; i <= n; i++) cin >> a[i]; for (int i = 1 ; i <= n; i++) { be[i] = (i - 1 ) / block + 1 ; s[be[i]].insert (a[i]); } for (int i = 1 ; i <= n; i++) { int f, x, y, z; cin >> f >> x >> y >> z; if (f == 0 ) { add (x, y, z); } if (f == 1 ) { cout << query (x, y, z) << "\n" ; } } return 0 ; }