以下内容主要借鉴oiwiki
分块思想
简介
- 分块是一种思想,而不是一种数据结构。
- 分块的基本思想是:通过对原数据的适当划分,并在划分后的每一个块上预处理部分信息,从而较一般的暴力算法取得更优的时间复杂度。
- 分块的时间复杂度主要取决于分块的块长,一般可以通过均值不等式求出某个问题下的最优块长,以及相应的时间复杂度。
- 分块是一种很灵活的思想,相较于树状数组和线段树,分块的优点是通用性更好,可以维护很多树状数组和线段树无法维护的信息。
- 当然,分块的缺点是渐进意义的复杂度,相较于线段树和树状数组不够好
例题
LibreOJ6284
题目描述
给出一个长为 的数列,以及 个操作,操作涉及区间加法,区间求和.
参考代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll maxn = 5e5 + 5;
ll a[maxn];
ll partLen; //块长
ll partID[maxn]; //块号
ll partSum[maxn]; //块和
ll tag[maxn]; //块标记
void add(ll l, ll r, ll c)
{
ll startID = partID[l], endID = partID[r];
if (startID == endID) //l和r在同一个块,直接暴力
{
for (ll i = l; i <= r; i++)
{
a[i] += c;
partSum[startID] += c;
}
return;
}
//不在同一个块,分成三段处理
for (ll i = l; partID[i] == startID; i++)//起始段
{
a[i] += c;
partSum[startID] += c;
}
for (ll i = startID + 1; i < endID; i++)//中间若干个整块
{
tag[i] += c;
partSum[i] += c * partLen;
}
for (ll i = r; partID[i] == endID; i--)//末尾段
{
a[i] += c;
partSum[endID] += c;
}
}
//查询的思想和修改基本相同
ll query(ll l, ll r, ll c)
{
ll ans = 0;
ll startID = partID[l], endID = partID[r];
if (startID == endID)
{
for (ll i = l; i <= r; i++)
{
ans += a[i] + tag[endID];
ans %= c;
}
return ans;
}
for (ll i = l; partID[i] == startID; i++)
{
ans += a[i] + tag[startID];
ans %= c;
}
for (ll i = startID + 1; i < endID; i++)
{
ans += partSum[i];
ans %= c;
}
for (ll i = r; partID[i] == endID; i--)
{
ans += a[i] + tag[endID];
ans %= c;
}
return ans;
}
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
ll n;
cin >> n;
partLen = sqrt(n);
memset(partID, -1, sizeof(partID));
for (ll i = 0; i < n; i++)
{
cin >> a[i];
partID[i] = i / partLen + 1;
partSum[partID[i]] += a[i];
}
for (ll i = 0; i < n; i++)
{
ll opt, l, r, c;
cin >> opt >> l >> r >> c;
l--, r--;
if (opt)
cout << query(l, r, c + 1) << '\n';
else
add(l, r, c);
}
return 0;
}
补充:对于简单的求和操作,在无需修改的情况下,还可以通过维护前缀和来实现O(1)询问。
基于分块思想的分块数组
块状数组,即把一个数组分为几个块,块内信息整体保存,若查询时遇到两边不完整的块直接暴力查询。一般情况下,块的长度为 n \sqrt n n。详细分析可以阅读 2017 年国家集训队论文中徐明宽的《非常规大小分块算法初探》。
例题
-
题目大意: 对n个数进行q次操作, 每次输入opt ,l, r,v 操作1:询问【l,r】区间大于v的数的个数
操作2:给【l,r】区间的数都加上v -
题解:
以 n \sqrt n n为块长进行分块处理。
对于询问:先判断l,r是否在同一个块内。在同一个块只需暴力求解即可,时间复杂度是块长O( n \sqrt n n);不在同一个块内则分成三段处理:两端暴力求解,中间段在数组有序的情况下可以通过二分快速得到答案,因此另外维护一个每个块是有序的t数组,时间复杂度是O( n \sqrt n n ∗ * ∗log n \sqrt n n)。
对于修改:先判断l,r是否在同一个块内。在同一个块只需暴力修改并更新t数组即可,时间复杂度O( n \sqrt n n ∗ * ∗log n \sqrt n n);不在同一个块内则分成三段处理:两端暴力修改并更新t数组,时间复杂度O( n \sqrt n n ∗ * ∗log n \sqrt n n),中间段由于是整块修改,顺序不会改变,所以只用打个标记即可,时间复杂度是整块的数量,即O( n \sqrt n n)。
所以总的时间复杂度是O(q* n \sqrt n n ∗ * ∗log n \sqrt n n) -
参考代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e6+5,maxm=1e3+5;
int st[maxm], ed[maxm], Size[maxm],dlt[maxn]; //每个块的起点、终点、块长,块上标记
int belong[maxn];//下标所在的块号
int num, n,q;
int a[maxn], t[maxn] ;//a为原来的(未被排序的)数组,t为对每个块排过序的数组
void Sort(int k) {
for (int i = st[k]; i <= ed[k]; i++) t[i] = a[i];
sort(t + st[k], t + ed[k] + 1);
}
void Modify(int l, int r, int c)
{
int x = belong[l], y = belong[r];
if (x == y)//在同一个块就直接修改
{
for (int i = l; i <= r; i++)
a[i] += c;
Sort(x);
return;
}
for (int i = l; i <= ed[x]; i++)//直接修改起始段
a[i] += c;
for (int i = st[y]; i <= r; i++)//直接修改结束段
a[i] += c;
for (int i = x + 1; i < y; i++)//中间的整块打上标记
dlt[i] += c;
Sort(x);Sort(y);//起始段和末尾段需要重排
}
int Answer(int l, int r, int c)
{
int ans = 0, x = belong[l], y = belong[r];
if (x == y)//l和r属于同一个块时,直接暴力
{
for (int i = l; i <= r; i++)
if (a[i] + dlt[x] >= c)
ans++;
return ans;
}
for (int i = l; i <= ed[x]; i++)//处理开头
if (a[i] + dlt[x] >= c)
ans++;
for (int i = st[y]; i <= r; i++)//处理结尾
if (a[i] + dlt[y] >= c)
ans++;
//用lower_bound找出中间每一个整块中第一个大于等于c的数的位置,注意考虑dlt
//然后取包含该位置的后面所有的数,即为大于等于c的数
for (int i = x + 1; i <= y - 1; i++)
ans += ed[i] - (lower_bound(t + st[i], t + ed[i] + 1, c - dlt[i]) - t) + 1;
return ans;
}
//根据需要初始化块状数组
void init()
{
num = sqrt(n);
for (int i = 1; i <= num; i++)
st[i] = n / num * (i - 1) + 1, ed[i] = n / num * i;
ed[num] = n;
for (int i = 1; i <= num; i++)
{
for (int j = st[i]; j <= ed[i]; j++)
belong[j] = i;
Size[i] = ed[i] - st[i] + 1;
sort(t+st[i],t+ed[i]+1);
}
}
int main()
{
cin>>n>>q;
for (int i = 1; i <= n; i++)
{
cin>>a[i];
t[i]=a[i];
}
init();
while (q--)
{
char c;int l,r,w;
cin>>c>>l>>r>>w;
if(c=='A')
cout<<Answer(l,r,w)<<'\n';
else
Modify(l,r,w);
}
return 0;
}
例题二
链接没找到,有兴趣自己去找找。
参考代码(没交过,不一定对)
#include <bits/stdc++.h>
#define inf 0x3f3f3f3f3f3f3f3fll
using namespace std;
const int maxn = 1e6 + 5, maxm = 1e3 + 5;
int st[maxm], ed[maxm], Size[maxm], dlt[maxn]; //每个块的起点、终点、块长,块上标记
int belong[maxn]; //下标所在的块号
int num, n, q;
int a[maxn], t[maxn]; //原来的(未被排序的)数组,对每个块排过序的数组
/*
注意点:
同样使用dlt来给每个块打上标记,所以在处理开头和结尾时要先将标记下传
下传时需将a数组和t数组同时更新
*/
void Sort(int k)
{
for (int i = st[k]; i <= ed[k]; i++)
t[i] = a[i];
sort(t + st[k], t + ed[k] + 1);
}
void PushDown(int x)
{
if (dlt[x] != inf)
for (int i = st[x]; i <= ed[x]; i++)
a[i] = t[i] = dlt[x];
dlt[x] = inf;
}
void Modify(int l, int r, int c)
{
int x = belong[l], y = belong[r];
PushDown(x);
if (x == y)
{
for (int i = l; i <= r; i++)
a[i] = c;
Sort(x);
return;
}
PushDown(y);
for (int i = l; i <= ed[x]; i++)
a[i] = c;
for (int i = st[y]; i <= r; i++)
a[i] = c;
Sort(x);Sort(y);
for (int i = x + 1; i < y; i++)
dlt[i] = c;
}
int Binary_Search(int l, int r, int c)//同lower_bound
{
int ans = l - 1, mid;
while (l <= r)
{
mid = (l + r) / 2;
if (t[mid] <= c)
ans = mid, l = mid + 1;
else
r = mid - 1;
}
return ans;
}
int Answer(int l, int r, int c)
{
int ans = 0, x = belong[l], y = belong[r];
PushDown(x);
if (x == y)
{
for (int i = l; i <= r; i++)
if (a[i] <= c)
ans++;
return ans;
}
PushDown(y);
for (int i = l; i <= ed[x]; i++)
if (a[i] <= c)
ans++;
for (int i = st[y]; i <= r; i++)
if (a[i] <= c)
ans++;
for (int i = x + 1; i <= y - 1; i++)
{
if (inf == dlt[i])//没有标记就直接二分查位置
ans += Binary_Search(st[i], ed[i], c) - st[i] + 1;
else if (dlt[i] <= c)//有标记,说明都是同一个数,直接比较即可
ans += Size[i];
}
return ans;
}
void init()
{
num = sqrt(n);
for (int i = 1; i <= num; i++)
st[i] = n / num * (i - 1) + 1, ed[i] = n / num * i;
ed[num] = n;
for (int i = 1; i <= num; i++)
{
for (int j = st[i]; j <= ed[i]; j++)
belong[j] = i;
Size[i] = ed[i] - st[i] + 1;
sort(t + st[i], t + ed[i] + 1);
}
}
int main()
{
cin >> n >> q;
for (int i = 1; i <= n; i++)
{
cin >> a[i];
t[i] = a[i];
}
init();
while (q--)
{
char c;
int l, r, w;
cin >> c >> l >> r >> w;
if (c == 'A')
cout << Answer(l, r, w) << '\n';
else
Modify(l, r, w);
}
return 0;
}
莫队算法
莫队算法简介
莫队算法是由莫涛(当初是OI中国队队长)提出的算法,因此被称为莫队算法。在莫涛提出莫队算法之前,莫队算法已经在 Codeforces 的高手圈里小范围流传,但是莫涛是第一个对莫队算法进行详细归纳总结的人,现在在国际上也是讲其称为“Mo‘s algorithm”。莫涛提出莫队算法时,只分析了普通莫队(顺序数据结构,不支持修改)算法,但是经过 OIer 和 ACMer 的集体智慧改造,莫队有了多种扩展版本。
莫队算法可以解决一类离线区间询问问题,适用性极为广泛。同时将其加以扩展,便能轻松处理树上路径询问以及支持修改操作。
普通莫队算法
对于关于一个序列的区间询问,如果满足以下两个条件:
1. 能从 区间【l,r】 的答案
O
(
1
)
O(1)
O(1)扩展到区间【l-1,r】,【l+1,r】,【l,r-1】,【l,r+1】 (即与
相邻的区间)的答案
2. 询问可以被离线处理
那么在n(序列长度)和m(询问次数)同阶的情况下,将序列以
n
\sqrt n
n为长度进行分块,则对于序列上的区间询问问题,可以在 O(
n
∗
n*
n∗
n
\sqrt n
n)的时间复杂度内求出所有询问的答案。
在n(序列长度)和m(询问次数)不同阶的情况下,将序列以
n
/
n/
n/
m
\sqrt m
m为长度进行分块,则对于序列上的区间询问问题,可以在 O(
n
∗
n*
n∗
m
\sqrt m
m)的时间复杂度内求出所有询问的答案。
莫队算法的基本思想为分块,精髓在于对询问进行排序,即对所有的区间询问’
【
l
,
r
】
【l,r】
【l,r】 , 以
l
l
l 所在块的编号为第一关键字,
r
r
r 为第二关键字从小到大排序。
排序后对所有询问顺序处理,即从上一次询问的答案扩展得到下一次询问。
具体的时间复杂度证明请参考OI wiki莫队算法复杂度证明以及罗勇军老师的博客
这是罗老师博客中有莫涛本人给出建议的复杂度对比图。
横坐标表示
l
l
l,纵坐标表示
r
r
r,点即表示询问。线段长度即表示从一个询问转移到下一个询问所要的计算次数。
补充说明:罗老师的博客写的很好也很仔细,但是推荐在具备线段树以及树状数组基础的时候再去看。
例题
例题一洛谷P2709 小B的询问
题解:
- 询问显然可以离线处理。
- 区间【l,r】转移到相邻区间会使得区间发生以下情况 增加一个数字,假设为X,则答案增加
(
x
+
1
)
2
−
x
2
=
2
∗
x
+
1
(x+1)^ 2-x^2=2*x+1
(x+1)2−x2=2∗x+1
减少一个数字,假设为X,则答案减少 x 2 − ( x − 1 ) 2 = 2 ∗ x − 1 x^ 2-(x-1)^2=2*x-1 x2−(x−1)2=2∗x−1
所以套用普通莫队板子即可解决。
参考代码:
#include <bits/stdc++.h>
#define N 50005
#define ll long long
using namespace std;
struct Query
{
int l, r, id, pos;
bool operator<(const Query &x) const
{
if (pos == x.pos)
return r < x.r;
else
return pos < x.pos;
}
} query[N];
int num[N], n, m, K;
ll cnt[N], Ans[N];
ll ans = 0;
//把区间改变对答案所造成的影响推出来,
//然后对add和del进行相应修改即可
void add(int x)
{
ans+=2*cnt[x] + 1;
cnt[x]++;
}
void del(int x)
{
ans -= 2 * cnt[x] - 1;
cnt[x]--;
}
int main()
{
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n>>m>>K;
int size = (int)sqrt(n);
for (int i = 1; i <= n; i++)
cin>>num[i];
//读入并排序询问
for (int i = 1; i <= m; i++)
{
cin>>query[i].l>>query[i].r;
query[i].id = i;
query[i].pos = (query[i].l - 1) / size + 1;
}
sort(query + 1, query + m + 1);
//离线处理询问
int l = 1, r = 0;
for (int i = 1; i <= m; i++)
{
while (l > query[i].l)//l需要往左移,区间增大
add(num[--l]);
while (r < query[i].r)//r需要往右移,区间增大
add(num[++r]);
while (l < query[i].l)//l需要往右移动,区间缩小
del(num[l++]);
while (r > query[i].r)//r需要往左移,区间缩小
del(num[r--]);
Ans[query[i].id] = ans;
}
for (int i = 1; i <= m; i++)
cout<<Ans[i]<<'\n';
return 0;
}
例题二洛谷P1494[国家集训队]小Z的妹子
题解:
和上一题大同小异,分析出转移到相邻区间给答案所带来的变化即可。
参考代码:
#include<bits/stdc++.h>
using namespace std;
const int N=50010;
int n,m;
int B,fz,fm,len,col[N],cnt[N],ans[N][2];
struct Query
{
int l,r,id;
bool operator<(Query& b)//l按块号排序,然后r按小到大排序
{
return l/B==b.l/B?r<b.r:l<b.l;
}
} q[N];
void add(int x)
{
fz+=cnt[x];
++cnt[x];
fm+=len;
++len;
}
void del(int x)
{
--cnt[x];
fz-=cnt[x];
--len;
fm-=len;
}
int gcd(int a,int b)
{
return b==0?a:gcd(b,a%b);
}
int main()
{
int i,l=1,r=0,g;
cin>>n>>m;
B=n/sqrt(m);
for (i=1;i<=n;++i)
{
cin>>col[i];
}
for (i=0;i<m;++i)
{
cin>>q[i].l>>q[i].r;
q[i].id=i;
}
sort(q,q+m);
for (i=0;i<m;++i)
{
if (q[i].l==q[i].r)
{
ans[q[i].id][0]=0;
ans[q[i].id][1]=1;
continue;
}
//从上一个区间转移到当前区间
while (l>q[i].l)
add(col[--l]);
while (r<q[i].r)
add(col[++r]);
while (l<q[i].l)
del(col[l++]);
while (r>q[i].r)
del(col[r--]);
g=gcd(fz,fm);
ans[q[i].id][0]=fz/g;
ans[q[i].id][1]=fm/g;
}
for (i=0;i<m;++i)
printf("%d/%d\n",ans[i][0],ans[i][1]);
return 0;
}
关于莫队算法的一些卡常技巧
-
采用奇偶排序法,即奇数块以 r r r从小到大排序,偶数块以 r r r从大到小排序。这就使得询问在块间转移时过渡更加平滑,如下图(红色部分即是普通排序所存在的骤变问题)。
-
压行写法。
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
using namespace std;
#define maxn 1010000
#define maxb 1010
int aa[maxn], cnt[maxn], belong[maxn];
int n, m, size, bnum, now, ans[maxn];
struct query {
int l, r, id;
} q[maxn];
int cmp(query a, query b) {
return (belong[a.l] ^ belong[b.l]) ? belong[a.l] < belong[b.l] : ((belong[a.l] & 1) ? a.r < b.r : a.r > b.r);
}
#define isdigit(x) ((x) >= '0' && (x) <= '9')
int read() {
int res = 0;
char c = getchar();
while(!isdigit(c)) c = getchar();
while(isdigit(c)) res = (res << 1) + (res << 3) + c - 48, c = getchar();
return res;
}
void printi(int x) {
if(x / 10) printi(x / 10);
putchar(x % 10 + '0');
}
int main() {
scanf("%d", &n);
size = sqrt(n);
bnum = ceil((double)n / size);
for(int i = 1; i <= bnum; ++i)
for(int j = (i - 1) * size + 1; j <= i * size; ++j) {
belong[j] = i;
}
for(int i = 1; i <= n; ++i) aa[i] = read();
m = read();
for(int i = 1; i <= m; ++i) {
q[i].l = read(), q[i].r = read();
q[i].id = i;
}
sort(q + 1, q + m + 1, cmp);
int l = 1, r = 0;
for(int i = 1; i <= m; ++i) {
int ql = q[i].l, qr = q[i].r;
while(l < ql) now -= !--cnt[aa[l++]];
while(l > ql) now += !cnt[aa[--l]]++;
while(r < qr) now += !cnt[aa[++r]]++;
while(r > qr) now -= !--cnt[aa[r--]];
ans[q[i].id] = now;
}
for(int i = 1; i <= m; ++i) printi(ans[i]), putchar('\n');
return 0;
}
带修莫队
普通莫队算法是不支持修改的,强制在线需要另寻他法,但是对于某些允许离线的带修改区间查询经过改进,还能使用。
即原本的查询是用
【
l
,
r
】
【l,r】
【l,r】二维表示,现在加一维时间戳
t
t
t。对于每个查询操作,时间戳不重合时,如果当前修改多了就把多出的修改回退,否则接着修改,直到时间戳重合。
这样,我们当前区间的移动方向从四个([l−1,r]、[l+1,r]、[l,r−1]、[l,r+1])变成了六个([l−1,r,t]、[l+1,r,t]、[l,r−1,t]、[l,r+1,t]、[l,r,t−1]、[l,r,t+1])。
带修莫队的块长一般取
O
(
n
2
/
3
)
O(n^{2/3})
O(n2/3),时间复杂度为
O
(
n
5
/
3
)
O(n^{5/3})
O(n5/3),所以请谨慎使用。
#include <bits/stdc++.h>
using namespace std;
#define maxn 50500
#define maxc 1001000
int a[maxn], cnt[maxc], ans[maxn], belong[maxn];
struct query {
int l, r, time, id;
} q[maxn];
struct modify {
int pos, color, last;
} c[maxn];
int cntq, cntc, n, m, size, bnum;
int cmp(query a, query b) {
return (belong[a.l] ^ belong[b.l]) ? belong[a.l] < belong[b.l] : ((belong[a.r] ^ belong[b.r]) ? belong[a.r] < belong[b.r] : a.time < b.time);
}
#define isdigit(x) ((x) >= '0' && (x) <= '9')
inline int read() {
int res = 0;
char c = getchar();
while(!isdigit(c)) c = getchar();
while(isdigit(c)) res = (res << 1) + (res << 3) + (c ^ 48), c = getchar();
return res;
}
int main() {
n = read(), m = read();
size = pow(n, 2.0 / 3.0);
bnum = ceil((double)n / size);
for(int i = 1; i <= bnum; ++i)
for(int j = (i - 1) * size + 1; j <= i * size; ++j) belong[j] = i;
for(int i = 1; i <= n; ++i)
a[i] = read();
for(int i = 1; i <= m; ++i) {
char opt[100];
scanf("%s", opt);
if(opt[0] == 'Q') {
q[++cntq].l = read();
q[cntq].r = read();
q[cntq].time = cntc;
q[cntq].id = cntq;
}
else if(opt[0] == 'R') {
c[++cntc].pos = read();
c[cntc].color = read();
}
}
sort(q + 1, q + cntq + 1, cmp);
int l = 1, r = 0, time = 0, now = 0;
for(int i = 1; i <= cntq; ++i) {
int ql = q[i].l, qr = q[i].r, qt = q[i].time;
while(l < ql) now -= !--cnt[a[l++]];
while(l > ql) now += !cnt[a[--l]]++;
while(r < qr) now += !cnt[a[++r]]++;
while(r > qr) now -= !--cnt[a[r--]];
while(time < qt) {
++time;
if(ql <= c[time].pos && c[time].pos <= qr) now -= !--cnt[a[c[time].pos]] - !cnt[c[time].color]++;
swap(a[c[time].pos], c[time].color);
}
while(time > qt) {
if(ql <= c[time].pos && c[time].pos <= qr) now -= !--cnt[a[c[time].pos]] - !cnt[c[time].color]++;
swap(a[c[time].pos], c[time].color);
--time;
}
ans[q[i].id] = now;
}
for(int i = 1; i <= cntq; ++i)
printf("%d\n", ans[i]);
return 0;
}
树上莫队
对于询问树上路径问题的(对于有点权的树,给出树上路径的起点和终点,求路径上的权值和,或者种类数等),可以通过欧拉序加LCA转换成区间询问问题(其他处理区间询问的数据结构而经常用到这个思想),然后再用莫队算法解决,有兴趣自行了解。