例题

1. 快速幂Pow(x, n)

描述
image.png
思路
本题的方法被称为「快速幂算法」,有递归和迭代两个版本。
当指数为负数的时候,可以转化为底数取倒数,指数取相反数的情况,这一点并不难理解。

为了避免一次又一次将底数相乘,我们采用这样「偷懒」的策略,比如要计算 位运算 - 图2,其实我们只要算出 位运算 - 图3,然后再自己乘自己就好了,这样就可以避免做 位运算 - 图4位运算 - 图5 的运算。(这种思想有点像「记忆化递归」。)

那么有一种机制就能帮助我们找到一个整数的合适的「分解」,那么就是将一个整数看成它的二进制形式。就那上面的例子来说,位运算 - 图6 的二进制表示为 位运算 - 图7_2#card=math&code=%2810010%29_2&height=20&width=62),即:

位运算 - 图8

那么:

位运算 - 图9

于是,我们可以把指数 位运算 - 图10 做「二进制分解」,在底数不断自身乘以自身的过程中,将最终结果需要的部分保存下来
位运算 - 图11
代码
Python递归版本:

  1. class Solution:
  2. def myPow(self, x: float, n: int) -> float:
  3. def mul(x, n):
  4. if n == 0:
  5. return 1.0
  6. half = mul(x, n // 2)
  7. return half * half if n % 2 == 0 else half * half * x
  8. if n >= 0:
  9. return mul(x, n)
  10. if n < 0:
  11. return mul(1/x, -n)

Java递归版本:

  1. public double quickMul(double x, long N) {
  2. if (N == 0) {
  3. return 1.0;
  4. }
  5. double y = quickMul(x, N / 2);
  6. return N % 2 == 0 ? y * y : y * y * x;
  7. }
  8. public double myPow(double x, int n) {
  9. long N = n;
  10. return N >= 0 ? quickMul(x, N) : 1.0 / quickMul(x, -N);
  11. }
  12. }

Python迭代版本:

  1. class Solution:
  2. def myPow(self, x: float, n: int) -> float:
  3. if n < 0:
  4. x = 1 / x
  5. n = - n
  6. res = 1
  7. while n:
  8. if n % 2 == 1:
  9. res *= x
  10. x *= x
  11. n = n >> 1
  12. return res

Java迭代版:

  1. class Solution {
  2. public double myPow(double x, int n) {
  3. long b = n;
  4. if (n < 0) {
  5. x = 1 / x;
  6. b = -b;
  7. }
  8. double ans = 1.0;
  9. while (b > 0) {
  10. if (b % 2 == 1) {
  11. ans *= x;
  12. }
  13. x *= x;
  14. b >>= 1;
  15. }
  16. return ans;
  17. }
  18. }

2. 64位整数乘法

求 a 乘 b 对 p 取模的值。
输入格式
第一行输入整数a,第二行输入整数b,第三行输入整数p。
输出格式
输出一个整数,表示a*b mod p的值。
数据范围
1≤a,b,p≤10
输入样例:

  1. 3
  2. 4
  3. 5

输出样例:

2

思路
(二进制思想) O(logn)
如果直接计算a乘b这会超过 long long 的最大范围,所以采用类似于快速幂的思想
把 b写成二进制形式,然后如果某位上为1就加上它a*(2^n)次方(n与这位的位置有关)
并且每次计算后取模就可以了

例:计算 3*7

7的二进制 111

3*(2^0)=3
3*(2^1)=6
3*(2^2)=12

观察可发现每次的可由上一次的结果2推出(记得取模)
*代码

#include <iostream>
#include <cstdio>
#define ll long long
using namespace std;
int main()
{
    ll a,b,p,res;
    cin>>a>>b>>p;
    res=0;
    while(b)
    {
        if(b&1)
            res=(res+a)%p;
        b>>=1;
        a=2*a%p;
    }
    cout<<res<<endl;
    return 0;
}
def mul(a, b, p):
    res = 0
    while b:
        if b & 1:
            res = (res + a) % p

        b >>= 1
        a = 2 * a % p
    return res

if __name__ == '__main__':

    a = int(input())
    b = int(input())
    p = int(input())
    print(mul(a, b, p))

3. 压缩状态DP

描述
image.png
思路
记忆化递归 + 状态压缩
题解链接
f(A)=max(2+f(A_1′),1+_f(A_2′),1+_f(_A_3′))

如何高效地表示 f 的参数 X?题目中使用字符串表示座位情况,但字符串处理起来不太方便,代码冗长且效率低下。
观察问题的性质可以发现,对于一个位置,座位情况与可能的学生安排都只有两种:座位情况可能是“可坐”或者“不可坐”,学生安排只可能是“有学生”或者“无学生”。因此,我们可以分别用一个二进制串来表示一排的座位情况,1 表示“可坐”,0 表示“不可坐”,以及这排的可能的学生安排,1 表示“有学生”,0 表示“无学生”。二进制串可以直接用一个整数来存储。这样,就将一排座位的状态由一个字符串压缩成了一个整数。

