二进制与位运算

二进制

二进制(\(Binary System\))是一种以 \(2\) 为基数的数制,只使用两个数字符号:\(0\) 和 \(1\), 每个数字称为一个比特(\(Bit\),\(Binary digit\)的缩写),是最基本的数位系统之一,其发现者是莱布尼茨

位(\(bit\)):二进制的最小单位,取值只有 \(0\) 或 \(1\)。


二进制与十进制的转换

十进制转二进制

公式化表述为对给定 \(x\ge 0\),构造比特 \(b_i\):

\(b_i=\left\lfloor \frac{x}{2^i} \right\rfloor \bmod 2,\quad x=\sum_{i\ge 0}b_i 2^i\)

实现思路

  1. 使用短除法,不断除以 \(2\),记录余数,倒序读出。
  2. 利用 \(STL\) 中的 \(bitset\)。

实现代码

// 方法1
string convert(int x)
{
    string ans;
    while (x)
    {
        ans.push_back('0' + (x & 1));
        x >>= 1;
    }

    reverse(ans.begin(), ans.end());
    return ans;
}

// 方法2
cout << bitset<32>(123456) << endl;

注:代码中使用的为 \(int\),可根据场景随时切换其他更大的变量或者无符号变量。


二进制转十进制

公式化表述为设基数 \(B = 2\),任意长度为 \(m\) 的 \(01\) 串,表示的值为:

\((b_{m-1}\dots b_0)_2 =\sum_{i=0}^{m-1} b_i \cdot 2^i,\quad b_i\in\{0,1\}\)

实现思路

  1. 遍历二进制 \(01\) 串,累加每一位的位权。
  2. 利用 \(STL\) 中的 \(bitset\)。

实现代码

// 方法1.1
ull convert1(string s)
{
    ull ans = 0;
    for (char c : s)
        ans = (ans << 1ull) + (c - '0');

    return ans;
}

// 方法1.2
ull convert2(string s)
{
    reverse(s.begin(), s.end());

    ull ans = 0;
    for (ull i = 0; i < s.size(); ++i)
        if (s[i] - '0')
            ans += 1ull << i;

    return ans;
}

// 方法2
bitset<64> bit(s);
ull x = bit.to_ullong();

注:代码中使用的为 \(usigned\) \(long\) \(long\) 类型,可根据场景随时切换其他整形变量;\(bitset\) 的类型转换方法只有无符号类型。


原码、反码、补码

为了便于运算,带符号的机器数可采用原码、反码、补码等不同的编码方法,机器上的这些编码方法称为码制。

原码:原码是最直观的表示方法,它直接用二进制数表示一个数,包括正负号。在原码中,最高位(最左边的位)是符号位,\(0\) 表示正数,\(1\) 表示负数。其余位表示数值本身。原码是人脑最容易理解和计算的表示方式。

反码:反码主要用于表示负数。对于正数,其反码与其原码相同。对于负数,其反码是将原码除符号位外的所有位取反(\(0\) 变 \(1\),\(1\) 变 \(0\))。

补码:补码是计算机中最常用的表示方法,用于进行二进制加法运算。对于正数,其补码与其原码相同。对于负数,其补码是其反码加 \(1\)。补码的一个重要特性是,任何数的补码加上该数本身,结果总是 \(0\)。补码的使用可以简化计算机中的算术运算,因为加法和减法可以统一为加法运算。当进行减法运算时,可以将减数的补码与被减数相加,从而得到结果。


位运算符与优先级

运算符含义优先级
~按位取反\(1\)
&按位与\(3\)
^按位异或\(4\)
|按位或\(5\)
<<左移\(2\)
>>右移\(2\)

上面的数字只是在这几个位运算符之间的相对顺序,仅用于这六个运算符之间的比较,与其他运算符的全局优先级无关。

按位取反

定义:对参与位运算的数值的二进制位进行”取反”运算。

运算规则:

以八位二进制举例。

~1 = 1111 1110
~0 = 1111 1111

