长春集训 Day 4 - 数据结构专题

长春集训 Day 4 - 数据结构专题

非常重要的一个体会:大多数情况下,数据结构只是算法思想的辅助。

运用数据结构的思维链应当是:想出解法 \(\to\) 应该用什么数据结构维护。而不是反过来的。

所以需要熟练掌握数据结构的应用场景,以便拿出来应用。

A1 - Omkar and Medians

link

Description

我们定义,对于长度为 \(2n-1\) 的数组 \(a\),存在长度为 \(n\) 的数组 \(b\),使得 \(b_i\) 等于 \(a_1,a_2,\cdots,a_{2i-1}\) 的中位数。现给出数组 \(b\),试问是否能构造出对应的数组 \(a\)(只需判断可行性即可)。

多测,\(1 \le T \le 10^4,\ 1 \le n \le 2 \times 10^5,\ -10^9 \le V \le 10^9\)

Solution

很好的一道性质构造题。需要仔细体会。

题中询问的是一段前缀的中位数,很难直接构造,但可以等价转化。我们根据定义可知,一段长度为 \(2i-1\) 的前缀,其中位数为 \(b_i\),等价于存在最多 \(i-1\) 个数严格小于 \(b_i\),且存在最多 \(i-1\) 个数严格大于 \(b_i\)。由于严格小于和大于是对称的,所以我们不妨只考虑小于的情况。

我们每次的操作都形如:向 \(a\) 末端添加 \(2\) 个数(设之为 \(x,y\)),尽力使得序列合法。因此,运用前缀的思想,不妨设 \(b_1 \sim b_{i-1}\) 均已完成合法构造。现在考察 \(b_i\) 能够产生合法 / 非法构造的等价条件。

我们手玩样例可以发现,\(b_i\) 的合法性跟 \(b_{i-1}\) 与之的相对大小关系有密切关联。因此讨论:

  • \(b_i = b_{i-1}\) 时,显然令 \(x = -\infin,y=\infin\) 即可。
  • \(b_i > b_{i-1}\) 时,则前缀中一定至少有 \(i-1\) 个数(这些数中一定包括 \(b_{i-1}\))严格小于 \(b_i\)。考虑到,\(b_i\) 序列构造一定非法的等价条件一定是:它控制的前缀中,严格小于 \(b_i\) 的数的数量大于 \(i-1\)。为了使得这样的数的个数大于 \(i-1\),我们需要考虑这种数的来源。
    • 由于 \(b_{i-1}\) 所控制的前缀一定是由更前面的前缀转移而来的,所以对于一个更靠前的位置 \(j(j<i-1)\),考察是否存在这样的 \(b_j\),使得 \(b_j > b_{i-1}\)。如果符合该条件,则 \(b_{i-1}\) 所控制的前缀中,大于 \(b_{i-1}\) 的数中,一定包括 \(b_j\),这就使得该前缀中严格小于 \(b_i\) 的数的数量至少为 \(i\),即保证了它大于 \(i-1\),这就是非法构造的来源。
    • 因此需要考察是否存在 \(j(j<i-1)\) 使得 \(b_{i-1} < b_j < b_i\),若存在则非法。
    • 如果不存在,则构造 \(x = b_i, y = \infin\) 即可。
  • \(b_i < b_{i-1}\) 时,同理,根据对称性,考察 \(b_{i-1} > b_j > b_i\) 即可。如果不存在,构造 \(x = b_i,y = -\infin\) 即可。

可以用 set<int> 查询前后缀。

另解° 注意到每次添加数字后中位数至多改变一位,因此可以考察添加前和添加后的中位数是否连续,如果不连续则一定非法,如果连续则合法。

Code

