📖 附录 G:数学算法基础

⏱ 预计阅读时间:50 分钟 | 难度:🟡 中等


前置条件

在学习本附录之前,请确保你已掌握:

  • 基本循环与数组(第 2.2 章)
  • 模运算的基本性质(第 3.1 章)

🎯 学习目标

学完本附录后,你将能够:

  1. 用埃氏筛和欧拉线性筛高效枚举质数
  2. 用快速幂在 O(log N) 内计算大幂次取模
  3. 用扩展欧几里得求逆元
  4. 理解区间 DP 的核心框架并解决石子合并类问题
  5. 运用矩阵快速幂加速线性递推

G.1 质数筛法

为什么需要筛法?

判断一个数是否是质数,朴素做法是枚举 [2, √N],时间 O(√N)。
但若要一次性求出 [2, N] 内所有质数,朴素做法是 O(N√N),当 N = 10^7 时太慢。

筛法 利用「质数的倍数是合数」这一事实,批量标记合数。


G.1.1 埃拉托斯特尼筛(埃氏筛)

核心思想:
从 2 开始,每发现一个质数,就把它的所有倍数标记为合数。

📄 从 2 开始,每发现一个质数,就把它的所有倍数标记为合数。
筛选 N = 20 的过程:

初始:2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

标记 2 的倍数(从 2*2=4 开始):
  删除 4, 6, 8, 10, 12, 14, 16, 18, 20

标记 3 的倍数(从 3*3=9 开始):
  删除 9, 15  (12, 18 已经被 2 删除)

4 已被删除,跳过

标记 5 的倍数(从 5*5=25 > 20,停止)

最终质数:2, 3, 5, 7, 11, 13, 17, 19

从 ii 而不是 2i 开始的原因:
2×i, 3×i, ..., (i-1)×i 这些倍数,已经被更小的质数筛过了(因为它们有更小的质因子)。

📄 2×i, 3×i, ..., (i-1)×i 这些倍数,已经被更小的质数筛过了(因为它们有更小的质因子)。
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 1e7 + 5;
bool is_prime[MAXN];   // is_prime[i] = true 表示 i 是质数
vector<int> primes;    // 所有质数列表

// 埃氏筛:筛出 [2, n] 内的所有质数
// 时间复杂度:O(N log log N)
void sieve_eratosthenes(int n) {
    fill(is_prime + 2, is_prime + n + 1, true);  // 初始全是质数
    
    for (int i = 2; (long long)i * i <= n; i++) {
        if (is_prime[i]) {
            // 从 i*i 开始标记(更小的倍数已经被筛过)
            for (int j = i * i; j <= n; j += i)
                is_prime[j] = false;
        }
    }
    
    // 收集质数
    for (int i = 2; i <= n; i++)
        if (is_prime[i]) primes.push_back(i);
}

int main() {
    sieve_eratosthenes(100);
    
    cout << "100 以内的质数:";
    for (int p : primes) cout << p << " ";
    cout << endl;
    cout << "质数个数:" << primes.size() << endl;
    // 输出:2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97
    // 个数:25
    return 0;
}

G.1.2 欧拉线性筛(最优筛法)

埃氏筛的问题:一个合数可能被多个质数重复标记(如 12 被 2 和 3 各标记一次)。

欧拉线性筛 保证每个合数只被其最小质因子筛一次,时间复杂度严格 O(N)。

核心规则: 对于每个 i,只用「i 乘以不超过 i 的最小质因子的那些质数」来筛合数。

📄 C++ 完整代码
const int MAXN = 1e7 + 5;
int min_prime[MAXN];   // min_prime[i] = i 的最小质因子
vector<int> primes;

