矩阵加速入门

· · 算法·理论

一、矩阵乘法

1.1 定义与运算

矩阵乘法常常指一般矩阵乘法。一个 n \times p 的矩阵 A 与一个 p \times m 的矩阵 B 相乘,得到一个 n \times m 的矩阵 P。具体地,有

P_{i,j} = \sum\limits_{k=1}^p A_{i,k} \times B_{k,j}

举个例子:

\begin{bmatrix} 1 & 2 \\ 3 & 4 \\ 5 & 6 \end{bmatrix} \times \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \end{bmatrix}

将得到

\begin{bmatrix} 1 \times 1 + 2 \times 4 & 1 \times 2 + 2 \times 5 & 1 \times 3 + 2 \times 6 \\ 3 \times 1 + 4 \times 4 & 3 \times 2 + 4 \times 5 & 3 \times 3 + 4 \times 6 \\ 5 \times 1 + 6 \times 4 & 5 \times 2 + 6 \times 5 & 5 \times 3 + 6 \times 6 \end{bmatrix}

通俗地理解,P_{i,j} 就是 A 矩阵的第 i 行与 B 矩阵的第 j 列的对应位置乘积之和。体现在代码上,就是:

for (int i = 1; i <= n; i ++) {
    for (int j = 1; j <= m; j ++) {
        for (int k = 1; k <= p; k ++) {
            c[i][j] += a[i][k] * b[k][j];
        }
    }
}

从上面可以看出,矩阵乘法 A \times B 要求矩阵 A 的列数与矩阵 B 的行数相等。

1.2 基本性质

  1. 具有:

    • 结合律(AB)C = A(BC)
    • 分配律A(B+C) = AB + AC(B+C)A = BA + CA

    这两点尤其重要。

  2. 不具有:

    • 往往不具有交换律A \times B 合法,B \times A 不一定合法。即使都合法,两者不一定相等。
    • 往往不具有消去律:满足 AB = AC 不一定有 B = C。满足 AB = 0 不一定有 AB 为零矩阵。

1.3 单位矩阵

单位矩阵 I 是一个方阵,其主对角线上的元素均为 1,其余元素均为 0。即

I = \begin{bmatrix} 1 & 0 & 0 & \cdots & 0 \\ 0 & 1 & 0 & \cdots & 0 \\ 0 & 0 & 1 & \cdots & 0 \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 0 & 0 & 0 & 0 & 1 \end{bmatrix}

其性质为:任何矩阵与单位矩阵相乘都得原矩阵。

1.4 矩阵快速幂

P3390 【模板】矩阵快速幂

前置芝士:快速幂

了解了矩阵乘法,矩阵快速幂就很简单了。将矩阵封装为结构体并重载乘号,然后按照普通快速幂的方法即可,时间复杂度 O(n^2 \log k)

::::success[Code]

#include <bits/stdc++.h>
#define int long long
//#define INF 0x3f3f3f3f
#define endl '\n'

using namespace std;

const int N = 105, Mod = 1e9 + 7;

int n, k;

struct Matrix {
    int a[N][N];
    Matrix() {
        memset(a, 0, sizeof a);
    }
    void build() {
        for (int i = 1; i <= n; i ++) a[i][i] = 1;
    }
};

void Add(int &x, int y) {
    x += y; if (x >= Mod) x -= Mod;
}

Matrix operator *(const Matrix &x, const Matrix y) {
    Matrix z;
    for (int i = 1; i <= n; i ++) {
        for (int j = 1; j <= n; j ++) {
            for (int k = 1; k <= n; k ++) {
                Add(z.a[i][j], x.a[i][k] * y.a[k][j] % Mod);
            }
        }
    }
    return z;
}

signed main() {
//  freopen("", "r", stdin);
//  freopen("", "w", stdout);
//  ios::sync_with_stdio(0); cin.tie(0);
    cin >> n >> k;
    Matrix x;
    for (int i = 1; i <= n; i ++) {
        for (int j = 1; j <= n; j ++) {
            cin >> x.a[i][j];
        }
    }
    Matrix ans; ans.build();
    while (k) {
        if (k & 1) ans = ans * x;
        x = x * x, k >>= 1;
    }
    for (int i = 1; i <= n; i ++) {
        for (int j = 1; j <= n; j ++) {
            cout << ans.a[i][j] << ' ';
        }
        cout << endl;
    }
    return 0;
}

::::

二、应用

一大堆题目来袭。

套路什么的,题目做多了就会了,这里先不讲。

2.1 加速递推式

P1962 斐波那契数列

::::info[省流]

\begin{bmatrix} F_n & F_{n-1} \\ F_{n-1} & F_{n-2} \end{bmatrix}= \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix} ^ {n-1}

::::

考虑斐波那契数列相邻两项构成的矩阵 \begin{bmatrix} F_n & F_{n-1} \end{bmatrix}。由于

