矩阵快速幂及其应用

矩阵快速幂广泛应用于将线性递推关系(如斐波那契数列)、图论中的路径计数、带固定状态的动态规划问题(如铺砖或字符串构造)等场景,转化为矩阵的高次幂运算,从而在 \( O (\log n)\) 时间内高效求解第 \( n \) 项或长度为 \( n \) 的方案数。

矩阵快速幂

由于矩阵乘法满足结合律,我们仍可类比标量快速幂的思想:只需将快速幂中的乘法运算替换为矩阵乘法,并将初始结果设为单位矩阵 \( I \),即可高效实现矩阵的快速幂运算。

矩阵操作封装模板

// 矩阵大小,记得改初始矩阵大小
const int SIZE = 100;
struct Matrix
{
    int ROW, COL; // 行数, 列数
    ll M[SIZE + 1][SIZE + 1];

    // 构造函数
    Matrix(int _ROW = 0, int _COL = 0)
    {
        ROW = _ROW;
        COL = _COL;
        clear();
    }

    // 清零,生成零矩阵
    void clear() { memset(M, 0, sizeof(M)); }

    // 初始化,生成大小为 n 的单位矩阵 (必须是方阵)
    void init()
    {
        clear();
        for (int i = 1; i <= ROW; ++i)
            M[i][i] = 1;
    }

    // 重载 * 运算符 (A 是 N * K 矩阵,B 是 K * M 矩阵)
    Matrix friend operator*(const Matrix &A, const Matrix &B)
    {
        // 前提:A 的列数必须等于 B 的行数
        // 结果:矩阵 ans 的大小为 A.ROW * B.COL
        Matrix ans(A.ROW, B.COL);

        for (int i = 1; i <= A.ROW; ++i)
        {
            for (int k = 1; k <= A.COL; ++k)
            {
                if (A.M[i][k] == 0)
                    continue;

                for (int j = 1; j <= B.COL; ++j)
                    ans.M[i][j] = (ans.M[i][j] + A.M[i][k] * B.M[k][j]) % MOD;
            }
        }
        return ans;
    }

    // 重载 + 运算符 (两个矩阵维度完全一致)
    Matrix friend operator+(const Matrix &A, const Matrix &B)
    {
        Matrix ans(A.ROW, A.COL);

        for (int i = 1; i <= A.ROW; ++i)
            for (int j = 1; j <= A.COL; ++j)
                ans.M[i][j] = (A.M[i][j] + B.M[i][j]) % MOD;

        return ans;
    }

    // 重载 ^ 运算符,实现矩阵快速幂
    Matrix friend operator^(Matrix base, ll exp)
    {
        Matrix ans(base.ROW, base.ROW);
        ans.init(); // 生成与 base 同阶的单位矩阵

        while (exp)
        {
            if (exp & 1)
                ans = ans * base;
            base = base * base;
            exp >>= 1;
        }

        return ans;
    }
};

矩阵加速递推

如果有一道题目让你求斐波那契数列第 \( n \) 项的值,最简单的方法莫过于直接递推了。但是如果 \( n \) 的范围达到了 \( 10^{18}\) 级别,递推就不行了,此时我们可以考虑矩阵加速递推。

1. 斐波那契数列

链接:https://www.luogu.com.cn/problem/P1962

思路与推导:

在斐波那契数列当中,\( F_1 = F_2 = 1 \),\( F_n = F_{n − 1} + F_{n − 2}(i ≥ 3)\)。

设 \(\text {Fib}(n)\) 表示一个 \( 1 \times 2 \) 的矩阵 \(\begin {bmatrix} F_n & F_{n-1} \end {bmatrix}\)。我们希望根据 \(\text {Fib}(n-1) = \begin {bmatrix} F_{n-1} & F_{n-2} \end {bmatrix}\) 推出 \(\text {Fib}(n)\)。

试推导一个矩阵 \(\text {base}\),使得 \(\text {Fib}(n-1) \times \text {base} = \text {Fib}(n)\), 即 \(\begin {bmatrix} F_{n-1} & F_{n-2} \end {bmatrix} \times \text {base} = \begin {bmatrix} F_n & F_{n-1} \end {bmatrix}\)。

怎么推呢?因为 \( F_n = F_{n-1} + F_{n-2}\),所以 \(\text {base}\) 矩阵的第一列应该是 \(\begin {bmatrix} 1 \\ 1 \end {bmatrix}\), 这样在进行矩阵乘法运算的时候才能令 \( F_{n-1}\) 与 \( F_{n-2}\) 相加,从而得出 \( F_n \)。

此时得到 \(\text {base} = \begin {bmatrix} 1 & x \\ 1 & y \end {bmatrix}\),代入原式得 \(\begin {bmatrix} F_{n-1} & F_{n-2} \end {bmatrix} \times \begin {bmatrix} 1 & x \\ 1 & y \end {bmatrix} = \begin {bmatrix} F_{n-1} + F_{n-2} & x \times F_{n-1} + y \times F_{n-2} \end {bmatrix} = \begin {bmatrix} F_n & F_{n-1} \end {bmatrix}\),

解得 \( x = 1, y = 0 \),因此 \(base\) 矩阵就为 \(\begin {bmatrix} 1 & 1 \\ 1 & 0 \end {bmatrix}\)。

综上所述:原式化为 \(\begin {bmatrix} F_{n-1} & F_{n-2} \end {bmatrix} \times \begin {bmatrix} 1 & 1 \\ 1 & 0 \end {bmatrix} = \begin {bmatrix} F_n & F_{n-1} \end {bmatrix}\)。转化为代码,应该怎么求呢?