// 欧拉线性筛:O(N)
void sieve_linear(int n) {
    fill(min_prime, min_prime + n + 1, 0);  // 0 表示还没被筛
    
    for (int i = 2; i <= n; i++) {
        if (!min_prime[i]) {
            // i 没有被任何更小的数筛过 → i 是质数
            min_prime[i] = i;   // 质数的最小质因子是自身
            primes.push_back(i);
        }
        
        // 用 i 去筛 i 的倍数
        for (int p : primes) {
            if ((long long)i * p > n) break;
            min_prime[i * p] = p;   // p 是 i*p 的最小质因子
            if (i % p == 0) {
                // p 是 i 的最小质因子
                // 若继续用更大的质数 q 筛 i*q,
                // i*q 的最小质因子仍是 p(因为 p | i | i*q)
                // 所以 i*q 会被 (i/p)*p*q 中较小的部分筛到
                // 此处终止,保证每个合数只被筛一次
                break;
            }
        }
    }
}

int main() {
    sieve_linear(30);
    
    cout << "30 以内的质数:";
    for (int p : primes) cout << p << " ";
    cout << endl;
    
    // 利用最小质因子做质因数分解
    auto factorize = [&](int x) {
        cout << x << " = ";
        while (x > 1) {
            int p = min_prime[x];
            int cnt = 0;
            while (x % p == 0) { x /= p; cnt++; }
            cout << p << "^" << cnt << " ";
        }
        cout << endl;
    };
    factorize(360);  // 360 = 2^3 3^2 5^1
    return 0;
}

线性筛的额外能力: 同时计算积性函数(欧拉函数 φ、莫比乌斯函数 μ、约数个数等),这在数论题中非常有用。


G.1.3 筛法对比

方法时间复杂度空间优势
朴素判质数O(√N) 每个O(1)单个数判断
埃氏筛O(N log log N)O(N)代码简单,实际快
欧拉线性筛O(N)O(N)同时记录最小质因子
Bitset 埃氏筛O(N log log N / 64)O(N/8)超大 N(≥ 10^8)时更快

G.2 快速幂

问题

计算 $a^n \mod p$,其中 n 可能高达 $10^{18}$。

朴素方法(循环乘 n 次)需要 O(N) 次运算,根本不可行。

核心思想:二进制分解指数

将 n 写成二进制形式,利用递推:

$$a^n = \begin{cases} (a^{n/2})^2 & n \text{ 为偶数} \ (a^{(n-1)/2})^2 \times a & n \text{ 为奇数} \end{cases}$$

示例: 计算 $3^{13}$

📄 Code 完整代码
13 = 1101₂ = 8 + 4 + 1

3^13 = 3^8 × 3^4 × 3^1

计算过程(从低位到高位):
  base = 3, exp = 13, result = 1

  exp & 1 = 1 → result = 1 × 3 = 3
  base = 3 × 3 = 9,exp = 6

  exp & 1 = 0 → result 不变(= 3)
  base = 9 × 9 = 81,exp = 3

  exp & 1 = 1 → result = 3 × 81 = 243
  base = 81 × 81 = 6561,exp = 1

  exp & 1 = 1 → result = 243 × 6561 = 1594323
  base = ..., exp = 0(结束)

验证:3^13 = 1594323 ✓

完整实现

📄 查看代码:完整实现
// 快速幂:计算 base^exp % mod
// 时间复杂度:O(log exp)
// 空间复杂度:O(1)
long long fast_pow(long long base, long long exp, long long mod) {
    long long result = 1;
    base %= mod;          // 先对底数取模
    
    while (exp > 0) {
        // 若当前位(最低位)为 1,乘入结果
        if (exp & 1)
            result = result * base % mod;
        
        // 底数平方,指数右移一位
        base = base * base % mod;
        exp >>= 1;
    }
    return result;
}

// 使用示例
int main() {
    long long MOD = 1e9 + 7;
    
    cout << fast_pow(2, 10, MOD)  << endl;  // 1024
    cout << fast_pow(3, 100, MOD) << endl;  // 981350898
    cout << fast_pow(2, 1e18, MOD) << endl; // 通过快速幂计算,不会超时
    
    return 0;
}

模逆元(求 a 关于 mod 的逆元)

