【根号科技】分块+莫队
分块
应该说并不是一种数据结构,而是一种思想。
相比于线段树和树状数组,分块的可拓展性更强,代码量和时空复杂度都适中,是一种“优美的暴力”。
本文将以“例题+题解+拓展”的形式逐步讲解分块的思想。
块状数组
LOJ6277 数列分块入门 1
线段树和树状数组都能轻松水过,但是现在你可以用分块过掉这个经典的数据结构问题。
分块的思想归纳起来就是 “大段维护,局部朴素”,我们把一个数组划分成几个区间(块),维护这些区间整体的一些信息,比如区间和,区间tag等等。
每次修改的时候,如果左右端点在同一个块内,那么我们就暴力维护,如果左右端点不在同一个块内,我们就把他们之间的块维护起来,剩下的“边边角角”暴力维护。
如图:
橙色部分维护整块的信息,l,r 之间剩下的部分直接暴力维护。
因为整个序列被分成了\(\sqrt{n}\)个块,所以单次操作的时间复杂度为\(\sqrt{n}\)。
值得注意的是,块长取不同值对代码效率也有影响,一般来说块长设为\(\sqrt{n}\)或者\(n^{\frac{2}{3}}\)较优。当然你也可以写常数块长或者随机块长让上帝决定你的常数。
代码:
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define dub long double
const int inf = 1e9+7;
const int MAXN = 5e4 + 10;
const dub eps = 1e-7;
using namespace std;
int n, m, block;
int a[MAXN];
int tag[MAXN], bl[MAXN];
inline int read( ){
int x = 0 ; short w = 0 ; char ch = 0;
while( !isdigit(ch) ) { w|=ch=='-';ch=getchar();}
while( isdigit(ch) ) {x=(x<<3)+(x<<1)+(ch^48);ch=getchar();}
return w ? -x : x;
}
void modify(int l, int r, int x){
if(bl[l] == bl[r]){//同一块内暴力处理
for(int i = l; i <= r; i++)
a[i] += x;
}
else{//处理散块
for(int i = l; i <= bl[l] * block; i++)
a[i] += x;
for(int i = (bl[r] - 1) * block + 1; i <= r; i++)
a[i] += x;
}
for(int i = bl[l] + 1; i <= bl[r] - 1; i++)
tag[i] += x;//处理整块
return;
}
int query(int x){
return a[x] + tag[bl[x]];
}
signed main( ){
ios :: sync_with_stdio(0);
n = read( );
block = pow(n, 0.666);
for(int i = 1; i <= n; i++){
a[i] = read( );
bl[i] = (i - 1) / block + 1;
}
for(int i = 1; i <= n; i++){
int op = read( ), l = read( ), r = read( ), c = read( );
if(op == 0) modify(l, r, c);
else cout << query(r) << '\n';
}
return 0;
}
LOJ6281 数列分块入门 5
这道题可以说很好的体现了分块的优势,涉及到开方操作,如果用线段树维护的话就得把整个线段树拍扁重建,但是对于分块来说,光脚的不怕穿鞋的,我们本身就是个暴力,那就直接暴力开方,维护区间和。
那复杂度呢?众所周知,一个int范围内的数开方 5 次左右就会变成1,那么我们维护一个flag表示这个块有没有被开成1,那么开方操作的时间复杂度就降下来了,剩下的就是分块板子。
代码:
点击查看代码
#include <bits/stdc++.h>
#define MAXN 1010101
#define int long long
#define mod 1e9+7
using namespace std;
int n, cnt;
int a[MAXN], in[MAXN];
int s[2000];
bool flag[2000];
void sqt( int k ){
if( flag[k] ) return;
flag[k] = 1;
s[k] = 0;
for( int i = ( k - 1 ) * cnt + 1; i <= k * cnt; i++ ){
a[i] = sqrt( a[i] );
s[k] += a[i];
if( a[i] > 1 ) flag[k] = 0;
}
return;
}
void updata( int l, int r ){
for( int i = l; i <= min( in[l] * cnt, r ); i++ )
s[in[i]] -= a[i], a[i] = sqrt( a[i] ), s[in[i]] += a[i];
if( in[l] != in[r] )
for( int i = ( in[r] - 1 ) * cnt + 1; i <= r; i++ )
s[in[i]] -= a[i], a[i] = sqrt( a[i] ), s[in[i]] += a[i];
for( int i = in[l] + 1; i <= in[r] - 1; i++ )
sqt( i );
}
int check( int l, int r ){
int sum = 0;
for( int i = l; i <= min( in[l] * cnt, r ); i++ )
sum += a[i];
if( in[l] != in[r] )
for( int i = ( in[r] - 1 ) * cnt + 1; i <= r; i++ )
sum += a[i];
for( int i = in[l] + 1; i <= in[r] - 1; i++ )
sum += s[i];
return sum;
}
signed main( ){
int op, l, r, c;
scanf("%lld",&n); cnt = sqrt( n );
for( int i = 1; i <= n; i++ ){
scanf("%lld",&a[i]);
in[i] = ( i - 1 ) / cnt + 1;
s[in[i]] += a[i];
}
for( int i = 1; i <= n; i++ ){
scanf("%lld%lld%lld%lld",&op,&l,&r,&c);
if( op == 0 ) updata( l ,r );
if( op == 1 ) printf("%lld\n",check( l, r ) );
}
return 0;
}
莫队
还是暴力
普通莫队
先从暴力开始说起,我们每次都把询问区间的答案暴力计算出来,显然会T。再考虑一下这个过程,用图表达一下:
假设我们的询问长成了这么一个优美的形状,很自然的我们可以想到:两个询问之间有很多重复的信息,那么能不能利用这个信息呢?进一步的,我们可以把一个询问到另一个询问的过程转化成一个询问通过左右端点的移动达到了另一个询问,这就是莫队的基本思想。
但是如果我们的询问长得非常不优美,就像这样:
这样左右端点的移动跨度就非常大,显然这不是我们想要的,那么就需要排序来达到我们“让左右端点的移动跨度尽量小的目的”。
所以莫队的基本思路就是:
把询问离线并排序(会在代码里介绍排序规则)
移动左右指针并对答案进行增删,记录答案。
可以看出来,莫队是一个离线算法,均摊复杂度是\(O(m \sqrt{n})\)。
具体的实现细节:
对询问进行排序的时候,我们以左端点所在块为第一关键字,右端点为第二关键字升序排序,这样保证了左端点递增移动,右端点只在一个块内移动,时间复杂度是\(O(m \sqrt{n})\)。
friend bool operator< (question x, question y){
return (x.l / block) == (y.l / block) ? x.r < y.r : x.l < y.l;
}
左右端点移动的常规形式是四个while,需要注意的是删除后自增,添加先自增。
while(l < ql) del(l++);
while(l > ql) add(--l);
while(r > qr) del(r--);
while(r < qr) add(++r);
最后就是这道题的实现了,题中要求求出区间平方和,假设我们已经求出了\([l,r)\)的答案 sum,r位置上的数原来有 k 个,那么\([l,r]\)的答案就是:
同理,删除就是
所以 add 和 del 函数就很好写了:
void add(int x){sum += ((cnt[a[x]]++) << 1) + 1;}
void del(int x){sum -= ((cnt[a[x]]--) << 1) - 1;}
这就是普通莫队。
updata:看到一张很好的解释了莫队对暴力复杂度优化的图,贴上来
图中的点代表询问,折线代表指针的移动,莫队算法把原本\(O(n)\)级别的单次移动优化到了一个块内的\(O(\sqrt{n})\)
代码:
点击查看代码
#include <bits/stdc++.h>
#define ll long long
#define dub long double
const int inf = 1e9 + 7;
const int MAXN = 5e5 + 10;
using namespace std;
int block;
struct question{
int l, r, id;
friend bool operator< (question x, question y){
return (x.l / block) == (y.l / block) ? x.r < y.r : x.l < y.l;
}
} ask[MAXN];
int n, m, k;
int a[MAXN];
int cnt[MAXN], l, r;
int ans[MAXN], sum;
inline int read( ){
int x = 0 ; short w = 0 ; char ch = 0;
while( !isdigit(ch) ) { w|=ch=='-';ch=getchar();}
while( isdigit(ch) ) {x=(x<<3)+(x<<1)+(ch^48);ch=getchar();}
return w ? -x : x;
}
void add(int x){sum += ((cnt[a[x]]++) << 1) + 1;}
void del(int x){sum -= ((cnt[a[x]]--) << 1) - 1;}
signed main( ){
n = read( ); m = read( ); k = read( );
block = sqrt(n);
for(int i = 1; i <= n; i++)
cin >> a[i];
for(int i = 1; i <= m; i++)
ask[i] = (question){read( ), read( ), i};
sort(ask + 1, ask + m + 1);
l = 1; r = 0;
for(int i = 1; i <= m; i++){
int ql = ask[i].l, qr = ask[i].r;
while(l < ql) del(l++);
while(l > ql) add(--l);
while(r > qr) del(r--);
while(r < qr) add(++r);
ans[ask[i].id] = sum;
}
for(int i = 1; i <= m; i++)
cout << ans[i] << endl;
return 0;
}
带修莫队
Luogu1903 [国家集训队] 数颜色 / 维护队列
这道题中除了莫队经典的操作之外又多了一个修改操作,那么我们就不能按照普通莫队的思路写这道题了,怎么办呢?既然莫队通过指针的移动完成了询问之间的转移,那么我们可不可以再加一个指针用来完成修改之间的转移呢?
答案是肯定的,以时间为维度,我们记录下每个询问之前有几个修改,之后通过now(表示现在进行了几次修改)的移动来更新修改对答案的影响。
思路很明确,来看几个细节:
在更新修改时,如果修改的画笔在当前的询问区间里的话就需要更新答案,但无论修改在不在当前的询问区间里都需要进行修改。
此外,如果对一个位置进行了修改就相当于把修改的颜色和本来的颜色交换位置,因为进行一次修改再删除这次修改相当于没改。
代码:
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define dub long double
const int MAXN = 3e6+7;
const int inf = 1e9+7;
using namespace std;
int l, r, bl[MAXN];
struct Qus{
int l, r, id, t;
friend bool operator<(Qus x, Qus y){
if(bl[x.l] != bl[y.l]) return x.l < y.l;
if(bl[x.r] != bl[y.r]) return x.r < y.r;
return x.t < y.t;
}
} q[MAXN];
struct Chg{
int pos, val;
} c[MAXN];
int n, m, tot1, tot2;
int a[MAXN];
int sum[MAXN], Ans[MAXN];
int now, ans;
inline int read( ){
int x = 0 ; short w = 0 ; char ch = 0;
while( !isdigit(ch) ) { w|=ch=='-';ch=getchar();}
while( isdigit(ch) ) {x=(x<<3)+(x<<1)+(ch^48);ch=getchar();}
return w ? -x : x;
}
void add(int x){if(sum[a[x]]++ == 0) ans++;}
void del(int x){if(sum[a[x]]-- == 1) ans--;}
void modify(int x){
if(l <= c[x].pos and c[x].pos <= r){
if(sum[a[c[x].pos]]-- == 1) ans--;
if(sum[c[x].val]++ == 0) ans++;
}
swap(a[c[x].pos], c[x].val);
return;
}
signed main( ){
n = read( ); m = read( );
const int block = pow(n, 0.666);
for(int i = 1; i <= n; i++)
a[i] = read( );
for(int i = 1; i <= m; i++){
char ch;
cin >> ch;
int x = read( ), y = read( );
if(ch == 'Q') q[++tot1] = (Qus){x, y, tot1, tot2}, bl[x] = x / block, bl[y] = y / block;
else c[++tot2] = (Chg){x, y};
}
sort(q + 1, q + tot1 + 1);
for(int i = 1; i <= n; i++){
int ql = q[i].l, qr = q[i].r, qt = q[i].t;
while(l < ql) del(l++);
while(l > ql) add(--l);
while(r < qr) add(++r);
while(r > qr) del(r--);
while(now < qt) modify(++now);
while(now > qt) modify(now--);
Ans[q[i].id] = ans;
}
for(int i = 1; i <= tot1; i++)
cout << Ans[i] << "\n";
return 0;
}