莫队

莫队算法 | Mo's Algorithm

前言 | Preface

其实还是分块的思想

莫队就是纯暴力,只不过修改了一下暴力的顺序,从而将复杂度降到了 \(\mathcal O((n+m)\sqrt m)\)\(n\) \(m\) 同阶的情况下就是 \(\mathcal O(n\sqrt n)\)

莫队是离线算法

莫队解决的是区间查询类的问题,所处理的操作多半构成了阿贝尔群,强调逆元(或者说逆操作)的存在性

普通莫队

思路

先考虑一种暴力思路:

对于第一个查询 \(l_1,r_1\),我们将它的答案记录下来

接下来的查询 \((l_i,r_i) \text{ s.t. } i \in [2,m]\),我们在上一个查询的答案的基础上进行更改(补充、删除),得到本次查询的答案

这样子,如果每两个查询之间的差距不大,那么复杂度还是不错的

可是没有如果,考虑下面的情况:

(1, 2) (99999, 100000) (1, 2) (99999, 100000) ...

如果像这样子,那么每次的指针移动 \(\mathcal O(n)\),总的就是 \(\mathcal O(n^2)\),然后挂掉了

优化

既然复杂度需要让每两次相邻的查询差距不大保证,那我们为什么不排个序呢?

在按照左端点所在的块的编号作为第一关键字排序后,我们发现差距显著的减小了

可是右端点如果全是单调递增的话,看起来又要走很远

那么,这个时候就要使用奇偶排序了:

当左端点块的编号 \(\equiv 1 \mod{2}\) 的时候,将右端点升序
相反则将右端点降序

在坐标系上则是天然排好序的

