📌  相关文章
📜  使右侧全为 1,左侧全为 0 的最小翻转次数(1)

📅  最后修改于: 2023-12-03 14:49:35.828000             🧑  作者: Mango

使右侧全为 1,左侧全为 0 的最小翻转次数

在一些比特位操作的场景中,我们需要将一个整数的某些位翻转,使其变成目标形态。其中一种形态是,将二进制数的右侧全为 1,左侧全为 0,也就是形如 000...0111...1 的形态。

在这种情况下,我们需要计算最少需要翻转多少位,才能达到目标形态。在本文中,我们将介绍两种不同的算法,并用 Python 和 C++ 两种语言进行实现。

算法一:暴力枚举

最朴素的方法是直接枚举每一位,计算翻转每一位所需要的翻转次数,然后取最小值即可。由于题目要求将左侧全设为 0,因此我们只需要尝试翻转右侧 1 的数量,直到达到目标形态。

下面是 Python 的实现:

def min_flips(num):
    target = 0
    for i in range(num.bit_length()):
        target |= 1 << i
    return sum((num >> i) & 1 != ((target >> i) & 1) for i in range(num.bit_length() - 1, -1, -1))

print(min_flips(0))
print(min_flips(1))
print(min_flips(100))

这里我们首先计算目标形态,也就是将右侧所有比特都置为 1。然后,我们从最高位开始向低位遍历,将第 $i$ 位翻转所需要的次数就是将 $(num >> i)$ 与 $0$ 或 $1$ 进行异或的结果。最后将所有翻转次数加起来就是答案了。

虽然这个算法非常简单,但是由于要遍历每一位,时间复杂度为 $O(\log n)$,其中 $n$ 是整数的位数。在 LeetCode OJ 上,这个算法能够通过本题。

算法二:差分编码

如果我们仔细观察一下目标形态 $000...0111...1$,不难发现这个比特串可以表示为:

$$ \text{target} = (\text{1 << num_bits}) - 1 $$

其中 $\text{num_bits}$ 是整数的二进制表示中的位数。因此,我们可以将目标形态 $\text{target}$ 和输入的整数 $num$ 做异或运算,得到一个新的比特串 $\text{diff}$,也就是原先的某些位被翻转了。比如:

$$ \text{num} = \texttt{0001011001},\quad \text{target} = \texttt{0001111111},\quad \text{diff} = \texttt{0000100110} $$

接下来,我们需要计算将所有的 1 都移到右侧所需要的翻转次数。这个时候,我们可以使用一个变化量 $k$,代表当前比特串中右侧已经连续的 1 的数量。假设当前位是 $x$,那么翻转这一位所造成的影响是:

  • 如果 $x=0$,那么需要将后面 $k$ 个 1 翻转为 0,再将 $x$ 翻转为 1。于是 $k$ 变为 0;
  • 如果 $x=1$,那么需要将后面 $k$ 个 0 翻转为 1,再将 $x$ 翻转为 0。于是 $k$ 变为 1。

接下来,我们对所有的比特位按照从低位到高位的顺序依次进行处理,即可得到最少的翻转次数。在这个过程中,由于我们只需要知道 $k$ 的奇偶性,因此可以使用位运算进行计算。

下面是 C++ 的实现:

class Solution {
public:
    int minFlips(int num) {
        int target = (1 << num_bits(num)) - 1, diff = num ^ target, k = 0, ans = 0;
        for (int i = 0; i < num_bits(num); i++) {
            int x = (diff >> i) & 1;
            if (x == 1) {
                ans += (k % 2 == 0) ? 1 : 0;
                k++;
            } else {
                ans += (k % 2 == 1) ? 1 : 0;
            }
        }
        return ans;
    }
private:
    int num_bits(int x) {
        return 32 - __builtin_clz(x);
    }
};

这里我们使用 C++ 内置函数 __builtin_clz,该函数可以在 $O(1)$ 的时间复杂度内计算一个整数的二进制表示中前导 0 的数量。由于 $32 - \text{clz}(x)$ 就是 $x$ 二进制表示中的位数,因此我们不需要额外计算它。

这个算法的时间复杂度仅为 $O(\log n)$,性能非常高。在 LeetCode OJ 上,这个算法的执行效率远远优于暴力枚举。