矩阵 学习笔记

引入

矩阵的引入来自线性方程组,将其左边每一项的系数右边的常数抽象出来就是矩阵。

\[\left\{ \begin{array}{} x_1 + 2x_2 = 4 \\ 2x_1 + 3x_2 = 5 \end{array} \right. \Leftrightarrow \left[ \begin{array}{} 1 & 2 \\ 2 & 3 \end{array} \right] \left[ \begin{array}{} x_1\\ x_2 \end{array} \right] = \left[ \begin{array}{} 4 \\ 5 \end{array} \right] \]

一般用 \([]\)\(()\) 表示矩阵。

定义

主对角线

对于一个矩阵 \(A\)\(A_{i,i}\) 为该矩阵主对角线。

单位矩阵

一般用 \(I\) 表示,它的主对角线是 \(1\),其余的是 \(0\)

全零矩阵

对于一个矩阵,如果其所有元素均为 \(0\),则称其为全零矩阵,记作 \(0\)

同型矩阵

两个矩阵,行数与列数对应相同,称为同型矩阵。

转置矩阵

如果矩阵 \(A\) 和矩阵 \(B\) 满足:

  1. 矩阵 \(A\) 的行数等于矩阵 \(B\) 的列数,矩阵 \(A\) 的列数等于矩阵 \(B\) 的行数。
  2. 对于 \(\forall i,j, A_{i,j} = B_{j,i}\)
    则称 \(A\)\(B\) 的转置矩阵,记作 \(A = B^T\)

方阵

一个行数和列数相等的矩阵。

一个大小为 \(n\) 的矩阵叫作 \(n\) 阶矩阵或 \(n\) 阶方阵。

三角矩阵

如果方阵主对角线左下方的元素均为 \(0\),称为上三角矩阵。如果方阵主对角线右上方的元素均为 \(0\),称为下三角矩阵。

两个上(下)三角矩阵的乘积仍然是上(下)三角矩阵。如果对角线元素均非 \(0\),则上(下)三角矩阵可逆,逆也是上(下)三角矩阵。

单位三角矩阵

如果上三角矩阵 \(A\) 的对角线全为 \(1\),则称 \(A\) 是单位上三角矩阵。如果下三角矩阵 \(A\) 的对角线全为 \(1\),则称 \(A\) 是单位下三角矩阵。

两个单位上(下)三角矩阵的乘积仍然是单位上(下)三角矩阵,单位上(下)三角矩阵的逆也是单位上(下)三角矩阵。

运算

线性运算

对两个同型矩阵可以作加(减)法,得到的答案是对应元素的和(差)。

矩阵的转置

即将一个矩阵变成它的转置矩阵

矩阵乘法

如果矩阵 \(A\) 的行数等于矩阵 \(B\) 的列数,列数等于矩阵 \(B\) 行数,则这两个矩阵可以相乘,相乘的积矩阵 \(C\),满足 \(C_{i,j} = \sum A_{i,k} \times B_{k,j}\),即矩阵 \(C\) 的第 \(i\) 行第 \(j\) 列等于矩阵 \(A\) 的第 \(i\) 行乘上矩阵 \(B\) 的第 \(j\) 列。

可以发现,矩阵 \(C\) 的行数等于矩阵 \(A\) 的行数,列数等于矩阵 \(B\) 的列数。

因此矩阵乘法不满足交换律

矩阵乘法运算律

  1. 乘法结合律,\(A \times (B \times C) = (A \times B) \times C\)
  2. 乘法分配律,\(A \times (B + C) = A \times B + A \times C\)

矩阵的逆

如果 \(A \times B = I\),那么 \(B\)\(A\) 的逆矩阵,记作 \(A^{-1}\)

注意:逆矩阵不一定存在,但如果逆矩阵存在则是唯一的,至于怎么求,可以用高斯消元

行列式

行列式

实现

可以发现矩阵可以使用二维数组进行模拟。

显然地,线性运算时间复杂度 \(\mathcal{O}(n ^ 2)\),乘法运算时间复杂度 \(\mathcal{O}(n^3)\)

对于普通的矩阵这两个复杂度是不能优化的。

常数优化

当然进行矩阵乘法时,可以通过改变循环顺序加速高速缓存的访问优化部分常数(还挺猛),对于小矩阵也可以通过循环展开实现。

struct Matrix{
	int mat[N][N];

	Matrix(){
		memset(mat, 0, sizeof(mat));
	}
	
	int * operator [] (const int & x){
		return mat[x];
	}
	
	Matrix operator * (const Matrix & x){
		Matrix ans;
		
        for (int k = 1; k <= m; k++) {
            for (int i = 1; i <= n; i++) {
                for (int j = 1; j <= x.m; j++) {
                        //multi
                }
            }
        }

        return ans;

    }
};

矩阵乘法的运用

矩阵快速幂

过程

因为矩阵有结合律,所以自然可以使用快速幂进行优化矩乘。

实现

Matrix qpow(int b, Matrix & mat){//mat 是转移矩阵
    Matrix ans;

	//prework
  
    for (; b; b >>= 1, mat = mat * mat) {
        if (b & 1) {
	        ans = ans * mat;// matrix multi
		}
	}
    return ans;
}

矩阵加速递推

一个经典的矩阵快速幂问题,斐波拉契数列

在斐波拉契数列中,

\[F_i = \begin{cases} 1 & i = 1 \vee i = 2 \\ F_{i - 1} + F_{i - 2} & \text{otherwise} \end{cases} \]

很显然的可以构造矩阵,

\[\left[ \begin {array} {} F_{i} & F_{i - 1} \end {array} \right] \left[ \begin {array} {} 1 & 0 \\ 1 & 1 \end {array} \right] = \left[ \begin {array} {} F_{i + 1} & F_{i} \end {array} \right] \]

实现

此处求斐波拉契第 \(n\) 项模 \(10 ^ 9 + 7\) 的结果的部分代码。

i64 n;

Matrix qpow(i64 b){
    Matrix mat, ans;
    
    ans[1][1] = 1, ans[1][2] = 0;
    ans[2][1] = 0, ans[2][2] = 1;
    
    mat[1][1] = 1, mat[1][2] = 1;
    mat[2][1] = 1, mat[2][2] = 0;
    
    for (; b; b >>= 1, mat = mat * mat) {
        if (b & 1) {
            ans = ans * mat;
        }    
    }
    
    return ans;
}
int main(){
    scanf("%lld", &n);
    Matrix ans = qpow(n - 1);
    printf("%lld", ans[1][1]);
    return 0;
}

线性常系数齐次递推式

当然,有时会在递推式中给定一些常数。

比如:

\(F_i = F_{i - 1} + F_{i - 2} + i ^ 2 + i\)

\(i\) 的递推可以由 \(i - 1 + 1\) 得到,\(i ^ 2\) 可以由 \((i - 1) ^ 2 + i ^ 2 - (i - 1) ^ 2 = 2i - 1 + (i - 1)^2\) 得到。

所以维护 \(i^0,i^1, i^2\) 即可,就可以构造如下矩阵实现状态转移。

\[\left [ \begin{array} {} F_i & F_{i - 1} & i^2 & i & 1 \end{array} \right] \left [ \begin{array} {} 1 & 1 & 0 & 0 & 0 \\ 1 & 0 & 0 & 0 & 0 \\ 1 & 0 & 1 & 0 & 0 \\ 1 & 0 & 2 & 1 & 0 \\ 0 & 0 &-1 & 1 & 1 \end{array} \right] = \left [ \begin{array} {} F_{i + 1} & F_{i} & {i + 1}^2 & i + 1 & 1 \end{array} \right] \]

矩阵表修改

THUSCH2017 大魔法师

对于每个修改都可以通过矩阵表示,然后整体使用线段树维护即可。

比如操作一,可以构造一下矩阵实现:

\[\left [ \begin{array} {} A & B & C & 1 \end{array} \right] \left [ \begin{array} {} 1 & 0 & 0 & 0 \\ 1 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{array} \right] \]

操作四,可以构造一下矩阵实现:

\[\left [ \begin{array} {} A & B & C & 1 \end{array} \right] \left [ \begin{array} {} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ v & 0 & 0 & 1 \end{array} \right] \]

参考代码
#include <cstdio>
#include <cstring>
#include <algorithm>

using i64 = long long ;

const int mod = 998244353 ;
const int N = 3e5 + 5 ;

struct Matrix{
    i64 mat[4][4];

    i64* operator [] (const int & _x) {return mat[_x];}

    Matrix operator * (const Matrix & _x) {
        Matrix ans;

		memset(ans, 0, sizeof(ans));

		for (int k = 0; k < 4; k++) {
			for (int i = 0; i < 4; i++) {
				for (int j = 0; j < 4; j++) {
					ans[i][j] = (ans[i][j] + mat[i][k] * _x.mat[k][j]) % mod;
				}
			}
		}
        
        return ans;
    }

    Matrix operator + (const Matrix & _x) {
        Matrix ans;

        for(int i = 0; i < 4; i++) {
	        for(int j = 0; j < 4; j++) {
	        ans.mat[i][j] = (ans.mat[i][j] + mat[i][j]) % mod;
		    }
	    }
        return _y;
    }
};

Matrix magic[N];
#define lson(x) x << 1
#define rson(x) x << 1 | 1

struct Segment{
    int l , r;
    Matrix now;
    Matrix lazy;
}s[N<<2];

struct SegmentTree{

    void pushup(int k){
        s[k].now = s[lson(k)].now + s[rson(k)].now;
    }

    void build(int k, int l, int r){
        for(int i = 0; i < 4; i++) s[k].lazy[i][i] = 1;
        s[k].l = l , s[k].r = r;
        if(l == r) {
            s[k].now = magic[l];
            return;
        }

        int mid = l + r >> 1;

        build(lson(k), l, mid);
        build(rson(k), mid + 1, r);

        pushup(k);
    }

    void pushdown(int k){
        s[lson(k)].now = s[lson(k)].now * s[k].lazy;
        s[rson(k)].now = s[rson(k)].now * s[k].lazy;
        s[lson(k)].lazy = s[lson(k)].lazy * s[k].lazy;
        s[rson(k)].lazy = s[rson(k)].lazy * s[k].lazy;

        memset(s[k].lazy.mat, 0, sizeof(s[k].lazy.mat));

        for(int i = 0; i < 4; i++)
        s[k].lazy[i][i] = 1;
    }

    void modify(int k, int l, int r,const Matrix & tag){
        if(l <= s[k].l && s[k].r <= r) {
            s[k].now = s[k].now * tag;
            s[k].lazy = s[k].lazy * tag;
            return;
        }

        pushdown(k);

        int mid = s[k].l + s[k].r >> 1;

        if (l <= mid) modify(lson(k), l, r, tag);
        if (r > mid) modify(rson(k), l, r, tag);

        pushup(k);
    }

    Matrix query(int k, int l, int r){
        if(l <= s[k].l && s[k].r <= r)
            return s[k].now;
        
        pushdown(k);

        int mid = s[k].l + s[k].r >> 1;
        Matrix val;
        memset(val.mat , 0, sizeof(val.mat));

        if (l <= mid) val = val + query(lson(k), l , r);
        if (r > mid) val = val + query(rson(k), l , r);

        return val;
    }
}t;

#undef lson
#undef rson

int n,m;
int main(){
    scanf("%d",&n);

    for(int i = 1; i <= n; i++)
        for(int j = 0; j < 3; j++)
            scanf("%lld",&magic[i][0][j]) , magic[i][0][3] = 1;
    
    t.build(1 , 1 , n);

    scanf("%d",&m);
    for(int i = 1; i <= m; i++) {
        int op , l , r ;
        i64 val;
        scanf("%d %d %d",&op,&l,&r);

        Matrix tag;
        memset(tag.mat, 0, sizeof(tag.mat));

        for(int j = 0; j < 4; j++) tag[j][j] = 1;

        if(op == 1) {
            tag[1][0] = 1;

            t.modify(1,l,r,tag);
        } else if(op == 2) {
            tag[2][1] = 1;

            t.modify(1,l,r,tag);
        } else if(op == 3) {
            tag[0][2] = 1;

            t.modify(1,l,r,tag);
        } else if(op == 4) {
            scanf("%lld",&val);

            tag[3][0] = val;

            t.modify(1,l,r,tag);
        } else if(op == 5) {
            scanf("%lld",&val);

            tag[1][1] = val;

            t.modify(1,l,r,tag);
        } else if(op == 6) {
            scanf("%lld",&val);

            tag[2][2] = 0;
            tag[3][2] = val;

            t.modify(1,l,r,tag);
        } else {
            Matrix ans = t.query(1,l,r);

            for(int j = 0; j < 3; j++)
                printf("%lld ",ans[0][j]);
            puts("");
        }
    }
    return 0;
}

矩阵统计定长路数量

TJOI2017 可乐

简要题意

有一个机器人,他初始在 \(1\) 号节点,它每秒可以去到与当前节点相邻的点或者原地不动或者自爆,问 \(t\) 秒钟,机器人一共有多少种行走方案。

节点总个数 \(n \land 1 \le n \le 30\),边数 \(m \land 1 \le m \le 30\)

思路

可以构造出一个 dp 式子,\(f_{i,j,k}\) 表示时间 \(i\) 机器人从节点 \(j\) 走到节点 \(k\) 的方案数。

至于自爆,可以使机器人前往一个 \(0\) 号节点且 \(0\) 号节点出度为 \(0\) 入度为 \(n\)

原地不动就直接连接自环即可。

因此,就有状态转移:

\(f_{i,j,k} = \sum f_{i - 1,j,p} + f_{i - 1, p, k}\)

这个 dp 显然可以使用矩阵加速,实现 \(\mathcal{O}(n ^ 3 \log t)\) 解决问题。

最后答案 \(ans = \sum\limits_{i = 0} ^ n f_{t, 1, i}\)

参考代码
#include <cstring>
#include <stdio.h>

using u32 = unsigned int ;
using i64 = long long ;
using u64 = unsigned long long ;

const int mod = 2017;
const int N = 35 ;

int n, m;
i64 t, ans;

struct Matrix {
    i64 mat[N][N];
    
    Matrix(){
        memset(mat, 0, sizeof(mat));    
    }
    
    i64 * operator [] (const int & x){return mat[x];}
    
    Matrix operator * (const Matrix & x) {
        Matrix ans;
        
        for (int k = 0; k <= n; k++) {
            for (int i = 0; i <= n; i++) {
                for (int j = 0; j <= n; j++) {
                    ans[i][j] = (ans[i][j] + mat[i][k] * x.mat[k][j] % mod) % mod;
                }        
            }
        }
        
        return ans;
    }
}mat;

void qpow(i64 k){
    Matrix Ans;
    for (int i = 0; i <= n; i++) {
        Ans[i][i] = 1;
    }
    
    for (; k; k >>= 1, mat = mat * mat) {
        if (k & 1) {
            Ans = Ans * mat;
        }    
    }
    
    for (int i = 0; i <= n; i++) {
        ans = (ans + Ans[1][i]) % mod;
    }
}

int main(){
    scanf("%d %d", &n, &m);
    
    for (int i = 1; i <= m; i++) {
        int u, v;
        scanf("%d %d", &u, &v);
        mat[u][v] = mat[v][u] = 1;
    }
    
    scanf("%lld", &t);
    for (int i = 0; i <= n; i++) {
        mat[i][i] = mat[i][0] = 1;
    }
    
    qpow(t);
    
    printf("%lld", ans);
    return 0;
}

推广

此方法可以推广,即只需要有一个矩阵表示时间 \(0\) (或者行走了 \(1\) 条边时)时的方案数,然后通过矩阵乘法加速转移就可以完成实现行走 \(k\) 条边时,\(i\)\(j\) 的方案数。

矩阵求定长最短路

问题

给定一个图,问从一号节点经过 \(k(1 \le k \le 10^9)\) 条边与 \(n(1 \le n \le 50)\) 节点的最短路。

思路

显然可以设状态 \(f_{k,i,j}\) 表示经过 \(k\) 条边从 \(i\) 节点到 \(j\) 节点的最短路。

显然这是 Floyd 算法,因此有状态装移 \(f_{k,i,j} = \min\{f_{k - 1, i, p} + f_{k - 1, p, j}\}\)

和上面的一样可以构造矩阵,实现 \(\mathcal{O}(n ^ 3 \log k)\) 解决。

注意

因为此题的转移是通过类似 Floyd 算法转移的,因此矩阵乘法需要改为 Floyd。

限长最短路/路径计数

路径计数

问题1

问一个图中,长度小于等于 \(k\) 的路径有多少条。

做法

可以通过添加一个虚点记录答案解决这个问题,并在图中 \((v, v^{'})\)\((v^{'}, v^{'})\) 这两条边即可。

当然也可以通过构造矩阵时,用代数方式直接添加新的变量统计答案即可。

最后使用矩阵快速幂求解即可。

问题2

问一个图中,经过的边数小于等于 \(k\) 的最短路。

做法

每个点向自己连边并且此边边权为 \(0\) 即可。

最后也是用矩阵快速幂迭代 \(k\) 此即可。

posted @ 2024-05-30 15:22  Z_drj  阅读(13)  评论(1编辑  收藏  举报