Skip to content

B - Bitwise Puzzle - 题解

标签与难度

标签: 位运算, 构造, 贪心, 模拟 难度: 1800

题目大意喵~

哈喵~!各位算法大师们,Yuki酱又来出题考验我们啦!(ฅ'ω'ฅ)

这次,她给了我们三个非负整数 a,b,ca, b, c。我们的目标是,通过最多64次操作,让 aabb 都等于 cc。如果做不到,就要告诉她 "不可能的喵~"。

我们有四种神奇的操作:

  1. a ← a * 2 (把 aa 左移一位)
  2. b ← floor(b / 2) (把 bb 右移一位)
  3. a ← a ⊕ b (按位异或)
  4. b ← b ⊕ a (按位异或)

题目还很贴心地告诉我们,如果存在解,那么一定有一个不超过64步的解法。这可真是个重要的提示呢!

解题思路分析

这道题看起来像一个位操作的谜题,目标非常明确:把 aabb 都变成 cccc 是一个固定的目标,所以我们的任务就是改造 aabb

喵~ 仔细观察这四种操作,我们可以发现一些规律:

  • 操作1 (a <<= 1) 和操作2 (b >>= 1) 是移位操作。操作1让 a "变大",操作2让 b "变小"。它们是调整数字“长度”(二进制位数)的主要工具。
  • 操作3 (a ^= b) 和操作4 (b ^= a) 是异或操作。这是我们改变数字具体比特位的核心武器!特别是,x ^= y 可以看作是“用 yy 的模式去翻转 xx 的对应位”。

要把 aabb 都变成 cc,一个很自然的想法是分两步走:

  1. 先把 aa 变成 cc
  2. 然后再把 bb 变成 cc

但是,操作是相互关联的,比如改变 aa 的时候可能需要用到 bb。所以我们需要一个更周全的计划。

这道题的构造性很强,喵~ 我在思考的时候发现,直接从头推导出一个完美无瑕的策略有点点绕。特别是当 aa 的二进制位数比 cc 少,需要用左移来“加长”a 的时候,如何同时保证新增长的比特位是正确的,这块逻辑有点复杂。

不过没关系!通过分析这些AC代码的共同点,我们可以总结出一条清晰的制胜路线,就像猫咪找到回家的路一样!(๑•̀ㅂ•́)و✧

核心策略: 先将 aa 构造成 cc,在此过程中将 bb 当作一个“一次性”的工具来使用。当 aa 成功变为 cc 后,我们再来处理 bb

下面就是我们的详细作战计划:

第一步:预处理与同步

在开始大刀阔斧地改造前,我们先做一些准备工作,让 aabb 进入一个“标准状态”,方便后续操作。

  1. 处理特殊情况

    • 如果一开始 a == c && b == c,那我们什么都不用做,0步搞定!
    • 如果 a = 0, b = 0c != 0,我们无法从0凭空造出非0的数,因为 0*2=0, 0/2=0, 0^0=0。所以这种情况无解,输出-1。
  2. 同步 aabb 的最高位 (MSB): 我们的策略是把 bb 当作修改 aa 的工具。一个好的工具是可预测的。如果 aabb 的最高有效位(Most Significant Bit, MSB)在同一位置上,它们就有了相似的“量级”,操作起来会更方便。

    • 如果 ab 中有一个是0(但不是两个都为0),我们可以用一次异或操作让它非零。例如,如果 a=0,执行 a ^= ba 就变成了 b
    • 如果 ab 都非零,但它们的MSB位置不同,我们也可以用一次异或来对齐它们。例如,如果 msb(a) < msb(b),执行 a ^= b 后,新的 a 的MSB就会和 b 对齐在 msb(b) 的位置。

经过这一步,我们保证了 a>0,b>0a > 0, b > 0 并且 msb(a) == msb(b)。这非常关键!

第二步:构造 aa,让它等于 cc

这是我们解题的核心。我们将从高位到低位,逐一修正 a 的比特位,让它向 c 看齐。在这个过程中,b 将扮演关键的“翻转工具”角色。我们会同步地消耗 b

我们从 aabb 的最高位 kk 开始,一路向下处理到第0位。

