📎 附录 E ⏱️ 约 50 分钟 🎯 参考资料 数学

附录 E:竞赛编程数学基础

💡 关于本附录: 算法竞赛中经常需要用到基础算术之外的数学工具。本附录涵盖 USACO Bronze、Silver、Gold 中常见的核心数学知识,并为每个主题提供可直接用于竞赛的代码模板。


E.1 模运算

为什么需要模运算?

很多题目要求输出"答案对 10⁹ + 7 取模"的结果。这并非随意为之——它是为了在答案极其庞大时防止整数溢出

举个例子:"N 个元素的排列有多少种?"答案是 N!。当 N = 20 时,N! = 2,432,902,008,176,640,000,已超过 long long 的最大值(约 9.2 × 10¹⁸)。当 N = 100 时,根本无法表示。

解决方案: 对一个素数 M(通常取 10⁹ + 7)取模,将所有运算在模意义下进行。

(a + b) mod M = ((a mod M) + (b mod M)) mod M (a × b) mod M = ((a mod M) × (b mod M)) mod M (a - b) mod M = ((a mod M) - (b mod M) + M) mod M ← 注意加上 M!

时钟类比与关键性质——记住每次算术运算后都要取模:

模运算性质

常用模数

常量数值为何选这个值?
1e9 + 71,000,000,007素数,适合 int(< 2³¹),使用最广泛
1e9 + 91,000,000,009素数,1e9+7 的备选
998244353998,244,353NTT 友好素数(用于多项式运算)

基础模运算模板

📄 查看代码:基础模运算模板
// 解答:模运算基础
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
const ll MOD = 1e9 + 7;  // 竞赛编程标准模数

// 安全加法:(a + b) % MOD
ll addMod(ll a, ll b) {
    return (a % MOD + b % MOD) % MOD;
}

// 安全减法:(a - b + MOD) % MOD(处理负数结果)
ll subMod(ll a, ll b) {
    return ((a % MOD) - (b % MOD) + MOD) % MOD;  // 加 MOD 防止负数!
}

// 安全乘法:(a * b) % MOD
// 关键:a 和 b 最大为 MOD-1 ≈ 10^9,乘积 ≈ 10^18 在 long long 范围内
ll mulMod(ll a, ll b) {
    return (a % MOD) * (b % MOD) % MOD;
}

// 示例:计算前 N 个正整数之和 mod MOD
ll sumFirstN(ll n) {
    // 公式 n*(n+1)/2,但除法需要用模逆元
    // 暂时用逐步累加:
    ll result = 0;
    for (ll i = 1; i <= n; i++) {
        result = addMod(result, i);
    }
    return result;
}

⚠️ 致命 Bug: 在 C++ 中,若 a < b(a - b) % MOD 可能为负数!请始终写成 (a - b + MOD) % MOD

E.1.1 快速幂(二进制取幂)

朴素方式计算 a^n mod M 需要 O(N) 次乘法。快速幂(反复平方法)只需 O(log N) 次。

核心思路:a^n = a^(n/2) × a^(n/2)          若 n 为偶数
              a^n = a × a^((n-1)/2) × a^((n-1)/2)  若 n 为奇数

示例:a^13 = a^(1101₂)
           = a^8 × a^4 × a^1
           = 3 次乘法,而非 12 次!
📄 C++ 完整代码
// 解答:模意义快速幂 — O(log n)
// 计算 (base^exp) % mod
ll power(ll base, ll exp, ll mod = MOD) {
    ll result = 1;
    base %= mod;                  // 先对底数取模
    
    while (exp > 0) {
        if (exp & 1) {            // 若当前位为 1
            result = result * base % mod;
        }
        base = base * base % mod; // 底数平方
        exp >>= 1;                // 移动到下一位
    }
    return result;
}

// 使用示例:
// power(2, 10) = 1024 % MOD = 1024
// power(2, 100, MOD) = 2^100 mod (10^9+7)

E.1.2 模逆元(费马小定理)

a 对模 M模逆元是一个数 a⁻¹,满足 a × a⁻¹ ≡ 1 (mod M)

这让我们可以进行模意义下的除法a / b mod M = a × b⁻¹ mod M

费马小定理: 若 M 为素数且 gcd(a, M) = 1,则:

a^(M-1) ≡ 1 (mod M) ⟹ a^(M-2) ≡ a⁻¹ (mod M)
📄
// 解答:利用费马小定理求模逆元
// 仅在 MOD 为素数且 gcd(a, MOD) = 1 时适用
ll modInverse(ll a, ll mod = MOD) {
    return power(a, mod - 2, mod);
}

// 模意义下的除法:
ll divMod(ll a, ll b) {
    return mulMod(a, modInverse(b));
}

// 示例:(n! / k!) mod MOD
// = n! × (k!)^(-1) mod MOD
// = n! × modInverse(k!) mod MOD

E.1.3 预处理阶乘与逆元

对于需要多次计算组合数 C(n, k) 的题目:

📄 对于需要多次计算组合数 `C(n, k)` 的题目:
// 解答:预处理阶乘,O(1) 查询组合数
const int MAXN = 1000005;
ll fact[MAXN], inv_fact[MAXN];

void precompute() {
    fact[0] = 1;
    for (int i = 1; i < MAXN; i++) {
        fact[i] = fact[i-1] * i % MOD;
    }
    inv_fact[MAXN-1] = modInverse(fact[MAXN-1]);
    for (int i = MAXN-2; i >= 0; i--) {
        inv_fact[i] = inv_fact[i+1] * (i+1) % MOD;
    }
}

// C(n, k) = n! / (k! * (n-k)!)
ll C(int n, int k) {
    if (k < 0 || k > n) return 0;
    return fact[n] * inv_fact[k] % MOD * inv_fact[n-k] % MOD;
}

// 用法:调用一次 precompute(),之后 C(n, k) 均为 O(1)

E.2 GCD 与 LCM

欧几里得算法

两个数的最大公因数(GCD)是能同时整除两者的最大整数。

欧几里得算法: 基于 gcd(a, b) = gcd(b, a % b)

每次递归调用都会缩小问题规模,逐步推导的过程一目了然:

GCD 欧几里得算法

📄 ![GCD 欧几里得算法](../images/gcd_euclidean.svg)
// 解答:GCD — O(log(min(a,b)))
int gcd(int a, int b) {
    while (b != 0) {
        a %= b;
        swap(a, b);
    }
    return a;
}
// 递归写法:
// int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); }

// C++17:<numeric> 中的 std::gcd
// int g = gcd(a, b);           // std::gcd,C++17(推荐)
// int g = __gcd(a, b);         // 旧版 GCC 内建函数,仍可用

追踪示例: gcd(48, 18)

gcd(48, 18) → gcd(18, 48%18=12) → gcd(12, 18%12=6) → gcd(6, 0) = 6

LCM 与溢出陷阱

📄 查看代码:LCM 与溢出陷阱
// 解答:LCM — 注意溢出!

// 错误写法:大数相乘时溢出
long long lcmWrong(long long a, long long b) {
    return a * b / gcd(a, b);  // a*b 即使是 long long 也可能溢出!
}

// 正确写法:先除后乘
long long lcm(long long a, long long b) {
    return a / gcd(a, b) * b;  // 先除再乘
}
// a / gcd(a,b) 一定是整数,不损失精度
// 然后乘以 b:最大约 10^18,在 long long 范围内
lcm(a, b) = a × b / gcd(a, b) = (a / gcd(a, b)) × b

⚠️ 始终先除后乘,避免溢出!

扩展欧几里得算法

找整数 x, y 满足 ax + by = gcd(a, b)——在 MOD 不是素数时计算模逆元非常有用:

📄 找整数 x, y 满足 `ax + by = gcd(a, b)`——在 MOD 不是素数时计算模逆元非常有用:
// 解答:扩展欧几里得算法 — O(log(min(a,b)))
// 返回 gcd(a,b),并设置 x,y 使得 a*x + b*y = gcd(a,b)
long long extgcd(long long a, long long b, long long &x, long long &y) {
    if (b == 0) { x = 1; y = 0; return a; }
    long long x1, y1;
    long long g = extgcd(b, a % b, x1, y1);
    x = y1;
    y = x1 - (a / b) * y1;
    return g;
}

// 使用 extgcd 求模逆元(即使 MOD 不是素数也适用):
long long modInverseExtGcd(long long a, long long mod) {
    long long x, y;
    long long g = extgcd(a, mod, x, y);
    if (g != 1) return -1;  // 无逆元(gcd != 1)
    return (x % mod + mod) % mod;
}