总结:将 \(0\) 变 \(1\),\(1\) 变 \(0\)。


按位与

定义:对参与位运算的数值的二进制位进行”与”运算。

运算规则

0 & 0 = 0
0 & 1 = 0
1 & 0 = 0
1 & 1 = 1

总结:只有两位同时为 \(1\) 时,结果才为 \(1\) ,否则结果为 \(0\)。

注意:负数按补码形式参与按位与运算。


按位或

定义:对参与位运算的数值的二进制位进行”与”运算。

运算规则

0 | 0 = 0
0 | 1 = 1
1 | 0 = 1
1 | 1 = 1

总结:只要有一个为 \(1\),其值为 \(1\)。

注意:负数按补码形式参与按位或运算。


按位异或

定义:对参与位运算的数值的二进制位进行”异或”运算。

异或也叫半加运算,其运算法则相当于不带进位的二进制加法:这些法则与加法是相同的,只是不带进位,所以异或常被认作不进位加法。

运算规则

0 ^ 0 = 0
0 ^ 1 = 1
1 ^ 0 = 1
1 ^ 1 = 0

总结:相同为 \(0\),不同为 \(1\)。

性质

  1. 交换律:\(a \oplus b = b \oplus a\)
  2. 结合律: \((a \oplus b) \oplus c == a \oplus (b \oplus c)\)
  3. 对于任何数 \(x\),都有 \(x \oplus x = 0\),\(x \oplus 0 = x\)

左移

定义:将一个运算对象的各二进制位全部左移若干位,高位丢弃,低位补 \(0\)。


右移

定义:将一个数的各二进制位全部右移若干位,高位补 \(0\) 或补符号位,右边丢弃。

关于右移:

  • 无符号右移逻辑右移(高位补 \(0\))。
  • 有符号右移在 \(C++\) 中对负数实现相关(大多数编译器做算术右移,高位补符号位)。

建议:位运算尽量用无符号类型(\(unsigned\))。


位运算常用技巧

判断奇偶

在数值的二进制表述中,最低位(第 \(0\) 位,既 \(bit0\))位权为:\(2 ^ 0 = 1\),其余位权都 \(> 1\),由此可知 \(bit0\) 直接决定数值的奇偶性:当 \(bit0\) 为 \(0\) 时数值为偶数;为 \(1\) 时数值为奇数。

实现代码

if (x & 1u)
    cout << "odd" << endl;
else
    cout << "even" << endl;

将指定位变为 \(0\)

只需构造 “保留位 \(= 1\)、清零位 \(= 0\)” 的掩码 \(mask\),然后 \(x \mathrel{\&}= \mathrm{mask}\)。

实现代码

// 将第 k 位变为 0
x &= ~(1u << k);
// 将区间 [l, r] 变为 0
x &= ~(((1u << (r - l + 1u)) - 1u) << l);

将指定位变为 \(1\)

只需将需要变为 \(1\) 的目标位的掩码或进去即可。

实现代码

// 将第 k 位变为 1
x |= (1u << k);
// 将区间 [l, r] 变为 1
x |= ((1u << (r - l + 1u)) - 1u) << l;

翻转指定位

只需将需要翻转的目标位的掩码异或进去即可。

实现代码

x ^= (1u << k);

判断指定位是否为 \(1\)

常见于状态压缩、标志位判断、筛选子集等。

构造一个只在第 \(k\) 位为 \(1\) 的掩码 \(M = (1u << k)\)。

  • 若 \(x\) 的第 \(k\) 位是 \(1\),那么 \(x \text{ & } M\) 的结果正好就是 \(M\)(非 \(0\))。
  • 若该位是 \(0\),那么 \(x \text{ & } M\) 的结果为 \(0\)。

实现代码

if (x >> k & 1u)
    cout << "YES" << endl;
else
    cout << "NO" << endl;

交换两个数字

