莫队算法(在更)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/xyyxyyx/article/details/90384205

莫队

适用范围

离线,多次区间查询,查询一些线段树之类数据结构做不了的事情(区间第一个未出现的自然数、区间不同的数个数……)。还有一些特性,比如:区间扩大或缩小一个位置要比较快,要能分块。

思路

首先我们考虑,假如我们已经知道了 ( l 1 , r 1 ) (l1, r1) 的答案,如何转移到另一个询问 ( l 2 , r 2 ) (l2, r2) 。需要更新 ( r 1 , r 2 ] (r1, r2] ( l 1 , r 1 ] (l1, r1] 两个区间。搞得不好的话,很显然,更新时间是会退化到 O ( n 2 ) O(n^2) 的。那么我们要做的就是尽量减少这两段区间的长度。

运用了分块的思想。排序来尽量减少重复的计算。

具体理解请看例题。

例题

【bzoj2038】小z的袜子

来源:bzoj2038

题意:给出序列,每个询问求一个区间内等概率随机取两个数,有多大概率两数相等。

那我们分三步思考:

一、单个询问

对于一个询问,总可能数:

C ( n , 2 ) = n ( n 1 ) / 2 C(n, 2) = n*(n-1)/2

设每种颜色的袜子有 a i ai 只,那么:

a n s = C ( a i , 2 ) C ( n , 2 ) ans = \frac{\sum C(ai, 2)}{C(n, 2)}

二、递推

如何从上一个状态转移到当前状态呢?假设转移后加入了一个颜色为 c i c_i 的袜子,加上后这种颜色的袜子总共有 n n 只。那么:

C ( n , 2 ) = n ( n 1 ) / 2 C(n, 2) = n*(n-1)/2
C ( i 1 , 2 ) = ( i 1 ) ( i 2 ) / 2 C(i-1, 2) = (i-1)*(i-2)/2

所以:

C ( i , 2 ) = C ( i 1 , 2 ) + i 1 C(i, 2) = C(i-1, 2)+i-1
边界:

C ( 0 , 2 ) = 0 C(0, 2) = 0

这样指针移动的同时就可以统计答案了。

三、排序

分块后,先按左端点属于哪个块,再按右端点,双关键字排序。

四、分块

设块大小为m。

右指针最多 O ( n n / m ) O(n*n/m) ,因为对于每一块,右指针都最多从最左到最右遍历一遍序列。

左指针最多 O ( n m ) O(n*m) ,因为对于每一个询问,他的左端点距离排在上一个的询问最多有一块的长度。

所以 m = sqrt(n)时复杂度最小。

因为是第一道例题所以贴一个代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 5e4+10;
int n, m, q, c[N];
struct NODE{
	int l, r, bel, id;
	ll ans;
}a[N];
int cnt[N];
ll out[N][2];

bool cmp(NODE x, NODE y){
	return x.bel == y.bel ? x.r < y.r : x.bel < y.bel;
}

int main()
{
	scanf("%d%d", &n, &q);
	m = sqrt(n);
	for (int i = 1; i <= n; ++ i)
		scanf("%d", c+i);
	for (int i = 1; i <= q; ++ i){
		scanf("%d%d", &a[i].l, &a[i].r);
		a[i].bel = a[i].l/m;
		a[i].id = i;
	}
	sort(a+1, a+q+1, cmp);
	{
		int i = 1, j;
		while (i <= q){
			int l = 1, r = 0;
			ll an = 0;
			memset(cnt, 0, sizeof(cnt));
			for (j = i; a[i].bel == a[j].bel; ++ j){
				while (r < a[j].r)
					++ r, an += cnt[c[r]], ++ cnt[c[r]];
				while (l < a[j].l)
					-- cnt[c[l]], an -= cnt[c[l]], ++ l;
				while (l > a[j].l)
					-- l, an += cnt[c[l]], ++ cnt[c[l]];
				a[j].ans = an;
			}
			i = j;
		}
	}
	for (int i = 1; i <= q; ++ i)
		out[a[i].id][0] = a[i].ans, out[a[i].id][1] = 1ll*(a[i].r-a[i].l+1)*(a[i].r-a[i].l)/2;
	for (int i = 1; i <= q; ++ i){
		ll x = out[i][0], y = out[i][1], d = __gcd(x, y);
		printf("%lld/%lld\n", x/d, y/d);
	}
	return 0;
}

看似只是排了一遍序,但是成功将复杂度由 O ( n 2 ) O(n^2) 降到了 O ( n n ) O(n\sqrt{n}) ,好腻害呀。

带修莫队

思路

唯一的变化就是增加了时间这一个维度。

加上时间之后,询问变成 ( t , l , r ) (t, l, r) 的三元组。然后的思路就和之前一模一样啦,在一块内让某一维的指针只会单向移动,然后其他两维在块内乱跳。

例题

【bzoj2120】数颜色

来源:bzoj2120

题意:一个序列,有修改操作,询问一段区间有多少个不同的数。

模板题。思路上面讲了,都是套路。

假如序列长度和询问个数同阶的话,复杂度最低是 O ( n 5 3 ) O(n^{\frac{5}{3}})

最简单的例题放上代码:

p.s.莫队写起来很短的而且代码思路很清晰。下面的代码因为头上一大坨加上离散化才这么长的