定义初始矩阵 \(\text {start} = \begin {bmatrix} F_2 & F_1 \end {bmatrix} = \begin {bmatrix} 1 & 1 \end {bmatrix}\), \(\text {base} = \begin {bmatrix} 1 & 1 \\ 1 & 0 \end {bmatrix}\)。那么, \( F_n \) 就等于 \(\text {start} \times \text {base}^{n-2}\) 这个矩阵的第一行第一列元素,也就是 \(\begin {bmatrix} 1 & 1 \end {bmatrix} \times \begin {bmatrix} 1 & 1 \\ 1 & 0 \end {bmatrix}^{n-2}\) 的第一行第一列元素。

注意:矩阵乘法不满足交换律,所以一定不能写成 \(\begin {bmatrix} 1 & 1 \\ 1 & 0 \end {bmatrix}^{n-2} \times \begin {bmatrix} 1 & 1 \end {bmatrix}\) 的第一行第一列元素。

另外,对于 \( n \leq 2 \) 的情况,直接输出 1 即可,不需要执行矩阵快速幂。

为什么要乘上 \(\text {base}\) 矩阵的 \( n-2 \) 次方而不是 \( n \) 次方呢?因为 \( F_1, F_2 \) 是不需要进行矩阵乘法就能求出的。也就是说,如果只进行一次乘法,就已经求出 \( F_3 \) 了。

代码:

// 矩阵大小,记得改初始矩阵大小
const int SIZE = 100;
struct Matrix
{
    int ROW, COL; // 行数, 列数
    ll M[SIZE + 1][SIZE + 1];

    // 构造函数
    Matrix(int _ROW = 0, int _COL = 0)
    {
        ROW = _ROW;
        COL = _COL;
        clear();
    }

    // 清零,生成零矩阵
    void clear() { memset(M, 0, sizeof(M)); }

    // 初始化,生成大小为 n 的单位矩阵 (必须是方阵)
    void init()
    {
        clear();
        for (int i = 1; i <= ROW; ++i)
            M[i][i] = 1;
    }

    // 重载 * 运算符 (A 是 N * K 矩阵,B 是 K * M 矩阵)
    Matrix friend operator*(const Matrix &A, const Matrix &B)
    {
        // 前提:A 的列数必须等于 B 的行数
        // 结果:矩阵 ans 的大小为 A.ROW * B.COL
        Matrix ans(A.ROW, B.COL);

        for (int i = 1; i <= A.ROW; ++i)
        {
            for (int k = 1; k <= A.COL; ++k)
            {
                if (A.M[i][k] == 0)
                    continue;

                for (int j = 1; j <= B.COL; ++j)
                    ans.M[i][j] = (ans.M[i][j] + A.M[i][k] * B.M[k][j]) % MOD;
            }
        }
        return ans;
    }

    // 重载 + 运算符 (两个矩阵维度完全一致)
    Matrix friend operator+(const Matrix &A, const Matrix &B)
    {
        Matrix ans(A.ROW, A.COL);

        for (int i = 1; i <= A.ROW; ++i)
            for (int j = 1; j <= A.COL; ++j)
                ans.M[i][j] = (A.M[i][j] + B.M[i][j]) % MOD;

        return ans;
    }

    // 重载 ^ 运算符,实现矩阵快速幂
    Matrix friend operator^(Matrix base, ll exp)
    {
        Matrix ans(base.ROW, base.ROW);
        ans.init(); // 生成与 base 同阶的单位矩阵

        while (exp)
        {
            if (exp & 1)
                ans = ans * base;
            base = base * base;
            exp >>= 1;
        }

        return ans;
    }
};

void miyan()
{
    ll n;
    cin >> n;

    if (n <= 2)
    {
        cout << 1 << endl;
        return;
    }

    Matrix base(2, 2);
    base.clear();
    base.M[1][1] = base.M[2][1] = base.M[1][2] = 1;

    base = base ^ (n - 2);

    Matrix start(1, 2);
    start.M[1][1] = start.M[1][2] = 1;

    auto ans = start * base;

    cout << ans.M[1][1] << endl;
}

2. 广义斐波那契数列

链接:https://www.luogu.com.cn/problem/P1349

思路:

本题与斐波那契数列的区别为该题中 \( F_1 = a_1 \), \( F_2 = a_2 \),\( F_n = p \times F_{n − 1} + q \times F_{n − 2} (i ≥ 3)\)。

定义 \(base\) 为二阶方阵,由 \( F_n = p \times F_{n − 1} + q \times F_{n − 2}\),可知 \(base\) 第一列为 \(\begin {bmatrix} p \\ q \end {bmatrix}\)。

此时得到 \(\text {base} = \begin {bmatrix} p & x \\ q & y \end {bmatrix}\),代入原式得 \(\begin {bmatrix} F_{n-1} & F_{n-2} \end {bmatrix} \times \begin {bmatrix} p & x \\ q & y \end {bmatrix} = \begin {bmatrix} p \times F_{n-1} + q \times F_{n-2} & x \times F_{n-1} + y \times F_{n-2} \end {bmatrix} = \begin {bmatrix} F_n & F_{n-1} \end {bmatrix}\),解得 \( x = 1, y = 0 \),因此 \(base\) 矩阵就为 \(\begin {bmatrix} p & 1 \\ q & 0 \end {bmatrix}\)。

综上所述:原式化为 \(\begin {bmatrix} F_{n-1} & F_{n-2} \end {bmatrix} \times \begin {bmatrix} p & 1 \\ q & 0 \end {bmatrix} = \begin {bmatrix} F_n & F_{n-1} \end {bmatrix}\)。剩余步骤与普通斐波那契数列相同。

代码:

int MOD;

