【题目来源】
https://www.acwing.com/problem/content/2494/
https://www.luogu.com.cn/problem/P1972
【题目描述】
HH 有一串由各种漂亮的贝壳组成的项链。
HH 相信不同的贝壳会带来好运,所以每次散步完后,他都会随意取出一段贝壳,思考它们所表达的含义。
HH 不断地收集新的贝壳,因此他的项链变得越来越长。
有一天,他突然提出了一个问题:某一段贝壳中,包含了多少种不同的贝壳?
这个问题很难回答,因为项链实在是太长了。
于是,他只好求助睿智的你,来解决这个问题。
【输入格式】
第一行:一个整数 N,表示项链的长度。
第二行:N 个整数,表示依次表示项链中贝壳的编号(编号为 0 到 1000000 之间的整数)。
第三行:一个整数 M,表示 HH 询问的个数。
接下来 M 行:每行两个整数,L 和 R,表示询问的区间。
【输出格式】
M 行,每行一个整数,依次表示询问对应的答案。
【数据范围】
1≤N≤50000,
1≤M≤2×10^5,
1≤L≤R≤N
【输入样例】
6
1 2 3 4 3 5
3
1 2
3 5
2 6
【输出样例】
2
2
4
【算法分析】
● 基础莫队算法是一种离线算法,通常用于“不修改、只查询”的一类区间问题。莫队算法的时间复杂度为。
● 莫队算法=离线+暴力+分块。
“在线”是交互式的,一问一答。特别的,如果前面的答案用于后面的提问,称为“强制在线”。
“离线”是非交互的,一次性读取所有问题,一起回答。
● 莫队算法的精髓在于奇偶性排序。也就是依据左端点 L 位于奇数块还是偶数块,决定右端点 R 从小到大排序还是从大到小排序。
(1)首先,利用分块算法将给定的 n 个数分成 sqrt(n) 个块;
(2)然后,将多个询问的左端点 L 按块从小到大排序。
若 L 位于奇数块,则对右端点 R 从小到大排序。
若 L 位于偶数块,则对右端点 R 从大到小排序。
反之亦可。示意图如下所示:
● 莫队算法中定义的数组 cnt[x],表示数字 x 出现的次数。莫队算法不同题目的代码区别主要在于 add() 函数和 del() 函数,其他部分代码基本一致。
● “分块”算法的基本要素
特别的,参考 https://blog.csdn.net/hnjzsyjyj/article/details/138955263 可知“分块”算法的一些技术细节。如下所述:
(1)块的大小用 block 表示。通常,令 block=sqrt(n)。其中,n 为元素个数。
(2)块的数量用 cnt 表示。计算块的数量的代码如下:
int block=sqrt(n);
int cnt=n/block;
if(n % block) cnt++;
(3)定义 pos[i] 为第 i 个元素所在的块。
若下标从 1 开始,则有 pos[i]=(i-1)/block+1。其中,block=sqrt(n)。
若下标从 0 开始,则有 pos[i]=i/block。其中,block=sqrt(n)。
(4)块的左边界 le[] 及右边界 ri[]。
若用 le[i] 和 ri[i] 分别表示块 i 的第一个和最后一个元素的位置。
若下标从 1 开始,则有:
le[1]=1, ri[1]=block;
le[2]=block+1, ri[2]=2*block;
……
le[i]=(i-1)*block+1, ri[i]=i*block;
……
若下标从 0 开始,则有:
le[0]=0, ri[0]=block-1;
le[1]=block, ri[1]=2*block-1;
……
le[i]=i*block, ri[i]=(i+1)*block-1;
……
综上,“分块”算法 build() 函数的构建细节如下。
(1)下标从 0 开始,build() 函数的构建如下。
void build(int n) {
int block=sqrt(n);
int cnt=n/block;
if(n%block) cnt++;
for(int i=0; i<cnt; i++) {
le[i]=i*block;
ri[i]=(i+1)*block-1;
}
ri[cnt-1]=n-1;
for(int i=0; i<n; i++) pos[i]=i/block;
}
(2)下标从 1 开始,build() 函数的构建如下。
void build(int n) {
int block=sqrt(n);
int cnt=n/block;
if(n%block) cnt++;
for(int i=1; i<=cnt; i++) {
le[i]=(i-1)*block+1;
ri[i]=i*block;
}
ri[cnt]=n;
for(int i=1; i<=n; i++) pos[i]=(i-1)/block+1;
}
特别注意,针对不同的问题利用“分块”算法进行分析时,下图将具有极高的应用价值。其对整块及碎块的处理一目了然。
分块算法示例详见:
洛谷P3372:线段树 1 → https://blog.csdn.net/hnjzsyjyj/article/details/138863063
洛谷 P3203:弹飞绵羊 → https://blog.csdn.net/hnjzsyjyj/article/details/138903837
HDU 5057:Argestes and Sequence → https://blog.csdn.net/hnjzsyjyj/article/details/138926594
【算法代码】
注意:本代码,AcWing 2492 能过,但洛谷 P1972 数据加强了,导致用莫队算法求解时部分样例会超时(TLE),故建议用树状数组或线段树来做洛谷 P1972 。
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+5;
long long a[maxn];
long long pos[maxn];
long long cnt[maxn],ans[maxn];
int num;
int n,m;
struct node {
int le,ri,id;
} q[maxn];
bool cmp(node a,node b) {
if(pos[a.le]!=pos[b.le]) return pos[a.le]<pos[b.le];
if(pos[a.le]&1) return a.ri<b.ri; //parity optimization
return a.ri>b.ri;
}
void add(int x) {
cnt[a[x]]++;
if(cnt[a[x]]==1) num++;
}
void del(int x) {
cnt[a[x]]--;
if(!cnt[a[x]]) num--;
}
int main() {
cin>>n;
for(int i=1; i<=n; i++) cin>>a[i];
int block=sqrt(n);
for(int i=1; i<=n; i++) pos[i]=(i-1)/block+1;
cin>>m;
for(int i=1; i<=m; i++) {
cin>>q[i].le>>q[i].ri;
q[i].id=i;
}
sort(q+1,q+m+1,cmp);
int L=1,R=0;
for(int i=1; i<=m; i++) {
while(L<q[i].le) del(L++);
while(L>q[i].le) add(--L);
while(R<q[i].ri) add(++R);
while(R>q[i].ri) del(R--);
ans[q[i].id]=num;
}
for(int i=1; i<=m; i++) cout<<ans[i]<<endl;
return 0;
}
/*
in:
6
1 2 3 4 3 5
3
1 2
3 5
2 6
out:
2
2
4
*/
【参考文献】
https://www.cnblogs.com/wner/p/18007108
https://www.acwing.com/solution/content/23426/
https://blog.sengxian.com/algorithms/mo-s-algorithm
https://blog.csdn.net/m0_63737271/article/details/125786194
https://blog.csdn.net/GROZAX/article/details/130069889
https://blog.csdn.net/weixin_75161465/article/details/137195888