\begin{bmatrix} F_n & F_{n-1} \end{bmatrix} = \begin{bmatrix} F_{n-1} + F_{n-2} & F_{n-1} \end{bmatrix}

所以

\begin{bmatrix} F_n & F_{n-1} \end{bmatrix} = \begin{bmatrix} F_{n-1} & F_{n-2} \end{bmatrix} \times \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix}

于是由 \begin{bmatrix} F_2 = 1 & F_1 = 1 \end{bmatrix} 进行 n - 2 次乘的操作即可得到 \begin{bmatrix} F_n & F_{n-1} \end{bmatrix}。即:

\times \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix} ^ {n-2}

用矩阵快速幂求出 A = \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix} ^ {n-2},则 F_n = A_{1,1} + A_{2,1}

::::success[Code]

#include <bits/stdc++.h>
#define ll long long
//#define INF 0x3f3f3f3f
#define endl '\n'

using namespace std;

const int Mod = 1e9 + 7;

struct Mat {
    ll a[2][2];
    Mat () { memset(a, 0, sizeof a); }
    Mat operator*(const Mat& b) const {
        Mat res;
        for (int i = 0; i < 2; i ++) {
            for (int j = 0; j < 2; j ++) {
                for (int k = 0; k < 2; k ++) {
                    res.a[i][j] = (res.a[i][j] + a[i][k] * b.a[k][j]) % Mod;
                }
            }
        }
        return res;
    }
};

int fib(ll n) {
    if (n <= 2) return 1;
    Mat ans, base;
    base.a[0][0] = p, base.a[1][0] = q, base.a[1][1] = 1;
    ans.a[0][0] = ans.a[1][1] = 1;
    n -= 2;
    while (n) {
        if (n & 1) ans = ans * base;
        base = base * base, n >>= 1;
    }
    return (ans.a[0][0] + ans.a[1][0]) % Mod;
}

signed main() {
//  freopen("", "r", stdin);
//  freopen("", "w", stdout);
//  ios::sync_with_stdio(0); cin.tie(0);
    ll n; cin >> n;
    cout << fib(n);
    return 0;
}

::::

P1306 斐波那契公约数

\gcd(F_n,F_m)。考察了斐波那契数列的性质 \gcd(F_n,F_m) = F_{\gcd(n,m)}。笔者数学不好,就不证了。

P1349 广义斐波那契数列

灵活变化转移矩阵。考虑

\begin{bmatrix} a_n & a_{n-1} \end{bmatrix} = \begin{bmatrix} p \times a_{n-1} + q \times a_{n-2} & a_{n-1} \end{bmatrix}

于是

\begin{bmatrix} a_n & a_{n-1} \end{bmatrix} = \begin{bmatrix} a_{n-1} & a_{n-2} \end{bmatrix} \times \begin{bmatrix} p & 1 \\ q & 0 \end{bmatrix}
P1939 矩阵加速(数列)

我们希望构造矩阵 B 使

\begin{bmatrix}a_x & a_{x-1} & a_{x-2}\end{bmatrix} = \begin{bmatrix}a_{x-1} & a_{x-2} & a_{x-3}\end{bmatrix} \times B

由于 a_x = 1 \cdot a_{x-1} + 0 \cdot a_{x-2} + 1 \cdot a_{x-3},所以转移矩阵第一列为 \begin{bmatrix}1 \\ 0 \\ 1\end{bmatrix}

同理求出转移矩阵第二、三列,最终得到

\begin{bmatrix} 1 & 1 & 0 \\ 0 & 0 & 1 \\ 1 & 0 & 0 \end{bmatrix}

求出 B \gets B^{n-3},答案即为 B_{1,1}+B_{2,1}+B_{3,1}

P1397 [NOI2013] 矩阵游戏

::::info[题意]{open} 给定 n,m,a,b,c,d,对于递推式

F(i,j) = \begin{cases} 1 & i = 1 \wedge j = 1 \\ c \times F(i-1,m) + d & i > 1 \wedge j = 1 \\ a \times F(i,j-1) + b & \text{otherwise} \end{cases}

F(n,m),答案对 10^9 + 7 取模。

