zoukankan      html  css  js  c++  java
  • 位运算的奇技淫巧(二)

    位运算就是基于整数的二进制表示进行的运算。由于计算机内部就是以二进制来存储数据,位运算是相当快的。

    之前有总结过位运算的技巧,但稍微对以前写的文章不太满意,所以重新总结一下

    常用的运算符共 6 种,分别为与( & )、或( | )、异或( ^ )、取反( ~ )、左移( << )和右移( >> )。

    与、或、异或

    与( & )或( | )和异或( ^ )这三者都是两数间的运算,因此在这里一起讲解。

    它们都是将两个整数作为二进制数,对二进制表示中的每一位逐一运算。

    运算符 解释
    & 只有两个对应位都为 1 时才为 1
    | 只要两个对应位中有一个 1 时就为 1
    ^ 只有两个对应位不同时才为 1

    异或运算的逆运算是它本身,也就是说两次异或同一个数最后结果不变,即 (a ext{^} b ext{^} b = a)

    举例:

    [egin{aligned} 5 &=(101)_2\ 6 &=(110)_2\ 5&6 &=(100)_2 = 4\ 5|6 &=(111)_2 = 7\ 5 ext{^}6 &=(011)_2 = 3\ end{aligned} ]

    取反

    取反是对一个数 (num) 进行的计算,即单目运算。

    ~(num) 的补码中的 0 和 1 全部取反(0 变为 1,1 变为 0)。有符号整数的符号位在 ~ 运算中同样会取反。

    补码:在二进制表示下,正数和 0 的补码为其本身,负数的补码是将其对应正数按位取反后加一。

    举例(有符号整数):

    [egin{aligned} 5&=(00000101)_2\ ext{~}5&=(11111010)_2=-6\ -5 ext{ 的补码}&=(11111011)_2\ ext{~}(-5)&=(00000100)_2=4 end{aligned} ]

    左移和右移

    num << i 表示将 (num) 的二进制表示向左移动 (i) 位所得的值。

    num >> i 表示将 (num) 的二进制表示向右移动 (i) 位所得的值。

    举例:

    [egin{aligned} 11&=(00001011)_2\ 11<<3&=(01011000)_2=88\ 11>>2&=(00000010)_2=2 end{aligned} ]

    在 C++ 中,右移操作中右侧多余的位将会被舍弃,而左侧较为复杂:对于无符号数,会在左侧补 0;而对于有符号数,则会用最高位的数(其实就是符号位,非负数为 0,负数为 1)补齐。左移操作总是在右侧补 0。

    复合赋值位运算符

    += , -= 等运算符类似,位运算也有复合赋值运算符: &= , |= , ^= , <<= , >>= 。(取反是单目运算,所以没有。)

    关于优先级

    位运算的优先级低于算术运算符(除了取反),而按位与、按位或及异或低于比较运算符,所以使用时需多加注意,在必要时添加括号。

    位运算的应用

    位运算一般有三种作用:

    1. 高效地进行某些运算,代替其它低效的方式。

    2. 表示集合。(常用于 状压 DP 。)

    3. 题目本来就要求进行位运算。

    需要注意的是,用位运算代替其它运算方式(即第一种应用)在很多时候并不能带来太大的优化,反而会使代码变得复杂,使用时需要斟酌。(但像“乘 2 的非负整数次幂”和“除以 2 的非负整数次幂”就最好使用位运算,因为此时使用位运算可以优化复杂度。)

    乘 2 的非负整数次幂

    int mulPowerOfTwo(int n, int m) {  // 计算 n*(2^m)
      return n << m;
    }
    

    除以 2 的非负整数次幂

    int divPowerOfTwo(int n, int m) {  // 计算 n/(2^m)
      return n >> m;
    }
    

    !!! warning
    我们平常写的除法是向 0 取整,而这里的右移是向下取整(注意这里的区别),即当数大于等于 0 时两种方法等价,当数小于 0 时会有区别,如: -1 / 2 的值为 (0) ,而 -1 >> 1 的值为 (-1)

    判断一个数是不是 2 的正整数次幂

    bool isPowerOfTwo(int n) { return n > 0 && (n & (n - 1)) == 0; }
    

    对 2 的非负整数次幂取模

    int modPowerOfTwo(int x, int mod) { return x & (mod - 1); }
    

    取绝对值

    在某些机器上,效率比 n > 0 ? n : -n 高。

    int Abs(int n) {
      return (n ^ (n >> 31)) - (n >> 31);
      /* n>>31 取得 n 的符号,若 n 为正数,n>>31 等于 0,若 n 为负数,n>>31 等于 -1
         若 n 为正数 n^0=n, 数不变,若 n 为负数有 n^(-1)
         需要计算 n 和 -1 的补码,然后进行异或运算,
         结果 n 变号并且为 n 的绝对值减 1,再减去 -1 就是绝对值 */
    }
    

    取两个数的最大/最小值

    在某些机器上,效率比 a > b ? a : b 高。

    // 如果 a>=b,(a-b)>>31 为 0,否则为 -1
    int max(int a, int b) { return b & ((a - b) >> 31) | a & (~(a - b) >> 31); }
    int min(int a, int b) { return a & ((a - b) >> 31) | b & (~(a - b) >> 31); }
    

    判断符号是否相同

    bool isSameSign(int x, int y) {  // 有 0 的情况例外
      return (x ^ y) >= 0;
    }
    

    交换两个数

    void swap(int &a, int &b) { a ^= b ^= a ^= b; }
    

    获取一个数二进制的某一位

    // 获取 a 的第 b 位,最低位编号为 0
    int getBit(int a, int b) { return (a >> b) & 1; }
    

    表示集合

    一个数的二进制表示可以看作是一个集合(0 表示不在集合中,1 表示在集合中)。比如集合 {1, 3, 4, 8} ,可以表示成 ((100011010)_2) 。而对应的位运算也就可以看作是对集合进行的操作。

    操作 集合表示 位运算语句
    交集 (a cap b) a & b
    并集 (a cup b) a|b
    补集 (ar{a}) ~a (全集为二进制都是 1)
    差集 (a setminus b) a & (~b)
    对称差 (a riangle b) a ^ b

    二进制的状态压缩

    二进制状态压缩,是指将一个长度为 (m)(bool) 数组用一个 (m) 位的二进制整数表示并存储的方法。利用下列位运算操作可以实现原 (bool) 数组中对应下标元素的存取。(xor 等价于 ^)

    操作 运算
    取出整数 n 在二进制表示下的第 k 位 (n >> k) & 1
    取出整数n 在二进制表示下的第 0 ~ k - 1 位 (后 k 位) n & ((1 << k) - 1)
    对整数 n 在二进制表示下的第 k 位取反 n xor (1 << k)
    对整数 n 在二进制表示下的第 k 位赋值 1 n | (1 << k)
    对整数 n 在二进制表示下的第 k 位赋值 0 n & (~(1 << k))

    这种方法运算简便,并且节省了程序运行的时间和空间。当m不太大时,可以直接使用一个整数类型存储。当m较大时,可以使用若干个整数类型(int数组),也可以直接利用 (C++STL) 为我们提供的 (bitset) 实现

    遍历某个集合的子集

    // 遍历 u 的非空子集
    for (int s = u; s; s = (s - 1) & u) {
      // s 是 u 的一个非空子集
    }
    

    用这种方法可以在 (O(2^{popcount(u)}))(popcount(u)) 表示 (u) 二进制中 1 的个数)的时间复杂度内遍历 (u) 的子集,进而可以在 (O(3^n)) 的时间复杂度内遍历大小为 (n) 的集合的每个子集的子集。(复杂度为 (O(3^n)) 是因为每个元素都有 不在大子集中/只在大子集中/同时在大小子集中 三种状态。)

    内建函数

    GCC 中还有一些用于位运算的内建函数:详细文章介绍

    1. int __builtin_ffs(int x) :返回 (x) 的二进制末尾最后一个 (1) 的位置,位置的编号从 (1) 开始(最低位编号为 (1) )。当 (x)(0) 时返回 (0)

    2. int __builtin_clz(unsigned int x) :返回 (x) 的二进制的前导 (0) 的个数。当 (x)(0) 时,结果未定义。

    3. int __builtin_ctz(unsigned int x) :返回 (x) 的二进制末尾连续 (0) 的个数。当 (x)(0) 时,结果未定义。

    4. int __builtin_clrsb(int x) :当 (x) 的符号位为 (0) 时返回 (x) 的二进制的前导 (0) 的个数减一,否则返回 (x) 的二进制的前导 (1) 的个数减一。

    5. int __builtin_popcount(unsigned int x) :返回 (x) 的二进制中 (1) 的个数。

    6. int __builtin_parity(unsigned int x) :判断 (x) 的二进制中 (1) 的个数的奇偶性。

    这些函数都可以在函数名末尾添加 lll (如 __builtin_popcountll )来使参数类型变为 ( unsigned ) long 或 ( unsigned ) long long (返回值仍然是 int 类型)。
    例如,我们有时候希望求出一个数以二为底的对数,如果不考虑 0 的特殊情况,就相当于这个数二进制的位数 -1 ,而一个数 n 的二进制表示的位数可以使用 32-__builtin_clz(n) 表示,因此 31-__builtin_clz(n) 就可以求出 n 以二为底的对数。

    由于这些函数是内建函数,经过了编译器的高度优化,运行速度十分快(有些甚至只需要一条指令)。

    更多位数

    如果需要操作的集合非常大,可以使用 bitset容器

    题目推荐

    Luogu P1225 黑白棋游戏

    参考

    位运算技巧: https://graphics.stanford.edu/~seander/bithacks.html

    Other Builtins of GCC: https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html

    英文文档参考:https://www.jjj.de/fxt/fxtbook.pdf

    The desire of his soul is the prophecy of his fate
    你灵魂的欲望,是你命运的先知。

  • 相关阅读:
    又是七月的尾巴
    在Macbook Air上安装Archlinux
    又是一年毕业季
    orgpage a static site generator for emacs and org mode
    看完了《三体》三部曲
    暴强的命令行git提交历史记录查询
    xaml的window的AllowsTransparency属性在winxp下好像有bug,不知还有谁遇到过
    Swift 计算字符串展示的区域
    SwiftUI Stack中的View被压缩的效果
    如果git pull拉取分支出错,如何返回
  • 原文地址:https://www.cnblogs.com/RioTian/p/13598747.html
Copyright © 2011-2022 走看看