E.3 质数与筛法

试除法

📄 查看代码:试除法
// 解答:试除法判素 — O(sqrt(N))
bool isPrime(long long n) {
    if (n < 2) return false;
    if (n == 2) return true;
    if (n % 2 == 0) return false;
    for (long long i = 3; i * i <= n; i += 2) {
        if (n % i == 0) return false;
    }
    return true;
}
// 高效原因:若 n 有大于 sqrt(n) 的因子,必然也有小于等于 sqrt(n) 的因子
// 只检查 2 之后的奇数(迭代次数减半)

埃拉托色尼筛(埃筛)

高效找出 N 以内的所有质数:

📄 高效找出 N 以内的所有质数:
// 解答:埃筛 — O(N log log N) 时间,O(N) 空间
// 运行后,isPrime[i] = true 当且仅当 i 是质数
const int MAXN = 1000005;
bool isPrime[MAXN];

void sieve(int n) {
    fill(isPrime, isPrime + n + 1, true);  // 初始假设全是质数
    isPrime[0] = isPrime[1] = false;        // 0 和 1 不是质数
    
    for (int i = 2; (long long)i * i <= n; i++) {
        if (isPrime[i]) {
            // 标记 i 的所有倍数为合数
            for (int j = i * i; j <= n; j += i) {
                isPrime[j] = false;
                // 从 i*i 开始(更小的倍数已被更小的素数标记过)
            }
        }
    }
}

// 统计 N 以内的质数个数:
void countPrimes(int n) {
    sieve(n);
    int count = 0;
    for (int i = 2; i <= n; i++) {
        if (isPrime[i]) count++;
    }
    cout << count << "\n";
}

为什么内层循环从 i² 开始? i 的所有小于 i² 的倍数(如 2i, 3i, ..., (i-1)i)已被更小的质数(2, 3, ..., i-1)标记过。从 i² 开始可以避免冗余操作。

线性筛(欧拉筛)— O(N)

欧拉筛确保每个合数只被标记一次:

📄 欧拉筛确保每个合数只被标记一次:
// 解答:线性筛(欧拉筛)— O(N) 时间
// 同时计算每个数的最小质因子(SPF)
const int MAXN = 1000005;
int spf[MAXN];      // 最小质因子
vector<int> primes;

void linearSieve(int n) {
    fill(spf, spf + n + 1, 0);
    for (int i = 2; i <= n; i++) {
        if (spf[i] == 0) {          // i 是质数
            spf[i] = i;
            primes.push_back(i);
        }
        for (int j = 0; j < (int)primes.size() && primes[j] <= spf[i] && (long long)i * primes[j] <= n; j++) {
            spf[i * primes[j]] = primes[j];  // 标记合数
        }
    }
}

// 利用 SPF 快速分解质因数:
// 每次分解 O(log N)
vector<int> factorize(int n) {
    vector<int> factors;
    while (n > 1) {
        factors.push_back(spf[n]);
        n /= spf[n];
    }
    return factors;
}

E.4 二进制表示与位运算

基本位操作

📄 查看代码:基本位操作
// 解答:常用位运算参考
int n = 42;   // 二进制:101010

// ── AND(&):两位都为 1 才为 1 ──
int a = 6 & 3;     // 110 & 011 = 010 = 2

// ── OR(|):至少一位为 1 则为 1 ──
int b = 6 | 3;     // 110 | 011 = 111 = 7

// ── XOR(^):恰好一位为 1 才为 1 ──
int c = 6 ^ 3;     // 110 ^ 011 = 101 = 5

// ── NOT(~):翻转所有位(补码) ──
int d = ~6;        // = -7(补码表示)

// ── 左移(<<):乘以 2^k ──
int e = 1 << 4;    // = 16 = 2^4

// ── 右移(>>):除以 2^k(算术右移) ──
int f = 32 >> 2;   // = 8 = 32/4

竞赛常用位运算技巧

📄 查看代码:竞赛常用位运算技巧
// 解答:竞赛编程位运算技巧

// ── 判断 n 是否为奇数 ──
bool isOdd(int n) { return n & 1; }  // 最低位为 1 当且仅当 n 为奇数

