濮阳市网站建设_网站建设公司_Node.js_seo优化
2026/1/22 7:32:52 网站建设 项目流程

矩阵乘法

考虑一个 \(n\times m\)(即 n 行 m 列)的矩阵乘上一个 \(m\times k\) 的矩阵,乘法后得到 \(n\times k\) 的矩阵。

代数的写法就是

\[C_{i,j}=\sum_{t=1}^m A_{it}\cdot B_{tj} \]

在写的时候,先枚举 \(i,k\),如果 \(A_{ik}\)\(0\) 的话就直接跳过这一维能够部分优化常数。

\(C_{i,j}\) 由矩阵 \(A\) 的第 \(i\) 行和矩阵 \(B\) 的第 \(j\) 列的乘积和构成,注意不要搞混了

注意到矩阵只有结合律而没有交换律

所以 \(A,B,C\) 是三个矩阵,那么 \(ABC\) 可以是 \(A(BC)\) 但不能是 \(BCA\)

矩阵快速幂优化计算

把矩阵看作一个数,用对于数做快速幂的方法对于矩阵做快速幂,就是矩阵快速幂了,实现上通常会定义一个矩阵类/结构体,封装一个矩阵乘法。

这样就能快速地求出一个矩阵的 \(n\) 次方了。

矩阵快速幂只适用于方阵,即 \(n\times n\) 的矩阵,并且约定 \(A^0=I\)\(I\) 为单位矩阵。

P1939

这道题目,要求求出一个数列的第 \(x\) 项,且 \(x\) 很大。

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

那么我们考虑使用一个矩阵 \(\begin{bmatrix}a_x\\a_{x-1}\\a_{x-2}\end{bmatrix}\) 来描述一个状态。

发现状态间有

\[\begin{bmatrix} a_x\\ a_{x-1}\\ a_{x-2} \end{bmatrix} = \begin{bmatrix} 1&0&1\\ 1&0&0\\ 0&1&0\\ \end{bmatrix} \cdot \begin{bmatrix} a_{x-1}\\ a_{x-2}\\ a_{x-3}\\ \end{bmatrix} \]

可以依照下图理解一下,对应行和对应列的相同颜色的项相乘,然后和就是红色的那个值了。

\[\begin{bmatrix} \color{red}{a_x}\\ a_{x-1}\\ a_{x-2} \end{bmatrix} = \begin{bmatrix} \color{blue}1&\color{green}0&\color{orange}1\\ 1&0&0\\ 0&1&0\\ \end{bmatrix} \cdot \begin{bmatrix} \color{blue}a_{x-1}\\ \color{green}a_{x-2}\\ \color{orange}a_{x-3}\\ \end{bmatrix} \\ \begin{bmatrix} a_x\\ \color{red}a_{x-1}\\ a_{x-2} \end{bmatrix} = \begin{bmatrix} 1&0&1\\ \color{blue}1&\color{green}0&\color{orange}0\\ 0&1&0\\ \end{bmatrix} \cdot \begin{bmatrix} \color{blue}a_{x-1}\\ \color{green}a_{x-2}\\ \color{orange}a_{x-3}\\ \end{bmatrix} \\ \begin{bmatrix} a_x\\ a_{x-1}\\ \color{red}a_{x-2} \end{bmatrix} = \begin{bmatrix} 1&0&1\\ 1&0&0\\ \color{blue}0&\color{green}1&\color{orange}0\\ \end{bmatrix} \cdot \begin{bmatrix} \color{blue}a_{x-1}\\ \color{green}a_{x-2}\\ \color{orange}a_{x-3}\\ \end{bmatrix} \]

然后我们发现

\[\begin{bmatrix} a_x\\ a_{x-1}\\ a_{x-2} \end{bmatrix} =\begin{bmatrix} 1&0&1\\ 1&0&0\\ 0&1&0\\ \end{bmatrix} \cdot \begin{bmatrix} a_{x-1}\\ a_{x-2}\\ a_{x-3}\\ \end{bmatrix} \]

