跳转至

矩阵

数据存在矩阵第一行,乘法后还在第一行!

矩阵的相关定义

矩阵运算是线性代数中的一个重要概念,涉及到矩阵的加法、减法、数乘、乘法、转置、逆矩阵等基本操作。以下是一些基本的矩阵运算法则:

  1. 矩阵加法和减法

    • 两个相同大小的矩阵A和B相加,结果矩阵C的每个元素是A和B对应元素的和的矩阵。

    • 两个相同大小的矩阵A和B相减,结果矩阵C的每个元素是A和B对应元素的差的矩阵。

  2. 矩阵数乘

    • 一个标量与一个矩阵相乘,结果矩阵的每个元素都是标量与矩阵对应元素的乘积。
  3. 矩阵乘法

    • 矩阵A乘以矩阵B,要求A的列数等于B的行数。结果矩阵C的每个元素是A的列向量与B的行向量的标量积的和。
  4. 矩阵转置

    • 矩阵A的转置矩阵记为A^T,是将A的行向量转换为列向量的矩阵。
  5. 逆矩阵

    • 如果矩阵A是可逆的,那么存在一个矩阵A-1,使得A乘以A-1等于A^-1乘以A等于单位矩阵I。
  6. 矩阵的幂

    • 矩阵A的幂是指A连乘自身多次,例如A^n,其中n是正整数。
  7. 分块矩阵

    • 分块矩阵是将矩阵分成若干个子矩阵,每个子矩阵保持原来的次序和位置。
  8. 列向量与矩阵的乘法

    • 列向量与矩阵相乘,结果是一个行向量,每个元素是列向量与矩阵对应列向量的标量积。
  9. 行向量与矩阵的乘法

    • 行向量与矩阵相乘,结果是一个列向量,每个元素是行向量与矩阵对应行向量的标量积。
  10. 矩阵的迹

    • 矩阵A的迹是指矩阵主对角线上的元素之和,记为tr(A)。

矩阵的加法与减法

矩阵的加法规则如下:

  1. 只有当两个矩阵的行数和列数相同时,它们才能进行加法。

  2. 相同位置的元素逐个相加。

  3. 结果仍然是一个矩阵,其行数和列数与原始矩阵相同。

  4. 加法满足交换律,即$A + B = B + A $。

  5. 存在一个零矩阵\(0\),使得对于任何矩阵A,都有$A + 0 = A $。

减法类似。

矩阵乘法

9zfncisd.png

图片是引用的

矩阵快速幂

应用

通常我们会遇到一些“大模拟”题,这里的大指的是要模拟的步数很多,但是每一步并不复杂。并且通常情况下,模拟的操作都是一个数组上的值的变化。

我们可以把矩阵看作是“过程”,并且其满足乘法的性质(即连续进行两次“过程”的变化就相当于将因此变化的矩阵平方后的矩阵)。那么我们就可以将通过矩阵快速幂来快手求出很多次操作之后的“变化”,然后对原始数组应用变化即可。

实现

原理

因为矩阵满足交换律和结合律(自行搜索),所以矩阵快速幂和快速幂一样,只不过快速幂中的数乘(res*=a,a*=a变成了两个矩阵相乘)

存矩阵

我们使用结构体存,这样在传参时方便许多

struct node {
    int a[N][N];
    node() {
        memset(a,0,sizeof a); //一定要写!
    }
}c;

在清空结构体时可以使用

memset(c.a,0,sizeof c.a);

或者再结构体中手动写入

node() {
        memset(a,0,sizeof a); //一定要写!
    }

这样可以在创建结构体时自动清空.

与快速幂的区别

在快速幂中,我们有一个初始值,即res=1ksm函数中,我们不断res=res*c%p;更新res,得到结果

在矩阵快速幂中,我们的操作对象是一个矩阵,所以我们需要这么一个初始矩阵.

其实就是

for(int i=1; i<=n; i++)res.a[i][i]=1;

n=5
1 0 0 0 0  
0 1 0 0 0  
0 0 1 0 0  
0 0 0 1 0  
0 0 0 0 1  

在快速幂中,我们的函数是

int ksm(int c,int k,int p) {    //c^k %p
    int res=1;
    ...
    return res;
}

我们把c替换为需要被快速幂的矩阵,res初始化为初始矩阵,重载一下*运算符,就完工啦!(c,res请使用结构体类型) 或者写一个矩阵乘的函数(如代码2)

重定义运算符,顺便将取模放进去

struct node{
    int a[N][N];
}node c;

node operator *(const node &x,const node &y){     //重载运算符
    for(int k=1;k<=n;++k)
        for(int i=1;i<=n;++i)
            for(int j=1;j<=n;++j)
                z.a[i][j]=(z.a[i][j]+x.a[i][k]*y.a[k][j]%mod)%mod;
    return z;
}

代码:

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=105,Mod=1e9+7;
int n,k;
struct node {
    int a[N][N];
    node(){
        memset(a,0,sizeof a); //一定要写!
    }
}c,res;


