Loading

莫队

普通莫队

概念

普通莫队算法可以解决不强制要求在线的部分区间问题,主要是通过将询问按照最优顺序暴力处理来优化复杂度。

考虑 分块。将整个数组分成若干个长度都为 \(\sqrt{n}\) 的块,把这些询问按左端点所属块大的编号为第一关键字,以右端点为第二关键字升序排序。这样做的时间复杂度较优,为 \(O(n\sqrt{n})\)

莫队一般要求移动端点的转移复杂度较优,但是有两种情况可以优化:

  1. 其中一种操作比较简单,考虑回滚莫队。

  2. 题目满足可减性,考虑莫队二离。

例题

P3901 数列找不同

这种套路通常都需要配合 离散化 来使用。

直接用桶讨论一下端点的转移就行。

注意 \(l, r\) 的初始值分别赋 \(1, 0\).

带修莫队

概念

考虑将修改一起离线。

在普通的莫队算法上增加一个指针 \(k\) ,表示当前的区间是经过了前 \(k\) 次修改的情况,\(k\) 的初始值为 \(0\)

处理询问的时候连带修改的指针一起移动就行。

优化

直接上手难优化,考虑传统艺能调块长。

假设我们取块长等于 \(n^x\) ,其中 \(x\) 满足 \(0 < x < 1\),且操作次数 \(m\)\(n\) 同阶。

考虑当 \(l\) 指针移动时,最坏的单次时间复杂度:

  1. \(l, r\) 指针全部都在块内移动时:

    最坏要遍历一个整块,单次时间复杂度为 \(O(n^x)\) 。一共有 \(O(n)\) 次询问,总时间复杂度为 \(O(n^{x + 1})\)

  2. \(l\) 指针移动到后面的若干块时:

    每移动一个整块的时间复杂度为 \(O(n^x)\) 。可以作为终点的块共有 \(O(\frac{n}{n^x})\) 个,所以时间复杂度最坏为 \(O(n)\)

所以,\(l\) 指针移动的最坏时间复杂度为 \(O(x + 1)\)

接下来考虑移动 \(r\) 指针的最坏复杂度,我们叠加上 \(l\) 指针的影响,不会干扰到总时间复杂度的分析:

  1. \(l\)\(r\) 指针都在块内移动时:

    两个指针可以移动的位置分别有 \(O(n^x)\) 个。一共有 \(\frac{n}{n^x}\) 个块,所以总时间复杂度为 \(O(n^{2x} \times \frac{n}{n^x})\) ,即 \(O(n^{2 - x})\)

  2. \(l\) 指针在块内移动,\(r\) 指针移动到后面的若干块时:

    \(r\) 指针每移动一个整块的单次时间复杂度为 \(O(n^x)\)。最坏情况下需要遍历 \(\frac{n}{n^x}\) 个块。

    由于右端点按照升序排列,假设每一个块需要付出 \(O(n)\) 的时间复杂度,则最坏复杂度接近于 \(O(\frac{n}{n^x} \times n)\),化简得 \(O(n^{2 - x})\)

  3. \(l\) 指针移动到下一块时:

    最坏单次需要 \(O(n)\) 的时间复杂度。向大估算,每个块都需要左指针遍历一次整个区间。共有 \(O(\frac{n}{n^x })\) 个块,每次需要 \(O(n)\) 遍历一次整个区间,则最坏复杂度接近于 \(O(n^{2 - x})\)

所以\(r\) 指针移动得时间复杂度最坏也接近于 \(O(n^{2 - x})\)

