【学习笔记】矩阵及其应用

· · 个人记录

目录

一、什么是矩阵?

二、矩阵的基本运算

三、矩阵快速幂

四、实际应用

五、模板代码

六、参考文章

一、什么是矩阵?

在数学中,矩阵(Matrix)是一个按照长方阵列排列的复数或实数集合,最早来自于方程组的系数及常数所构成的方阵。这一概念由19世纪英国数学家凯利首先提出。

——百度百科 - 矩阵

定义一个 n\times m(1\le n,1\le m) 的矩阵

A=\begin{bmatrix}a_{1,1}&a_{1,2}&\cdots&a_{1,m}\\a_{2,1}&a_{2,2}&\cdots&a_{2,m}\\\vdots&\vdots&\ddots&\vdots\\a_{n,1}&a_{n,2}&\cdots&a_{n,m}\\\end{bmatrix}

其中,n 叫做矩阵 A行数m 叫做矩阵 A列数a_{i,j}(\forall i\in[1,n],\forall j\in[1,m]) 叫做矩阵 A元素。矩阵的元素一般为数。

只有一行的矩阵叫做行向量,只有一列的矩阵叫做列向量

行数和列数相同的矩阵叫做方阵。可以用“n 阶方阵”描述一个 n\times n 的方阵。

举个例子,有矩阵

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

该矩阵的行数为 2,列数为 3。第 2 行第 1 列的元素 A_{2,1}=4

我们一般用 A,B,C 等大写字母表示一个矩阵。如果一个矩阵是 n\times m 的矩阵,则说明它的行数是 n,列数是 m

二、矩阵的基本运算

矩阵的加法&减法

只要两个矩阵的行数相同且列数相同,它们就可以进行加法和减法。具体做法是,将两个矩阵对应位置上的数相加减。例如对于两个 n\times m 的矩阵 AB,设 C=A+B,则

C_{i,j}=A_{i,j}+B_{i,j}(\forall i\in[1,n],\forall j\in[1,m])

举个例子,若

A=\begin{bmatrix}1&2\\3&4\\5&6\end{bmatrix},B=\begin{bmatrix}6&5\\4&3\\2&1\end{bmatrix},C=A+B,

C=\begin{bmatrix}1+6&2+5\\3+4&4+3\\5+2&6+1\end{bmatrix}=\begin{bmatrix}7&7\\7&7\\7&7\end{bmatrix}.

矩阵的数乘

任意一个矩阵都能进行数乘。具体做法是,将矩阵的每一个元素都乘上一个数。例如对于一个 n\times m 的矩阵 A,若 C=\lambda A,则

C_{i,j}=\lambda A_{i,j}(\forall i\in[1,n],\forall j\in[1,m])

举个例子,若

A=\begin{bmatrix}1&2\\3&4\end{bmatrix},C=5A,

C=\begin{bmatrix}1\times 5&2\times 5\\3\times 5&4\times 5\end{bmatrix}=\begin{bmatrix}5&10\\15&20\end{bmatrix}.

矩阵的乘法

首先给出一条定理:矩阵乘法满足结合律和分配律,而不满足交换律。

也就是说,(A\times B)\times C=A\times(B\times C)(A+B)\times C=A\times C+B\times C,而 A\times B\ne B\times A

有了这条定理,我们再来说矩阵乘法的性质。

两个矩阵 AB 能进行乘法运算得到 C=A\times B,当且仅当 A列数等于 B行数。注意,这里提到的乘积是 A\times B 而不是 B\times A,就是因为矩阵乘法不满足交换律。如果满足条件,得到的乘积矩阵 C 的行数一定等于 A 的行数,C 的列数一定等于 B 的列数。设 A 是一个 n\times m 的矩阵,B 是一个 m\times p 的矩阵,则 C 是一个 n\times p 的矩阵,满足:

C_{i,j}=\sum\limits^m_{k=1}A_{i,k}B_{k,j}(\forall i\in[1,n],\forall j\in[1,p])

如果还是不懂矩阵乘法是怎么一回事,没有关系,请看解释:

现有两个矩阵 AB,它们的具体信息如下:

A=\begin{bmatrix}a&b\\c&d\\e&f\end{bmatrix},B=\begin{bmatrix}\alpha&\beta\\\gamma&\delta\end{bmatrix}

要求 C=A\times B,首先可得 C 的规模为 32 列:

C=\begin{bmatrix}?&?\\?&?\\?&?\end{bmatrix}

然后我们手动模拟一下矩阵乘法的步骤,以 C_{1,1} 为例。

由上面的公式可得,C_{1,1}=A_{1,1}B_{1,1}+A_{1,2}B_{2,1}

A=\begin{bmatrix}\color{Red}a&\color{Blue}b\\c&d\\e&f\end{bmatrix},B=\begin{bmatrix}\color{Red}\alpha&\beta\\\color{Blue}\gamma&\delta\end{bmatrix}

如上,红色的元素之积加蓝色的元素之积就等于 C_{1,1}

C=\begin{bmatrix}a\alpha+b\gamma&?\\?&?\\?&?\end{bmatrix}

再来求 C_{1,2}=A_{1,1}B_{1,2}+A_{1,2}B_{2,2}

A=\begin{bmatrix}\color{Red}a&\color{Blue}b\\c&d\\e&f\end{bmatrix},B=\begin{bmatrix}\alpha&\color{Red}\beta\\\gamma&\color{Blue}\delta\end{bmatrix}

如上,红色的元素之积加蓝色的元素之积就等于 C_{1,2}

C=\begin{bmatrix}a\alpha+b\gamma&a\beta+b\delta\\?&?\\?&?\end{bmatrix}

……

如此这般,我们便可以求出 C

C=\begin{bmatrix}a\alpha+b\gamma&a\beta+b\delta\\c\alpha+d\gamma&c\beta+d\delta\\e\alpha+f\gamma&e\beta+f\delta\end{bmatrix}

概括来说,C_{i,j} 就等于 Ai 行的 m 个数与 Bj 列的 m 个数分别相乘得到的 m 个积的总和。

这里另外提一种特殊的矩阵:单位矩阵。一个 n\times m 矩阵 A 为单位矩阵,当且仅当它满足:

n=m,A_{i,i}=1,A_{i,j}=0(\forall i\in[1,n],\forall j\in[1,m],i\ne j)

即单位矩阵一定是个方阵,它的主对角线(即从左上角到右下角的对角线)上的所有元素都为 1,其余元素都为 0

下面这个矩阵就是一个单位矩阵:

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

单位矩阵有着如下性质:假设有单位矩阵 A,任何能与 A 相乘的矩阵 BA 后得到的积还是 B

矩阵的幂

一个矩阵能进行幂运算,当且仅当它是一个方阵(想一想,为什么)。矩阵 A 的幂 A^k 通常利用矩阵快速幂求出。

三、矩阵快速幂

矩阵快速幂的思想与数的快速幂的思想可以说是完全一致,除了将乘法替换成了矩阵乘法以外。

数的快速幂

如果你还不知道什么是数的快速幂,请看本节;反之可以跳过本节,直奔主题。

问题:给定数 a,n,要求 a^n,大多数情况还要将答案模 p。参见luoguP1226

要解决这个问题,首先可以将 n 二进制拆分

n=(n_{k-1}n_{k-2}\dots n_{0})_2,即在二进制表示下有 k 位,则 n=2^{k-1}n_{k-1}+2^{k-2}n_{k-2}+\dots2^0n_0。那么,a^n 就等价于

a^{2^{k-1}n_{k-1}+2^{k-2}n_{k-2}+\dots+2^0n_0}

也就是

a^{2^{k-1}n_{k-1}}\times a^{2^{k-2}n_{k-2}}\times\dots \times a^{2^0n_0}

也就是

{\left(a^{2^{k-1}}\right)}^{n_{k-1}}\times{\left(a^{2^{k-2}}\right)}^{n_{k-2}}\times\dots\times{\left(a^{2^0}\right)}^{n_0}

由于 n_i\in\{0,1\}(1\le i<k),因此当 n_i0 时,直接跳过;当 n_i1 时,将答案乘上 a^{2^i}即可。 另外,不难发现 a^{2^i}=a^{2^{i-1}\times 2}={\left(a^{2^{i-1}}\right)}^2,于是就有了如下这个 O(\log n) 的算法。

int Power(int a,int n){
    int ans=1;
    for(;n;n>>=1,a=a*a%k)
        if(n&1)//取出n在二进制表示下的的第0位
            ans=ans*a%k;
}

这种叫做快速幂的算法还有一种递归版本:

int Power(int a,int n){
    if(!n) return 1;
    if(n&1) return Power(a*a%p,n/2)*a%p;
    return Power(a*a%k,n/2);
}

它的正确性在此不再作赘述。理论上递归版本和非递归版本的时间复杂度相同,但实际操作告诉我们,很多时候非递归版本更快一些,因为递归会花费一定的开销。

矩阵快速幂

再看一遍数的快速幂的模板题。

问题:给定数 a,n,要求 a^n,大多数情况还要将答案模 p

将这个问题稍微改动一下:

问题:给定 n\times n 的矩阵 A,数 k,要求 A^k,大多数情况还要将答案的每一个元素模 p

这便是矩阵快速幂的模板题。

沿着数的快速幂的思想,我们可以写出这样一条式子:

A^k={\left(A^{2^{i-1}}\right)}^{k_{i-1}}\times{\left(A^{2^{i-2}}\right)}^{k_{i-2}}\times\dots\times{\left(A^{2^0}\right)}^{k_0}

其中 ik 在二进制表示下的位数。由于矩阵乘法满足结合律,因此只要在代码中定义了矩阵乘法,我们完全可以基本照搬数的快速幂。实现代码在后面有给出。

时间复杂度 O(nmp\log k),即矩阵乘法的时间复杂度乘上快速幂的时间复杂度。

四、实际应用

斐波那契数列

众所周知,斐波那契数列是一个形如

0,1,1,2,3,5,8,13,21,34,55,89,\dots

的数列。其中第 0F_0=0,第 1F_1=1,第 nF_n=F_{n-1}+F_{n-2}(n\ge2,n\in\Bbb N^*)

要求该数列的第 nF_n,可以直接利用递推式求解,时间复杂度 O(n),但这仅限于 n 较小的时候。如果 n 是一个很大的数,递推求解会超时,此时就需要一种 O(\log n) 级别的算法。

还记得矩阵快速幂的时间复杂度吗?

时间复杂度 O(nmp\log k),即矩阵乘法的时间复杂度乘上快速幂的时间复杂度。

观察到矩阵快速幂的时间复杂度正好是 O(\log n) 级别的,于是我们思考如何利用矩阵快速幂求出 F_n

构造如下两个矩阵(列向量):

\begin{bmatrix}F_n\\F_{n-1}\end{bmatrix},\begin{bmatrix}F_{n-1}\\F_{n-2}\end{bmatrix}.

设矩阵 A 满足:

\begin{bmatrix}F_n\\F_{n-1}\end{bmatrix}=A\times\begin{bmatrix}F_{n-1}\\F_{n-2}\end{bmatrix}\quad(1)

根据矩阵乘法的定义,A 是一个 22 列的方阵。

A=\begin{bmatrix}a&b\\c&d\end{bmatrix}

则式 (1) 可变为

\begin{bmatrix}F_n\\F_{n-1}\end{bmatrix}=\begin{bmatrix}a&b\\c&d\end{bmatrix}\times\begin{bmatrix}F_{n-1}\\F_{n-2}\end{bmatrix}\quad

由矩阵乘法,可列出如下方程组:

\begin{cases}a\cdot F_{n-1}+b\cdot F_{n-2}=F_n\\c\cdot F_{n-1}+d\cdot F_{n-2}=F_{n-1}\end{cases}\qquad(2)

容易发现,当 \begin{cases}a=1,\\b=1,\\c=1,\\d=0\end{cases} 时,方程组 (2) 成立。

于是,有

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

据此可得

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

以此类推,得:

\begin{bmatrix}F_n\\F_{n-1}\end{bmatrix}=\underbrace{\begin{bmatrix}1&1\\1&0\end{bmatrix}\times\begin{bmatrix}1&1\\1&0\end{bmatrix}\times\dots\times\begin{bmatrix}1&1\\1&0\end{bmatrix}}_{\footnotesize{n-1\textbf{个}\begin{bmatrix}1&1\\1&0\end{bmatrix}}}\times\begin{bmatrix}F_1\\F_0\end{bmatrix} \Rightarrow\begin{bmatrix}F_n\\F_{n-1}\end{bmatrix}=\begin{bmatrix}1&1\\1&0\end{bmatrix}^{n-1}\times\begin{bmatrix}F_1\\F_0\end{bmatrix}\qquad(3)