表现出来就是这样子的效果(突然发现 desmos 可以用3D了

黑色代表奇数块(为了方便,将 \(x\) 坐标表示左端点所在的块的编号)是向上“走”的,红色则是偶数,是向下走的

这样子我们发现,算法遍历的查询是按照“Z”字形来的

复杂度分析

参考了一下 OI-Wiki 的证明

假设这里 \(m = \mathcal O(n)\)\(B = \lfloor \sqrt n \rfloor\)

我们假设每一个块中的 \(l\) 最大值为:\(\{m_i \}_{i=1}^{\lceil \sqrt n \rceil}\)

考虑修改 \(r\),由于 \(r\) 的最大是 \(n\),所以这一块的总复杂度是 \(\mathcal O(n\sqrt n)\)

然后考虑修改 \(l\),每一次的时间复杂度为 \(\mathcal O(m_i - m_{i - 1})\)

则在一个块内就是 \(\mathcal O(\sqrt n(m_i - m_{i - 1}))\)

总的就为 \(\mathcal O(\sqrt n \sum\limits_{i=1}^{\lceil \sqrt n \rceil}(m_i - m_{i-1})) = \mathcal O(n\sqrt n)\)

所以最坏情况下复杂度为 \(\mathcal O(n \sqrt n)\)


现在考虑 \(n\) \(m\) 不同阶的情况

先考虑右端点,同一块内,右端点是单调递增的,最多 \(\mathcal O(n)\),总共 \(\frac{n}{B}\)\(T_r(n) = \mathcal O(\dfrac{n^2}{B})\)

然后考虑左端点,每次最多动 \(\mathcal O(B)\) 次,\(m\) 次询问,\(T_l(n) = \mathcal O(mB)\)

总共 \(T(n) = T_r(n) + T_l(n) = \mathcal O(\dfrac{n^2}{B} + mB)\)

由均值不等式易得 \(T_{\min}(n) = \mathcal O(\dfrac{n^2}{\frac{n}{\sqrt m}} + m \cdot \dfrac{n}{\sqrt n}) = \mathcal O((n + m)\sqrt m)\),此时 \(B = \frac{n}{\sqrt m}\)

例题

SPOJ D-Query

这就是一道版子题

View Code
int n, m, block;

struct node{
	int left, right;
	int id, block_id;
	
	bool operator<(const node& another) const {
		if (block_id != another.block_id) return block_id < another.block_id;
		return (block_id & 1) ? right < another.right : right > another.right;
	}
} query[maxq];

int cnt[V];
int answer = 0;
int l = 1, r = 0;
int a[maxn];
int ans[maxq];

void solve() {
	cin >> n;
	for (int i = 1; i <= n; i++) cin >> a[i];
	
	cin >> m;
	
	block = max(1, (int)(n / sqrt(m)));
	
	for (int i = 1; i <= m; i++) {
		cin >> query[i].left >> query[i].right;
		query[i].id = i;
		query[i].block_id = query[i].left / block;
	}
	
	sort(query + 1, query + m + 1);
	
	answer = 0;
	l = 1, r = 0;
	
	for (int i = 1; i <= m; i++) {
		int left = query[i].left, right = query[i].right;
		
		while (l > left) {
			l--;
			if (cnt[a[l]] == 0) answer++;
			cnt[a[l]]++;
		}
		while (r < right) {
			r++;
			if (cnt[a[r]] == 0) answer++;
			cnt[a[r]]++;
		}
		while (l < left) {
			cnt[a[l]]--;
			if (cnt[a[l]] == 0) answer--;
			l++;
		}
		while (r > right) {
			cnt[a[r]]--;
			if (cnt[a[r]] == 0) answer--;
			r--;
		}
		ans[query[i].id] = answer;
	}
	for (int i = 1; i <= m; i++) cout << ans[i] << "\n";
}

P2709 【模板】莫队 / 小 B 的询问

每一次的增量是 \(2\text{cnt}_{a[i]}+1\),还是十分的简单(然后没开 long long 被卡爆了

View Code
int n, m, block, k;

struct node{
	int left, right;
	int id, block_id;
	
	bool operator<(const node& another) const {
		if (block_id != another.block_id) return block_id < another.block_id;
		return (block_id & 1) ? right < another.right : right > another.right;
	}
} query[maxq];

int cnt[V];
ll answer = 0;
int l = 1, r = 0;
ll a[maxn];
ll ans[maxq];

void solve() {
	cin >> n;
	cin >> m;
	cin >> k;
	
	for (int i = 1; i <= n; i++) cin >> a[i];
	
	
	block = max(1, (int)(n / sqrt(m)));
	
	for (int i = 1; i <= m; i++) {
		cin >> query[i].left >> query[i].right;
		query[i].id = i;
		query[i].block_id = query[i].left / block;
	}
	
	sort(query + 1, query + m + 1);
	
	answer = 0;
	l = 1, r = 0;
	
	for (int i = 1; i <= m; i++) {
		int left = query[i].left, right = query[i].right;
		
		while (l > left) {
			l--;
			answer += 2ll * cnt[a[l]] + 1;
			cnt[a[l]]++;
		}
		while (r < right) {
			r++;
			answer += 2ll * cnt[a[r]] + 1;
			cnt[a[r]]++;
		}
		while (l < left) {
			cnt[a[l]]--;
			answer -= 2ll * cnt[a[l]] + 1;
			l++;
		}
		while (r > right) {
			cnt[a[r]]--;
			answer -= 2ll * cnt[a[r]] + 1;
			r--;
		}
		
		ans[query[i].id] = answer;
	}
	
	for (int i = 1; i <= m; i++) cout << ans[i] << "\n";
}

P1494 [国家集训队] 小 Z 的袜子

这是道经典的题目,据说莫队就是从这道题中获得了灵感

其实很简单,抽到两只相同颜色的袜子的概率是 \(\dfrac{n_i (n_i - 1)}{l(l-1)}\)\(n_i\) 表示颜色 \(i\) 的出现次数,\(l\) 表示区间的长度

还是很好维护的(每一次减去原本的,更新完了后再加回来

View Code
int n, m, block, k;

struct node{
	int left, right;
	int id, block_id;
	
	bool operator<(const node& another) const {
		if (block_id != another.block_id) return block_id < another.block_id;
		return (block_id & 1) ? right < another.right : right > another.right;
	}
} query[maxq];

int cnt[V];
ll answer = 0;
int l = 1, r = 0;
int a[maxn];

struct factor{
	ll u, d;
}ans[maxq];

void add(int value) {
	answer -= 1ll * cnt[value] * (cnt[value] - 1);
	cnt[value]++;
	answer += 1ll * cnt[value] * (cnt[value] - 1);
}

void erase(int value) {
	answer -= 1ll * cnt[value] * (cnt[value] - 1);
	cnt[value]--;
	answer += 1ll * cnt[value] * (cnt[value] - 1);
}

void solve() {
	cin >> n;
	cin >> m;
	
	for (int i = 1; i <= n; i++) cin >> a[i];
	
	
	block = max(1, (int)(n / sqrt(m)));
	
	for (int i = 1; i <= m; i++) {
		cin >> query[i].left >> query[i].right;
		query[i].id = i;
		query[i].block_id = query[i].left / block;
	}
	
	sort(query + 1, query + m + 1);
	
	answer = 0;
	l = 1, r = 0;
	
	for (int i = 1; i <= m; i++) {
		int left = query[i].left, right = query[i].right;
		
		while (l > left) add(a[--l]);
		while (r < right) add(a[++r]);
		while (l < left) erase(a[l++]);
		while (r > right) erase(a[r--]);
		
		ans[query[i].id] = {answer, 1ll * (right - left + 1) * (right - left)};
	}
	
	for (int i = 1; i <= m; i++) {
		if (ans[i].u == 0) {
			cout << "0/1\n";
			continue;
		}
		cout << ans[i].u / __gcd(ans[i].u, ans[i].d) << "/" << ans[i].d / __gcd(ans[i].u, ans[i].d) << "\n";
	}
}

带修改的莫队

很明显,刚才的操作是不支持修改操作的

如果有修改操作怎么办?

受着

思路

我们想一想为什么不能够进行修改操作?

因为多个查询之间可能混入了修改操作

嗯,如果我们把操作的时间记录下来不就行了吗?

我们队查询操作记录一个 \(t\) 表示在这个操作之前有 \(t\) 个修改操作

这样子,每一次就将动态的将数组进行修改不就行啦

但是如果这样子,我们就需要对排序逻辑进行修改

struct node{
	int left, right;
	int id, block_id_left, block_id_right; // 该查询的 id, left 的块的 id,right 的块的id
	
	int time;
	
	bool operator<(const node& another) const {
		if (block_id_left != another.block_id_left) return block_id_left < another.block_id_left;
		if (block_id_right != another.block_id_right) return (block_id_left & 1) ? block_id_right < another.block_id_right : block_id_right > another.block_id_right;
		return (block_id_right & 1) ? time < another.time : time > another.time;
	}
};

注意!这样只能够处理单点修改

时间复杂度分析

考虑块长为 \(B\)

共有 \(\lceil \dfrac{n}{B} \rceil\) 个块

考虑左右的端点不换块(换块了可以忽略不计

\(n_{i,j}\) 表示左右端点分别在第 \(i\) 个、第 \(j\) 个块中的个数

得到

\[\begin{equation} \begin{split} T(n) &= \sum_{i=1}^{\lceil \frac{n}{B} \rceil}\sum_{j=i + 1}^{\lceil \frac{n}{B} \rceil}(n_{i,j} * B + t) \\ &= \mathcal O(mB + \left(\dfrac{n}{B}\right)^2t) \\ &= \mathcal O(mB + \dfrac{n^2t}{B^2}) \end{split} \end{equation} \]

要让这个最小,考虑求导

\(f(B) = mB + \dfrac{n^2t}{B^2}\),则 \(f'(B) = m - \dfrac{2n^2t}{B^2} = 0\)

解得 \(B_0 = \dfrac{2^{\frac13}n^{\frac23}t^{\frac13}}{m^{\frac13}}\)

则最优复杂度为:\(\mathcal O(n^{\frac23}m^{\frac23}t^{\frac13})\)

\(n,m,t\) 同阶,则为 \(\mathcal O(n^{\frac35})\)

例题

P1903 【模板】带修莫队 / [国家集训队] 数颜色 / 维护队列

就是刚才的思路

View Code
struct Modify{
	int position, value;
} modify[maxq];

int cnt[V];
ll answer = 0;
int l = 1, r = 0, t;
int a[maxn];

int ans[maxq];

void add(int value) {
	if (cnt[value] == 0) answer++;
	cnt[value]++;
}

void erase(int value) {
	cnt[value]--;
	if (cnt[value] == 0) answer--;
}

void travel(int t, int l, int r) {
	int posi = modify[t].position;
	
	if (l <= posi and posi <= r) {
		erase(a[posi]);
		add(modify[t].value);
	}
	
	swap(a[posi], modify[t].value);
}

void solve() {
	cin >> n;
	cin >> m;
	
	for (int i = 1; i <= n; i++) cin >> a[i];
	
	
	block = max(1, int(pow(n, 2.0 / 3.0)));
	
	int modify_cnt = 0, query_cnt = 0;
	
	for (int i = 1; i <= m; i++) {
		char ch;
		
		cin >> ch;
		if (ch == 'Q'){
			query_cnt++;
			cin >> query[query_cnt].left >> query[query_cnt].right;
			query[query_cnt].time = modify_cnt;
			query[query_cnt].id = query_cnt;
			query[query_cnt].block_id_left = query[query_cnt].left / block;
			query[query_cnt].block_id_right = query[query_cnt].right / block;
		} else {
			modify_cnt++;
			cin >> modify[modify_cnt].position >> modify[modify_cnt].value;
		}
	}
	
	sort(query + 1, query + query_cnt + 1);
	
	answer = 0;
	l = 1, r = 0, t = 0;
	
	for (int i = 1; i <= query_cnt; i++) {
		int ql = query[i].left, qr = query[i].right, qt = query[i].time;
		
		while (t < qt) travel(++t, l, r);
		while (t > qt) travel(t--, l, r);
		
		while (l > ql) add(a[--l]);
		while (r < qr) add(a[++r]);
		while (l < ql) erase(a[l++]);
		while (r > qr) erase(a[r--]);
		
		ans[query[i].id] = answer;
	}
	
	for (int i = 1; i <= query_cnt; i++) cout << ans[i] << "\n";
	
}

这个 travel 起到的就是将自己的时间回溯或者跳跃到合适的位置(中间进行 modify 操作,也就是修改,这样子也可以撤回)

其余

SPOJ Ada and Unique Vegetable

回滚莫队

引入

回滚(Rollback)指的是程序或数据处理错误,将程序或数据恢复到上一次正确状态的行为。回滚包括程序回滚和数据回滚等类型。

删除由一个或多个部分完成的事务执行的更新。为保证应用程序、数据库或系统错误后还原数据库的完整性,需要使用回滚。

回滚泛指程序更新失败, 返回上一次正确状态的行为。

回滚对程序员意味着非常严重的失误。

所以回滚次数往往与程序员的薪金直接联系。主流互联网公司通常都将回滚定位为最严重的事故。

回滚与恢复有本质的区别。

回滚莫队将操作由群放轻到了半群,也就是不一定存在逆元(逆操作)

不存在逆元也就说明了我们的删除或者增加有一个操作会不行

思路

考虑只有增加操作的情况(反之亦然

考虑左端点在同一块内的查询,这个时候的查询像这样子:(中间的竖线代表这个块的右端点

可以发现,由于排序的原因,右端点都是单调的

这说明了什么,说明了我们的右端点是可以不断拓展而不用涉及逆操作的!

什么,你说左端点无序?

那你就没有看全,由于左端点一定只在一个块内,所以我们可以直接暴力回滚,最多也就 \(\mathcal O(\sqrt n)\) 的复杂度

如果觉得太慢了,那么可以考虑将值赋给局部变量进行操作,这样子就不用回滚了

具体的,将 lr 的初始值赋值为 block[i].right + 1 以及 block[i].right

由于在同一个块内,qr 一定是单调递增的,所以我们就可以一直扩大(r++

而左端点,每一次操作完,就要回滚到 block[i].right + 1,在回滚的过程中将操作栈内的操作回溯

注意,右端点不能够使用奇偶排序,否则右端点的性质就没了

例题

P14420 [JOISC 2014] 历史的研究 / Historical Research

很版的一道题(感觉比模版还要版

这里贴出来主要代码,然后分析

ll calc(int l, int r) {
	ll result = 0;
	for (int i = l; i <= r; i++) tmp_cnt[a[i]] = 0;
	for (int i = l; i <= r; i++) {
		tmp_cnt[a[i]]++;
		result = max(result, 1LL * origin[a[i]] * tmp_cnt[a[i]]);
	}
	for (int i = l; i <= r; i++) tmp_cnt[a[i]] = 0;
	return result;
}


for (int i = 1; i <= block_count; i++) {
	memset(cnt, 0, sizeof(int) * (n + 5));
	
	int l = block[i].right + 1;
	int r = block[i].right;
	ll now_answer = 0;
	
	while (j <= m and query[j].block_id == i) {
		if (id[query[j].right] == i) {
			ans[query[j].id] = calc(query[j].left, query[j].right);
			j++;
			continue;
		}
		
		int ql = query[j].left, qr = query[j].right;
		
		while (r < qr) {
			r++;
			cnt[a[r]]++;
			now_answer = max(now_answer, 1LL * origin[a[r]] * cnt[a[r]]);
		}
		
		ll temp_ans = now_answer;
		
		while (l > ql) {
			l--;
			cnt[a[l]]++;
			now_answer = max(now_answer, 1LL * origin[a[l]] * cnt[a[l]]);
		}
		
		ans[query[j].id] = now_answer;
		
		while (l <= block[i].right) {
			cnt[a[l]]--;
			l++;
		}
		
		now_answer = temp_ans;
		
		j++;
	}
}

calc 函数十分简单,就是两个端点在同一块内,就直接暴力计算了

while (r < qr) {
	r++;
	cnt[a[r]]++;
	now_answer = max(now_answer, 1LL * origin[a[r]] * cnt[a[r]]);
}

右端点的拓展

ll temp_ans = now_answer;

while (l > ql) {
	l--;
	cnt[a[l]]++;
	now_answer = max(now_answer, 1LL * origin[a[l]] * cnt[a[l]]);
}

ans[query[j].id] = now_answer;

while (l <= block[i].right) {
	cnt[a[l]]--;
	l++;
}

now_answer = temp_ans;

向左拓展 l 并且回滚

P5906 【模板】回滚莫队&不删除莫队

这才是模版

有了上一道题,做这个还是很简单的

View Code
int calc(int left, int right) {
	int result = 0;
	
	for (int i = left; i <= right; i++) last[a[i]] = 0;
	for (int i = left; i <= right; i++) {
		if (last[a[i]]) result = max(result, i - last[a[i]]);
		else last[a[i]] = i;
	}
	
	return result;
}

for (int i = 1, j = 1; i <= block_count; i++){
	int l = block[i].right + 1, r = block[i].right, now_answer = 0;
	top = 0;
	for (; query[j].block_id == i; j++) {
		if (id[query[j].right] == i) {
			ans[query[j].id] = calc(query[j].left, query[j].right); // in the same block
			continue;
		}
		
		int ql = query[j].left, qr = query[j].right;
		
		while (r < qr) {
			r++;
			__end[a[r]] = r;
			if (!start[a[r]]) start[a[r]] = r, clear_stack[++top] = a[r];
			now_answer = max(now_answer, r - start[a[r]]);
		}
		
		int temp = now_answer;
		while (l > ql) {
			l--;
			if (!__end[a[l]]) __end[a[l]] = l;
			now_answer = max(now_answer, __end[a[l]] - l);
		}
		ans[query[j].id] = now_answer;
		while (l <= block[i].right) {
			if (__end[a[l]] == l) __end[a[l]] = 0;
			l++;
		}
		now_answer = temp;
	}
	for (int k = 1; k <= top; k++) start[clear_stack[k]] = __end[clear_stack[k]] = 0;
}

其余

l = block[i].left;		
r = n;

树上莫队

引言

树是一种非线性拓扑结构,应用于线性操作的莫队应该是用不上去的,吧?

当然可以,回想一下,我们学过 DFS 序和欧拉序,都可将树压成一个序列

可是 DFS 序上面的路径是不连续的,这怎么办?

你是不是忘了欧拉序,路径在他上面可是连续的

接下来,用 \(\text{ance}(u)\)/ance(u) 表示节点 \(u\) 的祖先,\(\text{lca}(u,v)\)/lca(u,v) 表示 \(u\)\(v\) 的最近公共祖先

in[u] 表示 \(u\) 的 DFS 序,out[u] 表示 \(u\) 在欧拉序中的第二次出现

可以用下面代码求出:

void dfs(int u, int father) {
	euler[++timer] = u;
	in[u] = timer;
	
	depth[u] = depth[father] + 1;
	jump[u][0] = father;
	for (int i = 1; i < 17; i++) jump[u][i] = jump[jump[u][i - 1]][i - 1];
	
	for (int v : adj[u]) if (v != father) dfs(v, u);
	
	euler[++timer] = u;
	out[u]= timer;
}

接下来,我们看路径 \(u \rightsquigarrow v\) 如何表示

  • \(u \in \text{ance}(v)\),表示成 [in[u], in[v]]
  • \(u \not\in \text{ance}(v)\),表示成 [out[u],in[v]]

如果觉得不对,可以自己下去验证,我就不证啦(其实是懒得证

注意第二种情况,lca(u, v) 是没有包含进去的,要加上

然后就可以快乐地在树上进行操作啦

例题

二次离线莫队

引入

有些时候,增加和删除操作的效率介于一个十分奇怪的区间,用普通莫队、回滚莫队时间复杂度都很高

所以我们也就需要另一种算法

思路

定义函数 \(f:\Z \times (\Z \times \Z) \longrightarrow \Z\):

\(f(x, [l,r])\) 表示 \(x\) 对于区间 \([l,r]\) 的贡献

所以容易知道:

\[\text{answer}_{[l, r]} = f(a_{r+1},[1,r]) - f(a_{r+1},[1,l-1]) \]

对于 \(f(a_{r+1},[1,r])\),由于是直接从 \(r\) 拓展的,计算起来十分简单

所以考虑 \(f(a_{r+1}, [1,l-1])\),这里我们将 \([1,l-1]\) 再看做一次询问,然后存下来离线处理

这就是第二次离线

通过这种方式,莫队的时间复杂度降到了 \(\mathcal{O} (nq+n\sqrt n)\),其中 \(\mathcal O(q)\) 是拓展/删除的复杂度

例题

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

模版题

按照上面我们说的步骤去做就行了

View Code
int n, m, k;
int a[maxn];

int xor_freq[(1 << 14) + 1];

vector<int> valid_target;

int prefix[maxn];
int prefix_c_self[maxn];

ll sum_prefix[maxn];
ll sum_prefix_c_self[maxn];

int block;

struct node{
	int l, r, id;
	
	bool operator<(const node& other) const {
		if (l / block != other.l / block) return l < other.l;
		return ((l / block) & 1) ? r < other.r : r > other.r;
	}
} query[maxn];

struct pack{
	int l, r, id, sign;
};

vector<pack> deferred[maxn];

ll static_differences[maxn];
ll dynamic_difference[maxn];
ll answer[maxn];

void solve() {
	cin >> n >> m >> k;
	
	for (int i = 1; i <= n; i++) cin >> a[i];
	
	if (k > 14) {
		for (int i = 1; i <= m; i++) {
			int l, r;
			cin >> l >> r;
			cout << 0 << "\n";
		}
		return ;
	}
	
	for (int i = 0; i < (1 << 14); i++)
		if (__builtin_popcount(i) == k)
			valid_target.push_back(i);
	
	for (int i = 1; i <= n; i++) {
		prefix[i] = xor_freq[a[i]];
		for (int target : valid_target)
			xor_freq[target ^ a[i]]++;
		prefix_c_self[i] = xor_freq[a[i]];
		
		sum_prefix[i] = sum_prefix[i - 1] + prefix[i];
		sum_prefix_c_self[i] = sum_prefix_c_self[i - 1] + prefix_c_self[i];
	}
	
	for (int i = 1; i <= m; i++) {
		cin >> query[i].l >> query[i].r;
		query[i].id = i;
	}
	
	block = max(1, (int)(n / sqrt(m)));
	
	sort(query + 1, query + m + 1);
	
	int l = 1, r = 0;
	for (int i = 1; i <= m; i++) {
		int ql = query[i].l, qr = query[i].r;
		int id = query[i].id;
		
		if (l > ql) {
			static_differences[i] -= (sum_prefix_c_self[l - 1] - sum_prefix_c_self[ql - 1]);
			
			deferred[r].push_back({ql, l - 1, id, 1});
			l = ql;
		}
		
		if (r < qr) {
			static_differences[i] += (sum_prefix[qr] - sum_prefix[r]);
			
			deferred[l - 1].push_back({r + 1, qr, id, -1});
			r = qr;
		}
		
		if (l < ql) {
			static_differences[i] += (sum_prefix_c_self[ql - 1] - sum_prefix_c_self[l - 1]);
			
			deferred[r].push_back({l, ql - 1, id, -1});
			l = ql;
		}
		
		if (r > qr) {
			static_differences[i] -= (sum_prefix[r] - sum_prefix[qr]);
			
			deferred[l - 1].push_back({qr + 1, r, id, 1});
			r = qr;
		}
	}
	
	fill(xor_freq, xor_freq + (1 << 14) + 1, 0);
	for (int i = 1; i <= n; i++) {
		for (int target : valid_target) xor_freq[a[i] ^ target]++;
		for (const pack& p : deferred[i]) {
			ll summation = 0;
			for (int j = p.l; j <= p.r; j++) summation += xor_freq[a[j]];
			dynamic_difference[p.id] += p.sign * summation;
		}
	}
	
	ll current_answer = 0;
	
	for (int i = 1; i <= m; i++) {
		current_answer += static_differences[i] + dynamic_difference[query[i].id];
		answer[query[i].id] = current_answer;
	}
	
	for (int i = 1; i <= m; i++) cout << answer[i] << "\n";
}

代码中,static_difference 代表 \(f(a_{r+1},[1,r])\) 这一类的答案

dynamic_difference 代表 \(f(a{r+1},[l,r])\) 这一类的答案,作为第二次的查询

其余

居然全都是紫起步,且都是 Ynoi

posted @ 2026-07-22 20:37  Yangyihao  阅读(6)  评论(0)    收藏  举报