#include <bits/stdc++.h>
// #pragma GCC optimize(2)
#define int long long
#define inf 1e18
#define debug cout << '!';
#define filein(x) freopen(#x".in", "r", stdin);
#define fileout(x) freopen(#x".out", "w", stdout);
#define file(x) filein(x) fileout(x)
using namespace std;
const int N = 2e5+5;
int T, n, b[N];
set<int> s;
signed main() {
	cin.tie(0) -> sync_with_stdio(0);
	cin >> T;
	while (T--) {
		s.clear();
		bool legal = 1;
		cin >> n;
		for (int i = 1; i <= n; i++) {
			cin >> b[i];
		}
		for (int i = 1; i <= n; i++) {
			if (not s.count(b[i])) {
				s.insert(b[i]);
			}
			if (b[i] == b[i-1]) { // 一定合法
				continue;
			} else if (b[i] > b[i-1]) {
				if (s.upper_bound(b[i-1]) != s.find(b[i])) {
					legal = 0;
				}
			} else {
				if (s.upper_bound(b[i]) != s.find(b[i-1])) {
					legal = 0;
				}
			}
		}
		cout << (legal ? "YES\n" : "NO\n");
	}
	return 0;
}

B1 - GCD of an Array

link

Description

给定长度为 \(n\) 的数组 \(a\),有 \(q\) 次查询,每次查询给定数 \(i,x\),使 \(a_i \leftarrow a_i \times x\),并输出 \(\gcd\limits _{i=1}^n a_i\)\(10^9+7\) 取模的结果。

\(1 \le n,q,a_i,x \le 2 \times 10^5\)

Solution

\(\gcd\) 的转化我们也学了不少了。这道题显然不能用 \(\phi\) 或者 \(\mu\) 函数,因此我们从定义入手。

可将每个 \(a_i\) 质因数分解,记

\[a_i = \prod_j p_j^{e_j} \]

其中 \(p_j\) 为非平凡素数。

显然

\[\gcd_{i=1}^n a_i = \prod_j p_j^{\min\{e_j\}} \]

即所有数的 \(\gcd\) 等于它们公因数的最小幂次之积。

于是问题转化为,对于每个质数,求它们幂次的最小值。每次单点乘,将 \(x\) 质因数分解,相当于给原来的质数幂次加上一个 \(x\) 的质数幂次。即单点加全局 \(\min\),用线段树即可。空间显然不富裕,需要动态开点。

值得强调的是,朴素质因数分解的过程:

// 先欧拉筛一遍,获取质数表 primes
void factorize(int x) {
	for (int p : primes) {
		if (p * p > x) break;
		if (x % p == 0) {
			int cnt = 0;
			while (x % p == 0) {
				x /= p;
				cnt++;
			}
            // 含有因式 p^cnt
		}
	}
	if (x > 1) { // x 本身是大质数
        // TODO
	}
}

在预处理素数表的前提下,时间复杂度为 \(O\Big(\dfrac{\sqrt n}{\ln n}\Big)\),很小。

Code

注意:线段树空间尽量开大。线段树内变量必须用 int 型,否则空间不够。