当 mod 是质数时,由费马小定理 $a^{p-1} \equiv 1 \pmod{p}$,得 $a^{-1} \equiv a^{p-2} \pmod{p}$。

📄 C++ 完整代码
// 求 a 关于质数 mod 的模逆元
long long mod_inv(long long a, long long mod) {
    return fast_pow(a, mod - 2, mod);
}

// 应用:计算组合数 C(n, k) mod p
long long C(int n, int k, long long mod) {
    if (k > n || k < 0) return 0;
    
    // 预处理阶乘和逆阶乘
    vector<long long> fact(n + 1), inv_fact(n + 1);
    fact[0] = 1;
    for (int i = 1; i <= n; i++) fact[i] = fact[i-1] * i % mod;
    inv_fact[n] = mod_inv(fact[n], mod);
    for (int i = n - 1; i >= 0; i--) inv_fact[i] = inv_fact[i+1] * (i+1) % mod;
    
    return fact[n] % mod * inv_fact[k] % mod * inv_fact[n-k] % mod;
}

G.3 区间动态规划(区间 DP)

什么是区间 DP?

区间 DP 是一类特殊的 DP,状态定义在区间上

$$dp[i][j] = \text{将区间 } [i, j] \text{ 处理后的最优值}$$

关键特征:

  • 大区间的最优解由小区间合并得到
  • 必须先算小区间,再算大区间
  • 枚举分割点 k,将 [i,j] 分为 [i,k] 和 [k+1,j]

状态转移框架

dp[i][j] = min/max over all k in [i, j-1]:
              dp[i][k] + dp[k+1][j] + cost(i, j)

遍历顺序:按区间长度

📄 查看代码:遍历顺序:按区间长度
// 区间 DP 标准模板
int n;
int dp[305][305];  // dp[i][j] = 区间 [i,j] 的最优值

// 初始化:单个元素
for (int i = 1; i <= n; i++)
    dp[i][i] = 基础情况;

// 按区间长度从小到大
for (int len = 2; len <= n; len++) {          // 区间长度
    for (int i = 1; i + len - 1 <= n; i++) {  // 左端点
        int j = i + len - 1;                   // 右端点
        dp[i][j] = INF;  // 初始为极值
        
        // 枚举分割点 k
        for (int k = i; k < j; k++) {
            dp[i][j] = min(dp[i][j], dp[i][k] + dp[k+1][j] + cost(i, j));
        }
    }
}

// 答案
return dp[1][n];

G.3.1 经典例题:石子合并

N 堆石子排成一列,每次合并相邻两堆,代价为合并后石子总数,求合并所有石子的最小(或最大)代价。

状态定义: dp[i][j] = 合并 [i, j] 堆石子的最优代价
转移: 枚举最后一次合并的分割点 k,即 [i,k] 先合并完,[k+1,j] 先合并完,再把这两堆合并

$$dp[i][j] = \min_{i \le k < j} {dp[i][k] + dp[k+1][j] + sum[j] - sum[i-1]}$$

其中 sum[j] - sum[i-1] 是区间 [i,j] 的石子总数(也是最后合并这步的代价)。

📄 其中 `sum[j] - sum[i-1]` 是区间 [i,j] 的石子总数(也是最后合并这步的代价)。
#include <bits/stdc++.h>
using namespace std;

int n;
int a[305];
int sum[305];      // 前缀和
int dp[305][305];  // dp[i][j] = 合并 [i,j] 的最小代价

int main() {
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        sum[i] = sum[i-1] + a[i];
    }
    
    // 单堆:代价 = 0(已经"合并完了")
    // dp[i][i] = 0(默认初始化)
    
    // 区间 DP
    for (int len = 2; len <= n; len++) {
        for (int i = 1; i + len - 1 <= n; i++) {
            int j = i + len - 1;
            dp[i][j] = INT_MAX;
            
            for (int k = i; k < j; k++) {
                int cost = sum[j] - sum[i-1];          // 最后合并这步的代价
                dp[i][j] = min(dp[i][j], dp[i][k] + dp[k+1][j] + cost);
            }
        }
    }
    
    cout << dp[1][n] << endl;
    return 0;
}

