CC++ & Algorithm

二进制暗号:当灯泡学会用开关说话

你有没有在夜晚走过一条走廊,灯一盏一盏亮起来,像在跟你打招呼?每一盏灯只有两种状态:亮或灭。计算机的世界也是这样——无数个开关,只有 0 和 1。但人类厉害的地方在于,我们不只是看着灯发呆,而是学会用灯的亮灭来“说话”、来“约定暗号”。

今天要聊的按位与 & 和按位或 |,就是最基础的两种“灯光暗号”。它们看起来简单到不行,但如果你真正理解它们,会发现很多看似复杂的位运算题目,其实都是这两种暗号的变奏。

先感受一下:两个开关怎么控制一盏灯

按位与 & 的规则是:两个对应位都是 1,结果才是 1。你可以把它想象成两个串联的开关——必须两个都闭合,灯泡才亮。

按位或 | 的规则是:只要有一个是 1,结果就是 1。这是两个并联的开关——任意一个闭合,灯泡就亮。

这个比喻很多地方都讲过。但我想让你注意一个更关键的点:这两个操作本质上是在做“筛选”和“合并”。

  • & 是筛选:我想知道某个位是不是 1,就用一个只有那一位是 1 的掩码去“与”它。
  • | 是合并:我想把某个位强制变成 1,就用一个只有那一位是 1 的掩码去“或”它。

你看,一个在“问问题”,一个在“下命令”。这是两种完全不同的语义。

一道题检验你是不是真的懂了

来看这道题:

已知 int a = 0b1100, b = 0b1010; 则表达式 a & b 的值是(二进制形式)?

逐位比较:

  1 1 0 0
& 1 0 1 0
---------
  1 0 0 0

结果是 0b1000。

这道题本身不难,但它暴露了一个非常典型的错误:把 & 和 | 搞反。如果你算出来是 0b1110,那就是错把“与”当成了“或”。很多人记规则的时候只记“有 0 则 0”或者“有 1 则 1”,但在紧张的考试或面试里,一慌就容易混。

我的建议是:不要背规则,去理解语义。& 是“同时满足”,| 是“至少满足一个”。你想象两个条件,一个要求“既……又……”,一个要求“或者……或者……”,就永远不会搞反。

位运算的真正威力:从一个区间异或说起

掌握了 & 和 | 之后,你其实已经拿到了打开位运算世界大门的钥匙。接下来我想借一道更有意思的题,让你看到这些基础操作是如何组合出优雅解法的。

题目是这样的:

定义 f(A, B) 为 A, A+1, ..., B 这些数的按位异或值。给定 A 和 B(最大到 10^12),求 f(A, B)。

如果你没接触过这类问题,第一反应可能是:循环一遍不就完了?但 B 可以到 10^12,循环必然超时。

这时候我们需要一个关键洞察:异或运算满足自反性。也就是说,x ^ x = 0。这意味着,如果我们定义 g(n) = 0 ^ 1 ^ 2 ^ ... ^ n,那么 f(A, B) = g(B) ^ g(A-1)。因为从 A 到 B 的异或,等于前 B 个的异或再“抵消掉”前 A-1 个的异或。

那 g(n) 怎么快速算?这里有一个非常漂亮的规律——它和 n % 4 有关:

  • n % 4 == 0 时,g(n) = n
  • n % 4 == 1 时,g(n) = 1
  • n % 4 == 2 时,g(n) = n + 1
  • n % 4 == 3 时,g(n) = 0

这个规律怎么来的?其实就是连续四个数的异或结果会归零,因为每四个数的最低位模式会完整循环一次。你可以自己验证几组,很快就能感受到这个节奏。

所以核心代码就几行:

long long g(long long n) {
    if (n < 0) return 0;
    if (n % 4 == 0) return n;
    if (n % 4 == 1) return 1;
    if (n % 4 == 2) return n + 1;
    return 0;
}
long long f(long long A, long long B) {
    return g(B) ^ g(A - 1);
}

你看,这里虽然主要用的是异或,但整个思路的根基还是位运算的思维:把一个大问题拆成位级别的规律。而 & 和 | 就是训练这种思维的第一步。

校验和:异或的另一个经典舞台

再看一个更贴近实际应用的场景:数据传输中的校验和。

将 N 个整数进行按位异或,得到的结果作为校验码。给定 N 个整数,计算它们的异或校验码。

这道题几乎就是异或的定义题。但我想让你注意它背后的思想:异或是一种“无损混合”。你把一堆数异或在一起,得到一个新的数。如果其中任何一个数变了,异或结果几乎一定会变。这就是为什么它适合做校验。

代码简单到不需要贴:

int checksum = 0;
for (int i = 0; i < n; i++) {
    int x; cin >> x;
    checksum ^= x;
}

但这里有一个容易被忽略的点:异或校验码的顺序无关。因为异或满足交换律和结合律,你无论按什么顺序异或,结果都一样。这也是它适合做校验的原因之一——发送方和接收方不需要约定顺序。

那些年我们踩过的坑

学到这里,你可能会觉得位运算不过如此。但真正写代码的时候,有几个坑几乎人人都会踩。

第一个坑:& 和 && 混用。

if (a & b) 和 if (a && b) 看起来很像,但语义完全不同。前者是按位与,结果是一个整数;后者是逻辑与,结果是布尔值。如果你本意是判断两个条件是否同时成立,却写了 &,编译器不会报错,但逻辑可能完全不对。

第二个坑:优先级。

if (x & y == 0) 实际上会被解释成 if (x & (y == 0)),因为 == 的优先级比 & 高。这种错误非常隐蔽,因为代码看起来“很合理”。正确的写法永远是加括号:if ((x & y) == 0)。

第三个坑:负数。

负数的二进制是补码,-1 的所有位都是 1。所以 -1 & 3 = 3,-1 | 3 = -1。初学者最好先用无符号整数或正整数练习,等熟悉了再处理负数。

从“开关”到“思维”

回到开头那个比喻:灯泡只有亮和灭,但通过不同的组合方式,我们可以表达出无穷的信息。& 和 | 就是最基本的两种组合方式——一个在问“是不是都满足”,一个在说“至少满足一个”。

当你真正理解了这两种语义,你会发现很多位运算技巧都变得自然:

  • 用 n & 1 判断奇偶,因为最低位决定了奇偶性。
  • 用 n & (n - 1) 消掉最低位的 1,因为减一之后最低位的 1 会变成 0,后面的 0 会变成 1,再与一下就把那个 1 消掉了。
  • 用 n | (1 << k) 把第 k 位设为 1,因为或运算的“至少一个为 1”特性。

这些技巧不是背出来的,而是从对 & 和 | 的深刻理解中“长”出来的。

如果你已经掌握了这些,下一步可以继续探索异或 ^ 的自反性、取反 ~ 的补码本质,以及左右移 << >> 的乘除语义。它们都是同一套思维的不同侧面。

位运算不难,难的是把它变成你的直觉。多写、多验证、多思考“为什么”,你很快就能在二进制世界里自由穿行。


关于作者

我是赵老师,持有 NOI 信息学奥赛教练证书,拥有 15 年以上的软件开发经验,从事信息学少儿编程教学已有 8 年时间。

这些年累计帮助 多名 学生通过编程特长升入自己心仪的目标学校。

如果你在编程学习上有任何疑问,欢迎联系我:18620372957(微信同号)

这篇文章对你有帮助吗?

成为第一个评价的人

评论0

还没有评论,来抢沙发~

评论加载中...

想系统学习这个知识点?查看完整知识点 →