那么,对于座位情况的判断和操作如何完成呢?记座位情况的数字为 seats,学生安排的数字为 scheme,我们使用位运算解决:

首先是判断学生安排与本排的座位情况是否冲突,即“不可坐”的座位不可以安排为“有学生”。抽象为二进制运算,即对任意一位, seats=0,scheme=1 的情况不合法。因此可以得出结论,scheme&~seats!=0 时,座位安排不合法。
其次是判断学生是否有相邻的情况。即对于 scheme 的二进制表示,不可以出现相邻的 1。对于这种情况,可以计算 (scheme<<1)&scheme,不为 0 时说明 scheme 中存在相邻的 1.
最后是要根据本排的学生安排来决定下一排的座位情况。即如果 scheme 中某一位为 1,那么下一排的 seats 中,相邻的位需要置为 0.这一目的可以使用如下操作达成:

seats &= ~(scheme << 1)
seats &= ~(scheme >> 1)

代码
Python代码:

from functools import reduce
class Solution:
    def maxStudents(self, seats: List[List[str]]) -> int:
        m, n = len(seats), len(seats[0]),
        dp = [[0]*(1 << n) for _ in range(m+1)]  # 状态数组 dp
        a = [reduce(lambda x,y:x|1<<y,[0]+[j for j in range(n) if seats[i][j]=='#'])  for i in range(m)] # 将 # 设为 1,当遇到 . 时与运算结果为 0,表示可以坐人
        # print(a)
        for row in range(m)[::-1]: # 倒着遍历
            for j in range(1 << n):
                if not j & j<<1 and not j&j>>1 and not j & a[row]: # j & a[row]代表该位置可以坐人,j & j<<1 and not j&j>>1 表示该位置左右没人可以坐的
                    for k in range(1 << n):
                        if not j&k<<1 and not j&k>>1: # j状态的左上和右上没有人
                            dp[row][j] = max(dp[row][j], dp[row+1][k] + bin(j).count('1'))
        # print(dp)
        return max(dp[0])

4. 比特位计数

描述
image.png
思路:

  • 暴力法,使用库函数 Java Integer.bitCount() ,python bit().count("1")
  • 位运算 n & n - 1
  • 动态规划

代码
Java代码-位运算:

public int[] countBits(int num) {
        int[] ans = new int[num+1];
        for (int i = 0; i <= num; i++) {
            ans[i] = popCount(i);
        }
        return ans;
    }
    public static int popCount(int num) {
        int count = 0;
        while (num != 0) {
            num &= num - 1;
            count++;
        }
        return count;
    }
}

Java暴力法:

class Solution {
    public int[] countBits(int num) {
        int[] res = new int[num + 1];
        for (int i = 0; i <= num; i++) {
            res[i] = Integer.bitCount(i);
        }
        return res;
    }
}

Python代码-暴力法:

class Solution:
    def countBits(self, num: int) -> List[int]:
        if num == 0:
            return [0]
        res = []
        for i in range(num+1):
            res.append(bin(i).count("1"))
        return res

Python位运算:

class Solution:
    def countBits(self, num: int) -> List[int]:
        def pop_count(num):
            count = 0
            while num:
                num &= num - 1
                count += 1
            return count

        res = []
        for n in range(num+1):
            res.append(pop_count(n))
        return res

Java动态规划:

class Solution {
    public int[] countBits(int num) {
        int[] dp = new int[num+1];
        dp[0] = 0;
        for (int i = 1; i < num; i++) {
            // 特判,很重要,只有 1 个的话,就直接是 1
            if ((i & (i - 1)) == 0) {
                dp[i] = 1;
                continue;
            }
            dp[i] = dp[i-1] + 1;
        }
        return dp;
    }
}
0000
0001
0010
0011
0100
0101
0110
0111
1000
1001
1010
1011
1100
1101
1110
1111

5. 只出现一次的元素

描述
思路
两个相同元素异或等于0,任意一个元素和0异或等于元素本身。
Java 代码:

public class Solution {

    public int singleNumber(int[] nums) {
        int res = 0;
        for (int num : nums) {
            res ^= num;
        }
        return res;
    }
}

Python代码:

class Solution:
    def singleNumber(self, nums: List[int]) -> int:
        res = 0
        for num in nums:
            res ^= num
        return res

6. 形成两个异或相等数组的三元组数目

描述
image.png
思路

  • 异或和的前缀异或和性质

这里 preSum[i] ^ preSum[j] 就相当于「正常和」的前缀和相减得到区间和。
代码
Java代码:

public class Solution {