追踪示例(a = [3, 5, 2, 1]):

📄 Code 完整代码
前缀和:sum = [0, 3, 8, 10, 11]

初始:dp[1][1]=0, dp[2][2]=0, dp[3][3]=0, dp[4][4]=0

len=2:
  dp[1][2] = dp[1][1] + dp[2][2] + sum[2]-sum[0] = 0+0+8 = 8
  dp[2][3] = 0+0+7 = 7
  dp[3][4] = 0+0+3 = 3

len=3:
  dp[1][3]:
    k=1: dp[1][1]+dp[2][3]+(sum[3]-sum[0]) = 0+7+10 = 17
    k=2: dp[1][2]+dp[3][3]+(sum[3]-sum[0]) = 8+0+10 = 18
    dp[1][3] = min(17,18) = 17
  dp[2][4]:
    k=2: dp[2][2]+dp[3][4]+(sum[4]-sum[1]) = 0+3+8 = 11
    k=3: dp[2][3]+dp[4][4]+(sum[4]-sum[1]) = 7+0+8 = 15
    dp[2][4] = 11

len=4:
  dp[1][4]:
    k=1: dp[1][1]+dp[2][4]+11 = 0+11+11 = 22
    k=2: dp[1][2]+dp[3][4]+11 = 8+3+11 = 22
    k=3: dp[1][3]+dp[4][4]+11 = 17+0+11 = 28
    dp[1][4] = 22

答案:22

G.3.2 变形:括号匹配

给定一个括号序列,求添加最少多少个括号使其合法。

状态定义: dp[i][j] = 使 s[i..j] 合法需要添加的最少括号数

转移:

  1. 若 s[i] 和 s[j] 匹配(一对括号):dp[i][j] = dp[i+1][j-1]
  2. 枚举分割点:dp[i][j] = min(dp[i][k] + dp[k+1][j])
  3. 单个字符:dp[i][i] = 1(必须添加一个配对括号)
📄 3. 单个字符:`dp[i][i] = 1`(必须添加一个配对括号)
#include <bits/stdc++.h>
using namespace std;

bool match(char a, char b) {
    return (a == '(' && b == ')') ||
           (a == '[' && b == ']') ||
           (a == '{' && b == '}');
}

int main() {
    string s;
    cin >> s;
    int n = s.size();
    
    vector<vector<int>> dp(n, vector<int>(n, 0));
    
    // 单个字符需要 1 个括号配对
    for (int i = 0; i < n; i++) dp[i][i] = 1;
    
    for (int len = 2; len <= n; len++) {
        for (int i = 0; i + len - 1 < n; i++) {
            int j = i + len - 1;
            dp[i][j] = INT_MAX;
            
            // 两端匹配
            if (len >= 2 && match(s[i], s[j]))
                dp[i][j] = min(dp[i][j], (len == 2 ? 0 : dp[i+1][j-1]));
            
            // 枚举分割点
            for (int k = i; k < j; k++)
                dp[i][j] = min(dp[i][j], dp[i][k] + dp[k+1][j]);
        }
    }
    
    cout << dp[0][n-1] << endl;
    return 0;
}

G.4 矩阵快速幂(加速线性递推)

适用场景

如果一个递推关系的形式是:

$$\begin{pmatrix} f(n) \ f(n-1) \end{pmatrix} = M \times \begin{pmatrix} f(n-1) \ f(n-2) \end{pmatrix}$$

则可以用矩阵快速幂,在 O(K³ log N) 内计算第 N 项(K 为状态向量大小)。

以 Fibonacci 数列为例

$$f(n) = f(n-1) + f(n-2)$$

转化为矩阵形式:

$$\begin{pmatrix} f(n) \ f(n-1) \end{pmatrix} = \begin{pmatrix} 1 & 1 \ 1 & 0 \end{pmatrix} \times \begin{pmatrix} f(n-1) \ f(n-2) \end{pmatrix}$$

