跳转至

第 3 章 运算符与位运算

配套例题:BISHI30、BISHI31、BISHI32 被打乱的异或和、BISHI57 最大公因数与最小公倍数 来源:菜鸟教程 Python3 运算符;S2 bit_op.cpp

本章讲全部七类运算符。位运算只讲语法层面,算法技巧(lowbit、枚举子集、状压) 在 46-位运算 展开。


3.1 算术运算符

运算符 含义 例(a=7, b=2
+ - * 加减乘 9 5 14
/ 真除法,永远返回 float 3.5
// 向下取整除法 3
% 取模,符号随除数 1
** 49

/ 的陷阱

>>> 10 / 2
5.0                       # 是 float 不是 int!
>>> 10 ** 18 // 3         # 333333333333333333     精确
>>> 10 ** 18 / 3          # 3.333333333333333e+17  丢精度了

算法题里几乎永远该用 //。用了 / 会有两个后果:结果变成 float(大数丢精度), 以及不能作为列表下标(a[n/2] 直接 TypeError)。

mid = (lo + hi) // 2      # ✅ 二分永远用 //
mid = (lo + hi) / 2       # ❌ TypeError: list indices must be integers

**pow

2 ** 10                   # 1024
pow(2, 10)                # 1024,等价
pow(2, 10, 1000)          # 24,三参数版本 = 快速幂取模
2 ** 0.5                  # 1.4142135623730951,指数可以是小数

x ** 0.5math.sqrt(x) 都返回 float,大整数开方会丢精度。 需要整数平方根时用 math.isqrt(x)(Python 3.8+),它精确且返回 int:

math.isqrt(10 ** 18 - 1)     # 999999999     精确
int((10 ** 18 - 1) ** 0.5)   # 1000000000    多算了 1

原因:float 只有 53 位有效位(约 \(9 imes 10^{15}\))。 超过这个范围的整数转成 float 时先被舍入,开方结果可能比真值大也可能比真值小, 两个方向都会出错——大了让试除多跑几轮,小了直接漏掉因子,把合数判成质数。 isqrt 走的是纯整数牛顿迭代,没有浮点参与,任何规模都精确。

负数取整的完整对照

表达式 Python C++
-7 / 2 -3.5 -3(整数除法)
-7 // 2 -4 -3
-7 % 2 1 -1
divmod(-7, 2) (-4, 1)

牢记:Python 的 // 向下(floor),% 结果符号跟除数


3.2 比较运算符

== != > < >= <=,返回 bool

链式比较

Python 独有的语法糖,且语义正确(不像 C++ 里 1 < x < 3 会被解析成 (1<x)<3):

if 0 <= i < n:                    # ✅ 等价于 0 <= i and i < n
    ...
if 0 <= x < n and 0 <= y < m:     # 网格越界判断的标准写法
    ...

链式比较中间的表达式只求值一次

if 0 <= f(x) < 10:      # f 只调用一次

容器的比较

列表、元组按字典序逐元素比较:

[1, 2, 3] < [1, 3]          # True   第二个元素 2 < 3
(1, 2) < (1, 2, 0)          # True   前缀相同则短的小
"abc" < "abd"               # True   字符串按码点比较

这是元组排序的基础——排序时把关键字按优先级放进元组即可,见 12-自定义排序


3.3 赋值运算符

a = 1
a += 2          # 等价 a = a + 2
a -= 1; a *= 3; a /= 2; a //= 2; a %= 3; a **= 2
a &= 1; a |= 2; a ^= 3; a <<= 1; a >>= 1

多重赋值与解包

a, b = 1, 2
a, b = b, a                       # 交换,不需要临时变量
a = b = c = 0                     # 链式赋值
first, *rest = [1, 2, 3, 4]       # first=1, rest=[2,3,4]
*init, last = [1, 2, 3, 4]        # init=[1,2,3], last=4

a, b = b, a 的原理是先把右边求值成元组,再解包赋给左边, 所以真的能安全交换。C++ 里必须借助临时变量或 std::swap

同理,斐波那契可以写成一行:

a, b = b, a + b        # 右边用的都是旧值

增强赋值对可变对象的特殊行为

a = [1, 2]
b = a
a += [3]        # 就地扩展,等价 a.extend([3])
print(b)        # [1, 2, 3]   ← b 也变了

a = [1, 2]
b = a
a = a + [3]     # 创建新列表并重新绑定
print(b)        # [1, 2]      ← b 没变

+=list 是就地修改,对 int/str/tuple 是重新绑定。这个差异在函数传参时会咬人。

海象运算符 :=(3.8+)

在表达式内部赋值:

while (line := sys.stdin.readline()):
    process(line)

if (n := len(a)) > 10:
    print(f"太长了:{n}")

竞赛里最常见的用途是在 while 条件里读入并判断,省掉重复代码。


3.4 逻辑运算符

and or not——注意 Python 用英文单词,不是 && || !

短路求值与返回值

and/or 返回的是操作数本身,不是布尔值

>>> 1 and 2          # 2      前者为真,返回后者
>>> 0 and 2          # 0      前者为假,短路返回前者
>>> 1 or 2           # 1      前者为真,短路返回前者
>>> 0 or 2           # 2      前者为假,返回后者
>>> None or []       # []

实用写法:

x = val or default             # val 为假值时用默认值
if i < n and a[i] > 0:         # 短路保证不越界

x = val or 0val 本身就是 0 时也会取到 0,看似没问题; 但 x = val or 10val == 0 时会得到 10,这通常不是本意。 需要严格区分时用 x = val if val is not None else 10

三元表达式

y = a if cond else b
print("YES" if ok else "NO")

顺序是「值 - if - 条件 - else - 值」,和 C 的 cond ? a : b 相反,容易写反。


3.5 成员与身份运算符

x in s          # 成员判断
x not in s
x is y          # 同一对象
x is not y

in 的复杂度取决于容器类型,这是竞赛里的核心性能点:

容器 x in c 复杂度
list / tuple \(O(n)\) 线性扫描
str \(O(n \cdot m)\) 子串搜索
set / frozenset \(O(1)\) 平均
dict \(O(1)\) 平均(查的是键)
# ❌ 循环里用 list 判存在 → O(n²),n=1e5 就 TLE
seen = []
for x in a:
    if x not in seen:
        seen.append(x)

# ✅ 换成 set → O(n)
seen = set()
for x in a:
    if x not in seen:
        seen.add(x)

这是 Python 算法题最高频的 TLE 原因,没有之一。 只要写出 in 且左边是循环变量,就停下来问一句:右边是 set 还是 list


3.6 位运算符

运算符 名称 例(a=60=0b111100, b=13=0b1101
& 按位与 a & b = 12 = 0b1100
\| 按位或 a \| b = 61 = 0b111101
^ 按位异或 a ^ b = 49 = 0b110001
~ 按位取反 ~a = -61
<< 左移 a << 2 = 240
>> 右移 a >> 2 = 15

Python 位运算的三个特殊之处

1. 整数是无限位的补码。 ~x 等于 -x - 1,永远成立:

>>> ~5        # -6
>>> ~-1       # 0
>>> ~0        # -1

因为没有固定位宽,所以没有「符号位溢出」这回事, C++ 里 ~x 的结果依赖 int 是 32 还是 64 位,Python 里不会。

2. 左移不会溢出。 1 << 1000 是合法的巨大整数。这让状压 DP 的状态数不受 64 位限制, 但也意味着位运算不再是 \(O(1)\)——大整数的位运算是 \(O(\text{位数})\)

3. 负数右移是算术右移(补符号位):

>>> -8 >> 1      # -4
>>> -1 >> 10     # -1,永远填不满

所以不要对可能为负的数做右移当除法,除非确实需要向下取整的语义。

常用位技巧速查

x & 1                    # 取最低位,判奇偶
x >> 1                   # 除以 2(向下取整)
x & (x - 1)              # 消去最低位的 1
x & (-x)                 # lowbit:只保留最低位的 1(树状数组核心)
x | (1 << k)             # 把第 k 位置 1
x & ~(1 << k)            # 把第 k 位清 0
x ^ (1 << k)             # 翻转第 k 位
(x >> k) & 1             # 取第 k 位
bin(x).count("1")        # 数 1 的个数(3.9 环境下的写法)

Python 3.10 引入了 int.bit_count(),比 bin(x).count("1") 快很多。 本教程的运行环境是 3.9,所以统一用 bin(x).count("1"); 如果 OJ 的 Python 版本 ≥ 3.10,可以换成 x.bit_count()

异或的三条性质

这是位运算题的核心,BISHI32 直接考它:

  1. \(x \oplus x = 0\)(自反)
  2. \(x \oplus 0 = x\)(幺元)
  3. 满足交换律和结合律

推论:一组数全部异或起来,出现偶数次的会互相抵消


3.7 运算符优先级

从高到低(同一行内左结合):

**                          幂(右结合)
+x  -x  ~x                  一元
*  /  //  %                 乘除
+  -                        加减
<<  >>                      移位
&                           按位与
^                           按位异或
|                           按位或
in  not in  is  is not
<  <=  >  >=  !=  ==        比较
not                         逻辑非
and
or
if - else                   三元
lambda
:=                          海象

最容易踩的两条

# 1. 位运算的优先级「比比较高、比算术低」,前半截和 C 恰好相反
if x & 3 == 1:          # Python 解析成 (x & 3) == 1  ← 和直觉一致
                        #   同一行代码在 C/C++ 里是 x & (3 == 1) = x & 0 = 0,含义完全不同
mask = 1 << n + 1       # ← 真正的坑:加减比移位优先级高,这里是 1 << (n + 1)
mask = 1 << (n + 1)     # ✅ 想表达什么就写什么,别让读者去查表

# 2. ** 是右结合
2 ** 3 ** 2             # = 2 ** 9 = 512,不是 (2**3)**2 = 64

规则:写位运算就加括号。理由不是「记不住优先级」, 而是同一行代码在 C/C++ 和 Python 里可能解析成两个不同的表达式, 加了括号两边就都只有一种读法。


3.8 例题

BISHI32 被打乱的异或和(简单)

题面见 BISHI32 原题(牛客)

原数组 \(b\) 长度为 \(n-1\),令 \(x = b_1 \oplus \cdots \oplus b_{n-1}\), 把 \(x\) 追加到末尾得到长度 \(n\) 的数组并打乱。给出打乱后的数组,求 \(x\);答案不唯一时输出任意一个。

这题的答案比看上去简单得多,全部来自 3.6 的三条性质。

设新数组为 \(a\),则

\[a_1 \oplus a_2 \oplus \cdots \oplus a_n = (b_1 \oplus \cdots \oplus b_{n-1}) \oplus x = x \oplus x = 0\]

即新数组的全体异或恒为 0。现在要找一个 \(a_i\) 使得「去掉它之后其余元素的异或等于它自己」。 而去掉 \(a_i\) 后其余元素的异或是

\[\Big(\bigoplus_{j} a_j\Big) \oplus a_i = 0 \oplus a_i = a_i\]

对每一个 \(a_i\) 都成立。所以任意一个元素都是合法答案——直接输出第一个即可:

import sys


def main():
    data = sys.stdin.buffer.read().split()
    p = 0
    t = int(data[p]); p += 1
    out = []
    for _ in range(t):
        n = int(data[p]); p += 1
        out.append(data[p].decode())     # 任取一个元素即可,这里取第一个
        p += n
    sys.stdout.write("\n".join(out) + "\n")


main()

样例输出的是 37……而本节代码输出 46……两者都对, 因为题面写明「若存在多种可能的 \(x\),可输出任意一个」。 这类 special judge 题如果按样例逐字符比对,会误以为自己写错了。 见 20-输入输出处理 §20.6

顺带一提,「全体异或」的惯用写法是:

# [片段]
from functools import reduce
from operator import xor

reduce(xor, a, 0)                 # 等价于 a[0] ^ a[1] ^ ...,初值 0 保证空列表也正确

BISHI57 最大公因数与最小公倍数(简单)

求两个数的 gcd 与 lcm。

import math

a, b = map(int, input().split())
g = math.gcd(a, b)
print(g, a // g * b)              # 注意:先除后乘!

a // g * b 而不是 a * b // g——两者数学上等价,但前者的中间值更小。 在 C++ 里这是防溢出的必要写法;Python 虽然不会溢出,但大整数乘法是 \(O(\text{位数}^{1.58})\), 先除能显著减少中间值规模。养成习惯没坏处。

Python 3.9 起 math.gcd 支持多个参数:math.gcd(a, b, c)math.lcm 也是 3.9 引入的,可以直接用 math.lcm(a, b)


3.9 本章速查

要点 结论
整数除法 永远用 /// 返回 float 且丢精度
整数开方 math.isqrt(x),不要用 x ** 0.5
负数取整 // 向下,% 符号随除数
交换变量 a, b = b, a
链式比较 0 <= i < n 语义正确,放心用
and/or 返回操作数本身,可用于取默认值
in 的容器 set/dict\(O(1)\)list\(O(n)\) ← 最高频 TLE 原因
位运算优先级 一律加括号
~x 等于 -x - 1,Python 无位宽概念
数 1 的个数 3.9 用 bin(x).count("1"),3.10+ 用 x.bit_count()
lcm a // g * b,先除后乘