\[\begin{bmatrix} a_{x-1}\\ a_{x-2}\\ a_{x-3} \end{bmatrix} =\begin{bmatrix} 1&0&1\\ 1&0&0\\ 0&1&0\\ \end{bmatrix} \cdot \begin{bmatrix} a_{x-2}\\ a_{x-3}\\ a_{x-4}\\ \end{bmatrix} \]

于是考虑递归地将下面的式子带入到上面的式子,就有了

\[\begin{bmatrix} a_x\\ a_{x-1}\\ a_{x-2} \end{bmatrix} =\begin{bmatrix} 1&0&1\\ 1&0&0\\ 0&1&0\\ \end{bmatrix}^{x-3} \cdot \begin{bmatrix} a_{3}\\ a_{2}\\ a_{1}\\ \end{bmatrix} \]

发现这个是可以用矩阵快速幂快速计算的,并且比较容易。

我们把做快速幂的这个 \(3\times 3\) 的矩阵称作转移矩阵。

有些时候,在这个 \(A^{x-3}\) 的形式算完之后,因为 $$\begin{bmatrix}a_3\a_2\a_1\end{bmatrix}$$ 这个矩阵的形式比较简单,可以手动把这一步算出来,然后用 \(A\) 中的项直接表示出这次矩阵乘法的结果,代码会好写一些。

比如这道题目,可以计算出 \(A^{x-2}\) 的值,发现那么我们要求的 \(a_x\) 其实对应的就是目标矩阵的第二行那个数字(原图中 \(a_{x-1}\) 的位置),也就是 \(A_{2,1}\),就是这个转移矩阵的第二行的第一个数字。

关于写法的问题

可以发现其实上面的转移式还有一种写法

\[\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} \cdot \begin{bmatrix} 1&1&0\\ 0&0&1\\ 1&0&0 \end{bmatrix} \]

这样计算出来的意思是一样的。

(注意看这两种写法的转移矩阵是不一样的,准确来说做了个转置的操作。)

特别注意,在线段树维护矩阵的时候,两种写法的 pushup 操作是顺序不同的,后面会具体进行说明。

矩阵快速幂优化dp

那么我们考虑在大多数的 dp 中,转移的式子会写成这种形式

\(f_x=a_{x-1}f_{x-1}+a_{x-2}f_{x-2}+\dots +a_{x-k}f_{x-k}\)

这里的 \(a_{x-1}\) 这些系数对于每个 \(f_x\) 都是相同的,也就是说这些都是常数。

也可以说这个时候 \(f_x\) 为线性递推数列。

下面这句话不影响理解文章,可以选择跳过。

当然线性递推数列可以通过特征方程的方式求出通项公式,但是通向公式通常会带有根号导致无法直接计算,有时还有不可用根式表达的情况(Abel–Ruffini),还需要手动写出根号类运算支持根号的运算,也并不简单,并且在 mod 意义下(正确性存疑)。

考虑应用刚才的知识,类比方法

\[\begin{bmatrix} f_x\\ f_{x-1}\\ f_{x-2}\\ \vdots\\ f_{x-k+1} \end{bmatrix} = \begin{bmatrix} a_{x-1}&a_{x-2}&a_{x-3}&\cdots&a_{x-k+1}&a_{x-k}\\ 1&0&0&\cdots&0&0\\ 0&1&0&\cdots&0&0\\ \vdots&\vdots&\vdots&\vdots&\vdots&\vdots\\ 0&0&0&\cdots&1&0 \end{bmatrix} \cdot \begin{bmatrix} f_{x-1}\\ f_{x-2}\\ f_{x-3}\\ \vdots\\ f_{x-k} \end{bmatrix} \]

发现这个转移可以用这种形式来表示。

其实就是把原来 dp 转移的系数平推在第一行了。

