简单的分块
分块
望月悲叹的最初分块
分块,优雅的暴力
分块也是同线段树等结构一样,维护区间操作的,不同于线段树和树状数组的是,分块所维护的信息并不需要满足区间可加性,以此,分块可以处理许多线段树等结构不可以处理的问题
简单来说,分块就是将整个序列分为若干个大小相同的块(最后一个可能不同),然后对于每一个块再加以维护,在统计信息时,我们仅仅需要将所需区间\([l,r]\)中所完全包含的块的信息统计,然后对于两端零散的块执行暴力,修改同理
为了平衡复杂度,一般取块长\(block=\sqrt{n}\)
//块的基本操作
block=sqrt(n),siz=n%block==0?n/block:n/block+1;
int get(int x){//x属于哪一个块
return x%block?x/block+1:x/block;
}
int solve(int l,int r){
//枚举l,r属于的区间
if(get(r)-get(l)<2){
for(int i=l;i<=r;i++)……
}
else{
int lR=get(l)*block,rL=get(r)*(block-1)+1
for(int i=l;i<=lR;i++)……
for(int i=rL;i<=r;i++)……
for(int i=get(l)+1;i<=get(r)-1;i++)……
}
}
举一个简单的例子,分块维护区间加法,我们只需要将其分为块之后对于每一个块建立一个求和数组即可,然后类似线段树的,建立一个懒标记
int sum[1005],a[100005],lz[1005];
int block,siz,n,m;
int get(int x){
return x%block:x/block+1:x/block;
}
void init(){
scanf("%d",&n);
block=sqrt(n),siz=n%block?n/block+1:block;
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
}
for(int i=1;i<=siz;i++){
sum[get(i)]+=a[i];
}
}
void change(int l,int r,int d){//将[l,r]+d
if(get(r)-get(l)<2){
for(int i=l;i<=r;i++)a[i]+=d,sum[get(i)]+=d;
return ;
}
int lR=get(l)*block,rL=(get(r)-1)*block+1;
for(int i=l;i<=lR;i++)a[i]+=d,sum[get(i)]+=d;
for(int i=rL;i<=r;i++)a[i]+=d,sum[get(i)]+=d;
for(int i=get(l)+1;i<=get(r)-1;i++)lz[i]+=d;
}
int ask(int l,int r){
int ans=0;
if(get(r)-get(l)<2){
for(int i=l;i<=r;i++)ans+=a[i]+lz[get(i)];
}
int lR=get(l)*block,rL=(get(r)-1)*block+1;
for(int i=l;i<=lR;i++)ans+=a[i]+lz[get(i)];
for(int i=rL;i<=r;i++)ans+=a[i]+lz[get(i)];
for(int i=get(l)+1;i<=get(r)-1;i++)ans+=sum[i]+lz[i]*block;
}
下面我们来探讨分块模型的各种扩展
激进突刺的第二分块
蒲公英:静态维护区间众数问题,\(n\le 4\times 10^4,m\le 5\times 10^4\)
既然要维护区间众数,那么我们需要考虑和知晓的肯定是各个区间里各个数的出现的次数
于是进行分块,对于每一个块,求出一个\(sum\)数组,\(sum[i][j]\)表示在前\(i\)个块中数\(j\)的出现次数,下一步,我们需要统计出答案\(mx\)数组,\(mx[i][j]\)表示第\(i\)块到第\(j\)块的区间众数。因为有了\(sum\)数组,我们可以静态的枚举块,若块长为\(\sqrt{n}\),则有\(O(n)\)个区间,我们的问题就是如何\(O(n\sqrt{n})\)地预处理出\(mx\)了,可以如此设想:假若我们处理出了\(mx[i][j]\),求\(mx[i][j+1]\)的时候,我们可以扫描第\(j+1\)个块,当扫描到数\(x\)的时候,利用\(sum\)数组判断是否可以更新区间众数即可,我们就完成了\(\sqrt{n}\)时间内的状态转移,总计需要\(O(n\sqrt{n})\)的时间
下一步,我们应该考虑如何处理询问
可以采用二次扫描法,即对于区间\([l,r]\)来说,我们可以利用\(mx和sum\),然后对这个大块的两端零散部分,直接用这两个数组进行扫描,对于\(mx\)的更新操作类比预处理时即可,在得出答案之后,再重新倒过来执行一次过程,将修改操作撤销,就可以了。
在实际代码中,为了消去撤销操作,可以单独建立数组\(tot\)处理两端,为了代码的方便懒惰,在统计mx的时候可以建立一个一维数组\(cnt\),在枚举块起点的时候即可进行统计修改
int q,n,m,block,siz,sum[205][40005],vis[40005],a[40005],b[40005],c[40005];
int tot[40005],cnt[40005],last;
struct node {
int num,s;
}p[205][205];
int get(int x){
return x%block==0?x/block:x/block+1;
}
inline void init(){
scanf("%d%d",&n,&m);
block=sqrt(n),siz=n%block==0?n/block:n/block+1;
for(int i=1;i<=n;i++)
scanf("%d",&a[i]),c[i]=a[i];
sort(a+1,a+n+1);
int q=unique(a+1,a+n+1)-a-1;
for(int i=1;i<=n;i++){
int pos=lower_bound(a+1,a+q+1,c[i])-a;
b[pos]=c[i];
c[i]=pos;
}
for(int i=1;i<=siz;i++){
memset(cnt,0,sizeof(cnt));
node x;
x.num=x.s=0;
for(int j=i;j<=siz;j++){
for(int k=(j-1)* block+1;k<=min(n,j * block);k++){
cnt[c[k]]++;
if((cnt[c[k]]>x.s)||(cnt[c[k]]==x.s&&c[k]<x.num)){
x.num=c[k];
x.s=cnt[c[k]];
}
}
p[i][j]=x;
}
}
for(int i=1;i<=siz;i++){
for(int j=1;j<=q;j++)sum[i][j]=sum[i-1][j];
for(int j=(i-1)* block+1;j<=min(n,i * block);j++)sum[i][c[j]]++;
}
}
int solve(int l,int r){
int ans=0;
int L=get(l),R=get(r);
if(R-L<=2){//优雅暴力
for(int j=l;j<=r;j++)tot[c[j]]=0;
for(int j=l;j<=r;j++){
tot[c[j]]++;
if(tot[c[j]]>tot[ans]||(tot[c[j]]==tot[ans]&&ans>c[j]))ans=c[j];
}
}
else {
ans=p[L+1][R-1].num;
tot[ans]=0,vis[ans]=0;
for(int j=l;j<=min(n,L * block);j++)tot[c[j]]=0,vis[c[j]]=0;
for(int j=(R-1)* block+1;j<=r;j++)tot[c[j]]=0,vis[c[j]]=0;
for(int j=l;j<=min(n,L * block);j++)tot[c[j]]++;
for(int j=(R-1)* block+1;j<=r;j++)tot[c[j]]++;
int id,mx=0;
for(int j=l;j<=min(n,L * block);j++){
if(!vis[c[j]]){
vis[c[j]]=1;
int val=tot[c[j]]+sum[R-1][c[j]]-sum[L][c[j]];
if(mx < val||(mx==val&&id>c[j]))mx=val,id=c[j];
}
}
for(int j=(R-1)* block+1;j<=r;j++){
if(!vis[c[j]]){
vis[c[j]]=1;
int val=tot[c[j]]+sum[R-1][c[j]]-sum[L][c[j]];
if(mx < val||(mx==val&&id>c[j]))mx=val,id=c[j];
}
}
if(mx>tot[ans]+p[L+1][R-1].s||(mx==tot[ans]+p[L+1][R-1].s&&ans>id))ans=id;
}
last=b[ans];
return last;
}
int main(){
init();
for(int i=1;i<=m;i++){
int l,r;scanf("%d%d",&l,&r);
l=(l+last-1)%n+1;
r=(r+last-1)%n+1;
if(l>r)swap(l,r);
printf("%d\n",solve(l,r));
}
return 0;
}
其实对于本题,还有另外一种方法维护某个区间内某个数的出现次数,也即我门需要的\(cnt\),就是对每一个数开一个\(vector\),;里面存入每一个数在序列中每一次出现的位置(有序),然后对于查询\([l,r]\)中数出现的次数只需要二分查找然后将下标相减即可
值得一说的是,如果本题不强制在线,可考虑莫队解决,复杂度\(O(M\sqrt N)\)
作诗:静态维护区间出现正偶次数的数的个数,强制在线
这道题是上道题的一个变式
类似的,我们进行分块,\(cnt[i][j]\)表示第\(i\)块开始到结尾,\(j\)的出现次数;
\(f[i][j]\)表示\(i\)块开头到\(j\)块末尾的答案;
int get(int x){
return x%block?x/block:x/block+1;
}
//预处理部分
for (int i = 1; i <= siz; ++i) {
int t = 0;
for (int j = (i-1)*block+1; j <= n; ++j) {
cnt[i][a[j]]++;
if ((cnt[i][a[j]] & 1) && (cnt[i][a[j]] > 1)) t--;
else if ((cnt[i][a[j]] & 1) == 0) t++;
if (j%block==0) {
f[i][j/block] = t;
}
}
}
然后在统计答案的时候再新建立一个数组\(tot\)进行计数,记录两端的小块的数的出现次数,然后顺带就统计出这个数的出现次数进行分类讨论统计即可
#include<bits/stdc++.h>
using namespace std;
inline int read_int(){//快读
int x=0;char c=getchar();
while(c<'0'||c>'9')c=getchar();
while(c>='0'&&c<='9'){
x=(x<<3)+(x<<1)+(c-'0');
c=getchar();
}
return x;
}
int a[100005],num[350][100005];//块的专用桶,也就是上文的cnt
int ans[350][350];//从i到j的答案
int n,m,c;//意义如题
int lb,nb;//块长与块个数
int l,r;//意义如题
int ll[350],rr[350],mm[100005];//块的左端点、右端点及单点所在块//这样避免了get函数,常数会更小一点
inline int number(int l,int r,int x){//块l到块r中x的个数
return num[r][x]-num[l-1][x];
}
int A;//答案
int tong[100005];//临时桶,也即上文的tot
void solve(){
A=0;
int lb=mm[l],rb=mm[r];
if(rb-lb<=1){//没有整块,直接暴力
for(int i=l;i<=r;++i){
++tong[a[i]];
if(tong[a[i]]%2==0){
++A;
}else if(tong[a[i]]!=1){
--A;
}
}
//临时桶还原
for(int i=l;i<=r;++i){
--tong[a[i]];
}
}else{//一般情况
A=ans[lb+1][rb-1];//整块的答案
//左侧零散块暴力
for(int i=l;i<=rr[lb];++i){
++tong[a[i]];
if((tong[a[i]]+number(lb+1,rb-1,a[i]))%2==0){
++A;
}else if(tong[a[i]]+number(lb+1,rb-1,a[i])!=1){
--A;
}
}
//右侧零散块暴力
for(int i=ll[rb];i<=r;++i){
++tong[a[i]];
if((tong[a[i]]+number(lb+1,rb-1,a[i]))%2==0){
++A;
}else if(tong[a[i]]+number(lb+1,rb-1,a[i])!=1){
--A;
}
}
//临时桶还原
for(int i=l;i<=rr[lb];++i){
--tong[a[i]];
}
for(int i=ll[rb];i<=r;++i){
--tong[a[i]];
}
}
}
int main(){
n=read_int();c=read_int();m=read_int();
lb=sqrt(n);nb=n/lb;if(lb*nb<n)++nb;//块长与块个数
//预处理
for(int i=1;i<=nb;++i){//块端点预处理
ll[i]=(i-1)*lb+1;
rr[i]=(i<nb)?i*lb:n;
}
for(int i=1;i<=nb;++i){
for(int j=ll[i];j<=rr[i];++j){
a[j]=read_int();
mm[j]=i;//点所在块预处理
++num[i][a[j]];//桶预处理
}
for(int j=0;j<=c;++j){//前缀和
num[i][j]+=num[i-1][j];
}
}
for(int i=1;i<=nb;++i){//整块答案预处理
for(int j=i;j<=nb;++j){
ans[i][j]=ans[i][j-1];
for(int k=ll[j];k<=rr[j];++k){
++tong[a[k]];
if(tong[a[k]]%2==0){
++ans[i][j];
}else if(tong[a[k]]!=1){
--ans[i][j];
}
}
}
//临时桶还原
for(int j=0;j<=c;++j)tong[j]=0;
}
//解决
while(~--m){
l=read_int();r=read_int();
l=(l+A)%n+1;r=(r+A)%n+1;
if(l>r)swap(l,r);//解密
solve();
printf("%d\n",A);
}
return 0;
}
分块算法还有很多扩展,详见Ynoi
有时候遇到某些较为复杂的问题,甚至可以对分块算法进行扩展,比如先对序列进行分块,然后对于每一个块又进行值域分块
下面是一道分块结合桶与并查集的好题:
「DTOI-2」星之界
题目描述:
夜空中的星星组成了一个序列 \(a\),序列中的第 \(i\) 个数表示第 \(i\) 颗星星的亮度。
现在,作为星之眷顾者的你,拥有两种方式来操作星星。
-
操作一:输入格式为 \(\texttt{1 l r x y}\),表示将 \([l,r]\) 内所有亮为 \(x\) 的星星的亮度改为 \(y\)。
-
操作二:输入格式为 \(\texttt{2 l r}\),表示输出 \(\prod\limits_{i = l}^{r} C_{\sum_{j = l}^{i}a_j}^{a_i}\ \bmod 998244353\) 的值。
分析:
考虑化简这个可恶的式子
拆开,设\(S\)为前缀和数组
现在我们需要一个维护区间和和区间阶乘积并且支持区间定值修改的数据结构
貌似线段树之类的不行,考虑分块实际上是看见了1e5
统计比较容易,主要难点在于区间定值修改。注意到值域与\(n\)同级,考虑开桶来维护每个值,为了保证复杂度,对于每一个块开一个桶,空间复杂度\(O(N\sqrt N)\),勉强可以承受
但是一个桶只能表示这个值出现的数量,为了维护\(O(N\sqrt N )\)的复杂度,需要更好的工具来维护。或许可以类比懒标记的思想,延迟修改。那么我们只需要知道每个位置最终表示哪个数即可。如果把每个桶中的元素看作集合,那么区间定值修改就类似于合并集合,这启发我们联系并查集算法。那么最初将每个位置看做单独的集合,初始化块的时候将值相同的位置合并成一个集合,为了方便代码,可以选取每个值第一次出现的位置为代表元。在修改的时候,对于每一个块将表示值\(x\)的集合合并到\(y\)去,注意有可能\(y\)所在集合是空的,此时只需要将代表元的值更改即可。在散块更改的时候,可以直接暴力重置这个块。
核心代码
struct node {
int id, cnt;
}f[S][N];//桶
struct Node {
int sum, mul;
};
int get(int x) {
return x == fa[x] ? x : fa[x] = get(fa[x]);
}
inline void init(int id) {//重置这个块
mul[id] = 1;
sum[id] = 0;
for (re int i = L[id]; i <= R[id]; i++) {
if (f[id][a[i]].id) {
f[id][a[i]].cnt++;
fa[i] = f[id][a[i]].id;
}
else {
f[id][a[i]].id = i;
fa[i] = i;
val[i] = a[i];//val是真实值
f[id][a[i]].cnt = 1;
}
sum[id] += a[i];
mul[id] = 1ll * mul[id] * inv[a[i]] % p;
}
}
inline void clear(int id) {
for (re int i = L[id]; i <= R[id]; i++) {
a[i] = val[get(i)];
f[id][a[i]].cnt = f[id][a[i]].id = 0;
}
for (re int i = L[id]; i <= R[id]; i++) {
fa[i] = 0;
}
}
inline void change_only(int id, int l, int r, int x, int y) {
clear(id);
for (re int i = l; i <= r; i++) {
if (a[i] == x)a[i] = y;
}
init(id);
}
inline void change_all(int id, int x, int y) {
f[id][y].cnt += f[id][x].cnt;
sum[id] -= (x - y) * f[id][x].cnt;
mul[id] = 1ll * mul[id] * power_jc[x][f[id][x].cnt] % p * power_inv[y][f[id][x].cnt] % p;//为了保证复杂度,预处理阶乘及其逆元的幂
if (f[id][y].id == 0)f[id][y].id = f[id][x].id, val[f[id][x].id] = y;
else fa[f[id][x].id] = f[id][y].id;
f[id][x] = { 0,0 };
}
inline void change(int l, int r, int x, int y) {
if (pos[l] == pos[r]) {
change_only(pos[l], l, r, x, y);
return;
}
change_only(pos[l], l, R[pos[l]], x, y);
change_only(pos[r], L[pos[r]], r, x, y);
for (re int i = pos[l] + 1; i < pos[r]; i++)change_all(i, x, y);
}
查询的时候就很简单,将涉及到的积乘上,将和加上
inline Node find_only(int l, int r) {
Node ans = { 0,1 };
for (re int i = l; i <= r; i++) {
ans.sum += val[get(i)];
ans.mul = 1ll * ans.mul * inv[val[get(i)]] % p;
}
return ans;
}
inline Node find_all(int id) {
return { sum[id],mul[id] % p };
}
inline Node merge(Node a, Node b) {
return { a.sum + b.sum,1ll * a.mul * b.mul % p };
}
inline int find(int l, int r) {
if (pos[l] == pos[r]) {
Node ans = find_only(l, r);
return 1ll * jc[ans.sum] * ans.mul % p;
}
Node ans = merge(find_only(l, R[pos[l]]), find_only(L[pos[r]], r));
for (re int i = pos[l] + 1; i < pos[r]; i++) {
ans = merge(ans, find_all(i));
}
return 1ll * jc[ans.sum] * ans.mul % p;
}
预处理的时候就常规预处理,然后处理阶乘逆元,预处理他们的幂保证复杂度(反正空间够),注意逆元得递推求
inline void init() {
jc[1] = inv[1] = 1;
for (re int i = 2; i <= M - 50; i++) {
jc[i] = 1ll * jc[i - 1] * i % p;
inv[i] = p - (1ll * p / (1ll * i) * 1ll * inv[p % i] % p) % p;
}
for (re int i = 2; i <= M - 50; i++) {
inv[i] = 1ll * inv[i - 1] * inv[i] % p;
}
scanf("%d%d", &n, &m);
for (re int i = 1; i <= n; i++)scanf("%d", &a[i]);
block = sqrt(n);
siz = n % block ? n / block + 1 : n / block;
for (re int i = 1; i <= siz; i++) {
L[i] = (i - 1) * block + 1;
R[i] = min(i * block, n);
}
for (re int i = 1; i <= siz; i++) {
for (re int j = L[i]; j <= R[i]; j++) {
pos[j] = i;
}
}
for (re int i = 1; i <= N - 5; i++) {
power_jc[i][0] = power_inv[i][0] = 1;
for (re int j = 1; j <= block; j++) {
power_jc[i][j] = 1ll * power_jc[i][j - 1] * jc[i] % p;
power_inv[i][j] = 1ll * power_inv[i][j - 1] * inv[i] % p;
}
}
for (re int i = 1; i <= siz; i++)init(i);
}
注意有个细节(没注意到就是25pts,调我好久,还是看题解才发现的),当修改时\(x=y\),需要直接跳过不变,因为这会导致\(cnt\)等的值不正确
暴力骗分的分块变形:莫队算法
莫队算法的思想大概是这样的,对询问进行分块,必须离线操作,充分利用历史上的答案
基于分块思想,且\([l,r]\)的答案向\([l,r+1],[l,r-1],[l-1,r],[l+1,r]\)四个相邻状态任意一个转移都是\(O(1)\)的
离线思想,将读入排序,按\(\sqrt{n}\)分块,每\(\sqrt{n}\)个为一块
排序时,将左端点块号相同的询问放在一个块里,右端点按单增排
对于每个块内相邻的两个询问,每次左右端点移动不会超过\(\sqrt{n}\),是\(O(\sqrt{n})\)
块与块间相邻的两个询问,每次左右端点移动不会超过\(2\sqrt{n}\),也是\(O(sqrt{n})\)
试想以长度x为一块,\([1,x][x+1,2x]\),最坏就是\([1,1]->[2x,2x]\)
因此,对于\(m\)个询问,复杂度为\(O(m\sqrt{n})\)
本来可以通过将\([l,r]\)转化为平面坐标\((l,r)\),通过求曼哈顿最小生成树使得转移和最小,
但是,最坏情况下,曼哈顿最小生成树的复杂度和莫队分块的复杂度是一样的,
相比之下,莫队还好写,所以往往采用莫队算法来写
实现时,分块和下标移动套板子,魔改add函数即可
对于排序询问这个有一个小优化,如果l所在块的编号为奇数则按r升序排序,偶数则按r降序排序。
模板
//排序
inline bool cmp(query a,query b){
return a.bl!=b.bl?a.l<b.l:((a.bl&1)?a.r<b.r:a.r>b.r);
}
//处理问题
l=ask[1].l,r=ask[1].r;
……//暴力计算答案
for(int i=2;i<=m;i++){
while (l<ask[i].l) del(c[l++]);
while (l>ask[i].l) add(c[--l]);
while (r<ask[i].r) add(c[++r]);
while (r>ask[i].r) del(c[r--]);//将答案移动至指定范围
ans[ask[i].id]=……;
}
大概流程是这样的:
算法大概遵循一个这样的流程:
- 对于所有区间端点的移动,我们要设计出一种\(O(1)\)的方法使得我们可以快速维护移动端点后一个区间的答案。
- 有了这种方法之后,我们根据刚才的复杂度分析,我们对整个序列分块,每一块大小 \(O(\sqrt{n})\)
- 然后我们对所有询问的区间排序,排序完之后左端点每一次最多移动 \(\sqrt{n}\) 的距离总共 \(n\) 次,右端点单调不降所以每一个块移动 \(n\) 的距离总共 \(\sqrt{n}\) 次,所以总复杂度为 \(O(n\sqrt{n})\)
下面一个很简单的莫队应用,给定\(m\)次询问,每次询问\([l,r]\)中数\(x\)的出现次数
int block,ans[100005],siz,pos[100005],a[10005],cnt[100005],n,m;
struct node{
int l,r,x,id;
}ask[100050];
void add(int x){
cnt[x]++;
}
void del(int x){
cnt[x]--;
}
bool cmp(node a,node b){
return pos[a.l]==pos[b.l]?(pos[a.l]&1?a.r<b.r:a.r>b.r):pos[a.l]<pos[a.r];
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
block=sqrt(n),siz=n%block?n/block+1:block;
for(int i=1;i<=siz;i++){
int l=(i-1)*block+1,r=min(n,block*i);
for(int j=l;j<=r;j++)pos[j]=i;
}
for(int i=1;i<=m;i++){
scanf("%d%d%d",ask[i].l,ask[i].r,ask[i].x);
ask[i].id=i;
}
sort(ask+1,ask+m+1,cmp);
int l=ask[1].l,r=ask[1].r;
for(int i=l;i<=r;i++)add(a[i]);
ans[ask[1].id]=cnt[ask[i].x];
for(int i=2;i<=m;i++){
while(l>ask[i].l)add(a[--l]);
while(l<aks[i].l)del(a[l++]);
while(r<ask[i].r)add(a[++r]);
while(r>ask[i].r)del(a[r--]);
ans[ask[i].id]=cnt[ask[i].x];
}
for(int i=1;i<=m;i++)printf("%d\n",ans[i]);
}
当然这道题也可以用上文所述的朴素分块的\(O(n\sqrt{n})\)或者\(vector\)解法(更加优秀)
总而言之,分块就是运用了其大段维护,小段朴素和其常数优势
关于莫队算法,鄙人还有一篇拙作,欢迎阅读