目录

题目描述

1318. 或运算的最小翻转次数

题意分析

题目给出三个正整数 abc。一次操作可以挑 ab任意一个二进制位,把它从 0 改成 1、或者从 1 改成 0;c 是固定的目标,不允许改动。要求返回让 a OR b == c 成立所需的最少操作次数

约束里有两个信号值得留意。第一,只能改 abc 只读,所以「目标」是完全确定的,我们要做的是把 ab 调整到某个满足 a' | b' == c 的状态,代价是被改动的位数。第二,一次操作只动一个位,代价按位计数,这意味着总代价天然就是各个位上代价的累加,而不是某种整体度量。

最关键的观察是:各个二进制位互不影响。按位或的定义本身就是逐位独立计算的 —— a | b 的第 i 位只由 a 的第 i 位和 b 的第 i 位决定,和其它位没有半点关系;而一次翻转也只影响一个位,不会像加法那样产生进位去污染邻位。于是「让整个 a | b 等于 c」就等价于「对每一个 i,让第 i 位上的或值等于 c 的第 i 位」,而这些子目标之间没有任何冲突或竞争:为某一位付出的翻转对别的位毫无副作用,某一位的选择也不会限制别的位。

因此总的最少代价就等于每一位单独最少代价之和。这一步把一个看上去要在「到底翻哪些位」的指数级组合里搜索的问题,降成了对每一位做常数次判断的统计问题。

边界情形有几处要照顾到。其一,a | b 本来就等于 c 时答案是 0,这不需要特判,逐位统计自然会得出 0。其二,c 的二进制位数可能比 ab 都长(例如 a = 1, b = 1, c = 8),也可能比它们都短(例如 a = 8, b = 8, c = 1),所以扫描的位范围必须覆盖三个数中最长的那个,任何单独一个数提前变成 0 都不代表可以收工。其三,c 某一位是 1 而 ab 该位都是 0 时,只需要翻一个就够了,不是两个。

解法:逐位独立计数

核心思路

既然各位独立,就把注意力收缩到单独一个二进制位上,把这一位的所有可能情形穷尽列出来。设该位上 a 的取值是 xb 的取值是 yc 的取值是 z,三者都只能取 0 或 1,那么按 z 分成两大类、一共四种情形。

情形一,z = 1xy至少有一个 1。此时 x | y 已经是 1,正好等于 z,代价为 0。它显然是最小的,因为代价不可能为负。特别注意这里不需要把多出来的那个 1 翻掉 —— 或运算只要求「至少一个 1」,xy 同时为 1 时 x | y 仍然是 1。

情形二,z = 1xy 都是 0。此时 x | y 是 0,不等于 1,必须动手。把 xy任意一个翻成 1,或值立刻变成 1,代价为 1。它最小的理由是:一次都不翻时或值是 0、不满足要求,所以代价至少是 1,而 1 次已经足以达成,下界被取到。

情形三,z = 0xy 中恰好有一个 1。要让 x | y 等于 0,就必须让 xy 同时为 0(这是或值为 0 的唯一可能),所以那个 1 必须被翻成 0,代价为 1

情形四,z = 0xy 都是 1。理由同上,两个 1 都必须被翻成 0,少翻一个都不行,代价为 2

把情形三和情形四合起来看会更整齐:z = 0 时代价恰好等于 x + y,也就是 ab 在该位上 1 的个数。原因是「或值为 0」把 xy 的终态唯一地钉死成 (0, 0),没有任何选择空间,代价就是从当前状态变到这个唯一终态需要改的位数 —— 终态只有一个、别无他选,所以这个代价必然也是最小的。

反过来,z = 1 时合法终态有三种:(0, 1)(1, 0)(1, 1)。我们手上有选择权,于是挑离当前状态最近的那个:只要当前已经有 1,就选包含这个 1 的终态(代价 0);否则任选一位补上 1(代价 1)。永远不会需要翻两个位,因为翻一个就够了。

正确性到这里就完整了:每一位的代价都取到了该位的下界,而各位之间互不干扰、可以同时各自取到下界,所以这些下界之和就是全局最小值,不存在「牺牲某一位换取另一位」的余地。