void ksm() {

    while(k) {
        if(k&1) {
            node x;
            for(int k=1; k<=n; k++)
                for(int i=1; i<=n; i++) {
                    for(int j=1; j<=n; j++)x.a[i][j]=(x.a[i][j]+res.a[i][k]*c.a[k][j]%Mod)%Mod;
                }
            res=x;
        }
        {
            node x;
            for(int k=1; k<=n; k++)
                for(int i=1; i<=n; i++) {
                    for(int j=1; j<=n; j++)x.a[i][j]=(x.a[i][j]+c.a[i][k]*c.a[k][j]%Mod)%Mod;
                }
            c=x;
        }
        k>>=1;
    }

}

signed main() {
    cin>>n>>k;
    for(int i=1; i<=n; i++)for(int j=1; j<=n; j++) {
            cin>>c.a[i][j];
        }

    for(int i=1; i<=n; i++)res.a[i][i]=1;
    ksm();

    for(int i=1; i<=n; i++) {
        for(int j=1; j<=n; j++)cout<<res.a[i][j]<<' ';
        cout<<'\n';
    }
    return 0;
}

写法2

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=105,Mod=1e9+7;
int n,k;
struct node {
    int a[N][N];
    node() {
        memset(a,0,sizeof a); //一定要写!
    }
}c;

node f(node a,node b) { //矩阵乘矩阵,返回矩阵.这个只能暴力,没有其他办法
    node x;
    for(int k=1; k<=n; k++)
        for(int i=1; i<=n; i++) {
            for(int j=1; j<=n; j++)x.a[i][j]=(x.a[i][j]+a.a[i][k]*b.a[k][j]%Mod)%Mod;
        }
    return x;
}
node ksm() {
    node res;
    for(int i=1; i<=n; i++)res.a[i][i]=1;
    while(k) {
        if(k&1) {
            res=f(res,c);
        }
        c=f(c,c);
        k>>=1;
    }
    return res;
}


signed main() {
    cin>>n>>k;
    for(int i=1; i<=n; i++)for(int j=1; j<=n; j++) {
            cin>>c.a[i][j];
        }


    node res=ksm();

    for(int i=1; i<=n; i++) {
        for(int j=1; j<=n; j++)cout<<res.a[i][j]<<' ';
        cout<<'\n';
    }
    return 0;
}

例题 #1 矩阵加速(数列)

题目描述

已知一个数列 \(a\),它满足:

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

\(a\) 数列的第 \(n\) 项对 \(10^9+7\) 取余的值。

  • 对于 \(100\%\) 的数据 \(1 \leq T \leq 100\)\(1 \leq n \leq 2 \times 10^9\)

步骤

  • 总结递推矩阵

这一步是最难的地方也是最不容易理解的地方。那么现在我们就来推导一下这个地方。

对于\(a_x=a_{x-1}+a_{x-3}\)

\([a_{x-3},a_{x-2},a_{x-1}]\times [?]=[a_{x-2},a_{x-1},a_{x}]\)

现在我们要求出[?]就万事大吉了。那么结合矩阵的定义,我们先设\([?]=\begin{bmatrix}a1 & b1& c1 \\a2 & b2&c2 \\a3&b3&c3\end{bmatrix}\)

那么\([a_{x-3},a_{x-2},a_{x-1}]\times \begin{bmatrix}a1 & b1& c1 \\a2 & b2&c2 \\a3&b3&c3\end{bmatrix}= [a_{x-2},a_{x-1},a_{x}]\)

记住口诀:行同A,列同B

image.png

屏幕截.png

所以

\(a_{x-2}=a_{x-3}a1+a_{x-2}a2+a_{x-1}a3\)

\(a_{x-1}=a_{x-3}b1+a_{x-2}b2+a_{x-1}b3\)

\(a_{x}=a_{x-3}c1+a_{x-2}c2+a_{x-1}c3\)

那么现在结合题目,就很容易推出\([?]\)是什么了。

\(a_{x-2}=a_{x-3}(a1=0)+a_{x-2}(a2=1)+a_{x-1}(a3=0)\)

\(a_{x-1}=a_{x-3}(b1=0)+a_{x-2}(b2=0)+a_{x-1}(b3=1)\)

\(a_{x}=a_{x-3}(c1=1)+a_{x-2}(c2=0)+a_{x-1}(c3=1)\)

所以!!\([?]=\begin{bmatrix}0 & 0& 1 \\1 & 0&0 \\0&1&1\end{bmatrix}\)

注意这里与数组中的下标对应为\([?]=\begin{bmatrix}3,3 & 2,3& 1,3 \\3,2 & 2,2&1,2 \\3,1&2,1&1,1\end{bmatrix}\)

为什么会这样呢?因为数组的第一维是行!所以我们应该写成

\(\begin{bmatrix}a_{x-3}\\a_{x-2}\\a_{x-1}\end{bmatrix}\times \begin{bmatrix}0 & 1& 0 \\1 & 0&0 \\1&0&1\end{bmatrix}=\begin{bmatrix}a_{x-2}\\a_{x-1}\\a_{x}\end{bmatrix}\)