#include <bits/stdc++.h>
// #pragma GCC optimize(2)
#define int long long
#define inf 1e18
#define debug cout << '!';
#define filein(x) freopen(#x".in", "r", stdin);
#define fileout(x) freopen(#x".out", "w", stdout);
#define file(x) filein(x) fileout(x)
using namespace std;
const int V = 2e5, mod = 1e9+7, N = 2e5+5;
int n, q, a[N];
signed root[V+5]; // 某质数对应的线段树根节点编号
int tot, ans = 1;
vector<int> primes;
bool not_prime[V+5];
int qpow(int x, int y) {
	if (y == 0) return 1;
	int tmp = qpow(x, y/2);
	if (y & 1) return tmp * tmp % mod * x % mod;
	return tmp * tmp % mod;
}
void sieve() {
	for (int i = 2; i <= V; i++) {
		if (not not_prime[i]) {
			primes.push_back(i);
		}
		for (auto p : primes) {
			if (i * p > V) break;
			not_prime[i * p] = 1;
			if (i % p == 0) break;
		}
	}
}
struct Seg {
	#undef int
	#define mid ((l + r) / 2)
	#define lson t[id].lc
	#define rson t[id].rc
	struct Node {
		int lc, rc, val;
	} t[N << 6];
	void pushup(int id) {
		t[id].val = min(t[lson].val, t[rson].val);
	}
	void change(int &id, int l, int r, int pos, int d) {
		if (id == 0) id = ++tot;
		if (l == r) {
			t[id].val += d;
		} else {
			if (pos <= mid) change(lson, l, mid, pos, d);
			else change(rson, mid + 1, r, pos, d);
			pushup(id);
		}
	}
	int query(int p) {
		return t[root[p]].val;
	}
	#undef mid
	#undef lson
	#undef rson
	#define int long long
} seg;
// 在位置 pos 插入数 x,并将其因数化
void factorize(int pos, int x) {
	for (int p : primes) {
		if (p * p > x) break;
		if (x % p == 0) {
			int cnt = 0;
			while (x % p == 0) {
				x /= p;
				cnt++;
			}
			seg.change(root[p], 1, n, pos, cnt);
		}
	}
	if (x > 1) { // x 本身是大质数
		seg.change(root[x], 1, n, pos, 1);
	}
}
signed main() {
	cin.tie(0) -> sync_with_stdio(0);
	sieve();
	cin >> n >> q;
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
		factorize(i, a[i]);
	}
	for (int p : primes) {
		if (root[p]) {
			ans = ans * qpow(p, seg.query(p)) % mod;
		}
	}
	while (q--) {
		int pos, x; cin >> pos >> x;
		for (int p : primes) {
			if (p * p > x) break;
			if (x % p == 0) {
				int cnt = 0;
				while (x % p == 0) {
					x /= p;
					cnt++;
				}
				int pre = seg.query(p);
				seg.change(root[p], 1, n, pos, cnt);
				int cur = seg.query(p);
				if (cur > pre) {
					ans = ans * qpow(p, cur - pre) % mod;
				}
			}
		}
		if (x > 1) {
			int pre = seg.query(x);
			seg.change(root[x], 1, n, pos, 1);
			int cur = seg.query(x);
			if (cur > pre) {
				ans = ans * qpow(x, cur - pre) % mod;
			}
		}
		cout << ans << '\n';
	}
	return 0;
}

C1 - Bracket Walk

link

Description

定义一个括号串是仅由 () 字符构成的字符串。称一个括号串是可行走的,当且仅当从它的首位开始,向左或向右任意移动若干步数,最终到达末位,途中记录走过的字符,并将这些字符按顺序排列成一个新字符串,使得这个新字符串是合法括号串。

给出长度为 \(n\) 的括号串 \(s\)\(q\) 次询问,每次询问给定一个数 \(p\),使 \(s_p\) 的字符取反(从 ( 变为 ) 或从 ( 变为 ))。对于每次询问,判断修改后的字符串是否是可行走的。询问互相依赖。

\(1 \le n,q \le 2 \times 10^5\)

Solution

手玩样例可以很容易地发现,只有在 (())(我们称之为极小反复子段)中反复行走才能复制括号。在其它子段中反复走都是没用的。所以我们记录极小反复子段出现的位置并分类讨论:

  • \(n\) 是奇数时,一定不合法;
  • 否则,当不存在极小反复子段时,一定合法;
  • 否则,当存在且仅存在一种极小反复子段时,一定不合法;
  • 否则,当存在形如 ... (( ... )) ...(两边不存在极小反复子段,中间存在若干极小反复子段(也可以不存在))的极小反复子段时,中间的极小反复子段一定能被两边的子段经由若干轮复制来配对,一定合法;
  • 其余构造均不合法。

具体地,我们用 set<int> 维护每一个极小反复子段的左处,如果被改没了就 erase,如果新出现了就 insert

Code

