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\) 位。

通过这种方式从高到低确定每一位,我们就能构造出最小的可能答案。

算法步骤

  1. 确定最高位:首先,我们需要确定答案的位数范围。最终答案的最高位,至少是初始序列所有元素或起来的最高位。我们遍历一遍初始序列 \(a\),找到所有 \(a_i\) 中最大的二进制位数,记为 \(k\)(即最高位的下标是 \(k\))。由于我们无法通过左移消除一个已经存在的位,所以最终答案的第 \(k\) 位必定是 1。因此,我们可以将答案 ans 初始化为 1LL << k

  2. 从高到低贪心:我们从第 \(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 去判断下一位。

  3. 输出结果:当循环结束(从 \(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;
}
posted @ 2025-08-05 09:18  AFewMoon  阅读(13)  评论(0)    收藏  举报