因此:

$$\begin{pmatrix} f(n) \ f(n-1) \end{pmatrix} = \begin{pmatrix} 1 & 1 \ 1 & 0 \end{pmatrix}^{n-1} \times \begin{pmatrix} f(1) \ f(0) \end{pmatrix}$$

📄 C++ 完整代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef vector<vector<ll>> Matrix;

const ll MOD = 1e9 + 7;
const int K = 2;  // 状态维度

// 矩阵乘法
Matrix mat_mul(const Matrix& A, const Matrix& B) {
    int n = A.size();
    Matrix C(n, vector<ll>(n, 0));
    for (int i = 0; i < n; i++)
        for (int k = 0; k < n; k++)
            for (int j = 0; j < n; j++)
                C[i][j] = (C[i][j] + A[i][k] * B[k][j]) % MOD;
    return C;
}

// 矩阵快速幂:M^n
Matrix mat_pow(Matrix M, ll n) {
    int sz = M.size();
    // 初始化为单位矩阵
    Matrix result(sz, vector<ll>(sz, 0));
    for (int i = 0; i < sz; i++) result[i][i] = 1;
    
    while (n > 0) {
        if (n & 1) result = mat_mul(result, M);
        M = mat_mul(M, M);
        n >>= 1;
    }
    return result;
}

// 计算第 n 个 Fibonacci 数 mod MOD
ll fibonacci(ll n) {
    if (n <= 1) return n;
    
    // 转移矩阵
    Matrix trans = {{1, 1}, {1, 0}};
    Matrix result = mat_pow(trans, n - 1);
    
    // 初始状态 [f(1), f(0)] = [1, 0]
    // 结果 = result * [1, 0]^T 的第一个元素
    return result[0][0];  // result[0][0]*f(1) + result[0][1]*f(0) = result[0][0]
}

int main() {
    for (int i = 0; i <= 10; i++)
        cout << "f(" << i << ") = " << fibonacci(i) << endl;
    
    // 大数:f(10^18) mod (10^9+7)
    cout << "f(10^18) = " << fibonacci(1e18) << endl;
    return 0;
}

⚠️ 常见错误

错误原因修复方案
埃氏筛 i*i 溢出i 较大时 i*i 超 int 范围(long long)i*i <= ni <= n/i
快速幂底数未取模第一行漏了 base %= mod总是先 base %= mod
区间 DP 初始化错误忘记 dp[i][i] = 基础情况单元素的基础情况必须手动设置
区间 DP 遍历顺序错误没有按区间长度从小到大最外层循环必须是 len
矩阵乘法模运算位置累加时溢出再取模每次乘加后立即 % MOD

💪 练习题(共 8 道,全部含完整解答)

🟢 基础练习(1~3)

题目 1:质因数分解
利用欧拉线性筛的 min_prime 数组,在 O(log N) 时间内对任意正整数做质因数分解,输出格式为 2^3 * 3^2 * 5

✅ 完整解答
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 1e6 + 5;
int min_prime[MAXN];
vector<int> primes;

void sieve(int n) {
    for (int i = 2; i <= n; i++) {
        if (!min_prime[i]) { min_prime[i] = i; primes.push_back(i); }
        for (int p : primes) {
            if ((long long)i * p > n) break;
            min_prime[i * p] = p;
            if (i % p == 0) break;
        }
    }
}

void factorize(int x) {
    cout << x << " = ";
    bool first = true;
    while (x > 1) {
        int p = min_prime[x], cnt = 0;
        while (x % p == 0) { x /= p; cnt++; }
        if (!first) cout << " * ";
        cout << p << "^" << cnt;
        first = false;
    }
    cout << "\n";
}

int main() {
    sieve(1e6);
    factorize(360);      // 2^3 * 3^2 * 5^1
    factorize(1000000);  // 2^6 * 5^6
    factorize(97);       // 97^1(质数)
    return 0;
}

关键: min_prime[x] 是 x 的最小质因子,每次除尽后继续处理 x/p^k,循环 O(log x) 次。


