原题链接:F. Reverse and Swap
题目大意:
给出一个长度为 2 n 2^{n} 2n 的数组 a a a ,现在要你执行 q q q 次操作:
- 1. 1. 1. R e p l a c e ( x , k ) Replace(x,k) Replace(x,k) 将 a x a_{x} ax 改为 k k k 。
- 2. 2. 2. R e v e r s e ( x , k ) Reverse(x,k) Reverse(x,k) 对于每一个 i ( i ≥ 1 ) i (i \ge 1) i(i≥1) ,将区间 [ ( i − 1 ) ⋅ 2 k + 1 , i ⋅ 2 k ] [(i-1) \cdot 2^{k} + 1, i \cdot 2^{k}] [(i−1)⋅2k+1,i⋅2k] 的元素翻转。
- 3. 3. 3. S w a p ( k ) Swap(k) Swap(k) 对于每一个 i ( i ≥ 1 ) i (i \ge 1) i(i≥1) ,交换区间 [ ( 2 i − 2 ) ⋅ 2 k + 1 , 2 i ⋅ 2 k ] [(2i-2) \cdot 2^{k} + 1, 2i \cdot 2^{k}] [(2i−2)⋅2k+1,2i⋅2k] 的元素翻转。
- 4. 4. 4. S u m ( l , r ) Sum(l,r) Sum(l,r) 输出区间 [ l , r ] [l,r] [l,r] 中所有元素的和。
给出操作序列,你要按照顺序处理出每个操作的询问。
解题思路:
看题目像是线段树的题, 1 1 1, 4 4 4 操作都很好维护,但想想怎么执行 2 2 2, 3 3 3 操作。
操作 2 2 2 是对每个长度为 2 n 2^{n} 2n 的区间执行翻转操作,操作 3 3 3 是对两个长度为 2 n 2^{n} 2n 的区间执行交换操作,想想怎么让线段树维护这个操作。
(上图为操作 2 , 3 2,3 2,3 执行情况)
注意到数组长度是 2 2 2 的次幂,如果我们是用线段树进行维护的话,我们的线段树一定会形成一个满二叉树,树上的每个节点恰好就会代表一个 2 n 2^n 2n 的区间 。
然后你在线段树上手摸一下操作,发现了一个神奇的结论。
对于 3 3 3 操作来说,假设我们要交换长度为 2 n 2^{n} 2n 的两个区间,那我们是不是可以让代表着 2 n + 1 2^{n+1} 2n+1 的线段树节点,交换左右两个儿子就可以了?
而对于 2 2 2 操作来说,假设我们要翻转每个长度为 2 n 2^{n} 2n 的区间,那我们是不是可以让代表 2 n 2^{n} 2n 的线段树节点交换一下左右儿子,再让 2 n − 1 2^{n-1} 2n−1 的节点交换左右儿子,再让 2 n − 2 2^{n-2} 2n−2 的节点交换左右儿子 . . . ... ...一直到叶子节点。
对于上面两个操作,我们只需要一个懒标记就可以维护了!
你敲完代码,提交:
你会发现,对于每个操作,最坏情况下你会执行
n
log
n
n \log n
nlogn 次,复杂度为
O
(
q
n
log
n
)
O(q n \log n)
O(qnlogn) 那还不如直接写个
O
(
q
n
)
O(qn)
O(qn) 的算法暴力修改。
事实上,我们是可以不用 log n \log n logn 次递归跑到这 n n n 节点上,再去交换这 n n n 个节点的左右儿子的。因为我们每一次是对当前层中的所有节点都执行了一次操作,显然复杂度不对。
假设我们新开了一个数组 t a g tag tag,每个数组下标 i i i 代表线段树上所有代表着长度为 2 i 2^{i} 2i 的节点要不要交换左右儿子,我们用 0 / 1 0/1 0/1 来维护, 1 1 1 为交换, 0 0 0 为不交换。
想想我们交换的本质是什么,如果我们交换了,我们本来要跑左儿子,那么我们交换后会跑到右儿子去,如果没交换,那就正常跑左儿子。这个我们只需要在递归时判断 t a g tag tag 为是否为 1 1 1 即可,操作很好进行维护。
那么有了 t a g tag tag 之后,我们要执行操作 2 2 2 翻转所有长度为 2 n 2^{n} 2n 的数组,那么我们只需要反转 t a g [ i ] ( 0 ≤ i ≤ k ) tag[i](0 \leq i \leq k) tag[i](0≤i≤k) 的 ( 0 / 1 ) (0/1) (0/1)即可。
我们要执行操作 3 3 3 ,那么我们只需要反转 t a g [ n + 1 ] tag[n+1] tag[n+1] 的 ( 0 / 1 ) (0/1) (0/1) 即可。
然后询问时特判一下该节点的 t a g [ k ] tag[k] tag[k] ( 0 / 1 ) (0/1) (0/1) 即可。
这样,对于每个 2 2 2 操作,我们只用执行 k k k 次,对于三操作,我们直接 O ( 1 ) O(1) O(1) 修改,对于 1 , 4 1,4 1,4 操作,我们跑一下线段树即可 O ( log n ) O(\log n) O(logn)
时间复杂度可以通过,跑的还是蛮快的()
具体细节看代码即可:
时间复杂度: O ( q log n ) O(q \log n) O(qlogn)
AC代码:
#include <bits/stdc++.h>
#define YES return void(cout << "Yes\n")
#define NO return void(cout << "No\n")
using namespace std;
using u64 = unsigned long long;
using PII = pair<int, int>;
using i64 = long long;
i64 seg[(1LL << 20) + 10];
int arr[(1LL << 18) + 1];
int tag[20];
//判断是否交换左右儿子执行递归时 多加一个 tag 的判断即可 剩下的都是正常线段树
#define lson k << 1 | (tag[dep]), l, mid
#define rson k << 1 | (!tag[dep]), mid + 1, r
void build(int k, int l, int r, int dep) {
if (l == r) return void(seg[k] = arr[l]);
int mid = l + r >> 1;
build(lson, dep - 1), build(rson, dep - 1);
seg[k] = seg[k << 1] + seg[k << 1 | 1];
}
void upd(int k, int l, int r, int x, int y, int dep, int z) {
if (l == r) return void(seg[k] = z);
int mid = l + r >> 1;
if (x <= mid) upd(lson, x, y, dep - 1, z);
if (y > mid) upd(rson, x, y, dep - 1, z);
seg[k] = seg[k << 1] + seg[k << 1 | 1];
}
i64 qry(int k, int l, int r, int x, int y, int dep) {
if (l >= x && r <= y) return seg[k];
int mid = l + r >> 1; i64 res = 0;
if (x <= mid) res += qry(lson, x, y, dep - 1);
if (y > mid) res += qry(rson, x, y, dep - 1);
return res;
}
void solve() {
int n, q;
cin >> n >> q;
int Dep = n;
n = 1 << n;
for (int i = 1; i <= n; ++i) {
cin >> arr[i];
}
build(1, 1, n, Dep);
int op;
for (int i = 1; i <= q; ++i) {
cin >> op;
if (op == 1) {
int x, k; cin >> x >> k;
// 询问时我们多记一个 Dep 变量 代表当前区间的长度为 2^Dep
// 便于判断tag[Dep]的0/1 即左右儿子是否交换
upd(1, 1, n, x, x, Dep, k);
}
else if (op == 2) {
int k; cin >> k;
//操作2将 [0,k] 的所有 2^k 节点左右儿子都反转
for (int j = 0; j <= k; ++j) {
tag[j] ^= 1;
}
}
else if (op == 3) {
int k; cin >> k;
//要交换 2^k 的两个节点 那么交换 2^{k+1} 的左右儿子即可
tag[k + 1] ^= 1;
}
else if (op == 4) {
int l, r; cin >> l >> r;
// 询问时我们多记一个 Dep 变量 代表当前区间的长度为 2^Dep
// 便于判断tag[Dep]的0/1 即左右儿子是否交换
cout << qry(1, 1, n, l, r, Dep) << '\n';
}
}
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int t = 1; //cin >> t;
while (t--) solve();
return 0;
}