:::: 转移可以写成矩阵的形式: $$ \begin{bmatrix} f_{i,j} & 1 \end{bmatrix} = \begin{bmatrix} f_{i,j-1} & 1 \end{bmatrix} \times \begin{bmatrix} a & 0 \\ b & 1 \end{bmatrix} $$ $$ \begin{bmatrix} f_{i,1} & 1 \end{bmatrix} = \begin{bmatrix} f_{i-1,m} & 1 \end{bmatrix} \times \begin{bmatrix} c & 0 \\ d & 1 \end{bmatrix} $$ 令 $A = \begin{bmatrix} a & 0 \\ b & 1 \end{bmatrix}$,$B = \begin{bmatrix} c & 0 \\ d & 1 \end{bmatrix}$,则答案为: $$ \begin{bmatrix} 1 & 1 \end{bmatrix} \times A^{m-1}\underbrace{(A^{m-1}B)(A^{m-1}B)\dots(A^{m-1}B)}_{n - 1 \text{ 个}} $$ 使用矩阵快速幂计算 $A^{m-1}$ 和 $(A^{m-1}B)^{n-1}$。需要使用「十进制快速幂」。 ::::warning[注意]{open} 矩阵乘法不具有交换律,故不能写作 $A^{n(m-1)} B^{n-1}$。 :::: ::::success[Code] ```cpp #include <bits/stdc++.h> #define endl '\n' //#define int long long using namespace std; typedef long long ll; const int Mod = 1e9 + 7; struct Matrix { ll a[2][2]; Matrix () { memset(a, 0, sizeof a); } Matrix operator*(const Matrix& b) const { Matrix c; c.a[0][0] = (a[0][0] * b.a[0][0] + a[0][1] * b.a[1][0]) % Mod; c.a[0][1] = (a[0][0] * b.a[0][1] + a[0][1] * b.a[1][1]) % Mod; c.a[1][0] = (a[1][0] * b.a[0][0] + a[1][1] * b.a[1][0]) % Mod; c.a[1][1] = (a[1][0] * b.a[0][1] + a[1][1] * b.a[1][1]) % Mod; return c; } }; Matrix qpow(Matrix bs, string n) { Matrix res; res.a[0][0] = res.a[1][1] = 1; for (int i = (int)n.size() - 1; i >= 0; i --) { if (n[i] - '0') { int x = n[i] - '0'; Matrix bss = bs; while (x) { if (x & 1) res = res * bss; bss = bss * bss, x >>= 1; } } int x = 9; Matrix bss = bs; while (x) { if (x & 1) bs = bs * bss; bss = bss * bss, x >>= 1; } } return res; } string change(string s) { for (int i = (int)s.size() - 1; i >= 0; i --) { if (s[i] != '0') { s[i] --; break; } else { s[i] = '9'; } } return s; } signed main() { ios::sync_with_stdio(0), cin.tie(0); string n, m; int a, b, c, d; cin >> n >> m >> a >> b >> c >> d; Matrix A, B; A.a[0][0] = a, A.a[1][0] = b, A.a[1][1] = 1; B.a[0][0] = c, B.a[1][0] = d, B.a[1][1] = 1; Matrix nA = qpow(A, change(m)); Matrix res = qpow(nA * B, change(n)) * nA; cout << (res.a[0][0] + res.a[1][0]) % Mod; return 0; } ``` :::: ### 2.2 优化动态规划 ##### [P2106 Sam 数](https://www.luogu.com.cn/problem/P2106) 考虑朴素 dp,设 $f_{i,j}$ 表示考虑前 $i$ 位,第 $i$ 位为 $j$ 的方案数。易得转移 $f_{i,j} \gets \sum\limits_{|j - k| \le 2} f_{i-1,k}$。 容易得到转移矩阵 $B$,具体地,若 $f_{i-1,k}$ 可以转移到 $f_{i,j}$,则 $B_{j,i} = 1$。转移 $k-1$ 次得到 $B^{k-1}$。 初始矩阵为 $\begin{bmatrix}0 & 1 & 1 & \dots & 1\end{bmatrix}$,因为不能有前导零。特别地,当 $k=1$ 时答案为 $10$。 ::::success[Code] ```cpp #include <bits/stdc++.h> #define endl '\n' using namespace std; const int Mod = 1e9 + 7; struct Matrix { long long a[10][10]; Matrix () { memset(a, 0, sizeof a); } Matrix operator*(const Matrix& b) const { Matrix c; for (int i = 0; i < 10; i ++) { for (int j = 0; j < 10; j ++) { for (int k = 0; k < 10; k ++) { c.a[i][j] += a[i][k] * b.a[k][j]; } c.a[i][j] %= Mod; } } return c; } } bs; void init() { for (int i = 0; i < 10; i ++) { for (int j = max(0, i - 2); j <= min(9, i + 2); j ++) { bs.a[j][i] = 1; } } } Matrix get(long long n, Matrix b) { Matrix res; for (int i = 0; i < 10; i ++) res.a[i][i] = 1; while (n) { if (n & 1) res = res * b; b = b * b, n >>= 1; } return res; } signed main() { // freopen("", "r", stdin); // freopen("", "w", stdout); // ios::sync_with_stdio(0), cin.tie(0); long long k; cin >> k; if (k == 1) { cout << 10; return 0; } init(); Matrix res = get(k - 1, bs); long long ans = 0; for (int i = 0; i < 10; i ++) for (int j = 1; j < 10; j ++) ans = (ans + res.a[i][j]) % Mod; cout << ans; return 0; } ``` :::: --- ##### [P3216 [HNOI2011] 数学作业](https://www.luogu.com.cn/problem/P3216) 有转移 $f_i = f_{i-1} \times 10^k + i$,其中 $k = \log_{10}i$。写成矩阵形式: $$ \begin{bmatrix} f_i & i & 1 \end{bmatrix} = \begin{bmatrix} f_{i-1} & i-1 & 1 \end{bmatrix} \times \begin{bmatrix} 10^k & 0 & 0 \\ 1 & 1 & 0 \\ 1 & 1 & 1 \end{bmatrix} $$ 枚举 $k$ 分段做矩阵快速幂。 ::::success[Code] ```cpp #include <bits/stdc++.h> #define endl '\n' #define int long long using namespace std; int Mod; struct Matrix { int a[3][3]; Matrix () { memset(a, 0, sizeof a); } Matrix operator*(const Matrix& b) const { Matrix c; c.a[0][0] = (a[0][0] * b.a[0][0] + a[0][1] * b.a[1][0] + a[0][2] * b.a[2][0]) % Mod; c.a[0][1] = (a[0][0] * b.a[0][1] + a[0][1] * b.a[1][1] + a[0][2] * b.a[2][1]) % Mod; c.a[0][2] = (a[0][0] * b.a[0][2] + a[0][1] * b.a[1][2] + a[0][2] * b.a[2][2]) % Mod; c.a[1][0] = (a[1][0] * b.a[0][0] + a[1][1] * b.a[1][0] + a[1][2] * b.a[2][0]) % Mod; c.a[1][1] = (a[1][0] * b.a[0][1] + a[1][1] * b.a[1][1] + a[1][2] * b.a[2][1]) % Mod; c.a[1][2] = (a[1][0] * b.a[0][2] + a[1][1] * b.a[1][2] + a[1][2] * b.a[2][2]) % Mod; c.a[2][0] = (a[2][0] * b.a[0][0] + a[2][1] * b.a[1][0] + a[2][2] * b.a[2][0]) % Mod; c.a[2][1] = (a[2][0] * b.a[0][1] + a[2][1] * b.a[1][1] + a[2][2] * b.a[2][1]) % Mod; c.a[2][2] = (a[2][0] * b.a[0][2] + a[2][1] * b.a[1][2] + a[2][2] * b.a[2][2]) % Mod; return c; } }; Matrix qpow(Matrix bs, int b) { Matrix res; res.a[0][0] = res.a[1][1] = res.a[2][2] = 1; while (b) { if (b & 1) res = res * bs; bs = bs * bs, b >>= 1; } return res; } signed main() { // freopen("", "r", stdin); // freopen("", "w", stdout); ios::sync_with_stdio(0), cin.tie(0); int n; cin >> n >> Mod; long long p = 1; int now = 0; for (int i = 1; ; i ++) { Matrix b; b.a[0][0] = now, b.a[1][1] = (p - 1) % Mod, b.a[2][2] = 1; Matrix bs; bs.a[0][0] = p % Mod * 10 % Mod, bs.a[1][0] = bs.a[1][1] = bs.a[2][0] = bs.a[2][1] = bs.a[2][2] = 1; Matrix res; if (p - 1 >= n / 10 || (n % 10 != 0 && p - 1 >= n / 10)) { res = b * qpow(bs, n - p + 1); now = (res.a[0][0] + res.a[1][0] + res.a[2][0]) % Mod; break; } else { res = b * qpow(bs, p * 9); p *= 10; now = (res.a[0][0] + res.a[1][0] + res.a[2][0]) % Mod; } } cout << now; return 0; } ``` :::: --- ##### 模拟赛题,来源未知 ::::info[题意]{open} 给定 $n$ 位二进制数 $S$ 和常数 $k$,求 $$\sum\limits_{x = 0}^S [\operatorname{popcount}(x) = k] \times F_x$$ 其中 $\operatorname{popcount}(x)$ 为 $x$ 的二进制下 $1$ 的数量,$F_x$ 为斐波那契数列中的第 $x$ 项。具体地,$F_0 = F_1 = 1,F_n = F_{n-1} + F_{n-2}$。 $1 \le k \le n \le 3 \times 10 ^ 3$,保证 $S$ 无前导零。 :::: 本题考察矩阵乘法的结合律。 对于 $i \in [0,n)$,预处理 $F_{2^i}$ 的转移矩阵 $G_i = \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix}^{2^i} = G_{i-1}^2$ 即可快速求出 $F_x$。 先考虑 $S$ 全为 $1$ 的情况。此时相当于在 $n$ 位数中选择 $k$ 位的所有方案的转移矩阵乘积之和,设 $f_{i,j}$ 表示前 $i$ 位选 $j$ 位的答案矩阵,容易写出 $O(nk)$ 的 dp。 对于 $x \le S$ 的限制,类似数位 dp,在状态中加入一维 $0/1$ 表示前 $i$ 位是否紧贴上界即可。时间复杂度 $O(nk)$,带 $2^4$ 常数。 ::::success[Code] ```cpp #include <bits/stdc++.h> #define ll long long #define endl '\n' using namespace std; const int N = 2005, Mod = 1e9 + 7; char s[N]; struct Mat { ll a[2][2]; Mat () { memset(a, 0, sizeof a); } Mat operator*(const Mat& b) const { Mat c; for (int i = 0; i < 2; i ++) for (int j = 0; j < 2; j ++) c.a[i][j] = (a[i][0] * b.a[0][j] + a[i][1] * b.a[1][j]) % Mod; return c; } void operator+=(const Mat& b) { for (int i = 0; i < 2; i ++) for (int j = 0; j < 2; j ++) a[i][j] = (a[i][j] + b.a[i][j]) % Mod; } void build() { for (int i = 0; i < 2; i ++) a[i][i] = 1; } } g[N], f[2][N][2]; signed main() { ios::sync_with_stdio(0); cin.tie(0); int n, k; cin >> n >> k; for (int i = 1; i <= n; i ++) cin >> s[i]; g[n - 1].a[0][0] = g[n - 1].a[0][1] = g[n - 1].a[1][0] = 1; for (int i = n - 2; i >= 0; i --) g[i] = g[i + 1] * g[i + 1]; int now = 0, nx = 1; f[0][0][1].build(); for (int i = 0; i < n; i ++) { for (int j = 0; j <= min(i, k); j ++) { for (int l = 0; l < 2; l ++) { f[nx][j][l & (s[i + 1] == '0')] += f[now][j][l]; if (j + 1 <= k && (!l || s[i + 1] == '1')) { f[nx][j + 1][l & (s[i + 1] == '1')] += f[now][j][l] * g[i]; } f[now][j][l] = Mat(); } } swap(now, nx); } cout << (f[now][k][0].a[0][0] + f[now][k][1].a[0][0]) % Mod; return 0; } ``` :::: --- ##### [P1357 花园](https://www.luogu.com.cn/problem/P1357) 矩阵加速状压 dp。 ::::info[题意]{open} 给定 $n,m,k$,试求有多少长度为 $n$ 的**环状** $\texttt{01}$ 串满足任意长度为 $k$ 的子串中 $1$ 的数量不超过 $k$。 $2 \le n \le 10^{15}, 2 \le m \le \min(n, 5), 1 \le k < m$。 :::: $m$ 较小,考虑状压。设 $f_{i,S}$ 表示考虑前 $i$ 位,**最后 $m$ 位**的状态为 $S$ 的方案数。 若可以从 $f_{i-1,S'}$ 转移而来,易得 $S'$ 的后 $m-1$ 位必须与 $S$ 的前 $m-1$ 位相同,于是有转移 $$ f_{i,S} \gets f_{i-1, S >> 1} + f_{i-1, (S >> 1) + 2^{m-1}} $$ 对于环上问题,我们要求第 $n + 1$ 位的状态与第 $1$ 位相同。即枚举初始状态 $T$,答案取 $f_{n+1,T}$。直接实现的时间复杂度为 $O(2^m n)$。 --- 注意到状态 $i$ 到状态 $j$ 是否转移是固定的,考虑构造一个 $2 ^ m \times 2 ^ m$ 的转移矩阵 $B$,使: $$ \begin{bmatrix} f_{i,S_1} & f_{i,S_2} & \dots & \end{bmatrix} = \begin{bmatrix} f_{i-1,S_1} & f_{i-1,S_2} & \dots & \end{bmatrix} \times B $$ 转移矩阵的构造方法:当状态 $j$ 可以转移到状态 $i$ 时,$B_{i,j} = 1$,否则为 $0$。 由于最终状态必须与初始状态相同,答案即为 $\sum\limits_{i=1}^n B^n_{i,i}$。 时间复杂度 $O(2^{3m} \log n)$。 ::::success[Code] ```cpp #include <bits/stdc++.h> #define endl '\n' using namespace std; typedef long long ll; const int N = 1e5 + 5, Mod = 1e9 + 7; int f[N][40]; int h[N][40]; ll n; int m, k; struct Matrix { int a[32][32]; Matrix () { memset(a, 0, sizeof a); } Matrix operator*(const Matrix& other) const { Matrix res; for (int i = 0; i < (1 << m); i ++) { for (int j = 0; j < (1 << m); j ++) { for (int k = 0; k < (1 << m); k ++) { res.a[i][j] = (0ll + res.a[i][j] + 1ll * a[i][k] * other.a[k][j] % Mod) % Mod; } } } return res; } } bs; signed main() { ios::sync_with_stdio(0), cin.tie(0); cin >> n >> m >> k; for (int i = 0; i < (1 << m); i ++) { if (__builtin_popcount(i) > k) continue; if (__builtin_popcount(i >> 1) <= k) bs.a[i >> 1][i] = 1; int l = (i >> 1) + (1 << (m - 1)); if (__builtin_popcount(l) <= k) bs.a[l][i] = 1; } Matrix res; for (int i = 0; i < (1 << m); i ++) res.a[i][i] = 1; ll x = n; while (x) { if (x & 1) res = res * bs; bs = bs * bs, x >>= 1; } int ans = 0; for (int i = 0; i < (1 << m); i ++) { ans = (ans + res.a[i][i]) % Mod; } cout << ans; return 0; } ``` :::: ### 2.3 优化图上问题 ##### [P2886 [USACO07NOV] Cow Relays G](https://www.luogu.com.cn/problem/P2886) ::::info[题意]{open} 给定 $m$ 条边 $(u_i, v_i, w_i)$ 的简单无向连通图,求 $s$ 到 $t$ 经过 $N$ 条边的最短路。 :::: 考虑 Floyd 的过程: ```cpp for (int k = 1; k <= n; k ++) { for (int i = 1; i <= n; i ++) { for (int j = 1; j <= n; j ++) { f[i][j] = min(f[i][j], f[i][k] + f[k][j] + w(i, j)); } } } ``` 我们这样写: ```cpp for (int k = 1; k <= n; k ++) { for (int i = 1; i <= n; i ++) { for (int j = 1; j <= n; j ++) { w[i][j] = min(w[i][j], w[i][k] + w[k][j]); } } } ``` 其中 $w_{i,j}$ 为图上边 $(i,j)$ 的边权(若不存在则为无穷大)。 经过一次这样的乘法,$w_{i,j}$ 即为从 $i$ 到 $j$ 经过 $2$ 条边的最短路。推广可知,转移 $k$ 次后得到的是经过 $k + 1$ 条边的最短路。 #### 定义 **Min-plus 矩阵乘法**: $$ C_{i,j} = \min_{k=1}^n \{a_{i,k} + b_{k,j}\} $$ 得到的 $C$ 为两个 $n \times n$ 的矩阵 $A$ 与 $B$ 相乘的结果。成立由于 $\min$ 具有结合律。 转移 $N - 1$ 次即得答案。使用矩阵快速幂,时间复杂度 $O(T^3 \log N)$。 ::::success[Code] ```cpp #include <bits/stdc++.h> #define endl '\n' #define INF 0x3f3f3f3f using namespace std; const int N = 205, M = 1e6 + 5; int U[M], V[M], w[M]; int id[N], cnt; int f[N][N]; int n, t; struct Matrix { int a[N][N]; Matrix () { memset(a, INF, sizeof a); } Matrix operator*(const Matrix b) const { Matrix c; for (int k = 1; k <= t; k ++) { for (int i = 1; i <= t; i ++) { for (int j = 1; j <= t; j ++) { c.a[i][j] = min(c.a[i][j], a[i][k] + b.a[k][j]); } } } return c; } } bs, ans; signed main() { // freopen("", "r", stdin); // freopen("", "w", stdout); ios::sync_with_stdio(0), cin.tie(0); int m, S, T; cin >> n >> m >> S >> T; for (int i = 1; i <= m; i ++) { cin >> w[i] >> U[i] >> V[i]; id[ ++ cnt] = U[i], id[ ++ cnt] = V[i]; } sort(id + 1, id + cnt + 1); t = unique(id + 1, id + cnt + 1) - id - 1; S = lower_bound(id + 1, id + t + 1, S) - id, T = lower_bound(id + 1, id + t + 1, T) - id; for (int i = 1; i <= m; i ++) { U[i] = lower_bound(id + 1, id + t + 1, U[i]) - id, V[i] = lower_bound(id + 1, id + t + 1, V[i]) - id; bs.a[U[i]][V[i]] = bs.a[V[i]][U[i]] = min(bs.a[U[i]][V[i]], w[i]); } Matrix ans = bs; n --; while (n) { if (n & 1) ans = ans * bs; bs = bs * bs, n >>= 1; } cout << ans.a[S][T]; return 0; } ``` :::: --- ##### [P3758 [TJOI2017] 可乐](https://www.luogu.com.cn/problem/P3758) ::::info[题意]{open} 给定 $n$ 个点 $m$ 条边的无向图和正整数 $t$,初始位于 $1$,每秒选择一种操作进行: + 前进:从 $u$ 走到与它相向的点 $v$; + 停留; + 结束。 若第 $t$ 秒未结束,则在第 $t + 1$ 秒结束。 问 $t$ 秒后的不同行动方案数。两种方案不同当且仅当结束时刻不同或某一时刻所在节点不同。 :::: 考虑这样一个问题:从 $s$ 出发走 $t$ 步(必须走)的方案数。设 $f_{i,j}$ 表示从 $i$ 走到 $j$ 的方案数,则转移为: $$ f_{i,j} = \sum\limits_{k=1}^n (f_{i,k} \times f_{k,j}) $$ 发现这与矩阵乘法一致,于是设 $f$ 表示走一步的方案数矩阵,$f^2$ 即可代表走两步的方案数……依此类推,$f^t$ 即为走 $t$ 步。那么答案即为 $\sum\limits_{i=1}^n f^t_{s,i}$。 时间复杂度 $O(n^3 \log t)$。 --- 回到原问题,停留可以视为自环,但是结束似乎不好做。 考虑对每个位置 $i$ 添加边 $i \to 0$,并在 $0$ 上连自环。由于 $0$ 没有连向其他点的边,结束操作就被转化为:走到 $0$ 并一直停留在该状态。 很妙的 Trick。 ::::success[Code] ```cpp #include <bits/stdc++.h> #define endl '\n' using namespace std; typedef long long ll; const int N = 105, Mod = 2017; int n; struct Matrix { int a[N][N]; Matrix() { memset(a, 0, sizeof a); } Matrix operator*(const Matrix& b) const { Matrix c; for (int i = 0; i <= n; i ++) { for (int j = 0; j <= n; j ++) { for (int k = 0; k <= n; k ++) { c.a[i][j] += a[i][k] * b.a[k][j]; } c.a[i][j] %= Mod; } } return c; } } bs; signed main() { // freopen("", "r", stdin); // freopen("", "w", stdout); ios::sync_with_stdio(0), cin.tie(0); int m; cin >> n >> m; for (int i = 1; i <= m; i ++) { int u, v; cin >> u >> v; bs.a[u][v] = bs.a[v][u] = 1; } for (int i = 0; i <= n; i ++) bs.a[i][i] = 1, bs.a[i][0] = 1; int t; cin >> t; Matrix res; for (int i = 0; i <= n; i ++) res.a[i][i] = 1; while (t) { if (t & 1) res = res * bs; bs = bs * bs, t >>= 1; } int ans = 0; for (int i = 0; i <= n; i ++) ans = (ans + res.a[1][i]) % Mod; cout << ans; return 0; } ``` :::: --- ##### [P2233 [HNOI2002] 公交车路线](https://www.luogu.com.cn/problem/P2233) 类似上一题,考虑连边 $i \to next$、$i \to prev$,对于 $i = 5$(E)则不向外连边。 由于到 $5$ 结束,转化为 $n-1$ 步走到 $4$ 或 $6$ 的方案数和。 --- **模拟赛题,来源未知** ::::info[题意]{open} 给定 $x,y$ 和 $n$ 个节点的树,并给定长度为 $n$ 的正整数序列 $a$,定义递推式: $$ f_i = \begin{cases} i & 0 \le i \le 1 \\ x \times f_{i-1} + y \times f_{i-2} & i > 1 \end{cases} $$ 对于 $1 \le i \le n$,对于其子树内的无序点对 $(x,y)$($a_x \neq a_y$),求: $$ \sum f_{|a_x-a_y|} \bmod 998244353 $$ $1 \le n \le 10^5, 1 \le x,y,a_i < 998244353$。 :::: 考虑加入一个点 $x$ 对答案的贡献: $$ \sum_{a_y < a_x} f_{a_x - a_y} + \sum_{a_y > a_x} f_{a_y - a_x} $$ 把递推式写成矩阵形式: $$ \begin{bmatrix} f_i & f_{i-1} \end{bmatrix} = \begin{bmatrix} f_{i-1} & f_{i-2} \end{bmatrix} \times \begin{bmatrix} x & 1 \\ y & 0 \end{bmatrix} $$ 记转移矩阵为 $A$。则贡献为: $$ (\sum_{a_y < a_x} A^{a_x}) \times A^{-a_y} + (\sum_{a_y > a_x} A^y) \times A^x $$ ::::warning[注意]{open} 矩阵乘法具有分配律,不具有交换律。 :::: 容易求出 $A$ 的逆矩阵 $B = A^{-1} = \begin{bmatrix} 0 & \frac{1}{y} \\ 1 & \frac{x}{y}\end{bmatrix}$。dsu on tree 动态维护,树状数组维护 $\sum A^{x}$ 和 $\sum B^{x}$。时间复杂度 $O(n \log^2 n)$。 ::::success[Code] ```cpp #include <bits/stdc++.h> #define endl '\n' using namespace std; typedef long long ll; const int N = 5e5 + 5, Mod = 998244353; int n, t; int a[N], a1[N], id[N]; int dfn[N], siz[N], rnk[N], times, son[N]; ll ans[N], now; vector<int> G[N]; void Add(ll &x, ll v) { x += v; if (x >= Mod) x -= Mod; } void Del(ll &x, ll v) { x -= v; if (x < 0) x += Mod; } struct Matrix { ll a[2][2]; Matrix () { memset(a, 0, sizeof a); } Matrix operator*(const Matrix& b) const { Matrix c; c.a[0][0] = (a[0][0] * b.a[0][0] + a[0][1] * b.a[1][0]) % Mod; c.a[0][1] = (a[0][0] * b.a[0][1] + a[0][1] * b.a[1][1]) % Mod; c.a[1][0] = (a[1][0] * b.a[0][0] + a[1][1] * b.a[1][0]) % Mod; c.a[1][1] = (a[1][0] * b.a[0][1] + a[1][1] * b.a[1][1]) % Mod; return c; } void operator+=(const Matrix &b) { Add(a[0][0], b.a[0][0]), Add(a[0][1], b.a[0][1]), Add(a[1][0], b.a[1][0]), Add(a[1][1], b.a[1][1]); } void operator-=(const Matrix &b) { Del(a[0][0], b.a[0][0]), Del(a[0][1], b.a[0][1]), Del(a[1][0], b.a[1][0]), Del(a[1][1], b.a[1][1]); } Matrix operator+(const Matrix& b) const { Matrix c; c.a[0][0] = a[0][0], c.a[0][1] = a[0][1], c.a[1][0] = a[1][0], c.a[1][1] = a[1][1]; c += b; return c; } Matrix operator-(const Matrix& b) const { Matrix c; c.a[0][0] = a[0][0], c.a[0][1] = a[0][1], c.a[1][0] = a[1][0], c.a[1][1] = a[1][1]; c -= b; return c; } } bs, bsT, F[2][N]; Matrix get(int n, Matrix b) { Matrix res; res.a[0][0] = res.a[1][1] = 1; while (n) { if (n & 1) res = res * b; b = b * b, n >>= 1; } return res; } struct BIT { Matrix T[N]; void update(int x, int v, int f) { Matrix fst = F[f][x]; for (; x <= t; x += (x & (-x))) { if (v == 1) T[x] += fst; else T[x] -= fst; } } Matrix query(int x) { Matrix res; for (; x; x -= (x & (-x))) res += T[x]; return res; } } T1, T2; void dfs1(int u, int fa) { dfn[u] = ++ times, siz[u] = 1, rnk[times] = u; for (int i = 0; i < (int)G[u].size(); i ++) { int v = G[u][i]; if (v == fa) continue; dfs1(v, u); if (!son[u] || siz[v] > siz[son[u]]) son[u] = v; siz[u] += siz[v]; } } void add(int x) { T1.update(id[x], 1, 0), T2.update(id[x], 1, 1); Add(now, ((T1.query(t) - T1.query(id[x])) * F[1][id[x]] + F[0][id[x]] * T2.query(id[x] - 1)).a[0][1]); } void del(int x) { Del(now, ((T1.query(t) - T1.query(id[x])) * F[1][id[x]] + F[0][id[x]] * T2.query(id[x] - 1)).a[0][1]); T1.update(id[x], -1, 0), T2.update(id[x], -1, 1); } void dfs2(int u, int fa, bool keep) { for (auto v : G[u]) { if (v == fa || v == son[u]) continue; dfs2(v, u, 0); } if (son[u]) dfs2(son[u], u, 1); for (auto v : G[u]) { if (v == fa || v == son[u]) continue; for (int i = dfn[v]; i <= dfn[v] + siz[v] - 1; i ++) add(rnk[i]); } add(u); ans[u] = now; if (!keep) { for (int i = dfn[u]; i <= dfn[u] + siz[u] - 1; i ++) del(rnk[i]); } } int qpow(ll a, int b) { ll res = 1; while (b) { if (b & 1) res = res * a % Mod; a = a * a % Mod, b >>= 1; } return res; } signed main() { ios::sync_with_stdio(0), cin.tie(0); ll x, y; int sub; cin >> n >> x >> y >> sub; for (int i = 1; i <= n; i ++) cin >> a[i], a1[i] = a[i]; sort(a1 + 1, a1 + n + 1); t = unique(a1 + 1, a1 + n + 1) - a1 - 1; for (int i = 1; i <= n; i ++) id[i] = lower_bound(a1 + 1, a1 + t + 1, a[i]) - a1; bs.a[0][0] = x, bs.a[1][0] = y, bs.a[0][1] = 1; ll inv = qpow(y, Mod - 2); bsT.a[1][0] = 1, bsT.a[0][1] = inv, bsT.a[1][1] = Mod - x * inv % Mod; for (int i = 1; i <= t; i ++) { F[0][i] = get(a1[i], bs), F[1][i] = get(a1[i], bsT); } for (int i = 1; i < n; i ++) { int u, v; cin >> u >> v; G[u].push_back(v), G[v].push_back(u); } dfs1(1, 0); dfs2(1, 0, 1); for (int i = 1; i <= n; i ++) cout << ans[i] << endl; return 0; } ``` :::: (图上问题题目有待补充,[这里](https://www.luogu.com.cn/training/7403)有一些) ## 结语 这篇文章只触及了矩阵加速的冰山一角,更广的应用 / 更难的题目还待读者自己探索。 如发现文章有错误 / 有对本文的建议,还望指出,非常感谢!