题目 2:组合数取模
计算 C(N, K) mod (10^9+7),N 和 K 可高达 10^6。
要求预处理阶乘和逆元,查询 O(1)。

✅ 完整解答
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const ll MOD = 1e9 + 7;
const int MAXN = 1e6 + 5;
ll fact[MAXN], inv_fact[MAXN];

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

void preprocess(int n) {
    fact[0] = 1;
    for (int i = 1; i <= n; i++) fact[i] = fact[i-1] * i % MOD;
    inv_fact[n] = fast_pow(fact[n], MOD - 2, MOD);  // 费马小定理
    for (int i = n - 1; 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] % MOD * inv_fact[k] % MOD * inv_fact[n-k] % MOD;
}

int main() {
    preprocess(1e6);
    cout << C(10, 3)    << "\n";   // 120
    cout << C(1000000, 500000) << "\n";  // 大数取模
    cout << C(5, 0)     << "\n";   // 1
    return 0;
}

预处理流程:

1. 正向递推 fact[0..N]:O(N)
2. 用快速幂求 fact[N] 的逆元:O(log MOD)
3. 反向递推所有逆阶乘:inv_fact[i] = inv_fact[i+1] * (i+1),O(N)
4. 查询 C(n,k):O(1)

题目 3:石子合并(区间 DP 基础)
N 堆石子排成一排,每次合并相邻两堆,代价为合并后的总石子数。求总代价最小值。

✅ 完整解答
#include <bits/stdc++.h>
using namespace std;

int main() {
    int n; cin >> n;
    vector<int> a(n + 1);
    vector<int> sum(n + 1, 0);
    for (int i = 1; i <= n; i++) { cin >> a[i]; sum[i] = sum[i-1] + a[i]; }
    
    vector<vector<int>> dp(n + 1, vector<int>(n + 1, 0));
    
    // len = 区间长度,从 2 开始(长度 1 代价为 0)
    for (int len = 2; len <= n; len++) {
        for (int i = 1; i + len - 1 <= n; i++) {
            int j = i + len - 1;
            dp[i][j] = INT_MAX;
            for (int k = i; k < j; k++) {
                int cost = sum[j] - sum[i-1];  // 合并 [i..j] 的代价
                dp[i][j] = min(dp[i][j], dp[i][k] + dp[k+1][j] + cost);
            }
        }
    }
    
    cout << dp[1][n] << "\n";
    return 0;
}

输入示例:

4
3 5 2 1
输出:22

追踪: 见 G.3.1 节的详细分步追踪。


🟡 进阶练习(4~6)

题目 4:矩阵链乘(经典区间 DP)
给 N 个矩阵,第 i 个的维度为 p[i-1] × p[i](共 N+1 个维度值)。
求计算连乘 M1 × M2 × ... × MN 的最少乘法次数(通过改变括号顺序)。

提示: dp[i][j] = 计算第 i 到第 j 个矩阵乘积的最少乘法次数。
转移:枚举最后一次分割点 k,代价为 dp[i][k] + dp[k+1][j] + p[i-1]*p[k]*p[j]

✅ 完整解答
#include <bits/stdc++.h>
using namespace std;

int main() {
    int n; cin >> n;
    vector<int> p(n + 1);
    for (int& x : p) cin >> x;
    
    // dp[i][j] = 计算矩阵 i..j 的最少乘法次数(1-indexed)
    vector<vector<long long>> dp(n + 1, vector<long long>(n + 1, 0));
    
    for (int len = 2; len <= n; len++) {
        for (int i = 1; i + len - 1 <= n; i++) {
            int j = i + len - 1;
            dp[i][j] = LLONG_MAX;
            for (int k = i; k < j; k++) {
                long long cost = (long long)p[i-1] * p[k] * p[j];
                dp[i][j] = min(dp[i][j], dp[i][k] + dp[k+1][j] + cost);
            }
        }
    }
    
    cout << dp[1][n] << "\n";
    return 0;
}