#include<bits/stdc++.h>
#define rep(i, a, b) for (int i = a, ub##i = b; i <= ub##i; ++ i)
#define per(i, a, b) for (int i = a, ub##i = b; i >= ub##i; -- i)
template<class T>void chkMax(T &x, T y){if (x < y) x = y;}
template<class T>void chkMin(T &x, T y){if (x > y) x = y;}
#define rdi read<int>
#define rdl read<long long>
template<typename T> inline T read()
{
	T x = 0, fh = 1;
	char c = getchar();
	while (c < '0' || c > '9'){if (c == '-') fh = -1; c = getchar();}
	while (c >= '0' && c <= '9') x = (x<<3)+(x<<1)+c-'0', c = getchar();
	return x*fh;
}
using namespace std;
const int N = 5e4+10;
const int C = N<<1;
int n, t, rn, qn, m, c[N], cnt[C], an, ans[N], ls[C], lsn;
struct R{int t, pos, pre, now, bt;}r[N];
struct Q{int t, id, l, r, bt, bl;}q[N];

bool cmp(Q x, Q y){
	return x.bt == y.bt ? (x.bl == y.bl ? x.r < y.r : x.bl < y.bl) : x.bt < y.bt;
}

void update(int col, int inc)
{
	cnt[col] += inc;
	if (inc == 1 && cnt[col] == 1) ++ an;
	if (inc == -1 && cnt[col] == 0) -- an;
}

int main()
{
	n = rdi(); t = rdi();
	m = pow(sqrt(1.0*n*t), 2.0/3);
	lsn = 0;
	rep(i, 1, n)
		c[i] = rdi(), ls[++ lsn] = c[i];
	rn = qn = 0;
	rep(i, 1, t){
		char s[1]; int x, y;
		scanf("%s", s);
		x = rdi(); y = rdi();
		if (s[0] == 'R')
			r[++ rn] = (R){i, x, 0, y, i/m}, ls[++ lsn] = y;
		else
			q[++ qn] = (Q){i, qn, x, y, i/m, x/m};
	}
	sort(ls+1, ls+lsn+1);
	lsn = unique(ls+1, ls+lsn+1)-(ls+1);
	rep(i, 1, n)
		c[i] = lower_bound(ls+1, ls+lsn+1, c[i])-ls;
	rep(i, 1, rn)
		r[i].now = lower_bound(ls+1, ls+lsn+1, r[i].now)-ls;
	rep(i, 1, rn)
		r[i].pre = c[r[i].pos], c[r[i].pos] = r[i].now;
	per(i, rn, 1)
		c[r[i].pos] = r[i].pre;
	sort(q+1, q+qn+1, cmp);
	{
		int i = 1, j, k = 0, l, _r;
		while (i <= qn){
			l = 1; _r = 0;
			memset(cnt, 0, sizeof(cnt)); an = 0;
			per(o, k, 1)
				c[r[o].pos] = r[o].pre;
			k = 0;
			for (j = i; q[j].bt == q[i].bt && q[j].bl == q[i].bl && j <= qn; ++ j){
				while (q[j].r > _r) ++ _r, update(c[_r], 1);
				while (q[j].l > l) update(c[l], -1), ++ l;
				while (q[j].l < l) -- l, update(c[l], 1);
				while (k > 0 && q[j].t < r[k].t){
					if (r[k].pos >= l && r[k].pos <= _r)
						update(r[k].pre, 1), update(r[k].now, -1);
					c[r[k].pos] = r[k].pre;
					-- k;
				}
				while (k < rn && q[j].t > r[k+1].t){
					++ k;
					if (r[k].pos >= l && r[k].pos <= _r)
						update(r[k].pre, -1), update(r[k].now, 1);
					c[r[k].pos] = r[k].now;
				}
				ans[q[j].id] = an;
			}
			i = j;
		}
	}
	rep(i, 1, qn)
		printf("%d\n", ans[i]);
	return 0;
}

【CF940E】Machine Learning

div2最后一题居然是莫队模板,太可怕了。

套路,再用线段树或者树状数组维护一下 m e x mex 就好了。

树上莫队

思路

还是套路,只不过被分块的东西从线性的序列变成了一棵树。

例题

【bzoj1086】王室联邦

来源:bzoj1086

题意:

让你把树分成大小为 [ B , 3 B ] [B, 3B] 的块,要求每块所有点到这一块的关键点都只经过这一块的点。

思路:

一遍dfs,维护一个栈,dfs一个点时先记录初始栈顶高度,每dfs完当前节点的一棵子树就判断栈内(相对于刚开始dfs时)新增节点的数量是否 B \ge B ,是则将栈内所有新增点分为同一块,核心点为当前dfs的点,当前节点结束dfs时将当前节点入栈,整个dfs结束后将栈内所有剩余节点归入已经分好的最后一个块。

这也是树上莫队所需要的分块方法,这样使得每一块内任意两点间距离不超过 3 B 3B 。证明这个博客讲的很清楚ouuan的博客园

【bzoj4129】Haruna’s Breakfast

来源:bzoj4129

树上带修莫队维护 m e x mex 值。前面带修莫队的例题Machine Learning的进化版。

代码:


【WC2013】糖果公园

和上一题几乎一样,只是维护的东西变成了每一种糖果已经有了几个。

总结

莫队是一种很套路的方法,假如出题人想让你写,那你一眼就能看出来。

更重要的是学习分块的思想。反正我是这么觉得的。

猜你喜欢

转载自blog.csdn.net/xyyxyyx/article/details/90384205