#include <bits/stdc++.h>
// #pragma GCC optimize(2)
#define int long long
#define inf 1e18
#define debug cout << '!';
#define filein(x) freopen(#x".in", "r", stdin);
#define fileout(x) freopen(#x".out", "w", stdout);
#define file(x) filein(x) fileout(x)
using namespace std;
int n, q;
string s;
set<int> l, r; //  ((  ))
signed main() {
	cin.tie(0) -> sync_with_stdio(0);
	cin >> n >> q >> s; s = ' ' + s;
	for (int i = 1; i < n; i++) {
		if (s[i] == s[i+1]) {
			if (s[i] == '(') l.insert(i);
			else r.insert(i);
		}
	}
	while (q--) {
		int pos; cin >> pos;
		if (pos != 1 and s[pos] == s[pos-1]) {
			if (s[pos] == '(') l.erase(pos-1);
			else r.erase(pos-1);
		}
		if (pos != n and s[pos] == s[pos+1]) {
			if (s[pos] == '(') l.erase(pos);
			else r.erase(pos);
		}
		if (s[pos] == '(') s[pos] = ')';
		else s[pos] = '(';
		if (pos != 1 and s[pos] == s[pos-1]) {
			if (s[pos] == '(') l.insert(pos-1);
			else r.insert(pos-1);
		}
		if (pos != n and s[pos] == s[pos+1]) {
			if (s[pos] == '(') l.insert(pos);
			else r.insert(pos);
		}
		bool legal = 1;
		if (n & 1) legal = 0;
		else if (s[1] == ')' or s[n] == '(') legal = 0;
		else if (l.empty() and r.empty()) legal = 1;
		else if (l.empty() or r.empty()) legal = 0;
		else if (*l.begin() < *r.begin() and *r.rbegin() > *l.rbegin()) legal = 1;
		else legal = 0;
		cout << (legal ? "Yes\n" : "No\n");
	}
	return 0;
}

D1 - Case of Chocolate

link

Description

有一个 \(n \times n\) 的方格,其中副对角线以及副对角线之上的格子存在巧克力。

\(q\) 次操作,每次操作指定一个副对角线上的格子 \((x,y)\),并指定一个方向(上或左),从所选单元格除法,按照指定方向吃掉所有巧克力,直到遇到空格子或边界为止。

求每次操作后他吃掉了多少巧克力。

\(1 \le n \le 10^9,\ 1 \le q \le 2 \times 10^5\)

Solution

注意到限制的影响是一段连续的位置。

先考虑向上吃的情况。假定当前选择的起始坐标为 \((x,y)\),则区间 \((b,x)\) 向左吃的左边界会变为它与 \(y\) 的最大值;其中,\(b\) 为该坐标向上吃的边界。

向左吃的情况同理。

所有边界的初始值均为 \(0\)

这样的话,我们就需要一个数据结构支持区间取 \(\max\)、单点查的操作,想到带懒标记的线段树。

细节很多,注意边界特判,一定不要越界。还要特判并标记同一个位置吃多次的情况。

数据范围 \(10^9\),需要动态开点。空间开 \(5 \times 10^5\) 就够了,否则会超限。

Code

#include <bits/stdc++.h>
// #pragma GCC optimize(2)
#define int long long
#define inf 1e18
#define debug cout << '!';
#define filein(x) freopen(#x".in", "r", stdin);
#define fileout(x) freopen(#x".out", "w", stdout);
#define file(x) filein(x) fileout(x)
using namespace std;
const int N = 5e6+5;
int n, q, root_row, root_col;
struct Seg {
	#define mid ((l + r) / 2)
	#define lson t[id].lc
	#define rson t[id].rc
	struct Node {
		int lc, rc, lzy;
	} t[N];
	int tot = 0;
	void modify(int &id, int l, int r, int ql, int qr, int d) {
		if (not id) {
			id = ++tot;
			lson = rson = t[id].lzy = 0;
		}
		if (t[id].lzy >= d) return;
		if (ql == l and qr == r) {
			t[id].lzy = max(t[id].lzy, d);
		} else {
			if (qr <= mid) modify(lson, l, mid, ql, qr, d);
			else if (ql > mid) modify(rson, mid + 1, r, ql, qr, d);
			else {
				modify(lson, l, mid, ql, mid, d);
				modify(rson, mid + 1, r, mid + 1, qr, d);
			}
		}
	}
	int query(int id, int l, int r, int pos, int cur = 0) {
		if (not id) return cur;
		cur = max(cur, t[id].lzy);
		if (l == r) return cur;
		if (pos <= mid) return query(lson, l, mid, pos, cur);
		else return query(rson, mid + 1, r, pos, cur);
	}
	#undef mid
	#undef lson
	#undef rson
} seg_row, seg_col;
signed main() {
	cin.tie(0) -> sync_with_stdio(0);
	cin >> n >> q;
	while (q--) {
		int x, y; char op; cin >> y >> x >> op; // transfer to OI style
		if (op == 'U') {
			int border = seg_col.query(root_col, 1, n, y);
			cout << x - border  << '\n';
			if (border+1 <= x) {
				seg_col.modify(root_col, 1, n, y, y, x);
				seg_row.modify(root_row, 1, n, border+1, x, y);
			}
		} else {
			int border = seg_row.query(root_row, 1, n, x);
			cout << y - border << '\n';
			if (border+1 <= y) {
				seg_row.modify(root_row, 1, n, x, x, y);
				seg_col.modify(root_col, 1, n, border+1, y, x);
			}
		}
	}
	return 0;
}