但是并不是所有的 dp 的转移形式都长成这个样子,还有另外一种形式。

比如说有些 dp 的转移函数长成这个样子

\[dp_{i,j,k}=\sum a_{i-1,x,y}dp_{i-1,x,y} \]

\(j,k\) 这两位都不大。

那么这个时候就要把 \(j,k\) 压到一行上,然后做矩阵快速幂。

\(j\in\{0,1\},k\in\{0,1\}\) 举例子。

为了方便表示,下面记录 \(dp_{i,x,y}\)\(dp_{i-1,j,k}\) 的贡献为 \(f_{x,y,j,k}\),即 \(dp_{i,x,y}=\sum f_{x,y,j,k}dp_{i-1,j,k}\)

\[\begin{bmatrix} dp_{i,0,0}\\ dp_{i,0,1}\\ dp_{i,1,0}\\ dp_{i,1,1}\\ \end{bmatrix} = \begin{bmatrix} f_{0,0,0,0}&f_{0,0,0,1}&f_{0,0,1,0}&f_{0,0,1,1}\\ f_{0,1,0,0}&f_{0,1,0,1}&f_{0,1,1,0}&f_{0,1,1,1}\\ f_{1,0,0,0}&f_{1,0,0,1}&f_{1,0,1,0}&f_{1,0,1,1}\\ f_{1,1,0,0}&f_{1,1,0,1}&f_{1,1,1,0}&f_{1,1,1,1}\\ \end{bmatrix} \cdot \begin{bmatrix} dp_{i+1,0,0}\\ dp_{i+1,0,1}\\ dp_{i+1,1,0}\\ dp_{i+1,1,1}\\ \end{bmatrix} \]

这个转移显然是成立的,注意一下和上面的矩阵是有区别的,实现的时候通常写一个函数 id(j,k) 确定矩阵上的位置。

这个题,就是 \(id(j,k)=2j+k,id(j,k)\in\{0,1,2,3\}\)

也就是 \((0,0)\) 对应这个矩阵的第一个位置,\((0,1)\) 对应这个矩阵的第二个位置,\((1,0)\) 对应这个矩阵的第三个位置...。

例子是 P5303 和 P5371。

注意到有些时候可能会有更多维度,矩阵乘法都压到一行上。

考虑矩阵是一个 \(n\times n\) 的,那么时间复杂度就是 \(n^3\log x\),其中 \(x\) 是要求 \(dp_x\) 的值,这里的 \(n^3\) 使用上文说过的优化方法(跳过 \(a_{i,j}=0\) 的位置)通常跑不满。

注意到有些时候矩阵可能会很大,比如 P5303 我的做法\(n\)\(40+\) , P5371 我的做法 \(n\)\(20+\),这个时候可以先写一个暴力的 dp ,然后把暴力 dp 的每个状态用上述方法映射到一个 id ,原来暴力 dp 的转移\(dp_{i,j,k,l}+=a\cdot dp_{i-1,j2,k2,l2}\),写成\(M_{id(j,k,l),id(j2,k2,l2)}+=a\)或者\(M_{id(j2,k2,l2),id(j,k,l)}+=a\),取决于你的写法是那种,矩阵乘向量还是向量乘矩阵。

这里\(M\)是转移矩阵,\(a\)是 dp 时的系数。

线段树维护矩阵乘法

注意到上文所说的矩阵快速幂维护的矩阵都有一个特点,就是 \(dp_i\)\(dp_{i-1}\) 的转移系数 \(a_{i,j}\) 是固定的,并不会因为 \(i\) 的变化而变化。

但是考虑下面这个问题。

P12087

给出一个很大的数(\(10^5\) 位量级),每次询问不大于这个数的不包含子串 13 的数有多少个。

到这里是可以使用树形 dp 进行维护的。

\(dp_{i,j,k}\) 表示处理完了前 \(i\) 位的情况,第 \(j\) 位是不是 \(1\),是否(\(k\))卡满了上届,后续一共有多少种填法。

