更优体验请移步CSDNios
\(NOIP\)已过,训练难度瞬间变大。不少没有学过的知识点以各类方式出如今题目里。而本蒟蒻的脑子里只有那惨兮兮一点点的算法,因而本蒟蒻就开始走上恶补知识点的道路。算法
忽然想起来好久好久以前有道用分块作的题目,当时听的云里雾里,而后同年级的某位大佬表示:分块很简单。今天又听到同窗提及分块,就上OI-WIKI查了一下,没想到很快就理解而后敲题了……数组
分块实际上是一种思想,本质上跟暴力差很少,经常使用于处理区间问题。作法是将区间分红一个个小块,而后暴力维护,时间复杂度和分出来的块的个数及块的大小有关spa
结合例题讲解分块的具体操做.net
LOJ#6280.数列分块入门4code
给出一个长为\(n\)的数列,以及\(n\)个操做blog
操做有两种状况,每次操做输入4个数\(opt、l、r、c\)get
若\(opt=0\),表示将位于\([l,r]\)的之间的数字都加\(c\)。io
若\(opt=1\),表示询问位于\([l,r]\)的全部数字的和\(mod\ (c+1)\) 。入门
\(1\le n \le50000\),保证答案在\(long\ long\)范围内
只要学过线段树,第一秒想到的基本都会是线段树,可是这题有一个很大的不一样在于每次求值的时候会模一个非固定的数,那么若是要用线段树来作就须要在建树的时候不取模,求值的时候再取模,这样的话就有可能会致使线段树上的值超过\(long\ long\)甚至\(unsigned\ long\ long\)范围(\(\_\_int128\)就不要多想了)
因此咱们要抛弃线段树,去寻找另外一种解决方案。而分块,就是解决这个问题的一个很好的办法
对于一个区间,咱们把它分红若干个长度为\(s\)的块,最后一个块的长度能够不足\(s\),由于没有规定\(s\)必须是\(n\)的因数
一个区间\(a\)就能够分红
\(\underbrace{a_1,a_2\ldots,a_s}_{b_1},\underbrace{a_{s+1},\ldots,a_{2s}}_{b_2},\dots,\underbrace{a_{(s-1)\times s+1},\dots,a_n}_{b_{\frac{n}{s}}}\)
其中\(b_i\)维护第\(i\)个块内的和,能够在读入的时候就记录好每一个元素是哪一个块,同时维护\(b\)数组
分类讨论一下
若是\(l,r\)在同一块内,则直接暴力更改,时间复杂度\(O(s)\)
若是不在,就能够分三部分:(1)以\(l\)开头的一个不完整块;(2)中间若干个完整块;(3)以\(r\)结尾的一个不完整块。其中(1)(3)部分能够暴力更改,(2)部分就直接更改\(b_i\),以及\(x_i\)。其中\(x_i\)表示整个区间加上了多少,时间复杂度\(O(\dfrac{n}{s}+s )\)
跟更改很像,也是要分类讨论
\(l,r\)在同一个块内就直接暴力统计,同时注意加上\(x\)数组,时间复杂度\(O(s)\)
不在同一个块内也是分三部分,分法同更改部分,(1)(3)部分查询也是暴力,跟\(l,r\)同块同样,注意加上\(x\)数组。(2)部分直接加上中间完整块的\(b\)数组,这里就不用加上\(x\)数组。时间复杂度\(O(\dfrac{n}{s}+s)\)
综合更改和查询,一次操做的时间复杂度就是\(O(\dfrac{n}{s}+s)\),显然当\(s\)是\(\sqrt{n}\)的时候是最优的,那么一次操做的时间复杂度就是\(O(\sqrt{n})\),总时间复杂度\(O(n\sqrt{n})\)
#include<cstdio> #include<cmath> #define ll long long using namespace std; int n,s,opt,l,r,x; ll ans,a[50005],id[50005],b[50005],c[50005]; int read() { int res=0,fh=1;char ch=getchar(); while (ch<'0'||ch>'9') {if (ch=='-') fh=-1;ch=getchar();} while (ch>='0'&&ch<='9') res=res*10+ch-'0',ch=getchar(); return res*fh; } int main() { n=read(); s=sqrt(n); for (int i=1;i<=n;++i) { a[i]=read(); id[i]=(i-1)/s+1; b[id[i]]+=a[i]; } for (int i=1;i<=n;++i) { opt=read();l=read();r=read();x=read(); if (!opt) { if (id[l]==id[r]) { for (int j=l;j<=r;++j) a[j]+=x,b[id[l]]+=x; } else { for (int j=l;id[j]==id[l];++j) a[j]+=x,b[id[l]]+=x; for (int j=id[l]+1;j<id[r];++j) c[j]+=x,b[j]+=s*x; for (int j=r;id[j]==id[r];--j) a[j]+=x,b[id[r]]+=x; } } else { ans=0; if (id[l]==id[r]) { for (int j=l;j<=r;++j) ans=(ans+a[j]+c[id[l]])%(x+1); } else { for (int j=l;id[j]==id[l];++j) ans=(ans+a[j]+c[id[l]])%(x+1); for (int j=id[l]+1;j<id[r];++j) ans=(ans+b[j])%(x+1); for (int j=r;id[r]==id[j];--j) ans=(ans+a[j]+c[id[r]])%(x+1); } printf("%lld\n",ans); } } return 0; }
在\(N(1\le N\le100000)\)个数\(A_1\dots A_n\)组成的序列上进行\(M(1\le M\le100000)\)次操做,操做有两种:
(1)\(1\ L\ R\ C\):表示把\(A_L\)到\(A_R\)增长\(C\)\((|C|\le10000)\);
(2)\(2\ L\ R\):询问\(A_L\)到\(A_R\)之间的最大值。
其实这是线段树的模板题,放到这里来是想要体现分块在更改的时候的一个注意事项(实际上是为了加字数)
首先仍是分块的基本操做,每一个块长度为\(s\),同时维护每一个块内的最大值\(b\)数组
注意到\(C\)的取值范围加了绝对值,就说明\(C\)多是负数。那么若是修改区间内有最大值,就可能会影响最大值。因此说若是咱们不是修改整个块,那么就须要从新统计更改后块的最大值
\(l,r\)同块的无需多言,直接暴力更改,同时从新维护块的最大值。时间复杂度\(O(s)\)
\(l,r\)不一样块的仍是照样分红三部分,头和尾暴力更改,更新最大值,中间的块给整个区间加上\(C\),同时最大值能够直接加\(C\)(能够简单推理获得)。时间复杂度\(O(\dfrac{n}{s}+s)\)
\(l,r\)同块直接暴力,记得加上给挂在整个块的值
\(l,r\)不一样块也是分红三部分,中间部分直接与\(b_i\)进行比较,首尾暴力比较(不要漏掉挂在块上的值)
\(s\)仍是取\(\sqrt{n}\)最优,时间复杂度仍然是\(O(n\sqrt{n})\)
#include<cmath> #include<cstdio> #include<iostream> #define ll long long using namespace std; int n,m,len,l,r,x,opt; ll mx,a[100005],id[100005],b[100005],c[100005]; int read() { int res=0,fh=1;char ch=getchar(); while (ch<'0'||ch>'9') {if (ch=='-') fh=-1;ch=getchar();} while (ch>='0'&&ch<='9') res=res*10+ch-'0',ch=getchar(); return res*fh; } int main() { freopen("max10.in","r",stdin); freopen("max10.txt","w",stdout); n=read(); len=sqrt(n); for (int i=1;i<=n;++i) { a[i]=read(); id[i]=(i-1)/len+1; b[id[i]]=max(b[id[i]],a[i]); } m=read(); while (m--) { opt=read(); if (opt==1) { l=read();r=read();x=read(); if (id[l]==id[r]) { for (int i=l;i<=r;++i) a[i]+=x; b[id[l]]=-2147483647; for (int i=(id[l]-1)*len+1;id[i]==id[l];++i) b[id[l]]=max(b[id[i]],a[i]+c[id[i]]); } else { for (int i=l;id[i]==id[l];++i) a[i]+=x; for (int i=id[l]+1;i<id[r];++i) c[i]+=x,b[i]+=x; for (int i=r;id[i]==id[r];--i) a[i]+=x; b[id[l]]=b[id[r]]=-2147483647; for (int i=(id[l]-1)*len+1;id[i]==id[l];++i) b[id[l]]=max(b[id[i]],a[i]+c[id[i]]); for (int i=(id[r]-1)*len+1;id[i]==id[r];++i) b[id[r]]=max(b[id[i]],a[i]+c[id[i]]); } } else { mx=-2147483647; l=read();r=read(); if (id[l]==id[r]) { for (int i=l;i<=r;++i) mx=max(mx,a[i]+c[id[l]]); } else { for (int i=l;id[i]==id[l];++i) mx=max(mx,a[i]+c[id[l]]); for (int i=id[l]+1;i<id[r];++i) mx=max(mx,b[i]); for (int i=r;id[i]==id[r];--i) mx=max(mx,a[i]+c[id[r]]); } printf("%lld\n",mx); } } return 0; }
分块实际上是一种十分暴力的思想,旨在把区间分割开来,因此说在分块的时候,必定要合理控制块的大小及个数,并非全部题目\(s\)都是取\(\sqrt{n}\)最优,要根据问题选择合适的\(s\)
另外分块也能够作一些奇奇怪怪的毒瘤题,例如吉司机线段树……
祝你们新年快乐!!!