长春集训 Day 4 - 数据结构专题
长春集训 Day 4 - 数据结构专题
非常重要的一个体会:大多数情况下,数据结构只是算法思想的辅助。
运用数据结构的思维链应当是:想出解法 \(\to\) 应该用什么数据结构维护。而不是反过来的。
所以需要熟练掌握数据结构的应用场景,以便拿出来应用。
A1 - Omkar and Medians
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
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\) 质因数分解,记
其中 \(p_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
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
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
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
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
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;
}

浙公网安备 33010602011771号