转移可以考虑典型的树形 dp 的转移方法

int LIM;
if(lim)LIM=x[i+1]-'0';
else LIM=9;
for(int j=0;j<=LIM;++j){if(f and j==3)continue;(num+=dfs(i+1,(j==1),lim&(j==(x[i+1]-'0'))))%=mod;
}
return dp[i][f][lim]=num;

但是这个题目还有两个变化。

  • 查询区间 \([l,r]\) 直接组成一个数字,这个字问题的答案。
  • 修改原来的数字的某一位。

发现这一整个转移过程都可以使用上文提到的以 \(j\in\{0,1\},k\in\{0,1\}\) 举例子的部分用矩阵的方式表示出转移。

注意到转移和 \(a\) 的值有关,因此考虑使用线段树进行维护,每个节点都维护一个转移矩阵,线段树的性质可以支持单点修改矩阵和区间查询矩阵。

注意一个重要的细节
如果写法是

\[\begin{bmatrix} dp_{i,0,0}\\ dp_{i,0,1}\\ dp_{i,1,0}\\ dp_{i,1,1}\\ \end{bmatrix} = \begin{bmatrix} f_{0,0,0,0}&f_{0,0,0,1}&f_{0,0,1,0}&f_{0,0,1,1}\\ f_{0,1,0,0}&f_{0,1,0,1}&f_{0,1,1,0}&f_{0,1,1,1}\\ f_{1,0,0,0}&f_{1,0,0,1}&f_{1,0,1,0}&f_{1,0,1,1}\\ f_{1,1,0,0}&f_{1,1,0,1}&f_{1,1,1,0}&f_{1,1,1,1}\\ \end{bmatrix} \cdot \begin{bmatrix} dp_{i+1,0,0}\\ dp_{i+1,0,1}\\ dp_{i+1,1,0}\\ dp_{i+1,1,1}\\ \end{bmatrix} \]

记录 \(x_n\) 为转移矩阵。

\[f_n=x_n f_{n-1}\\ f_{n-1}=x_{n-1} f_{n-2}\\ f_n=x_nx_{n-1}\cdots x_2 f_1 \]

你会发现这里的矩阵是从大下标一直乘到小下标的,而矩阵乘法是不具有交换律的,因此写成 pushup 的时候要记得写右子树矩阵乘左子树矩阵。

不过如果转移矩阵写成

\[\begin{bmatrix} dp_{i,0,0}&dp_{i,0,1}&dp_{i,1,0}&dp_{i,1,1} \end{bmatrix} = \begin{bmatrix} dp_{i-1,0,0}&dp_{i-1,0,1}&dp_{i-1,1,0}&dp_{i-1,1,1} \end{bmatrix} \cdot \begin{bmatrix} f_{0,0,0,0}&f_{0,1,0,0}&f_{1,0,0,0}&f_{1,1,0,0}\\ f_{0,0,0,1}&f_{0,1,0,1}&f_{1,0,0,1}&f_{1,1,0,1}\\ f_{0,0,1,0}&f_{0,1,1,0}&f_{1,0,1,0}&f_{1,1,1,0}\\ f_{0,0,1,1}&f_{0,1,1,1}&f_{1,0,1,1}&f_{1,1,1,1}\\ \end{bmatrix} \]

就不存在这种问题了,展开递推式子是按照小下标到大下标的顺序的,线段树直接按照 \(ls*rs\) 就是对的,在使用线段树维护矩阵的时候推荐这张写法。

线段树维护链上问题的矩阵乘法

有些时候,树上查询链的问题,将链看作一个数组,也能按照上文的方法进行矩阵优化。

那么就有一个思路,把树做一个树剖,将重链看作一个正常的数列用线段树维护,就能解决链上问题了。

注意链上问题要特别注意顺序,是向上走还是向下走,因为矩阵没有交换律。

例题,P14468,代码细节或许比较多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询