CF1415D XOR-gun 题解 二分答案/贪心
题目链接
https://codeforces.com/problemset/problem/1415/D
题目大意
给定一个长为 的不降序列,每次操作可以任选相邻的两个数,并将这两个数替换为两个数按位异或的结果,现在需要破坏序列的不降,求最少操作次数,无解输出 。
方法一:二分答案
首先,要破坏数列的非严格单调递增特性,肯定会选择两个连续的元素 和 ,然后:
- 选择将 结尾的连续若干个元素()进行异或;
- 选择将 开头的连续若干个元素()进行异或
并令 前者(以 结尾的连续若干个数的异或和)大于 后者(以 开头的连续若干个数的异或和)。
为什么操作连续的若干个数?
举个形象一点的例子,这个就跟伐木一样,比如说我要砍倒一棵树,我肯定会选择同一个高度砍,不会说先在距离地面 米的位置砍两刀,然后再到距离地面 米的位置砍几刀,…… 肯定是在同一个位置。
因为我们的目的是破坏数列“非严格单调递增“的性质,所以我们只需要存在一对元素满足条件即可,很明显:
- 它们是相邻的
- 左边的那个数是一段连续的元素的异或和(对应以某个 结尾的连续若干个数)
- 右边的那个数也是一段连续的元素的异或和(对应以 开头的连续若干个数)
考虑可以操作 次时是否满足条件(破坏数列的“非严格单调递增“性质),因为要将一段连续的区间内的元素消到只剩 个数(然后左边那个数大于右边那个数),操作一次会消去一个数,所以 次操作对应一个长度为 的区间。
当 确定时,枚举每个下标 ,对于下标 ,我们要确定的事情是:
是否存在一个以 结尾的连续子序列(假设这个连续子序列的异或和为 )以及一个以 开头的连续子序列(假设这个连续子序列的异或和为 ),满足 且两个连续子序列包含的元素个数不超过 。
如果有的话就说明 消除不超过 次能够是数列不满足”非严格单调递增”的性质。
注意:是 ”不超过 次” 而不是 ”恰好 次”。
这是为什么呢?我们多定义两个状态 和 ,这两个状态在 和 时表示的含义不同,其中:
当 时:
表示的是 (即以 开头以 结尾的连续子序列中所有元素的异或和),推导公式:
表示的是 的最大值,推导公式:
此时的状态是从后往前推(),可以发现 从后往前是非降的,因为 表示的并不是从 异或到 的结果(这是 表示的),而是从 异或到 的过程中能够得到的结果的最大值。
也就是说, 表示的含义是 往前合并 不超过 次的情况下能够获得的最大值。
当 时:
表示的是 (即以 开头以 结尾的连续子序列中所有元素的异或和),推导公式:
表示的是 的最大值,推导公式:
(注意,区别于 的情况,这里 求的是最小值)
此时的状态是从前往后推(),可以发现 从前往后是非增的,因为 表示的是从 异或到 的过程中能够得到的结果的最小值。
也就是说,在 时, 表示的含义是 往后合并 不超过 次的情况下能够获得的最大值。
那么怎么能确保不超过 次操作能够存在两个相邻的数前者大于后者呢?
枚举合并的过程中最前面那个数的下标!
以 结尾合并 次能合并到 ,所以我们从 到 枚举下标 ,对于的下标区间范围是 —— 注意 可能大于 ,所可以开一个 ,对于的区间范围是 ,当 时就不用继续操作了(因为 是左边一段变小,而右边一段固定,继续 只会让左边一段异或和的最大值变小,而右边一段异或和的最小值是不会发生任何变换,所以继续 没意义)它表示的含义是:
- 能够合并到的最左边的数是
- 能够合并到的最右边的数是
那么 往左合并的过程中的最大值就是 , 往右合并的过程中的最小值就是 。只要 ,就说明不超过 次操作能够是数列变得不满足”非严格单调递增”性质。
对应的我可以开一个 check(int m)
函数进行判断。
但是这种方法只能判断在不超过 次合并时能够满足条件,不过可以发现,当 大于等于我们的最小操作次数时,check 函数总会返回 true;当 小于我们的最小操作次数时,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)
的时间复杂度是 的, 接近 ,所以总的时间复杂度是 的,之所以能过的原因最主要我觉得还是因为:
- 当 比较大时,答案很小
- 题目的 可能给的比较小
- 或者别的原因(欢迎评论区补充)
可以发现,因为 ,所以 的二进制表示最多 位,而数列是非严格单调递增的,所以根据鸽笼/雀巢/抽屉原理,当 时,必然存在连续的三个数 —— 假设这三个数是 —— 的最高位的位数是相同的,此时将后两个数(即 和 )异或能够消去最高位的 ,此时 (注意:本题中 ,若 可以取 那还需要考虑排除 的影响),即:当 时,答案为 。
而当 时,直接用二分或者枚举答案都是可以的。
也就是说,在题目里面可以加一行
if (n > 60) {
cout << 1 << endl;
return 0;
}
来优化我们的程序。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通