示例(N=3,矩阵维度 10×30,30×5,5×60):

p = [10, 30, 5, 60]

dp[1][2] = 10×30×5 = 1500
dp[2][3] = 30×5×60 = 9000
dp[1][3]:
  k=1: dp[1][1] + dp[2][3] + 10×30×60 = 0 + 9000 + 18000 = 27000
  k=2: dp[1][2] + dp[3][3] + 10×5×60 = 1500 + 0 + 3000 = 4500
  dp[1][3] = 4500

输出:4500(先算 M2×M3,再算 M1×(M2M3))

题目 5:Fibonacci 第 N 项(矩阵快速幂)
求斐波那契数列第 N 项 f(N) mod (10^9+7),N 可高达 10^18。

✅ 完整解答
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef vector<vector<ll>> Matrix;

const ll MOD = 1e9 + 7;

Matrix mat_mul(const Matrix& A, const Matrix& B) {
    int n = A.size();
    Matrix C(n, vector<ll>(n, 0));
    for (int i = 0; i < n; i++)
        for (int k = 0; k < n; k++)
            for (int j = 0; j < n; j++)
                C[i][j] = (C[i][j] + A[i][k] * B[k][j]) % MOD;
    return C;
}

Matrix mat_pow(Matrix M, ll n) {
    int sz = M.size();
    Matrix result(sz, vector<ll>(sz, 0));
    for (int i = 0; i < sz; i++) result[i][i] = 1;  // 单位矩阵
    while (n > 0) {
        if (n & 1) result = mat_mul(result, M);
        M = mat_mul(M, M);
        n >>= 1;
    }
    return result;
}

ll fibonacci(ll n) {
    if (n <= 1) return n;
    // [f(n), f(n-1)] = [[1,1],[1,0]]^(n-1) * [f(1), f(0)]
    Matrix trans = {{1, 1}, {1, 0}};
    auto R = mat_pow(trans, n - 1);
    // R * [1, 0]^T = R[0][0]*1 + R[0][1]*0 = R[0][0]
    return R[0][0];
}

int main() {
    cout << fibonacci(1)  << "\n";   // 1
    cout << fibonacci(10) << "\n";   // 55
    cout << fibonacci(50) << "\n";   // 586268941
    
    ll n = (ll)1e18;
    cout << fibonacci(n)  << "\n";   // 某个大数 mod 10^9+7
    return 0;
}

为什么递推不行? f(10^18) 需要 10^18 步,即便每步只是加法也需要 ~32 年(按 10^9 步/秒)。矩阵快速幂只需 ~120 次矩阵乘法。


题目 6:牛买卖(区间 DP 变形)
N 个人依次报价(整数),你可以买入卖出,每次操作只涉及相邻时刻。
合并相邻两个操作(买-卖一对),代价为两个价格之差的绝对值。求将所有操作合并成一笔交易的最小总代价。
(即:括号匹配最少插入次数的数值版本)

提示: 这与石子合并类似,用区间 DP,dp[i][j] = 将第 i 到第 j 笔操作合并完毕的最小代价。

✅ 完整解答

本质是:对区间 [i,j] 枚举分割点 k,左右子区间各自合并后,再将两段合并,追加代价 |price[i] - price[j]|(合并时左端与右端的价差)。

#include <bits/stdc++.h>
using namespace std;

int main() {
    int n; cin >> n;
    vector<int> p(n + 1);
    for (int i = 1; i <= n; i++) cin >> p[i];
    
    vector<vector<int>> dp(n + 1, vector<int>(n + 1, 0));
    
    for (int len = 2; len <= n; len++) {
        for (int i = 1; i + len - 1 <= n; i++) {
            int j = i + len - 1;
            dp[i][j] = INT_MAX;
            for (int k = i; k < j; k++) {
                // 左边合并后最终价格是 p[k](向左看),右边是 p[k+1](向右看)
                // 合并两段的额外代价:两段之间的「接口代价」= |p[i] - p[j]|
                // (用石子合并的模型,cost = 区间端点差值)
                dp[i][j] = min(dp[i][j],
                    dp[i][k] + dp[k+1][j] + abs(p[i] - p[j]));
            }
        }
    }
    
    cout << dp[1][n] << "\n";
    return 0;
}

