Codeforces Round #665 (Div. 2) F. Reverse and Swap (线段树)

原题链接: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(i1) ,将区间 [ ( i − 1 ) ⋅ 2 k + 1 , i ⋅ 2 k ] [(i-1) \cdot 2^{k} + 1, i \cdot 2^{k}] [(i1)2k+1,i2k] 的元素翻转。
  • 3. 3. 3. S w a p ( k ) Swap(k) Swap(k) 对于每一个 i ( i ≥ 1 ) i (i \ge 1) i(i1) ,交换区间 [ ( 2 i − 2 ) ⋅ 2 k + 1 , 2 i ⋅ 2 k ] [(2i-2) \cdot 2^{k} + 1, 2i \cdot 2^{k}] [(2i2)2k+1,2i2k] 的元素翻转。
  • 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} 2n1 的节点交换左右儿子,再让 2 n − 2 2^{n-2} 2n2 的节点交换左右儿子 . . . ... ...一直到叶子节点。

对于上面两个操作,我们只需要一个懒标记就可以维护了!

你敲完代码,提交:

在这里插入图片描述
你会发现,对于每个操作,最坏情况下你会执行 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](0ik) ( 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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柠檬味的橙汁

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值