// 矩阵大小,记得改初始矩阵大小
const int SIZE = 100;
struct Matrix
{
    int ROW, COL; // 行数, 列数
    ll M[SIZE + 1][SIZE + 1];

    // 构造函数
    Matrix(int _ROW = 0, int _COL = 0)
    {
        ROW = _ROW;
        COL = _COL;
        clear();
    }

    // 清零,生成零矩阵
    void clear() { memset(M, 0, sizeof(M)); }

    // 初始化,生成大小为 n 的单位矩阵 (必须是方阵)
    void init()
    {
        clear();
        for (int i = 1; i <= ROW; ++i)
            M[i][i] = 1;
    }

    // 重载 * 运算符 (A 是 N * K 矩阵,B 是 K * M 矩阵)
    Matrix friend operator*(const Matrix &A, const Matrix &B)
    {
        // 前提:A 的列数必须等于 B 的行数
        // 结果:矩阵 ans 的大小为 A.ROW * B.COL
        Matrix ans(A.ROW, B.COL);

        for (int i = 1; i <= A.ROW; ++i)
        {
            for (int k = 1; k <= A.COL; ++k)
            {
                if (A.M[i][k] == 0)
                    continue;

                for (int j = 1; j <= B.COL; ++j)
                    ans.M[i][j] = (ans.M[i][j] + A.M[i][k] * B.M[k][j]) % MOD;
            }
        }
        return ans;
    }

    // 重载 + 运算符 (两个矩阵维度完全一致)
    Matrix friend operator+(const Matrix &A, const Matrix &B)
    {
        Matrix ans(A.ROW, A.COL);

        for (int i = 1; i <= A.ROW; ++i)
            for (int j = 1; j <= A.COL; ++j)
                ans.M[i][j] = (A.M[i][j] + B.M[i][j]) % MOD;

        return ans;
    }

    // 重载 ^ 运算符,实现矩阵快速幂
    Matrix friend operator^(Matrix base, ll exp)
    {
        Matrix ans(base.ROW, base.ROW);
        ans.init(); // 生成与 base 同阶的单位矩阵

        while (exp)
        {
            if (exp & 1)
                ans = ans * base;
            base = base * base;
            exp >>= 1;
        }

        return ans;
    }
};

void miyan()
{
    ll p, q, a1, a2, n, m;
    cin >> p >> q >> a1 >> a2 >> n >> m;

    MOD = m;

    if (n == 1 || n == 2)
    {
        if (n == 1)
            cout << a1 % MOD << endl;
        else
            cout << a2 % MOD << endl;

        return;
    }

    Matrix base(2, 2);
    base.M[1][1] = p;
    base.M[2][1] = q;
    base.M[1][2] = 1;
    base.M[2][2] = 0;

    base = base ^ (n - 2);

    Matrix start(1, 2);
    start.M[1][1] = a2;
    start.M[1][2] = a1;

    auto ans = start * base;

    cout << ans.M[1][1] << endl;
}

3. 矩阵加速(数列)

链接:https://www.luogu.com.cn/problem/P1939

思路:

由递推数列

\(a_x = \begin{cases} 1 & x \in {1,2,3} \\ a_{x-1} + a_{x-3} & x \geq 4 \end{cases}\)

可得 \(base\) 矩阵第一列为 \(\begin {bmatrix} 1 \\ 0 \\ 1 \end {bmatrix}\),此时得到 \(\text {base} = \begin {bmatrix} 1 & a & d \\ 0 & b & e \\ 1 & c & f \end {bmatrix}\),

由 \(\begin {bmatrix} a_{x-1} & a_{x-2} & a_{x-3} \end {bmatrix} \times \begin {bmatrix} 1 & a & d \\ 0 & b & e \\ 1 & c & f \end {bmatrix} = \begin {bmatrix} a_{x} & a_{x-1} & a_{x-2} \end {bmatrix}\),

可得 \(\begin {bmatrix} a_{x-1} + a_{x-3} & a_{x-1} \times a + a_{x-2} \times b + a_{x-3} \times c & a_{x-1} \times d + a_{x-2} \times e + a_{x-3} \times f \end {bmatrix} = \begin {bmatrix} a_{x} & a_{x-1} & a_{x-2} \end {bmatrix}\)

手动代入前几项得 \(\text {base} = \begin {bmatrix} 1 & 1 & 0 \\ 0 & 0 & 1 \\ 1 & 0 & 0 \end {bmatrix}\),那么就有 \(\begin {bmatrix} a_{x-1} & a_{x-2} & a_{x-3} \end {bmatrix} \times \begin {bmatrix} 1 & 1 & 0 \\ 0 & 0 & 1 \\ 1 & 0 & 0 \end {bmatrix} = \begin {bmatrix} a_{x} & a_{x-1} & a_{x-2} \end {bmatrix}\)

定义初始矩阵 \(\text {start} = \begin {bmatrix} x_3 & x_2 & x_1 \end {bmatrix} = \begin {bmatrix} 1 & 1 & 1\end {bmatrix}\)。那么 \( x_n \) 就等于 \(\text {start} \times \text {base}^{n-3}\) 这个矩阵的第一行第一列元素,也就是 \(\begin {bmatrix} 1 & 1 & 1\end {bmatrix} \times \begin {bmatrix} 1 & 1 & 0 \\ 0 & 0 & 1 \\ 1 & 0 & 0 \end {bmatrix}^{n-3}\) 的第一行第一列元素。

代码:

// 矩阵大小,记得改初始矩阵大小
const int SIZE = 100;
struct Matrix
{
    int ROW, COL; // 行数, 列数
    ll M[SIZE + 1][SIZE + 1];