A2 - Boring Segments

link

Description

在数轴上有 \(n\) 条线段,其中第 \(i\) 条线段从 \(l_i\)\(r_i\),权值为 \(w_i\)

给定数 \(m\),试选出这些线段的一个子集,使得该集合中所有线段能够完全覆盖 \([1,m]\) 的区域,且集合中线段权值的极差最小。输出最小的极差。

\(1 \le n \le 3 \times 10^5,\ 2 \le m \le 10^6\)

Solution

好题,很综合。

判断一个线段集合是否合法,可以对 \(m\) 开线段树,添加线段视为区间 \(+1\),删除线段视为区间 \(-1\),统计区间 \(\min\) 是否为 \(0\) 即可。单次修改操作是 \(O(\log m)\) 的,查询是 \(O(1)\) 的(因为只需要查询一个节点)。

显然地,答案具有单调性,即大的区间长度一定比小的区间长度更有可能覆盖全部值域。求最小极差,可以维护一段变长区间,通过缩减区间长度来获得最优解。这让我们想到了双指针法。双指针法正用于求解这种具有区间的答案具有单调性的问题

具体地,维护指针 \(l\)\(r\),初始均指向起始位置。先往前遍历 \(r\),找到 \(r\) 最小的合法区间,然后往前遍历 \(l\),当区间再次不合法时,再往前遍历 \(r\),重复以上过程。\(r\) 碰到右边界时直接输出答案。这样可以使我们尽可能压缩区间长度来寻找最优解。

代码实现上,用 \([l,r-1]\) 代替区间 \([l,r]\),以实现区间覆盖的效果。

Code

#include <bits/stdc++.h>
// #pragma GCC optimize(2)
#define int long long
#define inf 1e18
#define debug cout << '!';
#define filein(x) freopen(#x".in", "r", stdin);
#define fileout(x) freopen(#x".out", "w", stdout);
#define file(x) filein(x) fileout(x)
using namespace std;
const int N = 1e6+5;
int n, m, a[N];
struct Segment {
	int l, r, w;
	friend bool operator < (Segment x, Segment y) {
		return x.w < y.w;
	}
} ele[N];
struct SegmentTree {
	#define mid ((l + r) / 2)
	#define lson (id << 1)
	#define rson (id << 1 | 1)
	struct Node {
		int minn, lzy;
	} t[N << 2];
	void pushup(int id) {
		t[id].minn = min(t[lson].minn, t[rson].minn);
	}
	void setlzy(int id, int lzy) {
		t[id].minn += lzy;
		t[id].lzy += lzy;
	}
	void pushdown(int id) {
		if (t[id].lzy) {
			setlzy(lson, t[id].lzy);
			setlzy(rson, t[id].lzy);
			t[id].lzy = 0;
		}
	}
	void build(int id, int l, int r) {
		if (l == r) {
			t[id].minn = a[l];
		} else {
			build(lson, l, mid);
			build(rson, mid + 1, r);
			pushup(id);
		}
	}
	void modify(int id, int l, int r, int ql, int qr, int lzy) {
		if (ql == l and qr == r) {
			setlzy(id, lzy);
			return;
		} else {
			pushdown(id);
			if (qr <= mid) modify(lson, l, mid, ql, qr, lzy);
			else if (ql > mid) modify(rson, mid + 1, r, ql, qr, lzy);
			else {
				modify(lson, l, mid, ql, mid, lzy);
				modify(rson, mid + 1, r, mid + 1, qr, lzy);
			}
			pushup(id);
		}
	}
	int query() {
		return t[1].minn;
	}
	#undef mid
	#undef lson
	#undef rson
} seg;
signed main() {
	cin.tie(0) -> sync_with_stdio(0);
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		cin >> ele[i].l >> ele[i].r >> ele[i].w;
	}
	sort(ele+1, ele+1+n);
	seg.build(1, 1, m-1);
	int l = 1, r = 0, ans = inf;
	for (; l <= n; l++) {
		while (seg.query() == 0) {
			if (++r > n) goto END;
			seg.modify(1, 1, m-1, ele[r].l, ele[r].r-1, 1);
		}
		ans = min(ans, ele[r].w - ele[l].w);
		seg.modify(1, 1, m-1, ele[l].l, ele[l].r-1, -1);
	}
	END: cout << ans << '\n';
	return 0;
}

