莫队算法入门

参考博客:莫队算法-从入门到黑题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就是答案。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值