    // 构造函数
    Matrix(int _ROW = 0, int _COL = 0)
    {
        ROW = _ROW;
        COL = _COL;
        clear();
    }

    // 清零,生成零矩阵
    void clear() { memset(M, 0, sizeof(M)); }

    // 初始化,生成大小为 n 的单位矩阵 (必须是方阵)
    void init()
    {
        clear();
        for (int i = 1; i <= ROW; ++i)
            M[i][i] = 1;
    }

    // 重载 * 运算符 (A 是 N * K 矩阵,B 是 K * M 矩阵)
    Matrix friend operator*(const Matrix &A, const Matrix &B)
    {
        // 前提:A 的列数必须等于 B 的行数
        // 结果:矩阵 ans 的大小为 A.ROW * B.COL
        Matrix ans(A.ROW, B.COL);

        for (int i = 1; i <= A.ROW; ++i)
        {
            for (int k = 1; k <= A.COL; ++k)
            {
                if (A.M[i][k] == 0)
                    continue;

                for (int j = 1; j <= B.COL; ++j)
                    ans.M[i][j] = (ans.M[i][j] + A.M[i][k] * B.M[k][j]) % MOD;
            }
        }
        return ans;
    }

    // 重载 + 运算符 (两个矩阵维度完全一致)
    Matrix friend operator+(const Matrix &A, const Matrix &B)
    {
        Matrix ans(A.ROW, A.COL);

        for (int i = 1; i <= A.ROW; ++i)
            for (int j = 1; j <= A.COL; ++j)
                ans.M[i][j] = (A.M[i][j] + B.M[i][j]) % MOD;

        return ans;
    }

    // 重载 ^ 运算符,实现矩阵快速幂
    Matrix friend operator^(Matrix base, ll exp)
    {
        Matrix ans(base.ROW, base.ROW);
        ans.init(); // 生成与 base 同阶的单位矩阵

        while (exp)
        {
            if (exp & 1)
                ans = ans * base;
            base = base * base;
            exp >>= 1;
        }

        return ans;
    }
};

void miyan()
{
    ll n;
    cin >> n;

    if (n <= 3)
    {
        cout << 1 << endl;
        return;
    }

    Matrix base(3, 3);
    base.clear();
    base.M[1][1] = base.M[1][2] = base.M[3][1] = base.M[2][3] = 1;

    base = base ^ (n - 3);

    Matrix start(1, 3);
    start.M[1][1] = start.M[1][2] = start.M[1][3] = 1;

    auto ans = start * base;

    cout << ans.M[1][1] << endl;
}

定长路径统计

模型

问题描述

给一个包含 \( n \) 个节点的有向图,每条边的边权均为 \(1\),然后给一个整数 \( k \),你的任务是对于所有点对 (\( u, v \)) 求出从 \( u \) 到 \( v \) 长度为 \( k \) 的路径的数量(不一定是简单路径,即路径上的点或者边可能走多次)。

解法

我们将这个图用邻接矩阵 \( G \) 表示(对于图中的边 \((u \to v)\),令 \( G [u,v] = 1 \),其余为 \(0\) 的矩阵;如果有重边,则设 \( G [u,v]\) 为重边的数量),表示这个有向图。下述算法同样适用于图有自环的情况。

显然,该邻接矩阵对应 \( k = 1 \) 时的答案。

假设我们知道长度为 \( k \) 的路径条数构成的矩阵,记为矩阵 \( C_k \),我们想要求 \( C_{k+1}\)。显然有 \(DP\) 转移方程:\( C_{k+1}[i,j] = \sum_{p=1}^{n} C_k [i,p] \cdot G [p,j]\)

我们可以把它看作矩阵乘法的运算,于是上述转移可以描述为:

\(C_{k+1} = C_k \cdot G\)

那么把这个递推式展开可以得到:\( C_k = \underbrace {G \cdot G \cdots G}_{k\text { 次}} = G^k \)

直接使用矩阵快速幂,在 \( O (n^3 \log k)\) 的复杂度内计算结果。


1. 公交车路线

链接:https://www.luogu.com.cn/problem/P2233

问题描述:

在一个由 \(8\) 个站点(\(A–H\))组成的环上,从 \(A\) 出发,每次只能移动到相邻站点,求恰好经过 \( n \) 次移动(即换乘 \( n \) 次车)后首次到达 \(E\) 的路径数量,结果对 \(1000\) 取模。

思路:

容易想到 \( G = \begin {bmatrix} 0 & 1 & 0 & 0 & 0 & 0 & 0 & 1 \\ 1 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\ 0 & 1 & 0 & 1 & 0 & 0 & 0 & 0 \\ 0 & 0 & 1 & 0 & 1 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 1 & 0 & 1 & 0 \\ 0 & 0 & 0 & 0 & 0 & 1 & 0 & 1 \\ 1 & 0 & 0 & 0 & 0 & 0 & 1 & 0 \end {bmatrix} = C_{1}\),那么答案就为 \( C_{n}[1, 5]\)。

代码:

// 矩阵大小,记得改初始矩阵大小
const int SIZE = 100;
struct Matrix
{
    int ROW, COL; // 行数, 列数
    ll M[SIZE + 1][SIZE + 1];

    // 构造函数
    Matrix(int _ROW = 0, int _COL = 0)
    {
        ROW = _ROW;
        COL = _COL;
        clear();
    }

    // 清零,生成零矩阵
    void clear() { memset(M, 0, sizeof(M)); }

    // 初始化,生成大小为 n 的单位矩阵 (必须是方阵)
    void init()
    {
        clear();
        for (int i = 1; i <= ROW; ++i)
            M[i][i] = 1;
    }