此题展示了区间 DP 的灵活性:只需修改「合并代价」的计算方式,即可处理不同的区间合并问题。


🔴 挑战练习(7~8)

题目 7:1000 以内所有质数之和
用埃氏筛求出 1000 以内的所有质数,输出它们的个数和总和。
再用线性筛改写,比较结果是否一致。

✅ 完整解答
#include <bits/stdc++.h>
using namespace std;

// 埃氏筛版本
void eratosthenes(int n) {
    vector<bool> is_prime(n + 1, true);
    is_prime[0] = is_prime[1] = false;
    for (int i = 2; (long long)i * i <= n; i++)
        if (is_prime[i])
            for (int j = i * i; j <= n; j += i)
                is_prime[j] = false;
    
    int cnt = 0; long long sum = 0;
    for (int i = 2; i <= n; i++)
        if (is_prime[i]) { cnt++; sum += i; }
    cout << "埃氏筛:" << cnt << " 个质数,总和 = " << sum << "\n";
}

// 欧拉线性筛版本
void euler(int n) {
    vector<int> min_p(n + 1, 0);
    vector<int> primes;
    for (int i = 2; i <= n; i++) {
        if (!min_p[i]) { min_p[i] = i; primes.push_back(i); }
        for (int p : primes) {
            if ((long long)i * p > n) break;
            min_p[i * p] = p;
            if (i % p == 0) break;
        }
    }
    long long sum = 0;
    for (int p : primes) sum += p;
    cout << "线性筛:" << primes.size() << " 个质数,总和 = " << sum << "\n";
}

int main() {
    eratosthenes(1000);
    euler(1000);
    // 两者输出应完全一致:168 个质数,总和 = 76127
    return 0;
}

题目 8:字符串加密(快速幂综合应用)
RSA 加密中,加密公式为:密文 = 明文^e mod n。
给定明文 M(一个整数)、指数 e、模数 n,计算密文。
其中 M, e, n 可高达 10^18。

✅ 完整解答

直接套用快速幂模板。注意当 M 和 n 都接近 10^18 时,M * M 会溢出 long long,需要用 __int128 或「龟速乘(二进制分解乘法)」。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef __int128 lll;

// 快速幂(支持 base 和 mod 都高达 10^18)
ll fast_pow(ll base, ll exp, ll mod) {
    ll result = 1;
    base %= mod;
    while (exp > 0) {
        if (exp & 1)
            result = (lll)result * base % mod;  // __int128 避免溢出
        base = (lll)base * base % mod;
        exp >>= 1;
    }
    return result;
}

// 龟速乘(不用 __int128 的替代方案)
ll safe_mul(ll a, ll b, ll mod) {
    ll result = 0;
    a %= mod;
    while (b > 0) {
        if (b & 1) result = (result + a) % mod;
        a = (a + a) % mod;
        b >>= 1;
    }
    return result;
}

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

int main() {
    ll M, e, n;
    cin >> M >> e >> n;
    cout << fast_pow(M, e, n) << "\n";  // 密文
    
    // 测试:M=2, e=10, n=1000 → 2^10=1024 mod 1000 = 24
    cout << fast_pow(2, 10, 1000) << "\n";  // 24
    
    return 0;
}

为什么用 __int128
long long 最大约 9.2×10^18,但 base * base 可达 (10^18)^2 = 10^36,远超范围。
__int128 支持到约 1.7×10^38,足以处理中间计算。


💡 章节联系: 质数筛是数论题的基础工具;快速幂是「取模运算」场景(组合数、大幂次、RSA)的标配;区间 DP 是 USACO Gold 的高频题型,掌握了石子合并后还可以扩展到括号匹配、矩阵链乘、凸包剖分等。