// ── 判断 n 是否为 2 的幂次 ──
bool isPow2(int n) { return n > 0 && (n & (n-1)) == 0; }
// 原因:2 的幂次:1=001, 2=010, 4=100。n-1 会翻转所有低位。
// 4 & 3 = 100 & 011 = 000。非 2 的幂次:6 & 5 = 110 & 101 = 100 ≠ 0。

// ── 取第 k 位(从右 0 开始计数) ──
bool getBit(int n, int k) { return (n >> k) & 1; }

// ── 将第 k 位置 1 ──
int setBit(int n, int k) { return n | (1 << k); }

// ── 将第 k 位清零 ──
int clearBit(int n, int k) { return n & ~(1 << k); }

// ── 翻转第 k 位 ──
int toggleBit(int n, int k) { return n ^ (1 << k); }

// ── lowbit:最低位的 1(树状数组中常用!) ──
int lowbit(int n) { return n & (-n); }
// 示例:lowbit(12) = lowbit(1100) = 0100 = 4

// ── 统计置位数(popcount) ──
int popcount(int n) { return __builtin_popcount(n); }   // 使用内建函数!
// long long 版本:__builtin_popcountll(n)

// ── 不用临时变量交换两数 ──
void swapXOR(int &a, int &b) {
    a ^= b;
    b ^= a;
    a ^= b;
}
// (通常直接用 std::swap——这主要是一个技巧性写法)

// ── 找最低位 1 的位置 ──
int lowestBitPos(int n) { return __builtin_ctz(n); }  // 统计尾部零个数
// __builtin_clz(n) = 统计前导零个数

子集枚举

一个强大的技巧:用位掩码枚举集合的所有子集。

📄 一个强大的技巧:用位掩码枚举集合的所有子集。
// 解答:位掩码子集枚举
// 枚举 N 元素集合的所有子集

void enumerateAllSubsets(int n) {
    // 共 2^n 个子集
    for (int mask = 0; mask < (1 << n); mask++) {
        // mask 表示一个子集:第 i 位为 1 表示包含元素 i
        cout << "子集: {";
        for (int i = 0; i < n; i++) {
            if (mask & (1 << i)) {
                cout << i << " ";
            }
        }
        cout << "}\n";
    }
}

// 枚举给定集合 S 的所有非空子集
void enumerateSubsetsOf(int S) {
    for (int sub = S; sub > 0; sub = (sub - 1) & S) {
        // 处理子集 sub
        // 技巧:(sub-1) & S 得到 S 的"下一个更小"子集
        // 摊还 O(1) 步枚举 S 的全部 2^|S| 个子集
    }
}

// 经典应用:状压 DP
// dp[mask] = 访问 mask 所表示的城市集合所需的最小代价
// dp[0] = 0(初始:没有访问任何城市)
// dp[mask | (1 << v)] = min(dp[mask | (1 << v)], dp[mask] + cost[last][v])

E.5 组合数学基础

计数公式

排列数:P(n, k) = n! / (n-k)! — 从 n 个中有序选 k 个 组合数:C(n, k) = n! / (k! × (n-k)!) — 从 n 个中无序选 k 个
📄
// 解答:带模运算的组合数学
// 假设已调用 E.1.3 中的 precompute()

// C(n, k) = n! / (k! * (n-k)!)
ll combination(int n, int k) {
    if (k < 0 || k > n) return 0;
    return fact[n] * inv_fact[k] % MOD * inv_fact[n-k] % MOD;
}

// P(n, k) = n! / (n-k)!
ll permutation(int n, int k) {
    if (k < 0 || k > n) return 0;
    return fact[n] * inv_fact[n-k] % MOD;
}

// 隔板法(星与条):将 n 个相同球放入 k 个不同盒的方案数
// = C(n + k - 1, k - 1)
ll starsAndBars(int n, int k) {
    return combination(n + k - 1, k - 1);
}

帕斯卡三角——无需预处理直接计算 C(n, k)

当 n 较小(n ≤ 2000)时,帕斯卡三角更简洁:

📄 当 n 较小(n ≤ 2000)时,帕斯卡三角更简洁:
// 解答:帕斯卡三角 DP — O(n^2) 预处理
const int MAXN = 2005;
ll C[MAXN][MAXN];

void buildPascal() {
    for (int i = 0; i < MAXN; i++) {
        C[i][0] = C[i][i] = 1;
        for (int j = 1; j < i; j++) {
            C[i][j] = (C[i-1][j-1] + C[i-1][j]) % MOD;
        }
    }
}
// 之后 C[n][k] 即为 0 <= k <= n < MAXN 时的组合数
// 完全避免了模逆元——当 MOD 可能不是素数时特别有用