解题步骤

  • 初始化答案 ans = 0。它将累加每一位的代价,因为各位独立、总代价就是逐位代价之和。
  • 从第 0 位到第 31 位遍历下标 i。之所以固定跑满 32 位、而不是「某个数右移成 0 就停」,是因为三个数的有效位数各不相同,c 的高位可能远超 ab,定长循环能一劳永逸地避免漏位。
  • 每一轮用 (a >> i) & 1(b >> i) & 1(c >> i) & 1 取出三个数在第 i 位上的值。右移 i 位把目标位挪到最低位,再与 1 做与运算把其余位清零,这是取单个位的标准写法。
  • c 的该位是 1:只有当 ab 该位都是 0 时才把 ans 加 1,否则不加。因为或运算只要求至少有一个 1,已经有 1 就等于白拿。
  • c 的该位是 0:把 ab 该位的取值直接加到 ans 上。因为终态被唯一钉死成两个 0,有几个 1 就得翻几次,绝不能只记一次。
  • 循环结束后返回 ans,它就是全局最少翻转次数。

a = 2, b = 6, c = 5 走一遍。三个数的二进制是 a = 010b = 110c = 101,第 3 位及更高位上三个数全是 0,属于「要 0 且本来没有 1」,代价都是 0,所以只看低三位。第 0 位上 a 是 0、b 是 0、c 是 1,属于「要 1 却一个都没有」,翻任意一个即可,代价 1。第 1 位上 a 是 1、b 是 1、c 是 0,属于「要 0 却有两个 1」,两个都得翻,代价 2。第 2 位上 a 是 0、b 是 1、c 是 1,属于「要 1 且已经有 1」,代价 0。三位相加得 1 加 2 加 0 等于 3,与官方答案一致。按这个方案翻完之后 a 变成 001b 变成 100a | b 正好是 101,也就是 5。

代码实现

class Solution {
    public int minFlips(int a, int b, int c) {
        int ans = 0;
        for (int i = 0; i < 32; i++) {          // 定长遍历 32 位,避免任一数提前归零导致漏位
            int bitA = (a >> i) & 1;            // 取出 a 的第 i 位
            int bitB = (b >> i) & 1;            // 取出 b 的第 i 位
            int bitC = (c >> i) & 1;            // 取出 c 的第 i 位
            if (bitC == 1) {
                if (bitA == 0 && bitB == 0) {   // 要 1 却都是 0:翻任意一个即可,代价 1
                    ans++;
                }
                // 已经至少有一个 1,代价 0,多余的 1 不必翻掉
            } else {
                ans += bitA + bitB;             // 要 0:a、b 该位上的每个 1 都必须翻掉
            }
        }
        return ans;
    }
}
func minFlips(a int, b int, c int) int {
    ans := 0
    for i := 0; i < 32; i++ { // 定长遍历 32 位,避免任一数提前归零导致漏位
        bitA := (a >> i) & 1  // 取出 a 的第 i 位
        bitB := (b >> i) & 1  // 取出 b 的第 i 位
        bitC := (c >> i) & 1  // 取出 c 的第 i 位
        if bitC == 1 {
            if bitA == 0 && bitB == 0 { // 要 1 却都是 0:翻任意一个即可,代价 1
                ans++
            }
            // 已经至少有一个 1,代价 0,多余的 1 不必翻掉
        } else {
            ans += bitA + bitB // 要 0:a、b 该位上的每个 1 都必须翻掉
        }
    }
    return ans
}

复杂度分析

  • 时间复杂度:$O(1)$。循环固定执行 32 次,每次只做常数次移位、与运算和加法,与输入的具体数值无关。若按有效位数来记,则是 $O(\log \max(a,b,c))$,因为真正需要检查的位数就是三个数中最大者的二进制长度。
  • 空间复杂度:$O(1)$。只用了一个答案累加器和三个存放当前位取值的临时变量,没有随输入规模增长的额外空间。

