Luogu P13565「CZOI-R5」按位或
题目分析
本题的目标是最小化序列 \(a\) 中所有元素的按位或(bitwise OR)结果。我们可以对任意元素 \(a_i\) 进行乘以 2 的操作,总操作次数不超过 \(m\) 次。
首先,我们需要理解操作的本质。将一个数 \(a_i\) 乘以 2,在二进制表示下等价于将其左移一位(a[i] << 1)。因此,题目可以转化为:我们有总共 \(m\) 次左移操作的机会,可以任意分配给序列 \(a\) 中的各个元素,目标是使最终所有元素的按位或和最小。
核心思路:按位贪心
这类要求最小化一个数值(尤其是位运算结果)的题目,一个非常经典且有效的思路是按位贪心。我们从高到低逐位确定最终答案的每一个二进制位。
假设我们正在考虑答案的第 \(i\) 位(从高到低)。我们的贪心策略是:尽可能让这一位为 0。
具体来说,我们从可能的最高位开始,向下遍历到第 0 位。对于当前考虑的第 \(i\) 位,我们先假设最终答案的这一位可以是 0。然后,我们检查这个假设是否成立。
如何检查假设?假设我们已经确定了答案中比 \(i\) 更高的位,构成了一个值 ans。现在我们尝试让第 \(i\) 位为 0。这意味着,我们希望最终的序列 \(a'\) 满足其或和 OR(a') 的第 \(i\) 位为 0,且所有高于 \(i\) 的位与我们已经确定的 ans 保持一致。
为了达成这个目标,对于序列中的每一个数 \(a_j\),我们需要通过若干次左移操作(乘以 2)将其变为 \(a'_j\),使得 \(a'_j\) 的所有为 1 的位,在我们的目标答案中也必须为 1。换句话说,对于我们当前尝试的目标值 target_ans(即 ans 本身,因为我们假设第 \(i\) 位为 0),必须满足 (a'_j | target_ans) == target_ans。
对于每个原始的 \(a_j\),我们计算将其变为满足上述条件的 \(a'_j\) 所需的最少左移次数。如果所有 \(a_j\) 的最少左移次数之和不超过我们的操作预算 \(m\),那么我们“可以”让答案的第 \(i\) 位为 0。我们就保持 ans 不变,继续考虑第 \(i-1\) 位。
反之,如果所需总操作次数超过了 \(m\),说明我们无法在预算内达成目标。因此,我们必须接受答案的第 \(i\) 位为 1。我们更新 ans,将它的第 \(i\) 位设为 1(ans |= (1LL << i)),然后继续考虑第 \(i-1\) 位。
通过这种方式从高到低确定每一位,我们就能构造出最小的可能答案。
算法步骤
-
确定最高位:首先,我们需要确定答案的位数范围。最终答案的最高位,至少是初始序列所有元素或起来的最高位。我们遍历一遍初始序列 \(a\),找到所有 \(a_i\) 中最大的二进制位数,记为 \(k\)(即最高位的下标是 \(k\))。由于我们无法通过左移消除一个已经存在的位,所以最终答案的第 \(k\) 位必定是 1。因此,我们可以将答案
ans初始化为1LL << k。 -
从高到低贪心:我们从第 \(k-1\) 位开始,一直循环到第 0 位。在每一轮循环中,我们处理第 \(i\) 位:
a. 计算代价:假设答案的第 \(i\) 位为 0。我们计算要达成这个目标所需的总左移次数cost。
b. 遍历序列:对每一个 \(a_j\),我们检查它是否与当前的目标ans冲突。一个数cur(初始为 \(a_j\))与ans冲突,意味着cur在某个位置 \(p \ge i\) 上是 1,而ans在位置 \(p\) 上是 0。
c. 模拟左移:如果 \(a_j\) 与ans冲突,我们就必须对它进行左移,直到它不再冲突为止。我们用一个循环来模拟这个过程:while (cur 与 ans 冲突) { cur <<= 1; shifts++; }。累加所有 \(a_j\) 需要的shifts到总代价cost。
d. 一个重要的剪枝:在模拟左移的过程中,如果cur的最高位已经超过或等于我们一开始确定的最高位 \(k\),但它仍然与ans冲突,这意味着我们为了消除一个较低位的冲突,不得不引入一个比ans最高位还高的位。这显然会使结果变得更大,得不偿失。因此,这种情况说明我们的假设(第 \(i\) 位为 0)是不可能实现的。我们可以直接判定失败。
e. 决策:计算完所有 \(a_j\) 的总代价cost后,如果cost > m,说明操作次数不够,我们无法让第 \(i\) 位为 0。因此,我们必须将ans的第 \(i\) 位设为 1 (ans |= (1LL << i))。如果cost <= m,说明可行,我们什么都不做,继续用这个更优的(第 \(i\) 位为 0)ans去判断下一位。 -
输出结果:当循环结束(从 \(k-1\) 到 0 都判断完毕),
ans就是我们求得的最小值。
代码解析
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int N = 1e6 + 5;
LL a[N];
int main() {
LL n, m;
scanf("%lld", &n);
LL k = 0; // k 用于记录初始序列中出现过的最高位
for (int i = 1; i <= n; i++) {
scanf("%lld", a + i);
if (a[i] > 0) k = max(k, (LL)__lg(a[i])); // __lg(x) 返回 x 的最高位的位置 (log2(x))
}
scanf("%lld", &m);
// 特判:如果所有数都是0,答案就是0
if (!k && a[1] == 0) {
printf("0\n");
return 0;
}
// 初始化答案,第k位必须是1
LL ans = 1LL << k;
// 从高到低(k-1 到 0)确定每一位
for (LL i = k - 1; i >= 0; i--) {
LL cost = 0;
bool possible = 1;
// mask 用于快速检查当前数字在 i 位及更高位上是否与 ans 冲突
// ~((1LL << i) - 1) 会生成一个 i 位以下全是0,i 位及以上全是1的掩码
LL mask = ~((1LL << i) - 1);
for (int j = 1; j <= n; j++) {
LL cur = a[j], shifts = 0;
// 检查 cur 是否与 ans 冲突。
// 冲突条件:cur 在 i 位及以上有1,而 ans 对应位是0。
// (cur & mask) 只保留 cur 的高位。
// ((cur & mask) | ans) 如果 cur 的高位不与 ans 冲突,结果就等于 ans。
// 如果结果不等于 ans,说明有冲突,需要左移。
while (((cur & mask) | ans) ^ ans) {
// 剪枝:如果 cur 已经变得很大(最高位超过k),但仍有冲突,说明此路不通
if (cur && __lg(cur) >= k) {
possible = 0;
break;
}
cur <<= 1;
shifts++;
}
if (!possible) {
break;
}
cost += shifts;
// 如果累计代价已经超出预算,提前终止
if (cost > m) {
possible = false;
break;
}
}
// 如果不可能(代价超了或触发了剪枝),则被迫将答案的第 i 位置为 1
if (!possible) {
ans |= (1LL << i);
}
}
printf("%lld\n", ans);
return 0;
}

浙公网安备 33010602011771号