参考博客:莫队算法-从入门到黑题,Luogu题解
莫队算法:离线区间询问,先对数组进行分块,然后通过对查询区间的排序,再利用双指针移动确定区间,从而得出结果。 O ( n n ) O(n\sqrt{n}) O(nn)
分块
将长度为 n n n 的数组,分成每一段长为 n 2 3 n^{\frac{2}{3}} n32 的块,防止退化为 O ( n 2 ) O(n^2) O(n2)
int t = pow(n,2.0/3.0);
int size = ceil((double)n / t);
for(int i=1;i<=size;i++) {
for(int j=(i-1)*t+1;j<=min(i*t,n);j++) {
pos[j] = i;
}
}
查询区间排序
按照左端点所在块的编号升序排序,如果所在块的编号相同,且块的编号为奇数,则按右端点升序排,反之降序。
bool cmp(node a1,node a2) {
if(pos[a1.l] != pos[a2.l]) return pos[a1.l] < pos[a2.l];
else if(pos[a1.l] & 1) return a1.r < a2.r;
else return a1.r > a2.r;
}
指针移动
移动左右指针 l , r l,r l,r ,当查询的区间 q l ≠ l , q r ≠ r ql \not= l,qr \not= r ql=l,qr=r ,则需要移动指针使得 q l = l , q r = r ql = l ,qr= r ql=l,qr=r,在移动的过程中记录区间 [ q l , q r ] [ql,qr] [ql,qr] 中的值。
void work() {
int l = 0,r = 0,now = 0;
for(int i=1;i<=m;i++) {
int ql = a[i].l;
int qr = a[i].r;
while(l < ql) {
cnt[num[l]] --;
if(cnt[num[l]] == 0) now --;
l ++;
}
while(l > ql) {
l --;
cnt[num[l]] ++;
if(cnt[num[l]] == 1) now ++;
}
while(r < qr) {
r ++;
cnt[num[r]] ++;
if(cnt[num[r]] == 1) now ++;
}
while(r > qr) {
cnt[num[r]] --;
if(cnt[num[r]] == 0) now --;
r --;
}
res[a[i].id] = now;
}
}
题号:P1972 [SDOI2009] HH的项链(莫队 - 60 % 60\% 60%,线段树维护 - 100 % 100\% 100%)
题意:给出一个长度为 n n n 的数列 a 1 , a 2 , . . . , a n a_{1},a_{2},...,a_{n} a1,a2,...,an,有 q q q 个询问,每个询问给出区间 ( l , r ) (l,r) (l,r),需要你给出 a l , a l + 1 , . . . , a r a_{l},a_{l+1} ,...,a_{r} al,al+1,...,ar 这一段中有多少不同的数字。
莫队代码
#include<stdio.h>
#include<math.h>
#include<algorithm>
using namespace std;
const int N = 1e6 + 10;
struct node{
int l,r;
int id,cnt;
}a[N];
int num[N],L[N],R[N],t,pos[N];
int res[N],cnt[N],n,m;
bool cmp(node a1,node a2) {
if(pos[a1.l] != pos[a2.l]) return pos[a1.l] < pos[a2.l];
else if(pos[a1.l] & 1) return a1.r < a2.r;
else return a1.r > a2.r;
}
void work() {
int l = 0,r = 0,now = 0;
for(int i=1;i<=m;i++) {
int ql = a[i].l;
int qr = a[i].r;
while(l < ql) {
cnt[num[l]] --;
if(cnt[num[l]] == 0) now --;
l ++;
}
while(l > ql) {
l --;
cnt[num[l]] ++;
if(cnt[num[l]] == 1) now ++;
}
while(r < qr) {
r ++;
cnt[num[r]] ++;
if(cnt[num[r]] == 1) now ++;
}
while(r > qr) {
cnt[num[r]] --;
if(cnt[num[r]] == 0) now --;
r --;
}
res[a[i].id] = now;
}
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&num[i]);
t = sqrt(n);
for(int i=1;i<=t;i++) {
L[i] = (i - 1) * t + 1;
R[i] = i * t;
}
if(R[t] < n) {
t ++;
L[t] = R[t-1] - 1;
R[t] = n;
}
for(int i=1;i<=t;i++) {
for(int j=L[i];j<=R[i];j++) {
pos[j] = i;
}
}
scanf("%d",&m);
for(int i=1;i<=m;i++) {
scanf("%d %d",&a[i].l,&a[i].r);
a[i].id = i;
}
sort(a+1,a+1+m,cmp);
work();
for(int i=1;i<=m;i++) printf("%d\n",res[i]);
return 0;
}
线段树或树状数组维护
对于若干个询问的区间[l,r],如果他们的r都相等的话,那么项链中出现的同一个数字,一定是只关心出现在最右边的那一个的,例如:
项链是:1 3 4 5 1
那么,对于r=5的所有的询问来说,第一个位置上的1完全没有意义,因为r已经在第五个1的右边,对于任何查询的[L,5]区间来说,如果第一个1被算了,那么他完全可以用第五个1来替代。
因此,我们可以对所有查询的区间按照r来排序,然后再来维护一个树状数组,这个树状数组是用来干什么的呢?看下面的例子:
1 2 1 3
对于第一个1,insert(1,1);表示第一个位置出现了一个不一样的数字,此时树状数组所表示的每个位置上的数字(不是它本身的值而是它对应的每个位置上的数字)是:1 0 0 0
对于第二个2,insert(2,1);此时树状数组表示的每个数字是1 1 0 0
对于第三个1,因为之前出现过1了,因此首先把那个1所在的位置删掉insert(1,-1),然后在把它加进来insert(3,1)。此时每个数字是0 1 1 0
如果此时有一个询问[2,3],那么直接求sum(3)-sum(2-1)=2就是答案。