最后考虑 \(k\) 指针移动的时间复杂度:

  1. \(l, r\) 指针都在块内移动时:

    \(k\) 指针按照排序规则递增,最坏时间复杂度为 \(O(n)\)

  2. \(l\) 指针在块内移动时:

    \(r\) 指针移动到后面的若干块时,每一次移动的复杂度最坏也是 \(O(n)\)

    根据上面的估算,每次移动 \(l, r\) 指针最坏需要 \(O(n^{2 - 2x})\) ,每次移动右指针的同时还要最坏 \(O(n)\) 地更新 \(k\) 指针,所以总时间复杂度为 \(O(n^{3 - 2x})\)

  3. \(l\) 指针移动到后面的若干块时:

    每次最坏时间复杂度为 \(O(n)\) 。假设每个块贡献一次移动的复杂度,则最坏复杂度不超过 \(O(n^{1 - x})\) ,考虑进 \(k\) 指针的影响,总时间复杂度不超过 \(O(n^{2 - x})\)

所以 \(k\) 指针移动的总时间复杂度不超过 \(O(n^{\max(3 - 2x, 2 - x)})\)

三个指针移动的时间复杂度最坏为 \(O(n^{max(1, 3 - 2x, 2 - x)})\) ,进行数学分析后易知 \(x = \frac{2}{3}\) 时时间复杂度最优,为 \(O(n^{\frac{5}{3}})\)

带修莫队还有另外一种块长 \(= \sqrt[3]{nt}\) 的写法,理论上可以达到理论最优复杂度 \(O(\sqrt[3]{n^4t})\)

例题

P1903 [国家集训队] 数颜色 / 维护队列

典题。

#include <cstdio>
#include <cmath>
#include <iostream>
#include <algorithm>
using namespace std;

const int maxn = 1e5 + 5e4;
const int maxq = 1e5 + 5e4;
const int maxv = 1e6 + 5;

int n, m, cur;
int l, r, k;
int qlen, clen;
int a[maxn], bel[maxn];
int cnt[maxv], ans[maxq]; 

struct ques
{
	int l, r, k, id;
	bool operator < (const ques& rhs) const
	{
		if (bel[l] ^ bel[rhs.l])
			return bel[l] < bel[rhs.l];
		if (bel[r] ^ bel[rhs.r])
			return (bel[l] & 1 ? r < rhs.r : r > rhs.r);
		return k < rhs.k;
	}
} q[maxq];

struct node
{
	int pos, color;
} c[maxq];

inline int read()
{
	int res = 0, flag = 1;
	char ch = getchar();
	while (ch < '0' || ch > '9')
	{
		if (ch == '-')
			flag = -1;
		ch = getchar();

	}
	while (ch >= '0' && ch <= '9')
	{
		res = res * 10 + ch - '0';
		ch = getchar();
	}
	return res * flag;
}

inline void write(int x)
{
	if (x < 0)
	{
		putchar('-');
		x = -x;
	}
	if (x > 9)
		write(x / 10);
	putchar(x % 10 + '0');
}

inline void add(int x)
{
	if (!cnt[a[x]])
		cur++;
	cnt[a[x]]++;
}

inline void del(int x)
{
	cnt[a[x]]--;
	if (!cnt[a[x]])
		cur--;
}

inline void update(int x)
{
	if (c[x].pos >= l && c[x].pos <= r)
	{
		if (!cnt[c[x].color])
			cur++;
		if (cnt[a[c[x].pos]] == 1)
			cur--;
		cnt[c[x].color]++;
		cnt[a[c[x].pos]]--;
	}
	swap(c[x].color, a[c[x].pos]);
}

int main()
{
	char ch;
	n = read();
	m = read();
	int block = pow(n, 0.666);
	for (register int i = 1; i <= n; i++)
	{
		a[i] = read();
		bel[i] = (i - 1) / block + 1;
	}
	for (register int i = 1; i <= m; i++)
	{
		scanf(" %c ", &ch);
		if (ch == 'Q')
		{
			qlen++;
			q[qlen].l = read();
			q[qlen].r = read();
			q[qlen].id = qlen;
			q[qlen].k = clen;
		}
		else
		{
			clen++;
			c[clen].pos = read();
			c[clen].color = read();
		}
	}
	sort(q + 1, q + qlen + 1);
	for (register int i = 1; i <= qlen; i++)
	{
		while (l < q[i].l)
			del(l++);
		while (l > q[i].l)
			add(--l);
		while (r < q[i].r)
			add(++r);
		while (r > q[i].r)
			del(r--);
		while (k < q[i].k)
			update(++k);
		while (k > q[i].k)
			update(k--);
		ans[q[i].id] = cur;
	}
	for (register int i = 1; i <= qlen; i++)
		write(ans[i]), puts("");
	return 0;
}