    // 重载 * 运算符 (A 是 N * K 矩阵,B 是 K * M 矩阵)
    Matrix friend operator*(const Matrix &A, const Matrix &B)
    {
        // 前提:A 的列数必须等于 B 的行数
        // 结果:矩阵 ans 的大小为 A.ROW * B.COL
        Matrix ans(A.ROW, B.COL);

        for (int i = 1; i <= A.ROW; ++i)
        {
            for (int k = 1; k <= A.COL; ++k)
            {
                if (A.M[i][k] == 0)
                    continue;

                for (int j = 1; j <= B.COL; ++j)
                    ans.M[i][j] = (ans.M[i][j] + A.M[i][k] * B.M[k][j]) % MOD;
            }
        }
        return ans;
    }

    // 重载 + 运算符 (两个矩阵维度完全一致)
    Matrix friend operator+(const Matrix &A, const Matrix &B)
    {
        Matrix ans(A.ROW, A.COL);

        for (int i = 1; i <= A.ROW; ++i)
            for (int j = 1; j <= A.COL; ++j)
                ans.M[i][j] = (A.M[i][j] + B.M[i][j]) % MOD;

        return ans;
    }

    // 重载 ^ 运算符,实现矩阵快速幂
    Matrix friend operator^(Matrix base, ll exp)
    {
        Matrix ans(base.ROW, base.ROW);
        ans.init(); // 生成与 base 同阶的单位矩阵

        while (exp)
        {
            if (exp & 1)
                ans = ans * base;
            base = base * base;
            exp >>= 1;
        }

        return ans;
    }
};

void miyan()
{
    int n;
    cin >> n;

    Matrix base(8, 8);
    base.clear();
    base.M[1][2] = base.M[1][8] = 1;
    base.M[2][1] = base.M[2][3] = 1;
    base.M[3][2] = base.M[3][4] = 1;
    base.M[4][3] = base.M[4][5] = 1;
    base.M[6][5] = base.M[6][7] = 1;
    base.M[7][6] = base.M[7][8] = 1;
    base.M[8][1] = base.M[8][7] = 1;

    auto ans = base ^ n;

    cout << ans.M[1][5] << endl;
}

2. 可乐

链接:https://www.luogu.com.cn/problem/P5789

问题描述:

给定一个 \( N \) 个点的无向图,可乐机器人从 \(1\) 号点出发,每秒可选择:留在原地、移动到相邻点、或自爆(终止);求 \( t \) 秒内所有可能的行为方案总数(包括中途自爆),结果对 \(2017\) 取模。注意:一旦自爆就不再行动,且每秒必须执行一种行为。

思路:

考虑建图:

  • 对于移动到相邻点,只需建一个无向邻接矩阵 \( G \);
  • 对于留着原地,就是每个点都有一个自环;
  • 对于自爆,定义一个超级终点 \( n + 1 \),可乐机器人在 \( u \) 点自爆,就是移动到了点 \( n + 1 \);
  • 注意,超级终点 \( n + 1 \) 也要有条自环。

关于 \( n + 1 \) 点为什么也要有条自环:

  • 在矩阵相乘中,如果不建自环,机器人不但不会停在那里,反而会直接凭空消失;
  • 当 \( t = 2 \) 并选择在起点直接自爆时,在题目中路径为 \( 1 \to \text {爆炸 } (n + 1)\),但是真实的路径应该为 \( 1 \to \text {爆炸 } (n + 1) \to \text {爆炸 } (n + 1)\),虽然在时刻 \(1\) 时已经自爆了,但是在真实的路径上仍然要考虑时刻 \(2\) 的情况,因此 \( n + 1 \) 点也要有自环。

综上答案为 \(\sum_{i = 1}^{n + 1} C_t [1, i] \text { mod 2017}\)。

代码:

// 矩阵大小,记得改初始矩阵大小
const int SIZE = 110;
struct Matrix
{
    int ROW, COL; // 行数, 列数
    ll M[SIZE + 1][SIZE + 1];

    // 构造函数
    Matrix(int _ROW = 0, int _COL = 0)
    {
        ROW = _ROW;
        COL = _COL;
        clear();
    }

    // 清零,生成零矩阵
    void clear() { memset(M, 0, sizeof(M)); }

    // 初始化,生成大小为 n 的单位矩阵 (必须是方阵)
    void init()
    {
        clear();
        for (int i = 1; i <= ROW; ++i)
            M[i][i] = 1;
    }

    // 重载 * 运算符 (A 是 N * K 矩阵,B 是 K * M 矩阵)
    Matrix friend operator*(const Matrix &A, const Matrix &B)
    {
        // 前提:A 的列数必须等于 B 的行数
        // 结果:矩阵 ans 的大小为 A.ROW * B.COL
        Matrix ans(A.ROW, B.COL);

        for (int i = 1; i <= A.ROW; ++i)
        {
            for (int k = 1; k <= A.COL; ++k)
            {
                if (A.M[i][k] == 0)
                    continue;

                for (int j = 1; j <= B.COL; ++j)
                    ans.M[i][j] = (ans.M[i][j] + A.M[i][k] * B.M[k][j]) % MOD;
            }
        }
        return ans;
    }

    // 重载 + 运算符 (两个矩阵维度完全一致)
    Matrix friend operator+(const Matrix &A, const Matrix &B)
    {
        Matrix ans(A.ROW, A.COL);

        for (int i = 1; i <= A.ROW; ++i)
            for (int j = 1; j <= A.COL; ++j)
                ans.M[i][j] = (A.M[i][j] + B.M[i][j]) % MOD;

        return ans;
    }

