分块
由于我在网上找不到定义,只好编一个。
分块 是一种将问题分解成若干个子问题,逐项解决子问题后得到原问题答案的思想。
块
考虑这样一个问题。
你有一个序列 \(a[1...N]\),你需要写一个数据结构维护它,支持以下操作:
- 修改 \(a[x]\) 的值;
- 查询 \(a[L...R]\) 的最大值、和、按位与和、或和、异或和、与非和、或非和、异或非和。
事实上,这道题可以加上很多很多个操作,这样 线段树 也无能为力了。
我们不妨想想如何优化朴素程序。
以求和为例,朴素程序写法如下:
for(int i=L;i<=R;++i)
ans+=a[i]; //flag
flag 行一共被执行了 \(R-L+1\) 次,这不是我们希望看到的。
如果是这样写呢?
for(int i=L;i<=R;i+=2)
ans+=a[i]+a[i+1]; //flag
if((R-L+1)&1) ans-=a[R+1];
不难想到,flag 行只被执行了原来次数的一半。
设 flag 行将连续的 \(d\) 个 \(a\) 累加给 \(ans\),则一共需要执行 flag 行 \(\frac{R-L+1}{d}\) 次。
如果我们用一个变量维护这 \(d\) 个数,那么我们查询的时间复杂度就是 \(O(\frac{R-L+1}{d})\),修改操作的时间复杂度是 \(O(d)\)。
不妨令两操作次数相同,则程序有最低复杂度时,$$\frac{R-L+1}{d}=d$$即$$d=\sqrt{R-L+1}$$
换言之,当 \(d\) 的值等于操作区间(一般是 \([1,N]\))的 \(\frac12\) 次方时,该程序的时间复杂度最低,是 \(O(N^\frac12)\)(单次执行)。
我们不妨将被一并处理的 \(d\) 个数视作一块,那么原区间就被分成了 \(\sqrt N\) 块。
举个例子。我们现在要维护一个序列的和,分块后每个块的信息如下:
其中 \(a[i]\) 表示第 \(i\) 个块内每个数的和。
至此, 分块 已经介绍完毕了。下面我们看一道例题。
例题 您需要写一种数据结构,维护一个有序数列,支持以下操作:
- 查询 \(k\) 在区间内的排名;
- 查询区间内排名为 \(k\) 的值;
- 修改某一位值上的数值;
- 查询 \(k\) 在区间内的前驱(前驱定义为严格小于 \(x\),且最大的数);
- 查询 \(k\) 在区间内的后继(后继定义为严格大于 \(x\),且最小的数)。
参考代码
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
const int MAXN=50010;
const int MAXE=230;
const int MAXA=(int)1e8;
int n,m,sq;
int sx,sy,sd;
struct point{
int x,y;
friend bool operator<(const point a,const point b){
return a.y<b.y;
}
}a[MAXN];
struct split{
int l,r;
int mininum;
point e[MAXE];
void update(){
sort(e+1,e+(r-l+1)+1);
mininum=e[1].y;
}
void build(point p[MAXN],int L,int R){
if(R>n) R=n;
l=L;r=R;
for(int i=l;i<=r;++i)
e[i-l+1]=p[i];
update();
}
int query(int x){
int left=1,right=(r-l+1),mid,sum=0;
while(left<=right){
mid=(left+right)/2;
if(e[mid].y<x){
sum=mid;
left=mid+1;
}else
right=mid-1;
}
return sum;
}
int que(int left,int right,int x){
int cnt=0;
for(int i=l;i<=r;++i)
if(e[i-l+1].x>=max(l,left)&&e[i-l+1].x<=min(r,right)&&e[i-l+1].y<x) ++cnt;
return cnt;
}
int check(int left,int right,int x){
if(left<=l&&r<=right)
return mininum==x;
for(int i=max(l,left);i<=min(r,right);++i)
if(e[i-l+1].y==x) return 1;
return 0;
}
void change(int x,int d){
for(int i=1;i<=r-l+1;++i)
if(e[i].x==x){
e[i].y=d;
break;
}
update();
}
}s[MAXE];
int len=0;
int getnum(int x){
return (x-1)/sq+1;
}
int rank(int l,int r,int x){
int L=getnum(l),R=getnum(r);
int cnt=0;
for(int i=L+1;i<=R-1;++i)
cnt+=s[i].query(x);
cnt+=s[L].que(l,r,x);
if(L!=R) cnt+=s[R].que(l,r,x);
if(!cnt){
for(int i=L;i<=R;++i)
if(s[i].check(l,r,x))
return cnt+1;
return 1;
}
return cnt+1;
}
int find(int l,int r,int x){
int left=0,right=MAXA,mid,s=0;
while(left<=right){
mid=(left+right)/2;
if(rank(l,r,mid)<=x){
s=mid;
left=mid+1;
}else
right=mid-1;
}
return s;
}
void change(int x,int d){
s[getnum(x)].change(x,d);
}
int findupper(int l,int r,int x){
int rk=rank(l,r,x);
if(rk>r-l+1) return 2147483647;
bool flag=(find(l,r,rk)==x);
return find(l,r,rk+flag);
}
int findlower(int l,int r,int x){
int rk=rank(l,r,x);
if(rk<=1) return -2147483647;
return find(l,r,rk-1);
}
inline int read(){
int x=0; char c;
do c=getchar(); while(c<'0'||c>'9');
while(c>='0'&&c<='9')
x=x*10+c-48,c=getchar();
return x;
}
int main(){
n=read();m=read();sq=sqrt(n);
for(int i=1;i<=n;++i)
a[i]=(point){i,read()};
int cnt=0;
while(cnt<n){
s[++len].build(a,cnt+1,cnt+sq);
cnt+=sq;
}
for(int i=1;i<=m;++i){
sd=read();
switch(sd){
case 1:
sx=read();sy=read();sd=read();
int c;
printf("%d\n",c=rank(sx,sy,sd));
break;
case 2:
sx=read();sy=read();sd=read();
printf("%d\n",find(sx,sy,sd));
break;
case 3:
sx=read();sd=read();
change(sx,sd);
break;
case 4:
sx=read();sy=read();sd=read();
printf("%d\n",findlower(sx,sy,sd));
break;
case 5:
sx=read();sy=read();sd=read();
printf("%d\n",findupper(sx,sy,sd));
break;
default:
break;
}
}
}
下面列出了几道难度较低的练习题,有兴趣的读者可自行尝试。
练习 1 您需要设计一种数据结构,维护一个序列 \(a[1...N]\),支持以下操作:
- 将 \(a[x]\) 的值加 \(d\);
- 查询 \(a[L...R]\) 中小于等于 \(d\) 的数的个数。
练习 2 您需要设计一种数据结构,维护一个序列 \(a[1...N]\),支持以下操作:
- 将 \(a[L...R]\) 的值加 \(d\);
- 查询 \(a[L...R]\) 中小于 \(a[x]\) 的最大的数。
练习 3 (luogu P3372)您需要设计一种数据结构,维护一个序列 \(a[1...N]\),支持以下操作:
- 将 \(a[L...R]\) 的值加 \(d\);
- 查询 \(a[L...R]\) 的数的和。
练习 4 您需要设计一种数据结构,维护一个序列 \(a[1...N]\),支持以下操作:
- 将 \(a[L...R]\) 的值开方(向下取整);
- 查询 \(a[L...R]\) 的数的和。
练习 5 (luogu P3373)您需要设计一种数据结构,维护一个序列 \(a[1...N]\),支持以下操作:
- 将 \(a[L...R]\) 的值加 \(d\);
- 将 \(a[L...R]\) 的值乘 \(d\);
- 查询 \(a[L...R]\) 的数的和。
练习 6 您需要设计一种数据结构,维护一个序列 \(a[1...N]\),支持以下操作:
- 查询 \(a[L...R]\) 的数有多少个与 \(d\) 相等。将 \(a[L...R]\) 的每个数改成 \(d\)。