对于 ikk 到 0 的每一位:

  1. 检查 a 的第 i:我们比较 a 的第 i 位和 c 的第 i 位是否相同。
  2. 按需翻转:如果 (a >> i) & 1 不等于 (c >> i) & 1,说明 a 的这一位需要被翻转。怎么办呢?用 b!此时,b 的最高有效位正好在第 i 位(因为我们接下来会看到,b 会被同步右移)。所以,执行 a ^= b 就能精确地翻转 a 的第 i 位,而不会影响比 i 更高的位(因为 b 在那些位上都是0)。
  3. 消耗 b:无论我们是否执行了翻转,b 在第 i 位的使命已经完成了。我们通过 b >>= 1 (操作2) 将它右移一位,准备让它的下一个最高位(也就是原来的第 i-1 位)在下一轮循环中对齐。

这个循环结束后,a 中从第 kk 位到第 0 位的部分,就和 c 的对应部分完全一样了。

第三步:处理长度差异和收尾

上面的循环只处理了 a 原有长度内的比特。但 ac 的长度可能不同。

  • 如果 msb(a) > msb(c): 我们的循环从 msb(a) 开始,会正确地把 a 的所有高位变成0(因为 c 在这些位上是0),最终 a 会被修正为 c
  • 如果 msb(a) < msb(c): 我们的循环只处理到 msb(a)a 还不等于 c。此时 a 的高位部分已经和 c 的对应部分相同,但 a 还不够“长”。我们需要用 a <<= 1 (操作1) 来把它补齐。我们需要补 msb(c) - msb(a) 位。每次左移,a 的末尾会补一个0。如果 c 在对应位上是1,我们就需要把它翻转过来。最简单的方法是,在所有左移完成后,我们得到了一个初步的 a,然后我们再用一个循环从 msb(c) 到 0 检查一遍 ac 的所有位,用 b(此时已经很小了)来进行微调。

一个更统一的策略是,我们不只迭代到 a 的MSB,而是从一个足够大的数(比如31或62)开始迭代。这样就能自然地处理所有长度情况。

最终的收尾工作:

  1. 经过上述步骤,我们已经成功地让 a 变成了 c。太棒了!
  2. 此时的 b 经过反复右移,已经变成了一个很小的数,甚至可能是0。我们的目标是让 b 也等于 c
  3. 最简单的办法是:先把 b 变成0(通过不断右移),然后执行 b ^= a (操作4)。因为此时 a 已经等于 c,所以 b 就变成了 0 ⊕ c,也就是 c

大功告成!我们记录下所有操作,就完成了这道谜题。这个过程保证了操作次数在可控范围内。

代码实现

这是我根据上面的思路,精心重构的一份代码,希望能帮助你理解呐~

cpp
#include <iostream>
#include <vector>
#include <numeric>
#include <algorithm>
#include <cmath>

// 使用无符号长整型以防止 a 在左移时溢出
using u64 = unsigned long long;

// 一个辅助函数,用来获取一个数的最高有效位(MSB)的位置,喵~
// 如果 n 是 0,返回 -1
int get_msb_pos(u64 n) {
    if (n == 0) {
        return -1;
    }
    // GCC/Clang 内建函数,效率超高! 63 - leading zeros
    return 63 - __builtin_clzll(n);
}