B2 - Equilibrium

link

Description

给定两个长度均为 \(n\) 的数组 \(a\)\(b\),有 \(q\) 次询问,每次询问给定一个区间 \([l,r]\),试问能否通过若干次如下操作使 \(\forall i \in [l,r], a_i = b_i\)

  • 选择偶数个下标,满足 \(l \le pos_1 \lt pos_2 \lt \cdots \lt pos_k \le r\),然后将 \(a_{pos_1}, a_{pos_3}, a_{pos_5} \cdots\)\(1\),将 \(b_{pos_2}, b_{pos_4}, b_{pos_6} \cdots\)\(1\)

如果能,输出最少的操作次数。如果不能,报告无解。

\(2 \le n \le 10^5,\ 1 \le q \le 10^5\)

Solution

好玩的转化题,思维链条不短。

显然这样的 \(i\),使得 \(a_i = b_i\) 已经成立,是无用的。而且注意到答案只会使得 \(b_i - a_i\) 的值发生 \(+1\)\(-1\) 的变化。因此记 \(c_i = b_i - a_i\),预处理出 \(c_i\)。显然 \(\sum\limits_i c_i\) 的值若非 \(0\) 则非法。

每次施加操作等价于对 \(c_i\) 的一个前缀 \(+1,-1,+1,-1,\cdots\)。发现对于一个 \((+,-)\) 的操作位置对,其总和不变,但 \(c_+\) 减小。由此我们可以想到做出 \(c\) 的前缀和 \(s\),那么对于任意的 \(i\),在每次操作后 \(s_i\) 均减小。所以处理出最大的 \(s_i\) 即可,因为它所需减小 \(1\) 的次数最多。

值得一提的是,如果存在 \(i\) 使得 \(s_i<0\),即 \(\min\{s_i\}<0\),则无论怎么加都无法加回 \(0\),一定非法。

ST 表或线段树处理区间 \(\min/\max\) 即可。时间复杂度 \(O(n\log n+q)\)

所以对于这种纯性质题,如果直接突破比较困难,那么多观察操作前后的变量与不变量是什么,可以转化题意。

Code

