学习笔记:莫队

离线算法。

算法核心思想是优化暴力。

一般形式是若干区间询问。

若能用较少的时间(\(O(1) - O(\log n)\))从 \([l, r]\) 的答案推演至 \([l, r + 1], [l - 1, r]\) 的答案,那么这个问题就可以用莫队优化。

普通莫队

设扩展一次的时间为 \(O(a)\)

\([l, r]\) 的答案快速推演至 \([l, r + 1], [l, r - 1], [l + 1, r], [l - 1, r]\)

我们考虑把询问排序,对于相邻的询问,\([l_i, r_i]\) 移动到 \([l_{i+1}, r_{r+1}]\),复杂度是 \(O(a(|r_i - r_{i-1}| + |l_{i+1}-l_i|))\)

排序方式?考虑分块,把序列分块,设 \(i\) 所属块为 \(pos_i\),设块长度为 \(S\) ,询问排序关键字,先按照左端点块数 \(pos_l\) 排序,若左端点所属块相等,右端点按从小到大排序。

复杂度:

  • 右指针:\(\frac{n^2}{S}\)
  • 左指针:
    • 块内:每次移动最多不会超过 \(S\) 次,总共 \(mS\)
    • 块间:即这一块最后询问到下一块第一个询问,下一个询问左端点一定是大于这个询问左端点的。所以总复杂度不超过 \(n\)

平衡算法 \(\frac{n^2}{S} = mS\)\(S = \frac{n}{\sqrt{m}}\) 最优。总复杂度 \(O(n \sqrt{m} \times a)\)

模板题:

带修莫队

其实就是多了一个时间维。

\([l, r, t]\) 代表区间为 \([l, r]\),第 \(t\) 次修改之后的答案。

排序:第一关键字为 \(pos_l\),第二关键字为 \(pos_r\),第三关键字为 \(t\)

\(pos_l , pos_r\) 相同的一段是一个块。

设每一块长度为 \(S\),一共有 \(\frac{n^2}{S}\) 块。

  • \(l\) 的移动不超过 \(mS + n\)

  • \(r\) 的移动不超过 \(mS + \frac{n^2}{S}\)

  • \(t\)\(\frac{n^2}{S^2}t\)

\(n, m\) 同阶时,\(S = \sqrt[3]{nt}\) 取到最优解,复杂度 \(O(\sqrt[3]{n^4t})\)

模板题:

回滚莫队

若只能插入,不能删除,可以用回滚莫队。

排序和普通莫队一样

\(R_i\)\(i\) 这个块最右边点的编号。

  • 暴力求块内的所有区间
  • 同一块左端点,他们所有的跨块询问,这么做:首先强行变成区间 \([R_{pos_l}, R_{pos_l}]\) 剩下的询问必然在 \(r > R_{pos_l}\)。转移一个询问让右端点跟着走,然后左端点暴力扩展,暴力撤销。

复杂度类似普通莫队,左端点每次暴力扩展暴力撤销都在同一个块里,所以复杂度还是跟普通莫队相同的。

模板题:

树上莫队

一般形式是对树上一条简单路径的某些询问。

考虑把树转化成链,跑欧拉序:

  • dfs 一遍,进入的时候把该节点 push 进序列,退出时再 push 一次。

\(L_x, R_x\) 分别为进入 / 退出 \(x\) 点对应的序列位置。

这样的序列,我们发现对于一条 \(u, v\)(设 \(L_u < L_v\)) 的路上简单路径上的所有点编号:

  • 如果 \(lca(u, v) = u\),这些编号恰好是 \([L_u, L_v]\) 内出现次数为一的编号。
  • 否则, 这些编号恰好是 \([R_u, L_v]\) 内出现次数为一的编号 + 一个 \(lca(u, v)\)

证明可以画画图,感性理解:

  • 如果 \(lca(u, v) = u\),那么 \(u\)\(v\) 的祖先,所以从 \(u\) 一路 dfs 到 \(v\) 的第一次,这之中可能会经过其他子树,但肯定会回溯,所以经过其他子树的就肯定出现的两次。
  • 如果 \(lca(u, v) \not= u\),那么他们不是祖先关系,那么跑 dfs 时,会先进到他们的 LCA,然后到 u,回溯会 LCA,再跑到 v,其中经过别的子树也都出现两次被搞掉了。