主要利用异或的三个性质:

  1. 交换律:\(a \oplus b = b \oplus a\)
  2. 结合律: \((a \oplus b) \oplus c == a \oplus (b \oplus c)\)
  3. 对于任何数 \(x\),都有 \(x \oplus x = 0\),\(x \oplus 0 = x\)

实现代码

a = a ^ b;
b = a ^ b;
a = a ^ b;

代数推导

设初值为 \((a, b)\):

  1. \(a = a \oplus b\)
    此时 \(a\) 里装的是 \((\)原\(a \oplus\) 原\(b\)\()\),\(b\) 还等于 原\(b\)。
  2. \(b = b \oplus a\)
    代入:\(b =\) 原\(b \oplus\) \((\)原\(a \oplus\) 原\(b\)\()\) \(=\) 原\(a\)(因为 \(x \oplus x = 0\),剩下 原\(a\))。
  3. \(a = a \oplus b\)
    代入:\(a =\) \((\)原\(a \oplus\) 原\(b\)\()\) \(\oplus\) 原\(a =\) 原\(b\)。

交换完成,得到 \((a, b) =\) \((\)原\(b\), 原\(a)\)。


只有一个出现奇数次的数字

题目链接

136. 只出现一次的数字 – 力扣(LeetCode)

题目描述

给定一个非空数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。需设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。

思路

利用异或性质 \(a \oplus a=0\), \(a \oplus 0=a\) 。把所有数按位异或起来,偶数次的都会抵消掉,只剩那个奇数次的。

关键代码

class Solution
{
public:
    int singleNumber(vector<int> &nums)
    {
        int ans = 0;
        for (auto x : nums) 
            ans ^= x;

        return ans;
    }
};

有两个出现奇数次的数字

题目链接

260. 只出现一次的数字 III – 力扣(LeetCode)

题目描述

给定一个非空数组,除了两个元素只出现一次以外,其余每个元素均出现两次。找出那两个出现了一次的元素。需设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。

思路

数组里恰有两个数出现奇数次,记为 \(u\) 与 \(v\),其余数都出现偶数次。

  1. 整体异或:\(X = u \oplus v\)。如果 \(u != v\),那么 \(X != 0\),它在某个比特位上为 \(1\),说明 \(u\) 与 \(v\) 在那一位的取值不同。
  2. 用 \(lowbit\) 把两类分开,取 \(t = X \text{&} -X\)(最低位 \(1\) 的掩码)。这位在 \(u\) 和 \(v\) 中必然一个为 \(1\)、一个为 \(0\)。把所有数按 \(t\) 这一位是否为 \(1\) 分成两组,并各自异或:
    • 组 \(B\)(与 \(t\) 按位与为 \(0\))最终得到另一个奇数次元素。
    • 两组里其他出现偶数次的数都会在各自组内抵消。

组 \(A\)(与 \(t\) 按位与不为 \(0\))最终得到其中一个奇数次元素;

关键代码

class Solution
{
public:
    vector<int> singleNumber(vector<int> &nums)
    {
        long long x = 0;
        for (auto val : nums)
            x ^= val;

        long long cnt = x & -x;

        long long y = 0;
        for (auto val : nums)
            if (val & cnt)
                y ^= val;

        return {int(x ^ y), int(y)};
    }
};

取最低位的 1(lowbit)

既仅保留最右侧的那一位 \(1\):\(x \mathrel{\&} -x\)(补码语义),最常用在树状数组中。

实现代码

int lowbit(int x)
{
    return x & -x;
}

重要结论

关于 \(lowbit\) 有一个重要结论,既 \(lowbit(x)\) 必然是 \(x\) 的因子。

结论证明

设正整数 \(x\) 的二进制表示为:\(x = 2^{a₁} + 2^{a₂} + … + 2^{a_k}\),其中 \(a₁ < a₂ < … < a_k\)。

要证明 \(2^{a₁}\) 是 \(x\) 的因子,只需证明 \(x\) 能被 \(2^{a₁}\) 整除,即 \(x / 2^{a₁}\) 为整数。

