B - Bitwise Puzzle - 题解
标签与难度
标签: 位运算, 构造, 贪心, 模拟 难度: 1800
题目大意喵~
哈喵~!各位算法大师们,Yuki酱又来出题考验我们啦!(ฅ'ω'ฅ)
这次,她给了我们三个非负整数 。我们的目标是,通过最多64次操作,让 和 都等于 。如果做不到,就要告诉她 "不可能的喵~"。
我们有四种神奇的操作:
a ← a * 2(把 左移一位)b ← floor(b / 2)(把 右移一位)a ← a ⊕ b(按位异或)b ← b ⊕ a(按位异或)
题目还很贴心地告诉我们,如果存在解,那么一定有一个不超过64步的解法。这可真是个重要的提示呢!
解题思路分析
这道题看起来像一个位操作的谜题,目标非常明确:把 和 都变成 。 是一个固定的目标,所以我们的任务就是改造 和 。
喵~ 仔细观察这四种操作,我们可以发现一些规律:
- 操作1 (
a <<= 1) 和操作2 (b >>= 1) 是移位操作。操作1让a"变大",操作2让b"变小"。它们是调整数字“长度”(二进制位数)的主要工具。 - 操作3 (
a ^= b) 和操作4 (b ^= a) 是异或操作。这是我们改变数字具体比特位的核心武器!特别是,x ^= y可以看作是“用 的模式去翻转 的对应位”。
要把 和 都变成 ,一个很自然的想法是分两步走:
- 先把 变成 。
- 然后再把 变成 。
但是,操作是相互关联的,比如改变 的时候可能需要用到 。所以我们需要一个更周全的计划。
这道题的构造性很强,喵~ 我在思考的时候发现,直接从头推导出一个完美无瑕的策略有点点绕。特别是当 的二进制位数比 少,需要用左移来“加长”a 的时候,如何同时保证新增长的比特位是正确的,这块逻辑有点复杂。
不过没关系!通过分析这些AC代码的共同点,我们可以总结出一条清晰的制胜路线,就像猫咪找到回家的路一样!(๑•̀ㅂ•́)و✧
核心策略: 先将 构造成 ,在此过程中将 当作一个“一次性”的工具来使用。当 成功变为 后,我们再来处理 。
下面就是我们的详细作战计划:
第一步:预处理与同步
在开始大刀阔斧地改造前,我们先做一些准备工作,让 和 进入一个“标准状态”,方便后续操作。
处理特殊情况:
- 如果一开始
a == c && b == c,那我们什么都不用做,0步搞定! - 如果
a = 0, b = 0但c != 0,我们无法从0凭空造出非0的数,因为0*2=0,0/2=0,0^0=0。所以这种情况无解,输出-1。
- 如果一开始
同步 和 的最高位 (MSB): 我们的策略是把 当作修改 的工具。一个好的工具是可预测的。如果 和 的最高有效位(Most Significant Bit, MSB)在同一位置上,它们就有了相似的“量级”,操作起来会更方便。
- 如果
a或b中有一个是0(但不是两个都为0),我们可以用一次异或操作让它非零。例如,如果a=0,执行a ^= b,a就变成了b。 - 如果
a和b都非零,但它们的MSB位置不同,我们也可以用一次异或来对齐它们。例如,如果msb(a) < msb(b),执行a ^= b后,新的a的MSB就会和b对齐在msb(b)的位置。
- 如果
经过这一步,我们保证了 并且 msb(a) == msb(b)。这非常关键!
第二步:构造 ,让它等于
这是我们解题的核心。我们将从高位到低位,逐一修正 a 的比特位,让它向 c 看齐。在这个过程中,b 将扮演关键的“翻转工具”角色。我们会同步地消耗 b。
我们从 和 的最高位 开始,一路向下处理到第0位。
对于 i 从 到 0 的每一位:
- 检查
a的第i位:我们比较a的第i位和c的第i位是否相同。 - 按需翻转:如果
(a >> i) & 1不等于(c >> i) & 1,说明a的这一位需要被翻转。怎么办呢?用b!此时,b的最高有效位正好在第i位(因为我们接下来会看到,b会被同步右移)。所以,执行a ^= b就能精确地翻转a的第i位,而不会影响比i更高的位(因为b在那些位上都是0)。 - 消耗
b:无论我们是否执行了翻转,b在第i位的使命已经完成了。我们通过b >>= 1(操作2) 将它右移一位,准备让它的下一个最高位(也就是原来的第i-1位)在下一轮循环中对齐。
这个循环结束后,a 中从第 位到第 0 位的部分,就和 c 的对应部分完全一样了。
第三步:处理长度差异和收尾
上面的循环只处理了 a 原有长度内的比特。但 a 和 c 的长度可能不同。
- 如果
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 检查一遍a和c的所有位,用b(此时已经很小了)来进行微调。
一个更统一的策略是,我们不只迭代到 a 的MSB,而是从一个足够大的数(比如31或62)开始迭代。这样就能自然地处理所有长度情况。
最终的收尾工作:
- 经过上述步骤,我们已经成功地让
a变成了c。太棒了! - 此时的
b经过反复右移,已经变成了一个很小的数,甚至可能是0。我们的目标是让b也等于c。 - 最简单的办法是:先把
b变成0(通过不断右移),然后执行b ^= a(操作4)。因为此时a已经等于c,所以b就变成了0 ⊕ c,也就是c!
大功告成!我们记录下所有操作,就完成了这道谜题。这个过程保证了操作次数在可控范围内。
代码实现
这是我根据上面的思路,精心重构的一份代码,希望能帮助你理解呐~
#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;
}复杂度分析
时间复杂度: 我们的主要逻辑是一个从高位到低位的循环。由于输入的数最大是 ,而我们在代码里为了安全使用了64位整数,循环最多执行约60多次。在循环内部,调整
b的大小也是对数级的。因此,对于每个测试用例,操作都是常数级别的,严格来说是 ,其中 是数值的上限。空间复杂度: 我们用一个
std::vector来存储操作序列。题目保证解法的步数不超过64,所以这个向量的大小也是常数级别的。因此空间复杂度也是 。
知识点总结
这道题是对位运算理解和应用的一次综合考察,喵~
- 位运算的性质: 深入理解左移(
<<,乘2)、右移(>>,除2)和异或(^,翻转/无进位相加)的含义是解题的基础。 - 构造性思维: 面对“如何从A到B”这类问题,构造性算法是一个常见的思路。我们需要设计一个明确的步骤序列,一步步地把初始状态转变为目标状态。
- MSB (Most Significant Bit): 最高有效位是数字“量级”的标志。通过对齐MSB,我们可以简化后续的操作,这是一个非常实用的技巧。
- 分步解决: 将一个复杂的目标(让
a=c且b=c)分解成更小的子目标(先让a=c,再处理b),可以使问题结构更清晰。 - 工具化思想: 在构造过程中,把一个变量(如本题的
b)看作是实现目标的“工具”,在完成任务后可以“丢弃”或“重置”,是一种有效的解题视角。
希望这篇题解能帮到你,喵~ 如果还有不明白的地方,随时可以再来问我哦!一起加油,成为更厉害的算法大师吧!(≧∇≦)/