关键点总结

  • 若一个整体最优问题在各个维度上互不影响(改动一个维度不会波及其它维度,各维度的目标也不互相冲突),就可以把整体最优拆成逐维度最优再求和。这条原则能把搜索问题直接降级为统计问题。
  • 判断「能否逐位独立处理」的判据是有没有跨位耦合。按位或、按位与、按位异或都不产生进位,天然逐位独立;加法、减法、乘法带进位或借位,绝不能这样拆。
  • 面对布尔取值的组合,穷尽枚举真值表比凭直觉推导可靠得多。本题只有三个布尔量、八种组合,归成四类后每一类的最小代价都能一眼定死,不给「想当然」留缝隙。
  • 求最小代价时,先看终态是否唯一。终态唯一时没有选择空间,代价就等于当前状态与终态的差异量;终态有多个时才需要在候选里挑离当前状态最近的那个。
  • 定长循环(如 32 位)遍历二进制位,而不是「某个数右移到 0 就停」,可以一次性消除多个数有效位数不相等所带来的漏位风险。
  • 取单个二进制位的惯用写法是 (x >> i) & 1;只需判断某位是否为 1 时也可以写成 (x & (1 << i)) != 0,省掉一次移位取值。

易错点总结

  • c 该位为 0 时只算一次翻转,没有把 ab 的两个 1 都算上(写成 if (bitA == 1 || bitB == 1) ans++):用例 a = 2, b = 6, c = 5 会输出 2,正确答案是 3。第 1 位上 ab 都是 1 而 c 是 0,两个 1 都必须翻成 0、代价是 2,只记一次就少算了一次翻转。
  • c 该位为 1 且 ab 都为 0 时算成两次翻转(写成 ans += 2):用例 a = 2, b = 6, c = 5 会输出 4、正确答案是 3;用例 a = 1, b = 1, c = 3 会输出 2、正确答案是 1。或运算只要有一个 1 就成立,翻第二个是纯粹的浪费。
  • c 该位为 1 时要求 ab 恰好有一个 1(写成 if (bitA + bitB != 1) ans++,把或误当成异或的成立条件):用例 a = 3, b = 3, c = 3 会输出 2,正确答案是 0。1 | 1 就是 1,多出来的那个 1 根本不需要翻掉。
  • 把目标条件整体当成 a XOR b == c(写成 if ((bitA ^ bitB) != bitC) ans++):用例 a = 2, b = 6, c = 5 会输出 1,正确答案是 3。题目要求的是或而不是异或,而且异或版本的逐位代价规则与或完全不同。
  • 直接数 (a | b) ^ c 里 1 的个数当答案(等价于误以为每个不匹配的位都只要翻一次就能修好):用例 a = 2, b = 6, c = 5 会输出 2,正确答案是 3。不匹配的位确实只有两个,但其中一位需要两次翻转 —— 不匹配位数和翻转次数不是一回事。
  • while (a != 0 && b != 0 && c != 0) 做循环终止条件:用例 a = 1, b = 1, c = 2 会输出 2,正确答案是 3。处理完第 0 位后 ab 都右移成 0,循环提前退出,c 第 1 位上那个「要 1 却都没有」的代价 1 被整整漏掉。
  • 循环终止条件漏掉 c,写成 while (a != 0 || b != 0):用例 a = 1, b = 1, c = 2 会输出 2,正确答案是 3。ab 归零后 c 剩下的高位里还有 1 没处理,同样少算。这类写法在 a = 2, b = 6, c = 5 上恰好能得到 3,光靠官方样例根本发现不了。
  • 循环终止条件漏掉 ab,写成 while (c != 0):用例 a = 1, b = 2, c = 1 会输出 0,正确答案是 1。c 归零后 ab 高位残留的 1 本来每一个都必须翻成 0,却一个也没被统计到。

相似题目

题目 难度 考察点
461. 汉明距离 简单 同样逐位比对两个数,但只数「不同的位数」,不存在本题那种「要 0 时一位可能要翻两次」的非对称代价
191. 位1的个数 简单 只统计单个数里 1 的个数、没有目标值约束,相当于本题「c 位为 0 时数 1 的个数」这一分支的最简形态
剑指 Offer 15. 二进制中1的个数 简单 重点在 n & (n - 1) 消除最低位 1 的技巧,用「跳过 0 位」替代本题的定长 32 位扫描
136. 只出现一次的数字 简单 靠异或的自反律整体消除成对元素,用的是运算律而非逐位统计,不需要拆位就能出答案
137. 只出现一次的数字 II 中等 也走「按位独立处理」的路子,但每一位要对出现次数取模 3,是本题逐位思想在计数场景下的推广
260. 只出现一次的数字 III 中等 用某一位是否为 1 把元素分成两组,把逐位信息当作分治的依据而不是代价的来源
371. 两整数之和 中等 反例式对照:加法的位之间存在进位耦合,正因为不能逐位独立处理,才必须反复迭代到进位为空