数据结构专题-学习笔记:莫队二次离线

1. 前言

莫队二次离线,是一种莫队,由 lxl 发明,专门用来处理莫队中转移不是 \(O(1)\),但是可以前缀和拆分的问题。

在学习之前,请先确保对莫队有一定的了解度,包括但不限于普通莫队及其优化。

如果您不知道普通莫队是什么,可以看一看我的 这篇博文

2. 模板

模板题:P4887 【模板】莫队二次离线(第十四分块(前体))

首先考虑莫队 废话

一种思路就是直接利用普通莫队,每一次移动指针的时候 \(O(C_{14}^k)\) 的复杂度来转移,但是这显然会超时。

那么考虑如何优化复杂度。

接下来设 \(f(x,[l,r])\) 表示 \(x\) 对区间 \([l,r]\) 的贡献,\(g([l_1,r_1],[l_2,r_2])\) 表示 \([l_1,r_1]\) 中的每一个数对 \([l_2,r_2]\) 的贡献,也就是 \(\sum_{i=l_1}^{r_1}f(a_i,[l_2,r_2])\)

那么在转移的时候,我们对式子做一个变形:

\[f(x,[l,r])=f(x,[1,r]) - f(x,[1,l-1]) \]

也就是可以差分。

那么如果记 \(h([l,r])\) 表示区间 \([l,r]\) 的答案,那么仿照上面做一遍差分:

\[h([l,r])=h([1,r])-h([1,l-1])-g([1,l-1],[l,r]) \]

于是我们发现此时的结果分为两部分:形如 \(h([1,k])\) 的和形如 \(g([l_1,r_1],[l_2,r_2])\) 的。

那么形如 \(h([1,k])\) 的可以采用一遍预处理与一遍莫队求出来,这样就解决了第一部分。

但是 \(g([l_1,r_1],[l_2,r_2])\) 要怎么办啊?

看一下前面给出的式子:\(g([l_1,r_1],[l_2,r_2])=\sum_{i=l_1}^{r_1}f(a_i,[l_2,r_2])\)

这与莫队指针移动的连续性恰好吻合。

然后如果您学过扫描线,您还可以发现所有形如 \(g([l_1,r_1],[l_2,r_2])\) 可以利用扫描线解决。

那么于是我们先用一次莫队算出形如 \(h([1,k])\) 的答案以及处理出所有形如 \(g([l_1,r_1],[l_2,r_2])\) 的询问,然后一遍扫描线解决答案。

然后最后的答案统计在 \(ans\) 数组内。

于是您连样例都过不去。

为什么?上面的过程有问题吗?

其实上面的过程没问题,关键的一点就是在执行上述过程的时候实际上 \(ans\) 记录的是答案的变化量,最后还要做一遍前缀和才能通过。

友情提醒:考虑到代码里面使用了先进的 STL:tuple,因此请采用 C++17 及以上编译。

当然可以写个结构体

代码:

/*
========= Plozia =========
	Author:Plozia
	Problem:P4887 【模板】莫队二次离线(第十四分块(前体))
	Date:本代码书写于 2020/12/16
========= Plozia =========
*/

#include <bits/stdc++.h>
using namespace std;

typedef long long LL;
const int MAXN = 1e5 + 10;
int n, m, k, a[MAXN], t[MAXN], p[MAXN], block, ys[MAXN];
LL ans[MAXN];
struct node
{
	int l, r, id;
	LL ans;
}q[MAXN];
vector <int> b;
vector < tuple <int, int, int, int> > v[MAXN];

int read()
{
	int sum = 0; char ch = getchar();
	while (ch < '0' || ch > '9') ch = getchar();
	while (ch >= '0' && ch <= '9') {sum = (sum << 3) + (sum << 1) + (ch ^ 48); ch = getchar();}
	return sum;
}

bool cmp(const node &fir, const node &sec)
{
	if (ys[fir.l] ^ ys[sec.l]) return ys[fir.l] < ys[sec.l];
	return fir.r < sec.r;
}

int Count(int x)
{
	int sum = 0;
	for (; x; x >>= 1)
		if (x & 1) sum++;
	return sum;
}//统计位数

int main()
{
	n = read(); m = read(); k = read(); block = ceil(n / sqrt(m));
	if (k > 14)
	{
		for (int i = 1; i <= m; ++i) printf("0\n");
		return 0;
	}//坑
	for (int i = 1; i <= n; ++i) {a[i] = read(); ys[i] = (i - 1) / block + 1;}
	for (int i = 1; i <= m; ++i) {q[i].l = read(); q[i].r = read(); q[i].id = i;}
	sort(q + 1, q + m + 1, cmp);
	for (int i = 0; i < 16384; ++i)
		if (Count(i) == k) b.push_back(i);
	for (int i = 1; i <= n; ++i)
	{
		p[i] = t[a[i]];
		for (int j = 0; j < b.size(); ++j) t[a[i] ^ b[j]]++;
	}//预处理
	memset(t, 0, sizeof(t));
	int l = 1, r = 0;
	for (int i = 1; i <= m; ++i)
	{
		if (l > q[i].l) v[r].emplace_back(q[i].l, l - 1, i, 1);
		while (l > q[i].l) q[i].ans -= p[--l];
		if (r < q[i].r) v[l - 1].emplace_back(r + 1, q[i].r, i, -1);
		while (r < q[i].r) q[i].ans += p[++r];
		if (l < q[i].l) v[r].emplace_back(l, q[i].l - 1, i, -1);
		while (l < q[i].l) q[i].ans += p[l++];
		if (r > q[i].r) v[l - 1].emplace_back(q[i].r + 1, r, i, 1);
		while (r > q[i].r) q[i].ans -= p[r--];
	}//莫队,处理 h([1,k]) 答案以及 g([l1,r1],[l2,r2]) 的询问
	for (int i = 1; i <= n; ++i)
	{
		for (int j = 0; j < b.size(); ++j) ++t[a[i] ^ b[j]];
		for (int j = 0; j < v[i].size(); ++j)
		{
            tuple x = v[i][j];
			for (int zzh = get<0>(x); zzh <= get<1>(x); ++zzh)
			{
				if (zzh <= i && k == 0) q[get<2>(x)].ans += get<3>(x) * (t[a[zzh]] - 1);
				else q[get<2>(x)].ans += get<3>(x) * t[a[zzh]];
			}
		}
	}//扫描线
	for (int i = 1; i <= m; ++i) q[i].ans += q[i-1].ans;//注意答案要差分
	for (int i = 1; i <= m; ++i) ans[q[i].id] = q[i].ans;//重新还原答案
	for (int i = 1; i <= m; ++i) printf("%lld\n", ans[i]);
	return 0;
}

3. 总结

莫队二次离线的式子:\(h([l,r])=h([1,r])-h([1,l-1])-g([1,l-1],[l,r])\)

posted @ 2022-04-17 14:46  Plozia  阅读(500)  评论(0编辑  收藏  举报