#include <bits/stdc++.h>
// #pragma GCC optimize(2)
#define int long long
#define inf 1e18
#define debug cout << '!';
#define filein(x) freopen(#x".in", "r", stdin);
#define fileout(x) freopen(#x".out", "w", stdout);
#define file(x) filein(x) fileout(x)
using namespace std;
const int N = 1e5+5;
int n, q, a[N], s[N];
struct SparseTable {
	int lg[N], f[N][25], g[N][25];
	void init() {
		memset(g, 0x3f, sizeof g);
		lg[1] = 0;
		for (int i = 2; i <= n; i++) {
			lg[i] = lg[i/2] + 1;
		}
		for (int i = 1; i <= n; i++) {
			f[i][0] = g[i][0] = s[i];
		}
		for (int j = 1; (1<<j) <= n; j++) {
			for (int i = 1; i <= n-(1<<j)+1; i++) {
				f[i][j] = max(f[i][j-1], f[i+(1<<(j-1))][j-1]);
				g[i][j] = min(g[i][j-1], g[i+(1<<(j-1))][j-1]);
			}
		}
	}
	int query_max(int l, int r) {
		int s = lg[r-l+1];
		return max(f[l][s], f[r-(1<<s)+1][s]);
	}
	int query_min(int l, int r) {
		int s = lg[r-l+1];
		return min(g[l][s], g[r-(1<<s)+1][s]);
	}
} st;
signed main() {
	cin.tie(0) -> sync_with_stdio(0);
	cin >> n >> q;
	for (int i = 1; i <= n; i++) {
		int x; cin >> x; a[i] -= x;
	}
	for (int i = 1; i <= n; i++) {
		int x; cin >> x; a[i] += x;
	}
	for (int i = 1; i <= n; i++) {
		s[i] = s[i-1] + a[i];
	}
	st.init();
	while (q--) {
		int l, r; cin >> l >> r;
		if (s[r] - s[l-1] != 0 or st.query_min(l, r) - s[l-1] < 0) {
			cout << "-1\n";
		} else {
			cout << st.query_max(l, r) - s[l-1] << '\n';
		}
	}
	return 0;
}

C2 - XOR and Favorite Number

link

Description

给定数字 \(k\) 和长度为 \(n\) 的数组 \(a_i\),有 \(q\) 次询问,每次询问给定一个区间 \([l,r]\),求 \(\#\{(i,j)|l \le i \le j \le r \land \bigoplus\limits_{p=i}^j a_p = k\}\)

\(1 \le n,m \le 10^5,\ 0 \le k \le 10^6\)

Solution

如果你会莫队那么这将是整场最板的一道题。

因为异或的逆运算还是异或,所以区间异或和可以转化为两个前缀异或和的异或。

然后问题就变为,区间 \([l,r]\) 中有多少对 \((i,j)\) 使得 \(s_j \oplus s_{i-1} = k\)。显然可以离线后莫队。

以后会写一篇文章专门介绍莫队。

Code

#include <bits/stdc++.h>
// #pragma GCC optimize(2)
#define int long long
#define inf 1e18
#define debug cout << '!';
#define filein(x) freopen(#x".in", "r", stdin);
#define fileout(x) freopen(#x".out", "w", stdout);
#define file(x) filein(x) fileout(x)
using namespace std;
const int N = 1e5+5, V = 1.2e6+5;
int n, q, k, a[N], cnt[V], bel[N], ans;
struct Event {
	int l, r, id, ans;
} evt[N];
bool cmp0(Event x, Event y) {
	if (bel[x.l] < bel[y.l]) return 1;
	if (bel[x.l] > bel[y.l]) return 0;
	return x.r < y.r;
}
bool cmp1(Event x, Event y) {
	return x.id < y.id;
}
void add(int x) {
	ans += cnt[a[x]^k];
	cnt[a[x]]++;
}
void del(int x) {
	cnt[a[x]]--;
	ans -= cnt[a[x]^k];
}
signed main() {
	cin.tie(0) -> sync_with_stdio(0);
	cin >> n >> q >> k;
	const int B = (int)sqrt(n);
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
		a[i] ^= a[i-1];
		bel[i] = (i-1)/B+1;
	}
	for (int i = 1; i <= q; i++) {
		cin >> evt[i].l >> evt[i].r;
		evt[i].l--;
		evt[i].id = i;
	}
	sort(evt+1, evt+1+q, cmp0);
	int l = 1, r = 0;
	for (int i = 1; i <= q; i++) {
		while (r < evt[i].r) add(++r);
		while (l > evt[i].l) add(--l);
		while (r > evt[i].r) del(r--);
		while (l < evt[i].l) del(l++);
		evt[i].ans = ans;
	}
	sort(evt+1, evt+1+q, cmp1);
	for (int i = 1; i <= q; i++) {
		cout << evt[i].ans << '\n';
	}
	return 0;
}
posted @ 2026-07-31 16:12  L-Coding  阅读(10)  评论(0)    收藏  举报