提取公因子,将 \(x\) 表示为:\(x = 2^{a₁} (1 + 2^{a₂-a₁} + 2^{a₃-a₁} + … + 2^{a_k-a₁})\)。

令 \(m = 1 + 2^{a₂-a₁} + … + 2^{a_k-a₁}\),则 \(x = 2^{a₁}·m\)。

因此,\(x / 2^{a₁} = m\) 为整数,即 \(2^{a₁}\) 整除 \(x\)。

\(lowbit(x) = 2^{a₁}\) 是 \(x\) 的因子。


通过以上可引出另外一个重要结论。

令 \(2 ^ t = lowbit(x)\),此时 \(x = 2 ^ t · m\),记 \(L = lowbit(x) = 2^t\)。

做一步操作 \(x\leftarrow x+L\):

\(x+L=(m+1)\cdot 2^t\)

因为 \(m\) 是奇数,\(m+1\) 至少含一个 \(2\),所以二进制尾随 \(0\) 的个数 \(v_2\)​ 至少增加 \(1\);换言之,每加一次 \(lowbit\),\(x\) 至少“多一个 \(2\) 的因子”。

把这个过程投影到“奇数部分”上更直观:令

\(g(m)=\frac{m+1}{2^{v_2(m+1)}}\quad(\text{把 }m+1\text{ 中的 2 都除掉,得到下一个奇数}).\)

当你反复执行 \(x\leftarrow x+lowbit(x)\) 时,奇数部分就按

\(m\mapsto\ g(m)\ \mapsto\ g(g(m))\ \mapsto\ \cdots\)

演化。对所有奇数 \(m\ge 3\),有 \(g(m)\le \frac{m+1}{2}<m\),所以这条奇数序列严格下降,必然在有限步内下降到 \(1\)。

结论:不停做 \(x\leftarrow x+lowbit(x)\),会在有限步内把 \(x\) 变成某个 \(2\) 的幂(因为奇数部分最终变成 \(1\))。

例题

题目链接

https://codeforces.com/gym/105977/problem/J

题目描述

给定 \(n\) \((\le 10^{12})\)。你可以在不超过 \(100\) 次的操作内,把它变成一个完全平方数。每次操作:选一个当前 \(n\) 的正因子 \(x\)(且所有选过的 \(x\) 彼此不同),令 \(n\gets n+x\)。整个过程 \(n\) 不得超过 \(10^{18}\)。

思路

  1. 若 \(n\) 已是完全平方数,直接输出 \(0\)。
  2. 否则循环做:\(x += lowbit(x)\),当 \(n\) 变成 \(2\) 的幂时停下来。
  3. 此时 \(n=2^k\)。若 \(k\) 是偶数,\(n\) 已是完全平方数;若 \(k\) 是奇数,再进行一次操作。此时指数为偶数,得到 \(n=2^{2t}\) 是完全平方数。

关键代码

ull lowbit(ull x)
{
    return x & -x;
}

void miyan()
{
    ull x;
    cin >> x;

    if (ull(sqrtl(x)) * ull(sqrtl(x)) == x)
    {
        cout << 0 << endl;
        return;
    }

    vector<ull> ans;
    while (lowbit(x) != x)
    {
        ans.push_back(lowbit(x));
        x += lowbit(x);
    }

    int cnt = log2(x);
    if (cnt & 1u)
    {
        ans.push_back(x);
        x *= 2;
    }

    cout << ans.size() << endl;
    for (auto x : ans)
        cout << x << ' ';
    cout << endl;
}

是否为 \(2\) 的幂

用于判断是否只有一个 \(1\),常见于分块、对齐、位集优化。

例题

https://leetcode.cn/problems/power-of-two

实现代码

// 方法 1
if (x > 0 && lowbit(x) == x)
    cout << "YES" << endl;

// 方法 2
if (x > 0 && (x & (x - 1)) == 0)
    cout << "YES" << endl;

注意:\(x \leq 0\) 要排除。


\(1\) 的个数

在状压 \(DP\)/组合计数里很常见。