统计是否出现两次一般都是开另外一个数组,看这个编号出现过没有,如果出现过你就加上,已经在了你就删掉。

模板:

二次离线莫队

如果对于 \([l, r]\) 快速扩展一步到 \([l, r + 1], [l, r - 1], [l + 1, r], [l - 1, r]\) 的操作,不好做,可以把所有扩展的询问离线下来,都查一遍,然后重做,每次用之前弄好的询问,复杂度可以降。

模板:

2535. 二次离线莫队

每次扩展的询问形式是 \([l, r]\) 中有多少数可以和 \(r+1,r,l,l-1\) 匹配。

这个东西差分成 \(sum_{[1, r]} - sum_{[1, l - 1]}\) 这样可以扫描线扫一遍。

至于扫描线,暴力 \(n \times C_{14}^7\) 是可以过的。

复杂度 \(O(n \sqrt{n} + n \times C_{14}^7)\)

如果不离线 \(\sqrt{n}\)\(C_{14}^7\) 会叠加起来。

然后空间就炸了。。

考虑其一,剩下是对称的,考虑 \([l, r]\) 跑到 \([l, R]\),其中 \(r < R\)

那么差分的左侧都是 \([1, l - 1]\) 中所有跟 \([r, R + 1]\) 里对称的个数,这样打包到一个结构体了。

差分的右侧时 \([1, x]\) 里和 \(x + 1\) 匹配的形式,这个东西区间和匹配的数相关,预处理一下就好了。

#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <vector>
const int N = 100005, L = 14;

typedef long long LL;

using namespace std;

int n, m, K, t, a[N], cnt[1 << L], d[1 << L], f[N], tot, pos[N];
LL ans[N], res[N];

// R[i] 表示 [1, i] 有多少和 i + 1 匹配的。

int inline get(int x) {
	int s = 0;
	while (x) x -= x & -x, s++;
	return s;
}

struct Q{
	int l, r, id;
	bool operator < (const Q &b) const {
		if (pos[l] != pos[b.l]) return l < b.l;
		return (pos[l] & 1) ? r < b.r : r > b.r;
	}
} q[N];

struct E{
	int l, r, opt, id;
};

vector<E> e[N];


int main() {
	scanf("%d%d%d", &n, &m, &K); t = sqrt(n);
	for (int i = 1; i <= n; i++) scanf("%d", a + i), pos[i] = (i - 1) / t + 1;
	for (int i = 0; i < (1 << L); i++)
		if (get(i) == K) d[++tot] = i;
	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= tot; j++) cnt[a[i] ^ d[j]]++;
        f[i] = cnt[a[i + 1]];
	}
	for (int i = 0; i < (1 << L); i++) cnt[i] = 0;
	for (int i = 1; i <= m; i++)
		scanf("%d%d", &q[i].l, &q[i].r), q[i].id = i;
	sort(q + 1, q + 1 + m);
	int l = 1, r = 0;
	for (int i = 1; i <= m; i++) {
		if (l < q[i].l) e[r].push_back((E) { l, q[i].l - 1, -1, i });
		while (l < q[i].l) res[i] += f[l - 1] + !K, l++;
		if (l > q[i].l) e[r].push_back((E) { q[i].l, l - 1, 1, i });
		while (l > q[i].l) res[i] -= f[l - 2] + !K, l--;
		if (r < q[i].r) e[l - 1].push_back((E) { r + 1, q[i].r, -1, i });
		while (r < q[i].r) res[i] += f[r++];
		if (r > q[i].r) e[l - 1].push_back((E) { q[i].r + 1, r, 1, i });
		while (r > q[i].r) res[i] -= f[--r];
	}
	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= tot; j++) cnt[a[i] ^ d[j]]++;
		for (int j = 0; j < e[i].size(); j++) {
			E u = e[i][j];
			for (int k = u.l; k <= u.r; k++)
				res[u.id] += cnt[a[k]] * u.opt;
		}
	}
	for (int i = 1; i <= m; i++) res[i] += res[i - 1], ans[q[i].id] = res[i];
	for (int i = 1; i <= m; i++) printf("%lld\n", ans[i]);
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值