学习了莫队,感觉这个算法是一个很玄学的的算法,分块的大小和排序方式就能决定这个算法的效率,现在就对莫队,和带修莫队做一个简单的总结。
适用环境:
莫队算法使用分块的思想,可以解决一类离线区间询问问题,在使用莫队算法的时候就需要将区间询问离线(在线也可以,但是那样复杂度很高,就没有必要使用这个算法了)。
算法核心:
莫队算法离线首先是按左端点分块,然后在同一个块当中区间右端点要递增(分块的大小很影响算法的效率,后面要专门提 到),然后在分完块后,对于题中的区间询问问题,莫队算法是能够从 [l,r] 的答案能够 O(1) 得到 [l−1,r],[l+1,r],[l,r+1],[l,r−1] 的答案,查询到相应询问区间后,然后将相应的询问答案保存下来。
分块大小:
分块直接影响了这个算法的复杂度,这里直接借鉴别人的一篇博客戳这里,下面u表示分块大小。
普通莫队例题:
CodeForces - 617E XOR and Favorite Number
SPOJ - DQUERY D-query
题意:现在给出一个序列,多次询问,每次询问给出一个区间,让你求出区间中有多少不同的数。
题解:直接上代码,细节理解下就好了。
#pragma comment(linker, "/STACK:102400000,102400000")
#include <iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<map>
#include<queue>
#include<set>
#include<cmath>
#include<stack>
#include<string>
const int mod = 998244353;
const int maxn = 1e6 + 5;
const int inf = 1e9;
const long long onf = 1e18;
#define me(a, b) memset(a,b,sizeof(a))
#define lowbit(x) x&(-x)
#define lson l,mid,rt<<1
#define rson mid+1,r,rt<<1|1
#define PI 3.14159265358979323846
typedef long long ll;
typedef unsigned long long ull;
using namespace std;
int a[maxn];
int sum[maxn], ans;
int Ans[maxn];
struct node {
int l, r, id, cmp;
bool friend operator<(node a, node b) {
if (a.cmp == b.cmp) {
if (a.cmp & 1)
return a.r < b.r;
return a.r > b.r;
}
return a.l < b.l;
}
} q[maxn];
void add(int x) {
if (++sum[x] == 1)
ans++;
}
void del(int x) {
if (--sum[x] == 0)
ans--;
}
int main() {
int n, m;
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%d", &a[i]);
int u = sqrt(n), l, r;
scanf("%d", &m);
for (int i = 1; i <= m; i++) {
scanf("%d%d", &l, &r);
q[i] = {l, r, i, (l - 1) / u + 1};
}
sort(q + 1, q + 1 + m);
l = r = 0;
///解释一下下面的四个while循环。
///当 l > q[i].l时:说明当前区间的左边还有一些在询问区间里面要加上那些数字对答案的影响,由于l这一位已经加上了,所以直接--l开始。
///当 l < q[i].l时:说明当前区间内还有一些不在询问区间里面要减那些数字对答案的影响,由于l这一位也没有在询问区间里面,所以直接l++开始。
///后面两个与前面思想一致,就不重复了,这里需要好好理解下。
for (int i = 1; i <= m; i++) {///这里面自己好好模拟一下,就能够明白。
while (l > q[i].l) add(a[--l]);
while (l < q[i].l) del(a[l++]);
while (r < q[i].r) add(a[++r]);
while (r > q[i].r) del(a[r--]);
Ans[q[i].id] = ans;
}
for (int i = 1; i <= m; i++)
printf("%d\n", Ans[i]);
return 0;
}
洛谷 P2709 小B的询问
题意:小B有一个序列,包含N个1~K之间的整数。他一共有M个询问,每个询问给定一个区间[L..R],求Sigma(c(i)^2)的值,其中i的值从1到K,其中c(i)表示数字i在[L..R]中的重复次数。小B请你帮助他回答询问。
#pragma comment(linker, "/STACK:102400000,102400000")
#include <iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<map>
#include<queue>
#include<set>
#include<cmath>
#include<stack>
#include<string>
const int mod = 998244353;
const int maxn = 1e5 + 5;
const int inf = 1e9;
const long long onf = 1e18;
#define me(a, b) memset(a,b,sizeof(a))
#define lowbit(x) x&(-x)
#define lson l,mid,rt<<1
#define rson mid+1,r,rt<<1|1
#define PI 3.14159265358979323846
typedef long long ll;
typedef unsigned long long ull;
using namespace std;
int a[maxn];
ll sum[maxn], tans;
struct node {
int l, r, id, cmp;
bool friend operator<(node a,node b){///排序,这里用了01优化
if(a.cmp==b.cmp){
if(a.cmp&1)
return a.r<b.r;
return a.r>b.r;
}
return a.l<b.l;
}
} q[maxn];
///添加和下面的删除tans为什么加这么多,简单推一下就好了。
///添加:要是前面出现x个,现在又出现,说明tans应该加上(x+1)^2-x^2=2*x+1。
///删除:要是前面出现x个,现在需要删除,说明tans应该加上(x)^2-(x-1)^2=-2*x+1。
void add(int pos) {
ll x = sum[a[pos]];
sum[a[pos]]++;
tans += 2 * x + 1;
}
void del(int pos) {
ll x = sum[a[pos]];
sum[a[pos]]--;
tans += -2 * x + 1;
}
int main() {
int n, m, k;
scanf("%d%d%d", &n, &m, &k);
for (int i = 1; i <= n; i++)
scanf("%d", &a[i]);
int u = sqrt(n);///分块的大小
for (int i = 1; i <= m; i++) {///将询问离线
scanf("%d%d", &q[i].l, &q[i].r);
q[i].id = i,q[i].cmp=(q[i].l-1)/u+1;
}
sort(q + 1, q + 1 + m);
int l = 1, r = 1;
sum[a[1]]=1,tans=1;///直接把第一位加上,也可以不加
ll ans[maxn];
for (int i = 1; i <= m; i++) {
while (l > q[i].l) add(--l);
while (l < q[i].l) del(l++);
while (r < q[i].r) add(++r);
while (r > q[i].r) del(r--);
ans[q[i].id] = tans;
}
for (int i = 1; i <= m; i++)
printf("%lld\n", ans[i]);
return 0;
}
洛谷 P1494 [国家集训队]小Z的袜子
题意:小Z把这N只袜子从1到N编号,然后求出从编号L到R抽到两只颜色相同的袜子的概率(数值相同既为相同的袜子)。
题解:莫队模板题,其实只要维护每个颜色出现的次数就好了,细节看代码。
#pragma comment(linker, "/STACK:102400000,102400000")
#include <iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<map>
#include<queue>
#include<set>
#include<cmath>
#include<stack>
#include<string>
const int mod = 998244353;
const int maxn = 1e5 + 5;
const int inf = 1e9;
const long long onf = 1e18;
#define me(a, b) memset(a,b,sizeof(a))
#define lowbit(x) x&(-x)
#define lson l,mid,rt<<1
#define rson mid+1,r,rt<<1|1
#define PI 3.14159265358979323846
typedef long long ll;
typedef unsigned long long ull;
using namespace std;
int a[maxn];
ll sum[maxn], ans;
struct node {
int l, r, id, cmp;
bool friend operator<(node a, node b) {///排序,这里用了01优化
if (a.cmp == b.cmp) {
if (a.cmp & 1)
return a.r < b.r;
return a.r > b.r;
}
return a.l < b.l;
}
} q[maxn];
void add(int pos) {
ll x = sum[a[pos]];
sum[a[pos]]++;
ans += x;
}
void del(int pos) {
ll x = sum[a[pos]];
sum[a[pos]]--;
ans += -(x - 1);
}
int main() {
int n, m;
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++)
scanf("%d", &a[i]);
int u = sqrt(n);
for (int i = 1; i <= m; i++) {
scanf("%d%d", &q[i].l, &q[i].r);
q[i].id = i, q[i].cmp = (q[i].l - 1) / u + 1;
}
sort(q + 1, q + 1 + m);
int l = 1, r = 1;
sum[a[1]] = 1, ans = 0;
ll Ans1[maxn], Ans2[maxn];
for (int i = 1; i <= m; i++) {
if (q[i].l == q[i].r) {
Ans1[q[i].id] = 0, Ans2[q[i].id] = 1;
continue;
}
while (l > q[i].l) add(--l);
while (l < q[i].l) del(l++);
while (r < q[i].r) add(++r);
while (r > q[i].r) del(r--);
ll len = q[i].r - q[i].l + 1;///区间长度
ll temp_fm = len * (len - 1) / 2, temp_fz = ans;
ll d = __gcd(temp_fm, temp_fz);///因为最简分数,这里要除以gcd
if (temp_fz == 0)///分子为0的情况,分母直接是1
temp_fm = 1, d = 1;
Ans1[q[i].id] = temp_fz / d, Ans2[q[i].id] = temp_fm / d;
}
for (int i = 1; i <= m; i++)
printf("%lld/%lld\n", Ans1[i], Ans2[i]);
return 0;
}
CodeForces - 617E XOR and Favorite Number
题意:给你一个大小为n的序列,然后给你一个数字k,再给出m组询问,询问给出一个区间,问这个区间里面有多少个区间的异或结果为k。
题解:a[n]表示前n个数的异或结果。如果我们要求求区间[i,j]上的异或结果,可以用a[j]^a[i-1]得到。我们可以从左端点开始枚举,用sum数组记录前缀异或和出现的次数。在查询的时候,a[i]^k就是我们要找的前缀和的大小,因为前面知道如果我们要求区间[i,j]上的异或结果,可以用a[j]^a[i-1]得到,所以我们要找之前有多少个前缀异或和现在的前缀异或值为k,对应到sum数组去找a[i]^k的个数,并更新答案就行了。
#pragma comment(linker, "/STACK:102400000,102400000")
#include <iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<map>
#include<queue>
#include<set>
#include<cmath>
#include<stack>
#include<string>
const int mod = 998244353;
const int maxn = 1e6 + 5;
const int inf = 1e9;
const long long onf = 1e18;
#define me(a, b) memset(a,b,sizeof(a))
#define lowbit(x) x&(-x)
#define lson l,mid,rt<<1
#define rson mid+1,r,rt<<1|1
#define PI 3.14159265358979323846
typedef long long ll;
typedef unsigned long long ull;
using namespace std;
int n, m, k;
int a[maxn];
ll ans[maxn], tans, sum[maxn<<2];
struct node {
int l, r, id, cmp;
bool friend operator<(node a, node b) {
if (a.cmp == b.cmp) {
if (a.cmp & 1)
return a.r < b.r;
return a.r > b.r;
}
return a.l < b.l;
}
} q[maxn];
void add(int x) {
tans += sum[x ^ k];
sum[x]++;
}
void del(int x) {
sum[x]--;
tans -= sum[x ^ k];
}
int main() {
scanf("%d%d%d", &n, &m, &k);
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
a[i] ^= a[i - 1];
}
int u = sqrt(n);
for (int i = 1; i <= m; i++) {
int l, r;
scanf("%d%d", &l, &r);
q[i] = {l - 1, r, i, (l-1) / u};
}
sort(q + 1, q + 1 + m);
sum[0] = 1;
int l = 0, r = 0;
for (int i = 1; i <= m; i++) {
while (l < q[i].l) del(a[l++]);
while (l > q[i].l) add(a[--l]);
while (r > q[i].r) del(a[r--]);
while (r < q[i].r) add(a[++r]);
ans[q[i].id] = tans;
}
for (int i = 1; i <= m; i++)
printf("%lld\n", ans[i]);
return 0;
}
带修莫队:普通的莫队只能解决没有修改的问题,带修莫队就是一种支持单点修改的莫队算法。
算法核心:
还是对询问区间进行分块排序,但是每个询问除了左端点和右端点还要记录这次询问是在第几次修改之后(时间),以左端点所在块为第一关键字,以右端点所在块为第二关键字,以时间为第三关键字进行排序。如果当前修改数比询问的修改数少就把没修改的进行修改,反之回退(其实跟区间修改差不多,看看代码就很容易理解的)。
带修莫队例题 :
CodeForces - 940F Machine Learning
洛谷 P1903 [国家集训队]数颜色 / 维护队列
题意:现在有一些序列有两个操作:
操作一:给出 L R,让你求出这个区间里有多少不同的数字。
操作二:给出P Col,将位置P的数改为Col。
// luogu-judger-enable-o2
#pragma comment(linker, "/STACK:102400000,102400000")
#include <iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<map>
#include<queue>
#include<set>
#include<cmath>
#include<stack>
#include<string>
#include <time.h>
const int mod = 998244353;
const int maxn = 5e4 + 5;
const int inf = 1e9;
const long long onf = 1e18;
#define me(a, b) memset(a,b,sizeof(a))
#define lowbit(x) x&(-x)
#define lson l,mid,rt<<1
#define rson mid+1,r,rt<<1|1
#define PI 3.14159265358979323846
typedef long long ll;
typedef unsigned long long ull;
using namespace std;
int color[maxn * 20];
int ans, Ans[maxn];
int a[maxn];
int n, m, x, y;
struct node {
///pre表示本次查询时,前面修改了几次
int l, r, id, pre, cmp1, cmp2;
///排序函数说一下。
///自己总结的一个小技巧,不知道对不对,带修莫队排序同时要看左端点,右端点,和本次查询前面的修改修改时间
///不带修莫队排序函数就只看左端点就行了。
///不知道对不对,大家可以和前面不带修莫队的排序函数比较下。要是有大佬发现这个规律不对,请不吝赐教哈
bool friend operator<(node a, node b) {
if (a.cmp1 != b.cmp1) {
return a.l < b.l;
}
if (a.cmp2 != b.cmp2) {
if (a.cmp1 & 1)
return a.r < b.r;
return a.r > b.r;
}
return a.pre < b.pre;
}
} query[maxn];
struct Node {
int pos, val;
} change[maxn];
void add(int x) {
if (++color[x] == 1)
ans++;
}
void del(int x) {
if (--color[x] == 0)
ans--;
}
///这个change_pos函数就是带修和不带修莫队写法上的区别。
void change_pos(int p, int i) {
///修改位置要在本次查询区间里面,不然则不用修改应该不影响本区间答案
if (change[p].pos >= query[i].l && change[p].pos <= query[i].r) {
add(change[p].val);///加上修改过后数字对答案的影响
del(a[change[p].pos]);///删除原数字对答案的影响
}
///这里交换函数是一个小小的技巧吧,因为我们可能在前面查询需要将数字修改,但是本次查询不用,则要修改回来。
///这是直接交换他们的值,要是需要修改,则直接交换值,要是需要修改回来,则把值交换回来就行了。
swap(change[p].val, a[change[p].pos]);
}
int main() {
scanf("%d%d", &n, &m);
int u = pow(n, 2.0 / 3.0);
for (int i = 1; i <= n; i++)
scanf("%d", &a[i]);
int sq = 0, sc = 0;
char str[5];
///查询和修改需要用两个结构体来装。
for (int i = 1; i <= m; i++) {
scanf("%s%d%d", str, &x, &y);
if (str[0] == 'Q')
++sq, query[sq] = {x, y, sq, sc, (x - 1) / u + 1, (y - 1) / u + 1};
else
++sc, change[sc] = {x, y};
}
sort(query + 1, query + 1 + sq);
int l = 0, r = 0, now = 0;
for (int i = 1; i <= sq; i++) {
while (l > query[i].l) add(a[--l]);
while (l < query[i].l) del(a[l++]);
while (r < query[i].r) add(a[++r]);
while (r > query[i].r) del(a[r--]);
///下面两个循环其实就是和上面的原理一下,仔细想想
while (query[i].pre > now) change_pos(++now, i);
while (query[i].pre < now) change_pos(now--, i);
Ans[query[i].id] = ans;
}
for (int i = 1; i <= sq; i++)
printf("%d\n", Ans[i]);
return 0;
}
CodeForces - 940F Machine Learning
题意:现在有一段序列,给出询问区间,求区间数字出现次数的mex,带修改。
题解:mex直接暴力求解,其他的都是基本操作,注意数据要离散化。
#pragma comment(linker, "/STACK:102400000,102400000")
#include <iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<map>
#include<queue>
#include<set>
#include<cmath>
#include<stack>
#include<string>
const int mod = 998244353;
const int maxn = 2e5 + 5;
const int inf = 1e9;
const long long onf = 1e18;
#define me(a, b) memset(a,b,sizeof(a))
#define lowbit(x) x&(-x)
#define Lson l,mid,rt<<1
#define Rson mid+1,r,rt<<1|1
#define lson rt<<1
#define rson rt<<1|1
#define PI 3.14159265358979323846
typedef long long ll;
typedef unsigned long long ull;
using namespace std;
int n, m, a[maxn], ans[maxn], sum[maxn];
int lens[maxn];
struct node {
int l, r, id, pre, cmp1, cmp2;
bool friend operator<(node a, node b) {
if (a.cmp1 != b.cmp1)
return a.l < b.l;
if (a.cmp2 != b.cmp2) {
if (a.cmp1 & 1)
return a.r < b.r;
return a.r > b.r;
}
return a.pre < b.pre;
}
} q[maxn];
struct Node {
int pos, val;
} c[maxn];
void add(int x) {
if (lens[sum[x]])
lens[sum[x]]--;
sum[x]++;
lens[sum[x]]++;
}
void del(int x) {
if (lens[sum[x]])
lens[sum[x]]--;
sum[x]--;
lens[sum[x]]++;
}
void change(int pos, int i) {
if (c[pos].pos >= q[i].l && c[pos].pos <= q[i].r) {
add(c[pos].val);
del(a[c[pos].pos]);
}
swap(a[c[pos].pos], c[pos].val);
}
int get_ans() {
for (int i = 1; i < maxn; i++)
if (!lens[i])
return i;
}
int main() {
scanf("%d%d", &n, &m);
int b[maxn], len = n;
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
b[i] = a[i];
}
int u = pow(n, 2.0 / 3.0);
int sq = 0, sc = 0;
for (int i = 1; i <= m; i++) {
int opt, x, y;
scanf("%d%d%d", &opt, &x, &y);
if (opt == 1)
++sq, q[sq] = {x, y, sq, sc, (x - 1) / u + 1, (y - 1) / u + 1};
else
++sc, c[sc] = {x, y}, b[++len] = y;
}
sort(b + 1, b + 1 + len);
for (int i = 1; i <= n; i++)
a[i] = lower_bound(b + 1, b + 1 + len, a[i]) - b;
for (int i = 1; i <= sc; i++) {
c[i].val = lower_bound(b + 1, b + 1 + len, c[i].val) - b;
}
sort(q + 1, q + 1 + sq);
int l = 0, r = 0, now = 0;
for (int i = 1; i <= sq; i++) {
while (l < q[i].l) del(a[l++]);
while (l > q[i].l) add(a[--l]);
while (r < q[i].r) add(a[++r]);
while (r > q[i].r) del(a[r--]);
while (q[i].pre > now) change(++now, i);
while (q[i].pre < now) change(now--, i);
ans[q[i].id] = get_ans();
}
for (int i = 1; i <= sq; i++)
printf("%d\n", ans[i]);
return 0;
}