帕斯卡恒等式: C(n, k) = C(n-1, k-1) + C(n-1, k)

含义:"从 n 个中选 k 个" = "包含第 n 个元素,从 n-1 个中选 k-1 个" + "不包含第 n 个元素,从 n-1 个中选 k 个"。

常用组合恒等式

📄 查看代码:常用组合恒等式
// 竞赛中常用的恒等式:

// 曲棍球恒等式:sum_{k=0}^{n} C(r+k, k) = C(n+r+1, n)
// 用途:二维前缀和、多项式求值

// 范德蒙德恒等式:sum_k C(m,k)*C(n,r-k) = C(m+n, r)
// 用途:涉及两组元素的计数问题

// 容斥原理:
// |A ∪ B| = |A| + |B| - |A ∩ B|
// |A ∪ B ∪ C| = |A| + |B| + |C| - |A∩B| - |A∩C| - |B∩C| + |A∩B∩C|
// 推广到 n 个集合时有 2^n 项(或用位掩码枚举)

E.6 常用数学结论(复杂度分析)

调和级数

1 + 1/2 + 1/3 + ... + 1/N ≈ ln(N) ≈ 0.693 × log₂(N)

这解释了为何埃筛运行时间为 O(N log log N)

以及为何树状数组操作是 O(log N):lowbit 操作每次前进 1、2、4...位。

关键估算值

表达式近似值含义
log₂(10⁵)≈ 1710⁵ 个元素的 BST/线段树深度
log₂(10⁹)≈ 30在 10⁹ 范围内的二分查找步数
√(10⁶)= 1000N ≤ 10⁶ 时试除法的上界
2²⁰≈ 10⁶状压 DP 上限(20 个元素)
20!≈ 2.4 × 10¹⁸勉强放进 long long
13!≈ 6 × 10⁹略超 int 上限

每秒操作数估算

时间限制安全操作数上限
1 秒~10⁸ 次简单操作
2 秒~2 × 10⁸
3 秒~3 × 10⁸

据此可以估算算法是否够快:


E.7 完整数学模板

以下是整合了本附录所有模板的单文件版本:

📄 以下是整合了本附录所有模板的单文件版本:
// 解答:竞赛编程完整数学模板
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;

// ═══════════════════════════════════════════════
// 模运算
// ═══════════════════════════════════════════════
const ll MOD = 1e9 + 7;

ll power(ll base, ll exp, ll mod = MOD) {
    ll result = 1;
    base %= mod;
    while (exp > 0) {
        if (exp & 1) result = result * base % mod;
        base = base * base % mod;
        exp >>= 1;
    }
    return result;
}

ll modInverse(ll a, ll mod = MOD) {
    return power(a, mod - 2, mod);
}

// ═══════════════════════════════════════════════
// 阶乘(预处理至 MAXN)
// ═══════════════════════════════════════════════
const int MAXN = 1000005;
ll fact[MAXN], inv_fact[MAXN];

void precomputeFactorials() {
    fact[0] = 1;
    for (int i = 1; i < MAXN; i++) fact[i] = fact[i-1] * i % MOD;
    inv_fact[MAXN-1] = modInverse(fact[MAXN-1]);
    for (int i = MAXN-2; i >= 0; i--) inv_fact[i] = inv_fact[i+1] * (i+1) % MOD;
}

ll C(int n, int k) {
    if (k < 0 || k > n) return 0;
    return fact[n] * inv_fact[k] % MOD * inv_fact[n-k] % MOD;
}

// ═══════════════════════════════════════════════
// GCD / LCM
// ═══════════════════════════════════════════════
ll gcd(ll a, ll b) { return b == 0 ? a : gcd(b, a % b); }
ll lcm(ll a, ll b)  { return a / gcd(a, b) * b; }

// ═══════════════════════════════════════════════
// 质数筛
// ═══════════════════════════════════════════════
const int MAXP = 1000005;
bool notPrime[MAXP];
vector<int> primes;

void sieve(int n = MAXP - 1) {
    notPrime[0] = notPrime[1] = true;
    for (int i = 2; i <= n; i++) {
        if (!notPrime[i]) {
            primes.push_back(i);
            for (long long j = (long long)i*i; j <= n; j += i)
                notPrime[j] = true;
        }
    }
}