至此,我们完成了对式 (3) 的推导。式 (3) 即为经典的利用矩阵快速幂求斐波那契数列第 n 项的公式,可用矩阵快速幂求得 \begin{bmatrix}1&1\\1&0\end{bmatrix}^{n-1}

(另:如果你不怕麻烦,你可以将 \begin{bmatrix}F_1\\F_0\end{bmatrix} 替换成 \begin{bmatrix}F_2\\F_1\end{bmatrix} 乃至更高,再根据你改的矩阵更改 \begin{bmatrix}1&1\\1&0\end{bmatrix} 的幂次即可。注意更改过后的矩阵元素不能超过 F_n。例如当 n\ge4 时,下面这个式子也被认为是正确的。但通常我们不会在这种细节上花时间,除非你想卡常)

\begin{bmatrix}F_n\\F_{n-1}\end{bmatrix}=\begin{bmatrix}1&1\\1&0\end{bmatrix}^{n-4}\times\begin{bmatrix}F_4\\F_3\end{bmatrix}

时间复杂度为一次矩阵快速幂复杂度加上一次矩乘复杂度,为 O(2^2+(2^3\log n)),可视为 O(\log n)

由上可见,矩阵乘法的终极奥义是构造矩阵

例题P1962AC代码(仅展示主程序部分,Matrix结构体实现代码会在下面放出):

#include<iostream>
#include<cstdio>
#define int long long
using namespace std;

const int MOD=1e9+7;
int n;

struct Matrix{
    ...
}A(2,1),B(2,1),C(2,2);

signed main(){
    ios::sync_with_stdio(false);
    cin>>n;
    A.a[1][1]=1,A.a[2][1]=0;
    C.a[1][1]=C.a[1][2]=C.a[2][1]=1,C.a[2][2]=0;
    B=C.power(n-1)*A;
    cout<<B.a[1][1]<<endl;
    return 0;
}

五、模板代码

struct Matrix{
    int r,c,a[maxN][maxN];
    Matrix(int rr,int cc,bool make_identity_matrix=0):r(rr),c(cc){
    //有了这个构造函数,定义一个矩阵A的代码就是:
    //Matrix A(行数,列数,是否定义一个单位矩阵(是填1,不是填0));
        for(int i=1;i<=r;i++)
            for(int j=1;j<=c;j++){
                if(make_identity_matrix && i==j) a[i][j]=1;
                else a[i][j]=0;
            }
    }
    /*
    int* operator[](int i) {return a[i];} //用A[i][j]表示A.a[i][j]
    const int* operator[](int i) const {return a[i];} //C++特性
    */
    //上面两行是我从yggdyy_大佬的blog里复制过来的(链接详见【参考文章】),有需要可以去掉注释
    Matrix operator =(const Matrix &b){//重载赋值符
        for(int i=1;i<=r;i++)
            for(int j=1;j<=c;j++)
                a[i][j]=b.a[i][j];
        return *this;
    }
    Matrix operator +(const Matrix &b) const{//重载加法运算符
        Matrix ans(r,c);
        for(int i=1;i<=r;i++)
            for(int j=1;j<=c;j++)
                ans.a[i][j]=a[i][j]+b.a[i][j];
        return ans;
    }
    Matrix operator +=(const Matrix &b){//重载+=运算符
        return (*this)=(*this)+b;
    }
    void numtime(int x){//定义数乘函数
        for(int i=1;i<=r;i++)
            for(int j=1;j<=c;j++)
                a[i][j]=a[i][j]*x%MOD;
    }
    Matrix operator *(const Matrix &b) const{//重载乘法运算符
        Matrix ans(r,c);
        for(int i=1;i<=r;i++)
            for(int j=1;j<=b.c;j++)
                for(int k=1;k<=c;k++)
                    ans.a[i][j]=(ans.a[i][j]+a[i][k]*b.a[k][j])%MOD;
        return ans;
    }
    Matrix operator *=(const Matrix &b){//重载*=运算符
        return (*this)=(*this)*b;
    }
    Matrix power(int x){//定义矩阵幂运算函数
        Matrix M=(*this),ans(r,c,1);
        for(;x;x>>=1,M*=M) if(x&1) ans*=M;
        return ans;
    }
    void print(){//打印矩阵
        for(int i=1;i<=r;i++){
            for(int j=1;j<=c;j++) cout<<a[i][j]<<" ";
            cout<<endl;
        }
    }
};

六、参考文章