回滚莫队

概念

假设有一种莫队,它的修改或者删除操作中有一个很难实现,另一个比较简单。

可以考虑使用回滚莫队维护。同样考虑暴力撤销,再通过合理排序来优化复杂度。

首先把询问按照左端点所属块的编号为第一关键字,以右端点为第二关键字升序排序。

把询问按照左端点所属块的编号分类。

对于同一类的询问,我们可以将 \(r\) 指针初始化为这个块的结尾,\(l\) 指针初始化为下一个块的开头。

每次对于当前询问,如果 \(r\) 指针在当前询问右端点的左端,我们就把右端点右移。

由于这些询问的左端点都在同一个块内,所以它们是按右端点升序排列的,因而可以沿用之前的 \(r\) 指针更新的信息,也就是这个块的末尾以后的信息。

完成更新以后保存答案,设其为 \(k\)

特殊情况是某个询问的左端点和右端点在同一个块内,此时暴力做法的时间复杂度不超过 \(O(\sqrt{n})\) ,直接暴力即可。

如果 \(l\) 指针在当前询问的左端点的右端,那么我们把 \(l\) 指针左移到当前询问的左端点并更新答案。

此时这个询问的答案已经被求出。我们还需要把左端点的影响删除,并且把左端点还原到下一个块的开头。

每次对于在同一个块内的询问重复以上操作,注意清除左指针带来的影响即可。总的时间复杂度不超过 \(O(n\sqrt{n} + m\sqrt{n})\)

证明

证明如下:我们把所有询问分成两类,设左右端点在同一块内的询问数量为 \(a\) ,左右端点不在同一块内的询问数量为 \(b\),所有询问的总数量为 \(m\)

对于第一类询问,左右指针每次最多扫描 \(O(\sqrt{n})\) 个位置,所以处理第一类询问的总时间复杂度为 \(O(a\sqrt{n})\)

对于第二类询问,我们再次分类,设左端点在第 \(i\) 个块内的询问为第 \(i\) 类询问。显然,对于每一个询问左指针最多扫描 \(\sqrt{n}\) 个位置。

由于每个块内的右端点按升序排列,每一类询问右指针最多扫描 \(n\) 个位置。最多有 \(\sqrt{n}\) 类询问,左指针需要扫描的总长度最长为 \(b \times \sqrt{n}\) ,右指针需要扫描的总长度最长为 \(\sqrt{n} \times n\) 。求和可知最长需要扫描 \(b \times \sqrt{n} + n \times \sqrt{n}\) 个位置。

两类询问求和后为 \(a\sqrt{n} + b\sqrt{n} + n\sqrt{n}\),整理可得总时间复杂度为 \(O(n\sqrt{n} + m\sqrt{n})\)

例题

AT1219 歴史の研究

典题,难点在于值域太大。

我们发现处理出现次数时只需要数的相对大小,也就是维护第 \(k\) 大数的出现次数。

所以可以考虑莫队时使用离散化后的数组,求值时再调用原本数组里的值。

离散化后维护每一个值的出现次数,出现次数增加时顺带修改答案即可。详见代码。

#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
using namespace std;

const int maxn = 1e5 + 5;
const int maxq = 1e5 + 5;
const int maxv = 1e5 + 5;
const int maxb = 1e3 + 5;

int n, m;
int l, r;
int st[maxb], ed[maxb];
int bel[maxn], cnt[maxv];
int a[maxn], b[maxn], c[maxn];
long long vis, cur;
long long ans[maxq];