    // 重载 ^ 运算符,实现矩阵快速幂
    Matrix friend operator^(Matrix base, ll exp)
    {
        Matrix ans(base.ROW, base.ROW);
        ans.init(); // 生成与 base 同阶的单位矩阵

        while (exp)
        {
            if (exp & 1)
                ans = ans * base;
            base = base * base;
            exp >>= 1;
        }

        return ans;
    }
};

void miyan()
{
    int n, m, t;
    cin >> n >> m;
    vector a(n + 2, vector<int>(n + 2, 0));
    for (int i = 0; i < m; ++i)
    {
        int u, v;
        cin >> u >> v;
        a[u][v] = a[v][u] = 1;
    }

    for (int i = 1; i <= n + 1; ++i)
    {
        a[i][i] = 1;
        a[i][n + 1] = 1;
    }

    cin >> t;

    Matrix base(n + 1, n + 1);
    base.clear();
    for (int i = 1; i <= n + 1; ++i)
        for (int j = 1; j <= n + 1; ++j)
            base.M[i][j] = a[i][j];

    base = base ^ t;

    int ans = 0;
    for (int i = 1; i <= n + 1; ++i)
        ans = (ans + base.M[1][i]) % MOD;

    cout << ans << endl;
}

3. 迷路

链接:https://www.luogu.com.cn/problem/P4159

问题描述:

给定一个 \( n \) 个节点的有向图,每条边的权重是 \(1\) 到 \(9\) 的整数(字符形式给出),求从节点 \(1\) 出发恰好用时 \( t \) 到达节点 \( n \) 的路径总数(不能停留,必须走完总时间 \( t \)),结果对 \(2009\) 取模。

思路:

由于权重不只为 \( 0,1 \),不能直接套模板。

考虑使用类似分层图的方法,既将点 \( u \) 拆分成 \(<u, i>(i \in [1, 9])\),此时边权全变为 1。

建图:

  • 由于每个点都拆分成了 \(9\) 份,因此邻接矩阵大小也变成的原有的 \(9\) 倍;
  • 当存在边 \((u, v, w)\) 时,将 \(<u, 1>\) 到 \(<u, w>\) 以及中间节点全部连成链,\(<u, w>\) 到 \(<v, 1>\) 建边;
  • 例:设存在边 \((u, v, 3)\),建 \(<u, 1> \to <u, 2> \to <u, 3> \to <v, 1>\)。

综上答案为 \( C_t [<1, 1>, <n, 1>]\)。

代码:

// 矩阵大小,记得改初始矩阵大小
const int SIZE = 100;
struct Matrix
{
    int ROW, COL; // 行数, 列数
    ll M[SIZE + 1][SIZE + 1];

    // 构造函数
    Matrix(int _ROW = 0, int _COL = 0)
    {
        ROW = _ROW;
        COL = _COL;
        clear();
    }

    // 清零,生成零矩阵
    void clear() { memset(M, 0, sizeof(M)); }

    // 初始化,生成大小为 n 的单位矩阵 (必须是方阵)
    void init()
    {
        clear();
        for (int i = 1; i <= ROW; ++i)
            M[i][i] = 1;
    }

    // 重载 * 运算符 (A 是 N * K 矩阵,B 是 K * M 矩阵)
    Matrix friend operator*(const Matrix &A, const Matrix &B)
    {
        // 前提:A 的列数必须等于 B 的行数
        // 结果:矩阵 ans 的大小为 A.ROW * B.COL
        Matrix ans(A.ROW, B.COL);

        for (int i = 1; i <= A.ROW; ++i)
        {
            for (int k = 1; k <= A.COL; ++k)
            {
                if (A.M[i][k] == 0)
                    continue;

                for (int j = 1; j <= B.COL; ++j)
                    ans.M[i][j] = (ans.M[i][j] + A.M[i][k] * B.M[k][j]) % MOD;
            }
        }
        return ans;
    }

    // 重载 + 运算符 (两个矩阵维度完全一致)
    Matrix friend operator+(const Matrix &A, const Matrix &B)
    {
        Matrix ans(A.ROW, A.COL);

        for (int i = 1; i <= A.ROW; ++i)
            for (int j = 1; j <= A.COL; ++j)
                ans.M[i][j] = (A.M[i][j] + B.M[i][j]) % MOD;

        return ans;
    }

    // 重载 ^ 运算符,实现矩阵快速幂
    Matrix friend operator^(Matrix base, ll exp)
    {
        Matrix ans(base.ROW, base.ROW);
        ans.init(); // 生成与 base 同阶的单位矩阵

        while (exp)
        {
            if (exp & 1)
                ans = ans * base;
            base = base * base;
            exp >>= 1;
        }

        return ans;
    }
};

void miyan()
{
    int n, t;
    cin >> n >> t;

    vector a(n + 1, vector<int>(n + 1, 0));
    for (int i = 1; i <= n; ++i)
    {
        for (int j = 1; j <= n; ++j)
        {
            char c;
            cin >> c;
            int w = c - '0';

            a[i][j] = w;
        }
    }

    int idx = 1;
    map<pii, int> mp;
    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= 9; ++j)
            mp[{i, j}] = idx++;

    vector b(n * 9 + 1, vector<int>(n * 9 + 1, 0));
    for (int i = 1; i <= n; ++i)
    {
        for (int j = 1; j <= n; ++j)
        {
            if (!a[i][j])
                continue;

            int x = a[i][j];
            for (int k = 1; k < x; ++k)
                b[mp[{i, k}]][mp[{i, k + 1}]] = 1;

            b[mp[{i, x}]][mp[{j, 1}]] = 1;
        }
    }

    Matrix base(n * 9, n * 9);
    base.clear();
    for (int i = 1; i <= n * 9; ++i)
        for (int j = 1; j <= n * 9; ++j)
            base.M[i][j] = b[i][j];

    base = base ^ t;
    cout << base.M[mp[{1, 1}]][mp[{n, 1}]] << endl;
}