常用解法

  1. 使用 \((x \text{>>} k \text{&} 1)\) 逐位判断。
  2. 使用 \(lowbit\) 判断,不断减最低 \(1\)。
  3. 使用内置函数 \(\text{__builtin_popcount(x)} / \text{__builtin_popcountll(x)}\)。

实现代码

// 方法 1
{
    ll ans = 0;
    for (int i = 0; i <= 62; ++i)
    {
        if (x >> i & 1u)
            ++ans;
    }

    cout << ans << endl;
}

// 方法 2
{
    ll ans = 0;
    while (x)
    {
        ++ans;
        x -= lowbit(x);
    }

    cout << ans << endl;
}

// 方法 3
cout << __builtin_popcountll(x) << endl;

前缀异或与区间异或

定义

构造前缀异或 \(p[0]=0\),\(p[i]=a_1\oplus a_2\oplus\cdots\oplus a_i​\)。

区间公式

和前缀和一模一样的用法,但减法换成异或:\( a_l\oplus a_{l+1}\oplus\cdots\oplus a_r \;=\;p[r]\ \oplus\ p[l-1]\)。

为什么成立?

异或满足交换律、结合律,且自反:\(x\oplus x=0、x\oplus 0=x\)。把 \(p[r]\) 展开,与 \(p[l−1]\) 逐项配对,区间外的项两两抵消成 \(0\),剩下正好是 \([l, r]\)。

例题

题目链接

https://www.51nod.com/Html/Challenge/Problem.html#problemId=2128

题目描述

给定长度为 \(n\) 的数组和 \(m\) 次询问,每次给 \((l,r)\),输出子数组 \([l,r]\) 的异或值。

思路

用上面的前缀异或。

  • 预处理:一次扫描得到 \(p[i]=p[i-1]\oplus a[i] (i=1..n)\)。
  • 回答查询:每个 \((l,r)\) 输出 \(p[r]\oplus p[l-1]\)。

关键代码

void miyan()
{
    int n, m;
    cin >> n;
    vector<int> a(n + 1), p(n + 1, 0);
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
        p[i] = p[i - 1] ^ a[i];
    }

    cin >> m;
    while (m--)
    {
        int l, r;
        cin >> l >> r;
        cout << (p[r] ^ p[l - 1]) << endl;
    }
}

1 ~ n 的异或值

\( f(n)=0\oplus1\oplus\cdots\oplus n\,\mbox{只取决于 }n\bmod 4\,\mbox{,并且恰好是:} \)

$$
f(n)=\left\{
\begin{array}{ll}
n, & n\bmod 4=0,\\
1, & n\bmod 4=1,\\
n+1, & n\bmod 4=2,\\
0, & n\bmod 4=3.
\end{array}
\right.
$$

例题

题目链接

https://www.luogu.com.cn/problem/U601307

题目描述

计算区间 [l, r] 元素不进位加法的值,数据范围:\(1≤T≤2×10^5, 1≤l≤r≤10^{18},r−l≤10^{18}\)。

思路

因为异或就是不进位加法,问题可转换为区间 \([l, r]\) 所有元素的异或值,通过上述结论可通过容斥原理快速求得,既由异或的逆运算就是异或可知:\(xor(l, r) = xor(1, r) \oplus xor(1, l – 1)\)。

关键代码

void miyan()
{
    ll l, r;
    cin >> l >> r;

    auto get = [&](ll x) -> ll
    {
        ll val = x % 4;
        if (!val)
            return x;
        else if (val == 1)
            return 1;
        else if (val == 2)
            return x + 1;
        return 0;
    };

    cout << (get(r) ^ get(l - 1)) << endl;
}

题目推荐

1. https://leetcode.cn/problems/power-of-three

2.https://leetcode.cn/problems/bitwise-and-of-numbers-range

3.https://leetcode.cn/problems/reverse-bits

4.https://leetcode.cn/problems/hamming-distance

5.https://leetcode.cn/problems/missing-number

6.https://codeforces.com/contest/2152/problem/D

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