CF1415D XOR-gun 题解 二分答案/贪心

题目链接

https://codeforces.com/problemset/problem/1415/D

题目大意

给定一个长为 n 的不降序列,每次操作可以任选相邻的两个数,并将这两个数替换为两个数按位异或的结果,现在需要破坏序列的不降,求最少操作次数,无解输出 1

方法一:二分答案

首先,要破坏数列的非严格单调递增特性,肯定会选择两个连续的元素 aiai+1,然后:

  • 选择将 ai 结尾的连续若干个元素(,ai2,ai1,ai)进行异或;
  • 选择将 ai+1 开头的连续若干个元素(ai+1,ai+2,)进行异或

并令 前者(以 ai 结尾的连续若干个数的异或和)大于 后者(以 ai+1 开头的连续若干个数的异或和)。

为什么操作连续的若干个数?

举个形象一点的例子,这个就跟伐木一样,比如说我要砍倒一棵树,我肯定会选择同一个高度砍,不会说先在距离地面 0.2 米的位置砍两刀,然后再到距离地面 0.5 米的位置砍几刀,…… 肯定是在同一个位置。

因为我们的目的是破坏数列“非严格单调递增“的性质,所以我们只需要存在一对元素满足条件即可,很明显:

  1. 它们是相邻的
  2. 左边的那个数是一段连续的元素的异或和(对应以某个 ai 结尾的连续若干个数)
  3. 右边的那个数也是一段连续的元素的异或和(对应以 ai+1 开头的连续若干个数)

考虑可以操作 m 次时是否满足条件(破坏数列的“非严格单调递增“性质),因为要将一段连续的区间内的元素消到只剩 2 个数(然后左边那个数大于右边那个数),操作一次会消去一个数,所以 m 次操作对应一个长度为 m+2 的区间。

m 确定时,枚举每个下标 i,对于下标 i,我们要确定的事情是:

是否存在一个以 ai 结尾的连续子序列(假设这个连续子序列的异或和为 x)以及一个以 ai+1 开头的连续子序列(假设这个连续子序列的异或和为 y),满足 x>y 且两个连续子序列包含的元素个数不超过 m+2

如果有的话就说明 消除不超过 m 次能够是数列不满足”非严格单调递增”的性质。

注意:是 ”不超过 m 次” 而不是 ”恰好 m 次”

这是为什么呢?我们多定义两个状态 bjcj,这两个状态在 jij>i 时表示的含义不同,其中:

ji 时:

bj 表示的是 ajaj+1ai(即以 aj 开头以 ai 结尾的连续子序列中所有元素的异或和),推导公式:

bj={aij=iajbj+1j<i

cj 表示的是 bj,bj+1,,bi 的最大值,推导公式:

cj={bij=imax{bj,cj+1}j<i

此时的状态是从后往前推(ii1i2),可以发现 cj 从后往前是非降的,因为 cj 表示的并不是从 ai 异或到 aj 的结果(这是 bj 表示的),而是从 ai 异或到 aj 的过程中能够得到的结果的最大值。

也就是说,cj 表示的含义是 ai 往前合并 不超过 ij 次的情况下能够获得的最大值。

j>i 时:

bj 表示的是 ai+1ai+2aj(即以 ai 开头以 aj 结尾的连续子序列中所有元素的异或和),推导公式:

bj={ai+1j=i+1ajbj1j>i+1

cj 表示的是 bi+1,bi+2,,bj 的最大值,推导公式:

cj={bi+1j=i+1min{bj,cj1}j>i+1

(注意,区别于 ji 的情况,这里 cj 求的是最小值)

此时的状态是从前往后推(i+1i+2i+3),可以发现 cj 从前往后是非增的,因为 cj 表示的是从 ai+1 异或到 aj 的过程中能够得到的结果的最小值。

也就是说,在 g>i 时,cj 表示的含义是 ai+1 往后合并 不超过 ji1 次的情况下能够获得的最大值。

那么怎么能确保不超过 m 次操作能够存在两个相邻的数前者大于后者呢?

枚举合并的过程中最前面那个数的下标!

ai 结尾合并 m 次能合并到 aim,所以我们从 max{im,1}i 枚举下标 j,对于的下标区间范围是 [j,j+m+1] —— 注意 j+m+1 可能大于 n,所可以开一个 k=j+m+1,对于的区间范围是 [j,k],当 k>n 时就不用继续操作了(因为 k>n 是左边一段变小,而右边一段固定,继续 j++ 只会让左边一段异或和的最大值变小,而右边一段异或和的最小值是不会发生任何变换,所以继续 j++ 没意义)它表示的含义是:

  • ai 能够合并到的最左边的数是 aj
  • ai+1 能够合并到的最右边的数是 ak

那么 ai 往左合并的过程中的最大值就是 cjai+1 往右合并的过程中的最小值就是 ck。只要 cj>ck,就说明不超过 m 次操作能够是数列变得不满足”非严格单调递增”性质。

对应的我可以开一个 check(int m) 函数进行判断。

但是这种方法只能判断在不超过 m 次合并时能够满足条件,不过可以发现,当 m 大于等于我们的最小操作次数时,check 函数总会返回 true;当 m 小于我们的最小操作次数时,check 函数总会返回 false。

所以可以对 check 函数二分答案。

示例程序:

#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e5 + 5;
int n, a[maxn], b[maxn], c[maxn];

bool check(int m) {
    for (int i = 1; i < n; i++) {
        b[i] = c[i] = a[i];
        for (int j = i-1; j >= max(1, i-m); j--) {
            b[j] = a[j] ^ b[j+1];
            c[j] = max(b[j], c[j+1]);
        }
        b[i+1] = c[i+1] = a[i+1];
        for (int j = i+2; j <= min(i+m+1, n); j++) {
            b[j] = a[j] ^ b[j-1];
            c[j] = min(b[j], c[j-1]);
        }
        for (int j = max(1, i-m); j <= i && j+m+1 <= n; j++)
            if (c[j] > c[j+m+1])
                return true;
    }
    return false;
}

int main() {
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];
    int l = 0, r = n-2, res = -1;
    while (l <= r) {
        int mid = (l + r) / 2;
        if (check(mid)) res = mid, r = mid - 1;
        else l = mid + 1;
    }
    cout << res << endl;
    return 0;
}

方法二:贪心思想+枚举答案

上面二分答案的解法虽然能通过,但是分析一下,由于 check(m) 的时间复杂度是 O(nm) 的,m 接近 n,所以总的时间复杂度是 O(n2logn) 的,之所以能过的原因最主要我觉得还是因为:

  1. n 比较大时,答案很小
  2. 题目的 n 可能给的比较小
  3. 或者别的原因(欢迎评论区补充)

可以发现,因为 1ai109,所以 ai 的二进制表示最多 30 位,而数列是非严格单调递增的,所以根据鸽笼/雀巢/抽屉原理,当 n>60 时,必然存在连续的三个数 —— 假设这三个数是 ai,ai+1,ai+2 —— 的最高位的位数是相同的,此时将后两个数(即 ai+1ai+2)异或能够消去最高位的 1,此时 ai>ai+1ai+2(注意:本题中 ai1,若 ai 可以取 0 那还需要考虑排除 0 的影响),即:当 n>60 时,答案为 1

而当 n60 时,直接用二分或者枚举答案都是可以的。

也就是说,在题目里面可以加一行

if (n > 60) {
    cout << 1 << endl;
    return 0;
}

来优化我们的程序。

posted @   quanjun  阅读(55)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
点击右上角即可分享
微信分享提示