定长最短路

模型

问题描述

给一个包含 \( n \) 个节点的加权有向图和一个整数 \( k \)。对于每个点对 (\( u, v \)) 找到从 \( u \) 到 \( v \) 的恰好包含 \( k \) 条边的最短路的长度。(不一定是简单路径,即路径上的点或者边可能走多次)

解法

我们仍构造这个图的邻接矩阵 \( G \),其中 \( G [i,j]\) 表示从 \( i \) 到 \( j \) 的边权。如果 \( i, j \) 两点之间没有边,那么 \( G [i,j] = \infty \)。(有重边的情况取边权的最小值)

显然上述矩阵对应 \( k = 1 \) 时问题的答案。我们仍假设我们知道 \( k \) 的答案,记为矩阵 \( L_k \)。现在我们想求 \( k+1 \) 的答案。显然有转移方程:

\(L_{k+1}[i,j] = \min_{1 \leq p \leq n} { L_k[i,p] + G[p,j] }\)

事实上我们可以类比矩阵乘法,你发现上述转移只是把矩阵乘法的乘积求和变成相加取最小值,于是我们定义这个运算为 \(\odot \),即

\(A \odot B = C \quad \Longleftrightarrow \quad C[i,j] = \min_{1 \leq p \leq n} { A[i,p] + B[p,j] }\)

于是得到

\(L_{k+1} = L_k \odot G\)

展开递推式得到

\( L_k = \underbrace {G \odot G \cdots G}_{k\text { 次}} = G^{\odot k}\)

我们仍然可以用矩阵快速幂的方法计算上式,因为它显然是具有结合律的。时间复杂度 \( O (n^3 \log k)\)。

模板

广义矩阵乘法(也叫 \((\min, +)\) 乘法)。

运算规则的对比
运算类型算术乘法 ×算术加法 +单位元 (Identity)
普通矩阵乘法乘法 (\times)加法 (+)1 (乘 1 不变,加 0 不变)
最短路矩阵乘法加法 (+)取最小值 (\min)0 (加 0 不变,\min 无穷大不变)
单位元性质

单位矩阵 \( I \) 必须满足:对于任何矩阵 \( A \),都有 \( A \times I = A \)。

让我们看广义乘法的计算公式:

$$(A \times I)_{i,j} = \min_{k=1}^{n} \{ A_{i,k} + I_{k,j} \}$$

为了让结果等于 \( A_{i,j}\),我们需要这个 \(\min \) 序列中:

当 \( k = j \) 时:\( A_{i,k} + I_{k,j}\) 必须等于 \( A_{i,j}\)。

这要求 \( I_{j,j} = 0 \)(对角线为 \(0\))。

当 \( k \neq j \) 时:\( A_{i,k} + I_{k,j}\) 必须大于或等于 \( A_{i,j}\),确保它不会影响 \(\min \) 的结果。

为了在任何情况下都不影响最小值,最保险的值就是 \( I_{k,j} = \infty \)(非对角线为无穷大)。

代码
// 矩阵大小,记得改初始矩阵大小
const int SIZE = 100;
struct Matrix
{
    int ROW, COL; // 行数, 列数
    ll M[SIZE + 1][SIZE + 1];

    // 构造函数
    Matrix(int _ROW = 0, int _COL = 0)
    {
        ROW = _ROW;
        COL = _COL;
        clear();
    }

    // 清零,生成零矩阵
    void clear() { memset(M, 0x3f, sizeof(M)); }

    // 初始化,生成大小为 n 的单位矩阵 (必须是方阵)
    void init()
    {
        clear();
        for (int i = 1; i <= ROW; ++i)
            M[i][i] = 0;
    }

    // 重载 * 运算符 (A 是 N * K 矩阵,B 是 K * M 矩阵)
    Matrix friend operator*(const Matrix &A, const Matrix &B)
    {
        // 前提:A 的列数必须等于 B 的行数
        // 结果:矩阵 ans 的大小为 A.ROW * B.COL
        Matrix ans(A.ROW, B.COL);
        ans.clear();

        for (int i = 1; i <= A.ROW; ++i)
            for (int k = 1; k <= A.COL; ++k)
                for (int j = 1; j <= B.COL; ++j)
                    ans.M[i][j] = min(ans.M[i][j], A.M[i][k] + B.M[k][j]);

        return ans;
    }

    // 重载 ^ 运算符,实现矩阵快速幂
    Matrix friend operator^(Matrix base, ll exp)
    {
        Matrix ans(base.ROW, base.ROW);
        ans.init(); // 生成与 base 同阶的单位矩阵

        while (exp)
        {
            if (exp & 1)
                ans = ans * base;
            base = base * base;
            exp >>= 1;
        }

        return ans;
    }
};

const ll inf = 0x3f3f3f3f3f3f3f3f;

1. Exactly K Steps 2

链接:https://atcoder.jp/contests/abc445/tasks/abc445_f

问题描述:

给定一个 \( N \) 个点的完全有向图(任意两点间都有单向边,边权为 \( C_{i,j}\)),对每个起点 \( s \),求一条从 \( s \) 出发、恰好走 \( K \) 步后回到 \( s \) 的路径,使得总边权最小。

思路:

题目已经给出邻接矩阵 \( G \),对该矩阵跑快速幂得到 \( L_k \),那么最终答案就为对于每个 \( s = 1, 2, \dots, N \),都输出 \( L_k [s, s]\)。