bool isPrime(int n) { return n >= 2 && !notPrime[n]; }

// ═══════════════════════════════════════════════
// 位运算技巧
// ═══════════════════════════════════════════════
bool isOdd(int n)       { return n & 1; }
bool isPow2(int n)      { return n > 0 && !(n & (n-1)); }
int  lowbit(int n)      { return n & (-n); }
int  popcount(int n)    { return __builtin_popcount(n); }
int  ctz(int n)         { return __builtin_ctz(n); }  // 尾部零个数

// ═══════════════════════════════════════════════
// 扩展 GCD
// ═══════════════════════════════════════════════
ll extgcd(ll a, ll b, ll &x, ll &y) {
    if (!b) { x = 1; y = 0; return a; }
    ll x1, y1, g = extgcd(b, a%b, x1, y1);
    x = y1; y = x1 - a/b * y1;
    return g;
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    
    precomputeFactorials();
    sieve();
    
    // 测试:C(10, 3) = 120
    cout << C(10, 3) << "\n";
    
    // 测试:2^100 mod (10^9+7)
    cout << power(2, 100) << "\n";
    
    // 测试:输出前 10 个质数
    for (int i = 0; i < 10; i++) cout << primes[i] << " ";
    cout << "\n";
    
    return 0;
}

E.8 数论快速参考

整除规则(手工验算用)

除数规则
2末位为偶数
3各位数字之和能被 3 整除
4末两位组成的数能被 4 整除
5末位为 0 或 5
9各位数字之和能被 9 整除
10末位为 0
11各位数字的交替和能被 11 整除

整数平方根

// 安全的整数平方根(避免浮点误差)
ll isqrt(ll n) {
    ll x = sqrtl(n);              // 浮点近似值
    while (x * x > n) x--;        // 若偏大则向下修正
    while ((x+1) * (x+1) <= n) x++; // 若偏小则向上修正
    return x;
}

向上取整除法

// 正整数的向上取整除法:ceil(a/b)
ll ceilDiv(ll a, ll b) {
    return (a + b - 1) / b;
    // 等价写法:(a - 1) / b + 1(a > 0 时相同)
}

❓ 常见问题

Q1:什么时候应该用 long long

A:当数值可能超过 2 × 10⁹(大约是 int 的上限)时。典型场景:① 两个大 int 相乘(10⁹ × 10⁹ = 10¹⁸);② 累加路径权重(N 条边,每条权重最大 10⁶,合计最大 10¹¹);③ 阶乘/组合数(即使取模,中间计算也用 long long)。经验法则:竞赛代码中只要有乘法,就用 long long

Q2:为什么用 10⁹ + 7 而非 10⁹

A:10⁹ 不是素数(= 2⁹ × 5⁹),无法用费马小定理求模逆元。10⁹ + 7 = 1,000,000,007 是素数,且 (10⁹ + 7)² < 2⁶³long long 的上限),因此取模后的两数相乘不会溢出 long long

Q3:快速幂中的位运算技巧是怎么工作的?

A:将指数 n 写成二进制:n = b_k × 2^k + ... + b_1 × 2 + b_0。那么 a^n = a^(b_k × 2^k) × ... × a^(b_1 × 2) × a^b_0。每次循环将底数平方(代表 a 的 2^k 次幂),当前位为 1 时乘入结果。只需 log₂(n) 次乘法。

Q4:埃筛的内层循环为什么从 i×i 开始?

A:i 的倍数 2i, 3i, ..., (i-1)i 已被更小的质数 2, 3, ..., i-1 标记过。例如,6 = 2×3 已被 2 标记;7×5=35 已被 5 标记。从 i×i 开始可以避免冗余,优化常数因子。

Q5:为什么 n & (n-1) 能检测 n 是否为 2 的幂次?

A:2 的幂次在二进制中只有一个 1 位(如 8 = 1000)。减 1 会把最低的 1 位变为 0,并把其下所有 0 位翻转为 1(如 7 = 0111)。因此 n & (n-1) 清除了最低 1 位。若 n 是 2 的幂次(只有一个 1 位),结果为 0;否则不为 0。


附录 E 结束 — 另请参阅:算法模板库 | 竞赛编程技巧