这样就没问题了吧?嘿嘿。

  • 应用矩阵结合律,将 \(S\times A\times A\times ...\) 转化为 \(S\times A^n\)

  • 使用矩阵快速幂

  • 在运行得到的最后矩阵中找到需要的值,(特判),输出

代码

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=105,Mod=1e9+7;
int n,k;
struct node {
    int a[5][5];
    node() {
        memset(a,0,sizeof a); //一定要写!
    }
};

node f(node a,node b) {
    node x;
    for(int k=1; k<=3; k++)
        for(int i=1; i<=3; i++) {
            for(int j=1; j<=3; j++)x.a[i][j]=(x.a[i][j]+a.a[i][k]*b.a[k][j]%Mod)%Mod;
        }
    return x;
}
node ksm() {
    node res,c;
    for(int i=1; i<=3; i++)res.a[i][i]=1;
    c.a[1][1]=c.a[1][3]=c.a[2][1]=c.a[3][2]=1;
    while(k) {
        if(k&1) {
            res=f(res,c);
        }
        c=f(c,c);
        k>>=1;
    }
    return res;
}

signed main() {
    int T;
    cin>>T;
    while(T--) {
        cin>>k;
        if(k<=3){
            cout<<1<<endl;
            continue;
        }
        node res;
        res=ksm();

        cout<<res.a[2][1]<<endl;
    }


    return 0;
}

注意:

从矩阵快速幂模板copy时,记得修改矩阵大小n,总结出递推矩阵

如本题的递推矩阵为

c.a[1][1]=c.a[1][3]=c.a[2][1]=c.a[3][2]=1;其余为0;

例题 #2 斐波那契数列

题目描述

大家都知道,斐波那契数列是满足如下性质的一个数列:

\(F_n = \left\{\begin{aligned} 1 \space (n \le 2) \\ F_{n-1}+F_{n-2} \space (n\ge 3) \end{aligned}\right.\)

请你求出 \(F_n \bmod 10^9 + 7\) 的值。

输入格式

一行一个正整数 \(n\)

输出格式

输出一行一个整数表示答案。

【数据范围】 对于 \(60\%\) 的数据,\(1\le n \le 92\); 对于 \(100\%\) 的数据,\(1\le n < 2^{63}\)

思路

有了上面一道题的经历,这道题是不是步骤~一模一样~?

复制一份水一水字数捏~

  • 总结递推矩阵

这一步是最难的地方也是最不容易理解的地方。那么现在我们就来推导一下这个地方。

对于\(a_x=a_{x-1}+a_{x-2}\)

\([a_{x-2},a_{x-1}]\times [?]=[a_{x-1},a_{x}]\)

现在我们要求出[?]就万事大吉了。那么结合矩阵的定义,我们先设\([?]=\begin{bmatrix}a1 & b1 \\a2 & b2\end{bmatrix}\)

那么\([a_{x-2},a_{x-1}]\times \begin{bmatrix}a1 & b1 \\a2 & b2\end{bmatrix}= [a_{x-1},a_{x}]\)

记住口诀:行同A,列同B

屏幕截图+2023-11-05+150758.png

所以

\(a_{x-1}=a_{x-2}a1+a_{x-1}a2\)

\(a_{x}=a_{x-2}b1+a_{x-1}b2\)

那么现在结合题目,就很容易推出\([?]\)是什么了。

\(a_{x-1}=a_{x-2}(a1=0)+a_{x-1}(a2=1)\)

\(a_{x}=a_{x-2}(b1=1)+a_{x-1}(b2=1)\)

所以!!\([?]=\begin{bmatrix}0 & 1 \\1 & 1\end{bmatrix}\)

注意这里与数组中的下标对应为\([?]=\begin{bmatrix}3,3 & 2,3& 1,3 \\3,2 & 2,2&1,2 \\3,1&2,1&1,1\end{bmatrix}\)

为什么会这样呢?因为数组的第一维是行!所以我们应该写成

\(\begin{bmatrix}a_{x-2}\\a_{x-1}\end{bmatrix}\times \begin{bmatrix}1& 0 \\1 & 1\end{bmatrix}=\begin{bmatrix}a_{x-1}\\a_{x}\end{bmatrix}\)

这样就没问题了吧?嘿嘿。

代码

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=105,MOD=1e9+7;
int n,k;
struct node {
    int a[5][5];
    node() {
        memset(a,0,sizeof a); //一定要写!
    }
};

node f(node a,node b) {
    node x;
    for(int k=1; k<=2; k++)
        for(int i=1; i<=2; i++) {
            for(int j=1; j<=2; j++)x.a[i][j]=(x.a[i][j]+a.a[i][k]*b.a[k][j]%MOD)%MOD;
        }
    return x;
}
node ksm() {
    node res,c;
    for(int i=1; i<=2; i++)res.a[i][i]=1;
    c.a[1][1]=c.a[1][2]=c.a[2][1]=1;
    while(k) {
        if(k&1) {
            res=f(res,c);
        }
        c=f(c,c);
        k>>=1;
    }
    return res;
}

signed main() {

    cin>>k;
    if(k<=2){
        cout<<1<<endl;
        return 0;
    }
    node res;
    res=ksm();
    cout<<res.a[2][1]<<endl;
    return 0;
}

例题 #3 矩阵加速模拟 石头游戏

石头游戏在一个 \(n\)\(m\) 列的网格上进行,每个格子对应一种操作序列,操作序列至多有 \(10\) 种,分别用 \(0 \sim 9\)\(10\) 个数字指明。

操作序列是一个长度不超过 \(6\) 且循环执行、每秒执行一个字符的字符串。

每秒钟,所有格子同时执行各自操作序列里的下一个字符。

序列中的每个字符是以下格式之一:

  1. 数字 \(0 \sim 9\):表示拿 \(0 \sim 9\) 个石头到该格子。

  2. NWSE:表示把这个格子内所有的石头推到相邻的格子,N 表示上方,W 表示左方,S 表示下方,E 表示右方。

  3. D:表示拿走这个格子的所有石头。

给定每种操作序列对应的字符串,以及网格中每个格子对应的操作序列,求石头游戏进行了 \(t\) 秒之后,石头最多的格子里有多少个石头。

在游戏开始时,网格是空的。

输入格式

第一行 \(4\) 个整数 \(n, m, t, act\)

接下来 \(n\) 行,每行 \(m\) 个字符,表示每个格子对应的操作序列。

最后 \(act\) 行,每行一个字符串,表示从 \(0\) 开始的每个操作序列。

输出格式

一个整数:游戏进行了 \(t\) 秒之后,所有方格中石头最多的格子有多少个石头。

对于所有的测试数据, 满足 \(1 \le m,n \le 8\)\(1 \le t \le 10^8\)\(1 \le act \le 10\)


同样是使用矩阵来加速模拟过程的题目。

注意到我们的矩阵之所以是二维的,是表示从要给位置向另外一个位置的注意,所以他们在的矩阵事实上我们应该把它重新编号为一维,要不然我们就在做立方体乘法了。

那么我们定义id(x,y)为(x,y)的重编号,我们怎么样来用矩阵表示出操作过程呢?

  1. 拿石子操作:[0][id(i, j)] 赋值为石子数量,[id(i, j)][id(i, j)]=1

  2. NESW 操作:[sw(i, j)][id(移动终位置)]=1。比如 N[id(i, j)][id(i - 1, j)]

  3. D 操作:无需赋值,因为矩阵默认值为零。

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


#define ell dbg('\n')
const char el='\n';
const bool enable_dbg = 1;
template <typename T,typename... Args>
void dbg(T s,Args... args) {
    if constexpr (enable_dbg){
    cerr << s;
    if(1)cerr<<' ';
        if constexpr (sizeof...(Args))
            dbg(args...);
    }
}

#define zerol = 1
#ifdef zerol
#define cdbg(x...) do { cerr << #x << " -> "; err(x); } while (0)
void err() { cerr << endl; }
template<template<typename...> class T, typename t, typename... A>
void err(T<t> a, A... x) { for (auto v: a) cerr << v << ' '; err(x...); }
template<typename T, typename... A>
void err(T a, A... x) { cerr << a << ' '; err(x...); }
#else
#define dbg(...)
#endif



#define int long long
int n,m,t,act; 
int a[30][30];
string s[30];//字符串
int get(int x,int y){
    return (x-1)*m+y;
}
void mulself(int a[110][110],int b[110][110]){//矩阵乘矩阵
    int c[110][110]={{0}};
    for(int i=0;i<=n*m;i++)
        for(int j=0;j<=n*m;j++)
            for(int k=0;k<=n*m;k++)
                c[i][j]+=a[i][k]*b[k][j];
    memcpy(a,c,sizeof c); 
}
void mul(int f[110],int a[110][110]){
    int c[110]={0};
    for(int j=0;j<=n*m;j++)
        for(int k=0;k<=n*m;k++)
            c[j]+=f[k]*a[k][j];
    memcpy(f,c,sizeof c);
}
int mat[110][110][110],q[110][110];//60个状态矩阵与60个状态矩阵的乘积(打包计算的)
int f[110];//二维网格压缩的一维矩阵
signed main() {
    cin>>n>>m>>t>>act;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            scanf("%1d",&a[i][j]),a[i][j]++;
    for(int i=1;i<=act;i++){//复制字符串到 60 个
        cin>>s[i];
        string t=s[i];
        while(s[i].size()!=60) s[i]+=t;
        s[i]=" "+s[i]; 
    }
    for(int k=1;k<=60;k++){//建立60个状态矩阵
        for(int i=1;i<=n;i++)
            for(int j=1;j<=m;j++){
                int x=a[i][j];
                if(isdigit(s[x][k])){
                    mat[k][0][get(i,j)]=s[x][k]-'0';
                    mat[k][get(i,j)][get(i,j)]=1;
                }
                else if(s[x][k]=='N' && i>1/*边界问题*/) mat[k][get(i,j)][get(i-1,j)]=1;
                else if(s[x][k]=='W' && j>1) mat[k][get(i,j)][get(i,j-1)]=1;
                else if(s[x][k]=='S' && i<n) mat[k][get(i,j)][get(i+1,j)]=1;
                else if(s[x][k]=='E' && j<m) mat[k][get(i,j)][get(i,j+1)]=1;
            }
        mat[k][0][0]=1; //赋值为 1 保证 f[0]=1
    }
    memcpy(q,mat[1],sizeof mat[1]);
    for(int k=2;k<=60;k++) mulself(q,mat[k]);//计算需要矩阵快速幂的矩阵

    for(int i=0;i<=n;i++){
        for(int j=0;j<=m;j++)dbg(q[i][j]);ell;
    }
    f[0]=1;//压缩成一维数组,且 f[0]=1
    int u=t/60;
    while(u){//矩阵快速幂
        if(u & 1) mul(f,q);
        mulself(q,q);
        u>>=1;
    }


    // for(int i=1;i<=m*n;i++)cdbg(f[i]);
    u=t%60;
    for(int i=1;i<=u;i++){ mul(f,mat[i]);//计算剩余部分

        // for(int j=1;j<=m*n;j++)cdbg(f[i]);

    }
    int ans=0;
    for(int i=1;i<=n*m;i++) ans=max(ans,f[i]);//计算
    cout<<ans;
    return 0;
}

例题 #4 矩阵加速Floyd [SDOI2009] HH去散步

在这之前可以写矩阵加速floyd模板题

www.luogu.com.cn


题目描述

HH 有个一成不变的习惯,喜欢饭后百步走。所谓百步走,就是散步,就是在一定的时间内,走过一定的距离。但是同时 HH 又是个喜欢变化的人,所以他不会立刻沿着刚刚走来的路走回。又因为 HH 是个喜欢变化的人,所以他每天走过的路径都不完全一样,他想知道他究竟有多少种散步的方法。

现在给你学校的地图(假设每条路的长度都是一样的都是 \(1\)),问长度为 \(t\),从给定地点 \(A\) 走到给定地点 \(B\) 共有多少条符合条件的路径。

输入格式

第一行:五个整数 \(N,M,t,A,B\)。其中 \(N\) 表示学校里的路口的个数,\(M\) 表示学校里的路的条数,\(t\) 表示 HH 想要散步的距离,\(A\) 表示散步的出发点,而 \(B\) 则表示散步的终点。

接下来 \(M\) 行,每行一组 \(A_i,B_i\),表示从路口 \(A_i\) 到路口 \(B_i\) 有一条路。数据保证 \(A_i \neq B_i\),但不保证任意两个路口之间至多只有一条路相连接。路口编号从 \(0\)\(N-1\)。同一行内所有数据均由一个空格隔开,行首行尾没有多余空格。没有多余空行。答案模 \(45989\)

对于 \(100\%\) 的数据,\(N \le 50\)\(M \le 60\)\(t \le 2^{30}\)\(0 \le A,B\)


首先考虑普通的写法怎么写。定义\(f_{i,j}\)为走i步,到达点j的方案数量。

但是有一个问题,就是我们不能立刻沿着刚刚走来的路走回,以我们貌似还需要记录来的边。

那么既然我们都知道来的边了,我们很显然就可以知道当前在那个点了。所以就是

\(f_{i,j}\)表示走i步,走到边j的终点的方案数量。

如果是普通的dp,那么我们直接枚举边i,对i终点的的邻边进行更新即可。

那么我们怎么样去放在矩阵快速幂上进行优化呢?

首先矩阵快速幂优化的条件是我们要将方程写成\(f_{i,\dots}=\text{op}f_{i-1,\dots}\)的形式。恰好这里就是。

那么我们考虑矩阵的乘法运算。\(res_{i,j}\)由a的第i行和j的第j列逐项相乘后相加得到。因此我们枚举两条边a,b,如果a,b不是同一条,且a,b首尾相连,那么我们就可以走到有一个转移是\(f_{i,b}=f_{i-1,a}\text{op}\dots\)。这里的op自然是加法了,也正好对应矩阵乘法中相加的运算。

因此对于这种a,b,我们就有初始矩阵中的\(f_{a,b}=1\)

然后我们的矩阵快速幂需要3个东西,f,base,t ,代表的运算是\(base\times f^t\)。那么这里的t就是输入的t,base是?

base就是单位矩阵了,即对角线是1,其余是0的矩阵。当然我们也可以直接将f作为base,但是t要-1.

#include<bits/stdc++.h>
using namespace std;
#define int long long
const double eps=1e-6;
const int MOD=45989;
const int N=1e3+5;

#define rd read()
int read(){
    int x;
    cin>>x;
    return x;
}
#define cdbg(x) cerr<<x<<endl;

string s;
int tot;

struct math{
    int a[122][122];

    math operator *(const math b)const{
        math r;
        memset(r.a,0,sizeof r.a); //注意清零
        for(int k=1;k<=tot;k++){
            for(int i=1;i<=tot;i++){
                for(int j=1;j<=tot;j++){
                    r.a[i][j]+=a[i][k]*b.a[k][j];
                    r.a[i][j]%=MOD;
                }
            }
        }

        return r;
    }
}a;

int n;
int t;

math ksm(math a,int b){
    math res;
    memset(res.a,0,sizeof res.a);
    for(int i=1;i<=tot;i++)res.a[i][i]=1;
    math base=a;
    while(b){
        if(b&1)res=res*base;
        b>>=1;
        base=base*base;
    }
    return res;
}


int x[N];
int y[N];


signed main(){
     n=rd;
     int m=rd;
     t=rd;
     int A=rd+1,B=rd+1;
     x[++tot]=0;
     y[tot]=A;
     for(int i=1;i<=m;i++){
        int a=rd+1,b=rd+1;
        x[++tot]=a;
        y[tot]=b;
        x[++tot]=b;
        y[tot]=a;
     }

     for(int i=1;i<=tot;i++){
        for(int j=1;j<=tot;j++){
            if(i==j||i==(j^1))continue;
            if(y[i]==x[j])a.a[i][j]=1;//两条边可以接上
        }
     }

     math ans=ksm(a,t);

     int res=0;
     for(int i=1;i<=tot;i++){
        (res+=(y[i]==B)*ans.a[1][i])%=MOD;
     }

     cout<<res<<endl;
}

题单

矩阵乘法题单

矩阵求逆

矩阵求逆是线性代数中的一个概念,指的是对于一个给定的方阵(即行数和列数相等的矩阵),寻找另一个方阵,使得这两个方阵相乘的结果是单位矩阵。 具体来说,假设有一个 \(n \times n\) 的方阵 \(A\),如果存在另一个 \(n \times n\) 的方阵 \(B\),使得 \(A \times B = B \times A = I\),其中 \(I\)\(n \times n\) 的单位矩阵(单位矩阵的主对角线上的元素都是1,其余元素都是0),那么矩阵 \(B\) 就被称为矩阵 \(A\) 的逆矩阵,记作 \(A^{-1}\)。 不是所有的方阵都有逆矩阵。一个方阵 \(A\) 存在逆矩阵的充分必要条件是它的行列式(determinant)不为零,即 \(\text{det}(A) \neq 0\)。如果行列式为零,则矩阵 \(A\) 是奇异的,它没有逆矩阵。 矩阵求逆的方法有很多,以下是一些常见的方法:

  1. 高斯-约当消元法:通过行变换将矩阵 \(A\) 转换成单位矩阵,同时对单位矩阵进行相同的行变换,最终得到的矩阵就是 \(A\) 的逆矩阵。

  2. 伴随矩阵法:首先计算 \(A\) 的伴随矩阵(adjugate matrix),然后利用公式 \(A^{-1} = \frac{1}{\text{det}(A)} \text{adj}(A)\) 来计算 \(A\) 的逆矩阵。

  3. 直接公式法:对于2x2或3x3的矩阵,可以直接使用特定的公式来计算逆矩阵。 矩阵求逆在多个领域都有应用,比如在解决线性方程组、在物理学中的变换、在计算机图形学中的坐标变换等。它是线性代数中的一个重要概念,也是许多数学和工程问题的基础。

矩阵乘法解决实际问题

在图论中

设现在有一个邻接矩阵\(A\)

那么\(A^k\)的意义是什么?(两个点之间若有边则\(A[u][v]=1\)

\(floyd\)算法的角度考虑,不难发现\(A^k\)的第\(i\)行第\(j\)列的数字含义是从\(i\)\(j\)经过\(k\)步的路径方案总数。

[TJOI2017] 可乐(数据加强版)

加里敦星球的人们特别喜欢喝可乐。因而,他们的敌对星球研发出了一个可乐机器人,并且放在了加里敦星球的 \(1\) 号城市上。这个可乐机器人有三种行为: 停在原地,去下一个相邻的城市,自爆。它每一秒都会随机触发一种行为。现在给加里敦星球城市图,在第 \(0\) 秒时可乐机器人在 \(1\) 号城市,问经过了 \(t\) 秒,可乐机器人的行为方案数是多少?

输出可乐机器人的行为方案数,答案可能很大,请输出对 \(2017\) 取模后的结果。

对于 \(100\%\) 的数据, \(n,m\leq 100\) , $ t\leq 10^9$ .


这道题我们可以从**邻接矩阵**的**幂**的意义考虑。

设现在有一个邻接矩阵\(A\)

那么\(A^k\)的意义是什么?(两个点之间若有边则\(A[u][v]=1\)

\(floyd\)算法的角度考虑,不难发现\(A^k\)的第\(i\)行第\(j\)列的数字含义是从\(i\)\(j\)经过\(k\)步的路径方案总数。

从这个角度考虑,这个点就有了一种做法。

首先将这个图的邻接矩阵建出来,然后直接算这个矩阵的\(k\)次方。

最后统计\(\sum\limits_{i=1}^{n}A[1][i]\)就是答案。

停留

在原地停留很简单,我们只要认为每个点都有一个从自己到自己的自环即可。

自爆

我们可以将自爆这个**状态**也看成一个**城市**,就设它为编号\(0\)

我们在邻接矩阵上从每个点都向这个点连一条边,这个点除了自己外不连其他出边。

这样就满足了任何一个点随时可以自爆,且无法恢复到其他状态。

最后,统计答案。\(Ans=\sum\limits_{i=0}^{n}A[1][i]\)

/*
CB Ntsc
*/

#include <bits/stdc++.h>
using namespace std;
#define int long long
#define mp make_pair

#define rd read()
inline int read() {
    int xx = 0, ff = 1;
    char ch = getchar();
    while (ch < '0' || ch > '9') {
        if (ch == '-')
            ff = -1;
        ch = getchar();
    }
    while (ch >= '0' && ch <= '9') xx = xx * 10 + (ch - '0'), ch = getchar();
    return xx * ff;
}
inline void write(int out) {
    if (out < 0)
        putchar('-'), out = -out;
    if (out > 9)
        write(out / 10);
    putchar(out % 10 + '0');
}



const int N = 100 + 5;
const int M = 40;
const int INF = 1e16 + 5;
const int MOD = 2010 + 7;

int t,m,n;

struct node {
    int a[N][N];
    node() {
        memset(a,0,sizeof a); //一定要写!
    }
}c;

void add(int u,int v){
    c.a[u][v]=c.a[v][u]=1;  

}

node f(node a,node b) {
    node x;
    for(int k=0; k<=n; k++)
        for(int i=0; i<=n; i++) {
            for(int j=0; j<=n; j++)x.a[i][j]=(x.a[i][j]+a.a[i][k]*b.a[k][j]%MOD)%MOD;
        }
    return x;
}
node ksm(node c,int k) {
    node res;
    for(int i=0; i<=n; i++)res.a[i][i]=1;

    while(k) {
        if(k&1) {
            res=f(res,c);
        }
        c=f(c,c);
        k>>=1;
    }
    return res;
}

int ans;
signed main(){
    n=rd;m=rd;
    for(int i=1;i<=m;i++){
        int u=rd,v=rd;
        add(u,v);
    }
    t=rd;

    for(int i=0;i<=n;i++){
        c.a[i][i]=1;//建立自环,即不动的情况
    }
    for(int i=1;i<=n;i++)c.a[i][0]=1;//建立单向边到0节点表示自爆

    node res=ksm(c,t);//快速幂求矩阵乘法
    for(int i=0;i<=n;i++){
        (ans+=res.a[1][i])%=MOD;//统计答案
    }
    write(ans),putchar('\n');//输出答案
    return 0;
}
/*
1
2 5 1 
0 0 1 
0 0 4 

*/

在递推与变换中 | 矩阵的结合律应用

P5550 Chino的数列

没有背景,我写不出来了qwq

Chino可可爱爱涅!

题目描述

Chino给定了\(n\)个数\(a_1...a_n\),给定常数\(s,m\),她会轮流对这\(n\)个数做\(k\)组操作,每组操作包含以下两步:

\(1.swap(a_s,a_m)\)(交换\(a_s\),\(a_m\))

\(2.\)\(n\)个数都向前平移一位(第\(1\)个移动到第\(n\)个位置上)

Chino想知道,\(k\)组操作后,这\(n\)个数分别是多少?

对于\(100\%\)的数据,\(1 \leq n \leq 80\)\(1 \leq s < m\leq n\)\(1 \leq k \leq 10^{18}\)

所有数字均在\(long~ long\)以内


我们要构建矩阵来快速转移。

那么我们怎么样构建矩阵呢?对于操作一,我们可以构建矩阵\(a_{i,i}=1\),但是\(a_{s,m}=a_{m,s}=1,a_{s,s}=a_{m,m}=0\),然后矩阵快速幂转移就行了。

对于操作2,我们也可以构建矩阵\(b_{i,i+1}=1\),对于\(i=n,b_{n,1}=1\)然后矩阵快速幂转移。

那么我们分开的情况可以处理了,我们怎么样把这两个操作一起转移呢?很简单,矩阵是满足结合律的,所以我们让快速幂中转移的\(base=a\times b\)就可以啦!

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=105,MOD=1e9+7;
int n,k,s,m;
struct node {
    int a[88][88];
    node() {
        memset(a,0,sizeof a); //一定要写!
    }
}a,b,c;

node f(node a,node b) {
    node x;
    for(int k=1; k<=n; k++)
        for(int i=1; i<=n; i++) {
            for(int j=1; j<=n; j++)x.a[k][i]=(x.a[k][i]+a.a[j][i]*b.a[k][j]%MOD)%MOD;
        }
    return x;
}
node ksm(node base) {
    node res,c=base;
    for(int i=1; i<=n; i++)res.a[i][i]=1;

    while(k) {
        if(k&1) {
            res=f(res,c);
        }
        c=f(c,c);
        k>>=1;
    }
    return res;
}

signed main() {
    cin>>n>>s>>m>>k;
    for(int i=1;i<=n;i++)cin>>c.a[i][1];
    for(int i=1;i<=n;i++)if(i!=s&&i!=m)a.a[i][i]=1;
    a.a[s][m]=1;a.a[m][s]=1;
    for(int i=1;i<=n-1;i++)b.a[i][i+1]=1;
    b.a[n][1]=1;

    node base=f(a,b);
    node res;
    res=ksm(base);
    c=f(c,res);
    for(int i=1;i<=n;i++)cout<<c.a[i][1]<<' ';
    return 0;
}

在递推与变换中 | 广义矩阵乘法应用

[GZOI2017] 河神

给出数列 \(\{a_n\}\)\(\{b_n\}\) 以及 \(\{A_n\}\) 的递推关系, 试求出数列 \(\{A_n\}\)\(N\) 项.

递推关系为:

\(A_n=\begin{cases}a_n & 0 \le n < K \\ \bigoplus (A_{n-K+t} \otimes b_t) & n \ge K \end{cases}\)

其中,\(\otimes\) 表示与操作,\(\oplus\) 表示或操作。


现在我们要解决的问题就是 \([a_1,a_2,\dots,a_{K-1}]→[\dots,A_n]\)的问题,忽略0≤n<K的情况,这个只不过是表示数组的初始值。

考虑用矩阵快速幂来解决,此处我们更改矩阵乘法的定义:将原本的乘法改为按位与,原本的加法改为按位或。那么因为是位运算,所以在快速幂中的基[a_{i,i}=1]就应该变成\([a_{i,i}=\text{Bbase}=(111\dots111)_2]\)

\(\begin{bmatrix} a_{n-1} & a_{n-2} &\dots &a_{n-k}\end{bmatrix} \times \begin{bmatrix} b_1 &\text{Bbase} &0 & \dots & 0 \\ b_{2} & 0 & \text{Bbase} & \dots & 0 \\b_3 & 0 & 0 & \dots & 0 \\ \dots & \dots & \dots & \dots & \dots \\ b_{k-1} & 0 & 0 & \dots & \text{Bbase}\\ b_k & 0 & 0 & \dots & 0\end{bmatrix}=\begin{bmatrix} a_n & a_{n-1} &\dots &a_{n-k+1}\end{bmatrix}\)

#include<bits/stdc++.h>
using namespace std;
#define ull unsigned long long
#define inf 0xffffffffffffffffull
const int N=205;
int n,K,s,m;
struct node {
    ull a[N][N];
    node() {
        memset(a,0,sizeof a); //一定要写!
    }
}c;


node f(node a,node b) {
    node x;
    for(int i=1; i<=K; i++)
        for(int j=1; j<=K; j++) 
            for(int k=1; k<=K; k++)
                x.a[i][j]|=a.a[i][k]&b.a[k][j];

    return x;
}
node ksm(node base,int k) {
    node res,c=base;
    for(int i=1; i<=K; i++)res.a[i][i]=inf;

    while(k) {
        if(k&1) {
            res=f(res,c);
        }
        c=f(c,c);
        k>>=1;
    }
    return res;
}

ull a[N],b[N];
ull ans;
signed main() {
    cin>>n>>K;
    for(int i=1;i<=K;i++)cin>>a[i];
    for(int i=1;i<=K;i++)cin>>b[i];

    if(n<=K){cout<<a[n]<<endl;return 0;}
    for(int i=1;i<=K;i++)c.a[i][1]=b[K-i+1];
    for(int i=1;i<=K;i++)c.a[i][i+1]=inf;

    c=ksm(c,n-K+1);
    for(int i=1;i<=K;i++){
        ans|=a[K-i+1]&c.a[i][1];
    }

    cout<<ans<<endl;
    return 0;
}

特别注意

在“斐波那契数列”中我们推导出来的矩阵和存储在数组中的矩阵存在映射关系。这种关系不是一定的!而是根据你写的矩阵乘法(即重定义*)的写法相关!

在Chino的数列这一题的代码中,我就使用了不同的矩阵乘法,并且这种方法貌似更优秀,因为其恰好让我们推导出来的矩阵和存储在数组中的矩阵一一对应。请理解和对比不同乘法的写法的本质和区别!

以下的矩阵转移方法更加常用,对于网络平台等。

node f(node a,node b) {
    node x;
    for(int i=1; i<=n; i++)
        for(int j=1; j<=n; j++) 
            for(int k=1; k<=n; k++)
                x.a[i][j]=(x.a[i][j]+a.a[i][k]*b.a[k][j]%MOD)%MOD;

    return x;
}

矩阵乘法常用变式

基础矩阵乘法:\(c=a\times b→c_{i,j}=\sum\limits_k a_{i,k}b_{k,j}\)

变式\(c=a\times b→c_{i,j}=\min\limits_k (a_{i,k}+b_{k,j})\)

\(c=a\times b→c_{i,j}=\min\limits_k a_{i,k}b_{k,j}\)

\(c=a\times b→c_{i,j}=\sum\limits_k a_{i,k}+b_{k,j}\)

等等。