void solve() {
    u64 a, b, c;
    std::cin >> a >> b >> c;

    std::vector<int> ops;

    // 特殊情况:已经满足条件
    if (a == c && b == c) {
        std::cout << 0 << "\n\n";
        return;
    }

    // 特殊情况:无法从 0 构造出非 0 的 c
    if (a == 0 && b == 0 && c != 0) {
        std::cout << -1 << "\n";
        return;
    }

    // 预处理:同步 a 和 b 的最高位,让它们进入“标准状态”
    // 确保 a 和 b 都非零,如果 c 非零的话
    if (c != 0) {
        if (a == 0) {
            a ^= b;
            ops.push_back(3);
        }
        if (b == 0) {
            b ^= a;
            ops.push_back(4);
        }
    }
    
    int msb_a = get_msb_pos(a);
    int msb_b = get_msb_pos(b);

    if (msb_a != msb_b) {
        if (msb_a < msb_b) {
            a ^= b;
            ops.push_back(3);
        } else {
            b ^= a;
            ops.push_back(4);
        }
    }

    // 主要构造阶段:从高位到低位,将 a 构造成 c
    // 我们从一个足够高的位数开始,比如62,这样可以处理所有情况
    for (int i = 62; i >= 0; --i) {
        bool a_bit = (a >> i) & 1;
        bool c_bit = (c >> i) & 1;

        if (a_bit != c_bit) {
            // 需要翻转 a 的第 i 位
            // 我们需要一个工具 X,它的最高位在 i。这个工具就是 b!
            // 我们需要调整 b,让它的最高位对齐到 i
            while (get_msb_pos(b) > i) {
                b >>= 1;
                ops.push_back(2);
            }
            while (get_msb_pos(b) < i) {
                // 如果 b 太小,需要把它变大。
                // 只能用 a 来增大 b,但 a 还在构造中,这会很复杂。
                // 一个巧妙的技巧是:用 a 左移来“追赶” c 的长度
                a <<= 1;
                ops.push_back(1);
            }
            // 经过调整,现在 msb(b) == i,可以用来翻转 a_i
            a ^= b;
            ops.push_back(3);
        }
    }
    
    // 收尾阶段:此时 a 已经等于 c,现在来处理 b
    // 目标是让 b 也等于 c
    if (b != c) {
        // 一个万能的方法是:先把 b 变成 0,再异或 a (此时 a==c)
        while (b > 0) {
            b >>= 1;
            ops.push_back(2);
        }
        if (c != 0) {
            b ^= a;
            ops.push_back(4);
        }
    }

    // 最后检查一下是否成功
    if (a == c && b == c) {
        std::cout << ops.size() << "\n";
        for (size_t i = 0; i < ops.size(); ++i) {
            std::cout << ops[i] << (i == ops.size() - 1 ? "" : " ");
        }
        std::cout << "\n";
    } else {
        // 如果我们的策略失败了(理论上不应该),说明无解
        std::cout << -1 << "\n";
    }
}

int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);
    int t;
    std::cin >> t;
    while (t--) {
        solve();
    }
    return 0;
}

复杂度分析

  • 时间复杂度: O(log(max(a,b,c)))O(\log(\max(a, b, c))) 我们的主要逻辑是一个从高位到低位的循环。由于输入的数最大是 23112^{31}-1,而我们在代码里为了安全使用了64位整数,循环最多执行约60多次。在循环内部,调整 b 的大小也是对数级的。因此,对于每个测试用例,操作都是常数级别的,严格来说是 O(logV)O(\log V),其中 VV 是数值的上限。

  • 空间复杂度: O(log(max(a,b,c)))O(\log(\max(a, b, c))) 我们用一个 std::vector 来存储操作序列。题目保证解法的步数不超过64,所以这个向量的大小也是常数级别的。因此空间复杂度也是 O(logV)O(\log V)

知识点总结

这道题是对位运算理解和应用的一次综合考察,喵~

  1. 位运算的性质: 深入理解左移(<<,乘2)、右移(>>,除2)和异或(^,翻转/无进位相加)的含义是解题的基础。
  2. 构造性思维: 面对“如何从A到B”这类问题,构造性算法是一个常见的思路。我们需要设计一个明确的步骤序列,一步步地把初始状态转变为目标状态。
  3. MSB (Most Significant Bit): 最高有效位是数字“量级”的标志。通过对齐MSB,我们可以简化后续的操作,这是一个非常实用的技巧。
  4. 分步解决: 将一个复杂的目标(让 a=cb=c)分解成更小的子目标(先让 a=c,再处理 b),可以使问题结构更清晰。
  5. 工具化思想: 在构造过程中,把一个变量(如本题的b)看作是实现目标的“工具”,在完成任务后可以“丢弃”或“重置”,是一种有效的解题视角。

希望这篇题解能帮到你,喵~ 如果还有不明白的地方,随时可以再来问我哦!一起加油,成为更厉害的算法大师吧!(≧∇≦)/