struct ques
{
	int l, r, id;
	bool operator < (const ques& rhs) const
	{
		if (bel[l] ^ bel[rhs.l])
			return bel[l] < bel[rhs.l];
		return r < rhs.r;
	}
} q[maxq];

inline int read()
{
	int res = 0, flag = 1;
	char ch = getchar();
	while (ch < '0' || ch > '9')
	{
		if (ch == '-')
			flag = -1;
		ch = getchar();

	}
	while (ch >= '0' && ch <= '9')
	{
		res = res * 10 + ch - '0';
		ch = getchar();
	}
	return res * flag;
}

inline void write(long long x)
{
	if (x < 0)
	{
		x = -x;
		putchar('-');
	}
	if (x > 9)
		write(x / 10);
	putchar(x % 10 + '0');
}

inline void add(int x)
{
	cnt[a[x]]++;
	cur = max(cur, (long long)cnt[a[x]] * b[a[x]]);
}

inline long long solve(int l, int r)
{
	long long res = 0;
	for (register int i = l; i <= r; i++)
		c[a[i]] = 0;
	for (register int i = l; i <= r; i++)
	{
		c[a[i]]++;
		res = max(res, (long long)c[a[i]] * b[a[i]]);
	}
	return res;
}

int main()
{
	int idx = 1;
	n = read();
	m = read();
	int block = sqrt(n);
	int size = ceil(n * 1.0 / block);
	for (register int i = 1; i <= size; i++)
	{
		st[i] = (i - 1) * block + 1;
		ed[i] = i * block;
	}
	for (register int i = 1; i <= n; i++)
	{
		a[i] = b[i] = read();
		bel[i] = (i - 1) / block + 1;
	}
	for (register int i = 1; i <= m; i++)
	{
		q[i].l = read();
		q[i].r = read();
		q[i].id = i;
	}
	sort(q + 1, q + m + 1);
	sort(b + 1, b + n + 1);
	int len = unique(b + 1, b + n + 1) - b - 1;
	for (register int i = 1; i <= n; i++)	
		a[i] = lower_bound(b + 1, b + len + 1, a[i]) - b;
	for (register int i = 1; i <= size; i++)
	{
		memset(cnt, 0, (n + 1) * sizeof(int));
		cur = 0;
		r = ed[i];
		while (bel[q[idx].l] == i)
		{
			l = ed[i] + 1;
			if (bel[q[idx].l] == bel[q[idx].r])
			{
				ans[q[idx].id] = solve(q[idx].l, q[idx].r);
				idx++;
				continue;
			}
			while (r < q[idx].r)
				add(++r);
			vis = cur;
			while (l > q[idx].l)
				add(--l);
			ans[q[idx].id] = cur;
			cur = vis;
			while (l <= ed[i])
				cnt[a[l++]]--;
			idx++;
		}
	}
	for (register int i = 1; i <= m; i++)
		write(ans[i]), putchar('\n');
	return 0;
}

树上莫队

概念

考虑用 dfs 序或者欧拉序拍平成序列问题做。

例题

SP10707 COT2 - Count on a tree II

考虑欧拉序拍平。

设结点 \(i\) 在欧拉序中第一次出现的位置为 \(s_i\) ,最后一次出现的位置为 \(t_i\)

假设有两个结点 \(x, y\) 满足 \(s_x < s_y\) ,此时若 \(x, y\)\(lca = x\) ,说明 \(y\)\(x\) 的后代。

对于欧拉序中的区间 \([s_x, s_y]\) ,其中出现且仅出现 \(1\) 次的结点一定在 \(x\)\(y\) 的路径上。

如果 \(lca(x, y) \neq x\) ,说明 \(x\)\(y\) 在两棵不同的子树内,此时取区间 \(t_x, s_y\)

对于第二种情况,可能会缺掉 lca。在维护莫队的时候还需要更新 \(x, y\)\(lca\)