代码:

// 矩阵大小,记得改初始矩阵大小
const int SIZE = 100;
struct Matrix
{
    int ROW, COL; // 行数, 列数
    ll M[SIZE + 1][SIZE + 1];

    // 构造函数
    Matrix(int _ROW = 0, int _COL = 0)
    {
        ROW = _ROW;
        COL = _COL;
        clear();
    }

    // 清零,生成零矩阵
    void clear() { memset(M, 0x3f, sizeof(M)); }

    // 初始化,生成大小为 n 的单位矩阵 (必须是方阵)
    void init()
    {
        clear();
        for (int i = 1; i <= ROW; ++i)
            M[i][i] = 0;
    }

    // 重载 * 运算符 (A 是 N * K 矩阵,B 是 K * M 矩阵)
    Matrix friend operator*(const Matrix &A, const Matrix &B)
    {
        // 前提:A 的列数必须等于 B 的行数
        // 结果:矩阵 ans 的大小为 A.ROW * B.COL
        Matrix ans(A.ROW, B.COL);
        ans.clear();

        for (int i = 1; i <= A.ROW; ++i)
            for (int k = 1; k <= A.COL; ++k)
                for (int j = 1; j <= B.COL; ++j)
                    ans.M[i][j] = min(ans.M[i][j], A.M[i][k] + B.M[k][j]);

        return ans;
    }

    // 重载 ^ 运算符,实现矩阵快速幂
    Matrix friend operator^(Matrix base, ll exp)
    {
        Matrix ans(base.ROW, base.ROW);
        ans.init(); // 生成与 base 同阶的单位矩阵

        while (exp)
        {
            if (exp & 1)
                ans = ans * base;
            base = base * base;
            exp >>= 1;
        }

        return ans;
    }
};

const ll inf = 0x3f3f3f3f3f3f3f3f;

void miyan()
{
    int n, k;
    cin >> n >> k;
    vector a(n + 1, vector<ll>(n + 1, inf));
    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= n; ++j)
            cin >> a[i][j];

    Matrix base(n, n);
    base.clear();
    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= n; ++j)
            base.M[i][j] = a[i][j];

    base = base ^ k;

    for (int i = 1; i <= n; ++i)
        cout << base.M[i][i] << endl;
}

2. Cow Relays G

链接:https://www.luogu.com.cn/problem/P2886

问题描述:

在无向图中,求从起点 \( S \) 到终点 \( E \) 恰好经过 \( N \) 条边的最短路径长度(允许重复经过点和边)。

思路:

本题与 1 题的不同仅为只需输出 \( S \) 到 \( E \) 的最短路径长度并且需要自己建邻接矩阵。

代码:

// 矩阵大小,记得改初始矩阵大小
const int SIZE = 200;
struct Matrix
{
    int ROW, COL; // 行数, 列数
    ll M[SIZE + 1][SIZE + 1];

    // 构造函数
    Matrix(int _ROW = 0, int _COL = 0)
    {
        ROW = _ROW;
        COL = _COL;
        clear();
    }

    // 清零,生成零矩阵
    void clear() { memset(M, 0x3f, sizeof(M)); }

    // 初始化,生成大小为 n 的单位矩阵 (必须是方阵)
    void init()
    {
        clear();
        for (int i = 1; i <= ROW; ++i)
            M[i][i] = 0;
    }

    // 重载 * 运算符 (A 是 N * K 矩阵,B 是 K * M 矩阵)
    Matrix friend operator*(const Matrix &A, const Matrix &B)
    {
        // 前提:A 的列数必须等于 B 的行数
        // 结果:矩阵 ans 的大小为 A.ROW * B.COL
        Matrix ans(A.ROW, B.COL);
        ans.clear();

        for (int i = 1; i <= A.ROW; ++i)
        {
            for (int k = 1; k <= A.COL; ++k)
            {
                for (int j = 1; j <= B.COL; ++j)
                    ans.M[i][j] = min(ans.M[i][j], A.M[i][k] + B.M[k][j]);
            }
        }
        return ans;
    }

    // 重载 ^ 运算符,实现矩阵快速幂
    Matrix friend operator^(Matrix base, ll exp)
    {
        Matrix ans(base.ROW, base.ROW);
        ans.init(); // 生成与 base 同阶的单位矩阵

        while (exp)
        {
            if (exp & 1)
                ans = ans * base;
            base = base * base;
            exp >>= 1;
        }

        return ans;
    }
};

const ll inf = 0x3f3f3f3f3f3f3f3f;

void miyan()
{
    int n, m, s, e;
    cin >> n >> m >> s >> e;

    vector<array<int, 3>> edge(m);
    for (int i = 0; i < m; ++i)
    {
        int w, u, v;
        cin >> w >> u >> v;

        edge[i] = {u, v, w};
    }

    int idx = 1;
    map<int, int> mp;
    if (!mp.count(s))
        mp[s] = idx++;
    if (!mp.count(e))
        mp[e] = idx++;
    for (auto [u, v, w] : edge)
    {
        if (!mp.count(u))
            mp[u] = idx++;
        if (!mp.count(v))
            mp[v] = idx++;
    }

    vector a(idx, vector<ll>(idx, inf));
    for (auto [u, v, w] : edge)
        a[mp[u]][mp[v]] = a[mp[v]][mp[u]] = w;

    Matrix base(idx - 1, idx - 1);
    base.clear();
    for (int i = 1; i < idx; ++i)
        for (int j = 1; j < idx; ++j)
            base.M[i][j] = a[i][j];

    base = base ^ n;

    cout << base.M[mp[s]][mp[e]] << endl;
}

暂无评论

发送评论 编辑评论


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