    public int countTriplets(int[] arr) {
        int len = arr.length;
        if (len == 0) {
            return 0;
        }

        int[] preSum = new int[len + 1];
        int count = 0;
        for (int i = 0; i < len; i++) {
            preSum[i + 1] = arr[i] ^ preSum[i];
        }

        // 调整好边界
        for (int i = 0; i < len - 1; i++) {
            for (int j = i + 1; j < len; j++) {
                for (int k = j + 1; k <= len ; k++) {
                    if ((preSum[i] ^ preSum[j]) == (preSum[j] ^ preSum[k])){
                        count++;
                    }
                }
            }
        }
        return count;
    }
}

Python代码:

class Solution:
    def countTriplets(self, arr: List[int]) -> int:
        pre_sum = [0]
        for num in arr:
            pre_sum.append(pre_sum[-1] ^ num)
        m = len(arr)
        res = 0
        for i in range(m):
            for j in range(i+1, m):
                for k in range(j+1, m+1):
                    if pre_sum[j] ^ pre_sum[i] == pre_sum[k] ^ pre_sum[j]:
                        res += 1

        return res

7. 只出现一次的元素II

描述
image.png
思路

  • 将输入数组存储到 HashSet,然后使用 HashSet 中数字和的三倍与数组之和比较。
  • 遍历输入数组,统计每个数字出现的次数,最后返回出现次数为 1 的数字。
  • 位运算符:NOT,AND 和 XOR

使用位运算符可以实现 O(1) 的空间复杂度。
image.png
XOR
该运算符用于检测出现奇数次的位:1、3、5 等。
0 与任何数 XOR 结果为该数。
两个相同的数 XOR 结果为 0。
以此类推,只有某个位置的数字出现奇数次时,该位的掩码才不为 0。

位运算 - 图17
AND 和 NOT
为了区分出现一次的数字和出现三次的数字,使用两个位掩码:seen_onceseen_twice
思路是:

  • 仅当 seen_twice 未变时,改变 seen_once
  • 仅当 seen_once 未变时,改变seen_twice

位运算 - 图18
位掩码 seen_once 仅保留出现一次的数字,不保留出现三次的数字。
代码
Python 代码:

class Solution:
    def singleNumber(self, nums: List[int]) -> int:
        seen_once = seen_twice = 0

        for num in nums:
            # first appearance: 
            # add num to seen_once 
            # don't add to seen_twice because of presence in seen_once

            # second appearance: 
            # remove num from seen_once 
            # add num to seen_twice

            # third appearance: 
            # don't add to seen_once because of presence in seen_twice
            # remove num from seen_twice
            seen_once = ~seen_twice & (seen_once ^ num)
            seen_twice = ~seen_once & (seen_twice ^ num)

        return seen_once

8. 剑指 Offer 56 - I. 数组中数字出现的次数

image.png
思路

  • 分组异或
  • 哈希表

如果除了一个数字以外,其他数字都出现了两次,那么如何找到出现一次的数字?

答案很简单:全员进行异或操作即可。考虑异或操作的性质:对于两个操作数的每一位,相同结果为 00,不同结果为 11。那么在计算过程中,成对出现的数字的所有位会两两抵消为 00,最终得到的结果就是那个出现了一次的数字。

如果我们可以把所有数字分成两组,使得:

  • 两个只出现一次的数字在不同的组中;
  • 相同的数字会被分到相同的组中。

那么对两个组分别进行异或操作,即可得到答案的两个数字。这是解决这个问题的关键。
两个相同的数字的对应位都是相同的,所以一个被分到了某一组,另一个必然被分到这一组。
代码
Python代码:

class Solution:
    def singleNumbers(self, nums: List[int]) -> List[int]:
        a = 0
        for num in nums:
            a = a ^ num
        b = a & (-a)
        x, y = 0, 0
        for num in nums:
            if b & num:
                x ^= num
            else:
                y ^= num
        return [x, y]

Java代码:

class Solution {
    public int[] singleNumbers(int[] nums) {
        int a = 0;
        for (int num : nums) {
            a ^= num;
        }
        a = a & (-a);
        int x = 0, y = 0;
        for (int num : nums) {
            if ((a & num) == 0) {
                x ^= num;
            } else {
                y ^= num;
            }
        }
        return new int[]{x, y};
    }
}

9.数字范围按位与

image.png
思路

  • Brian Kernighan 算法

位运算 - 图21

  • 位移找公共前缀

位运算 - 图22
代码
Brian Kernighan 算法

class Solution {
    public int rangeBitwiseAnd(int m, int n) {
        while (m < n) {
            n = n & (n - 1);
        }
        return n;
    }
}

位移算法:

class Solution {
    public int rangeBitwiseAnd(int m, int n) {
        int shift = 0;
        while (m < n) {
            m = m >> 1;
            n = n >> 1;
            shift++;
        }
        return m << shift;
    }
}