注意树上莫队因为对欧拉序进行处理,所以 bel 数组要处理 \(2n\) 个位置。

代码

#include <cstdio>
#include <cmath>
#include <iostream>
#include <algorithm>
using namespace std;

const int maxn = 4e4 + 5;
const int maxm = 1e5 + 5;

int n, m;
int tot, Cnt, cur;
int f[maxn][20], st[maxn], ed[maxn];
int cnt[maxn], ans[maxm], p[maxn * 2];
int head[maxn], dep[maxn], a[maxn], b[maxn], bel[maxn * 2];
bool vis[maxn * 2];

struct edge
{
	int to, nxt;
} edge[2 * maxn];

struct ques
{
	int l, r, lca, id;
	bool operator < (const ques& rhs) const
	{
		if (bel[l] ^ bel[rhs.l])
			return bel[l] < bel[rhs.l];
		return (bel[l] & 1 ? r < rhs.r : r > rhs.r);
	}
} q[maxm];

inline int read()
{
	int res = 0, flag = 1;
	char ch = getchar();
	while (ch < '0' || ch > '9')
	{
		if (ch == '-')
			flag = -1;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9')
	{
		res = res * 10 + ch - '0';
		ch = getchar();
	}
	return res * flag;
}

void add_edge(int u, int v)
{
	Cnt++;
	edge[Cnt].to = v;
	edge[Cnt].nxt = head[u];
	head[u] = Cnt;
}

void dfs(int u)
{
	p[++tot] = u;
	st[u] = tot;
	dep[u] = dep[f[u][0]] + 1;
	for (int i = head[u]; i; i = edge[i].nxt)
	{
		int v = edge[i].to;
		if (v != f[u][0])
		{
			f[v][0] = u;
			dfs(v);
		}
	}
	p[++tot] = u;
	ed[u] = tot;
}

void add(int x)
{
	if (!vis[x])
	{
		if (!cnt[a[x]])
			cur++;
		cnt[a[x]]++;
	}
	else
	{
		cnt[a[x]]--;
		if (!cnt[a[x]])
			cur--;
	}
	vis[x] ^= 1;
}

int lca(int u, int v)
{
	if (dep[u] < dep[v])
		swap(u, v);
	int k = 0;
	while ((1 << (k + 1)) <= n)
		k++;
	for (int i = k; i >= 0; i--)
		if (dep[f[u][i]] >= dep[v])
			u = f[u][i];
	if (u == v)
		return u;
	for (int i = k; i >= 0; i--)
	{
		if (f[u][i] != f[v][i])
		{
			u = f[u][i];
			v = f[v][i];
		}
	}
	return f[u][0];
}

int main()
{
	int u, v, x;
	int l = 1, r = 0;
	n = read();
	m = read();
	for (int i = 1; i <= n; i++)
		a[i] = b[i] = read();
	for (int i = 1; i <= n - 1; i++)
	{
		u = read();
		v = read();
		add_edge(u, v);
		add_edge(v, u);
	}
	sort(b + 1, b + n + 1);
	int len = unique(b + 1, b + n + 1) - b - 1;
	for (int i = 1; i <= n; i++)
		a[i] = lower_bound(b + 1, b + len + 1, a[i]) - b;
	int block = sqrt(n);
	for (int i = 1; i <= 2 * n; i++)
		bel[i] = (i - 1) / block + 1;
	dfs(1);
	for (int j = 1; (1 << j) <= n; j++)
		for (int i = 1; i <= n; i++)
			f[i][j] = f[f[i][j - 1]][j - 1];
	for (int i = 1; i <= m; i++)
	{
		u = read();
		v = read();
		if (st[u] >= st[v])
			swap(u, v);
		x = lca(u, v);
		q[i].id = i;
		if (x == u)
		{
			q[i].l = st[u];
			q[i].r = st[v];
		}
		else
		{
			q[i].l = ed[u];
			q[i].r = st[v];
			q[i].lca = x;
		}
	}
	sort(q + 1, q + m + 1);
	for (int i = 1; i <= m; i++)
	{
		while (l < q[i].l)
			add(p[l++]);
		while (l > q[i].l)
			add(p[--l]);
		while (r < q[i].r)
			add(p[++r]);
		while (r > q[i].r)
			add(p[r--]);
		if (q[i].lca)
			add(q[i].lca);
		ans[q[i].id] = cur;
		if (q[i].lca)
			add(q[i].lca);
	}
	for (int i = 1; i <= m; i++)
		printf("%d\n", ans[i]);
	return 0;
}

优化

奇偶性优化

若左端点所属的块不同,我们按左端点所属块的编号排序,否则若左端点所属块的编号为奇数,我们按右端点升序排序,否则按右端点降序排序。理论最多可以优化 \(\frac{1}{2}\) 的常数。

bool operator < (const ques& rhs) const
{
	if (bel[l] ^ bel[rhs.l])
		return bel[l] < bel[rhs.l];
	return (bel[l] & 1 ? r < rhs.r : r > rhs.r);
}

莫队二离

莫队二次离线,用于移动端点难以 \(O(1)\) 转移贡献的情况。

考虑对莫队的移动操作进行再次离线维护,从而简化问题。

要求维护的信息满足可加减性。

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

\(f(x, [l, r])\) 为位置 \(x\) 对区间 \([l, r]\) 的贡献。

考虑分讨一下端点移动的情况:

  1. 左端点左移,\([l, r]\) -> \([l - k, r]\),贡献增加 \(\sum\limits_{i = l - k}^{l - 1} f(i, [1, r]) - f(i, [1, i - 1])\)

  2. 左端点右移,\([l, r]\) -> \([l + k, r]\),贡献减少 \(\sum\limits_{i = l}^{l + k - 1} f(i, [1, r]) - f(i, [1, i - 1])\)

  3. 右端点左移,\([l, r]\) -> \([l, r - k]\),贡献减少 \(\sum\limits_{i = r - k + 1}^{r} f(i, [1, i - 1]) - f(i, [1, l - 1])\)

  4. 右端点右移,\([l, r]\) -> \([l, r + k]\),贡献增加 \(\sum\limits_{i = r + 1}^{r + k} f(i, [1, i - 1]) - f(i, [1, l - 1])\)

不妨以最后一种情况为例,考虑两边的式子如何维护。

首先左边的 \(f(i, [1, i - 1])\) 可以预处理。

对于右边的 \(f(i, [1, l - 1])\) 实际上是询问在某个前缀位置与 \(i\) 产生的贡献总和,可以考虑从前向后扫描线维护。

也就是在 \(l - 1\) 位置挂上一个对 \([r + 1, r + k]\) 区间的询问,求该区间内与 \([1, l - 1]\) 产生的贡献总和。这里因为 \(k \leq \sqrt{n}\),所以可以直接暴力扫描区间。

具体到这个题直接开桶计数就好了。

时间复杂度 \(O({n \choose 17}n + n \sqrt{m})\)

#include <cstdio>
#include <cmath>
#include <cstring>
#include <vector>
#include <algorithm>
using namespace std;

typedef long long ll;

namespace IO
{
    //by cyffff
	int len = 0;
	char ibuf[(1 << 20) + 1], *iS, *iT, out[(1 << 26) + 1];
	#define gh() (iS == iT ? iT = (iS = ibuf) + fread(ibuf, 1, (1 << 20) + 1, stdin), (iS == iT ? EOF : *iS++) : *iS++)
	#define reg register

	inline int read()
    {
		reg char ch = gh();
		reg int x = 0;
		reg char t = 0;
		while (ch < '0' || ch > '9') t |= ch == '-', ch = gh();
		while (ch >= '0' && ch <= '9') x = x * 10 + (ch ^ 48), ch = gh();
		return t ? -x : x;
	}

	inline void putc(char ch) { out[len++] = ch; }

	template<class T>

	inline void write(T x)
    {
		if (x < 0) putc('-'), x = -x;
		if (x > 9) write(x / 10);
		out[len++] = x % 10 + 48;
	}

	inline void flush()
    {
		fwrite(out, 1, len, stdout);
		len = 0;
	}
}
using IO::read;
using IO::write;
using IO::flush;
using IO::putc;

const int maxn = 1e5 + 5;
const int maxm = 1e5 + 5;
const int maxv = (1 << 14);

int n, m, k;
int buc[maxv];
int a[maxn], bel[maxn], pre[maxn];
ll ans[maxm];
vector<int> num;

struct Query
{
    int l, r, idx;
    ll ans;

    Query() : l(), r(), idx(), ans() {}
    Query(int _l, int _r, int _idx) : l(_l), r(_r), idx(_idx), ans() {}
    bool operator < (const Query& rhs) const { return (bel[l] == bel[rhs.l] ? r < rhs.r : l < rhs.l); }
} qry[maxn];

vector<Query> opt[maxn];

int main()
{
    // freopen("P4887_13.in", "r", stdin);
    // freopen("P4887_13.res", "w", stdout);
    n = read(), m = read(), k = read();
    if (k > 14)
    {
        while (m--) puts("0");
        return 0;
    }
    int blk = sqrt(n);
    for (int i = 1; i <= n; i++) bel[i] = (i - 1) / blk + 1, a[i] = read();
    for (int i = 1; i <= m; i++) qry[i].idx = i, qry[i].l = read(), qry[i].r = read();
    sort(qry + 1, qry + m + 1);
    for (int i = 0; i < maxv; i++)
        if (__builtin_popcount(i) == k) num.push_back(i);
    for (int i = 1; i <= n; i++)
    {
        for (int v : num) buc[a[i] ^ v]++;
        pre[i] = buc[a[i + 1]];
    }
    for (int i = 1, l = 1, r = 0; i <= m; i++)
    {
        if (l < qry[i].l) opt[r].push_back(Query(l, qry[i].l - 1, -i));
        while (l < qry[i].l) qry[i].ans += pre[l - 1], l++;
        if (l > qry[i].l) opt[r].push_back(Query(qry[i].l, l - 1, i));
        while (l > qry[i].l) qry[i].ans -= pre[l - 2], l--;
        if (r < qry[i].r) opt[l - 1].push_back(Query(r + 1, qry[i].r, -i));
        while (r < qry[i].r) qry[i].ans += pre[r], r++;
        if (r > qry[i].r) opt[l - 1].push_back(Query(qry[i].r + 1, r, i));
        while (r > qry[i].r) qry[i].ans -= pre[r - 1], r--;
    }
    memset(buc, 0, sizeof(buc));
    for (int i = 1; i <= n; i++)
    {
        for (int v : num) buc[a[i] ^ v]++;
        for (Query x : opt[i])
        {
            for (int j = x.l, tmp; j <= x.r; j++)
            {
                tmp = buc[a[j]];
                if ((j <= i) && (k == 0)) tmp--;
                if (x.idx < 0) qry[-x.idx].ans -= tmp;
                else qry[x.idx].ans += tmp;
            }
        }
    }
    for (int i = 1; i <= m; i++) qry[i].ans += qry[i - 1].ans, ans[qry[i].idx] = qry[i].ans;
    for (int i = 1; i <= m; i++) write(ans[i]), putc('\n');
    flush();
    return 0;
}

来自 lxl 的尖端莫队科技。

如果想将莫队的复杂度优化至 \(\mathcal{O}(n \sqrt{q})\)

您只需将块长改成 \(\frac{n}{\sqrt{q}}\)

posted @ 2021-07-24 23:47  kymru  阅读(501)  评论(0编辑  收藏  举报