矩阵乘法

矩阵乘法与矩阵加速

矩阵乘法

矩阵乘法比较简单,就是两个矩阵相乘得到一个新矩阵的运算.

乘法的过程就是:

第一个矩阵的每一行和第二个矩阵的每一列对应位置相乘相加,放入新矩阵.

不太显然,矩阵乘法对于参与运算的矩阵是有限制的:

[[n imes m] * [m imes k] = [n imes k] ]

即,一个 (n imes m) 的矩阵和一个 (m imes k) 的矩阵相乘得到一个 (n imes k) 的矩阵.

必须是形如上面那样的矩阵才能进行乘法.

从这里可以看出,一般的矩阵乘法是不满足交换律的.(个别情况除外)

显然,一次矩阵乘法的复杂度是 (Theta(n imes m imes k)) 的.

两个矩阵相乘的代码如下:

struct Matrix {
    LL e[N][M] , line , row ;

    inline void clear () { line = row = 0 ; MEM ( e , 0 ) ; return ; }

    friend Matrix operator * (Matrix a , Matrix b) {
        Matrix c ; c.clear () ; c.line = a.line ; c.row = b.row ;
        rep ( k , 0 , a.row - 1 ) rep ( i , 0 , a.line - 1 ) rep ( j , 0 , b.row - 1 )
            c.e[i][j] = ( c.e[i][j] + a.e[i][k] * b.e[k][j] % mod ) % mod ;
        return c ;
    }
} ;

上面的代码采用了结构体的封装形式,用起来更方便.

矩阵快速幂

有了矩阵乘法,我们再来考虑和乘法密切相关的一种运算:幂运算.

即一个矩阵的若干次幂如何计算,显然,只有正方形的矩阵可以计算高次幂.

那么类比于实数幂次,一个矩阵的 (p) 次幂只需要把它自乘 (p) 次即可.

讨论到了幂运算的问题,就不得不提快速幂.

众所周知,快速幂的复杂度是 (Theta(log_2{P})) 的,其中 (P) 是指数.

这样的复杂度实在令人眼馋,相比于自乘若干次快了不知道多少倍.

那矩阵的幂次也可以像快速幂那样分治处理吗?

我们知道矩阵乘法是不满足交换律的,那么它满足结合律吗?

如果它满足结合律,那么它就可以像快速幂那样运算.

事实上,矩阵乘法是满足结合律的.

于是矩阵快速幂的代码也出炉了:

inline Matrix quick (Matrix a , LL p) {
    Matrix c ; c.clear () ; c.line = a.line ; c.row = a.row ;
    rep ( i , 0 , c.line - 1 ) c.e[i][i] = 1 ;
    while ( p ) {
        if ( p & 1 ) c = c * a ;
        a = a * a ; p >>= 1 ;
    }
    return c ;
}

十分简单!

矩阵加速

注意,由于方法的不同,同一道题目的转移矩阵的形式是不尽相同的

根据一些资料,我们知道矩乘的本质是高维向量卷积,方程代换和线性变换.

由此得到启发,能否用矩阵来转移递推方程? 答案是肯定的.

(Fibonacci) 数列的递推为例,我们来考虑构造转移矩阵.

众所周知, (Fibonacci) 数列的递推式是 (f_n=f_{n-1}+f_{n-2}).

可以发现, (Fibonacci) 的递推只和某一项的前两项相关.

所以我们考虑的矩阵应该是 (2 imes 2) 的.

我们的初始矩阵是这样的:

[left[egin{array}{ll}{f_i}&{f_{i-1}}end{array} ight] ]

而目标矩阵是这样的:

[left[egin{array}{ll}{f_{i+1}}&{f_{i}}end{array} ight] ]

所以转移矩阵长这样:

[left[egin{array}{ll}{a} & {b} \ {c} & {d} end{array} ight] ]

我们要的矩阵转移式就是这样的:

[left[egin{array}{ll}{f_{i}}&{f_{i-1}} end{array} ight] imes left[egin{array}{ll}{a} & {b} \ {c} & {d} end{array} ight] = left[egin{array}{l}{f_{i+1}} & {f_{i}}end{array} ight] ]

根据矩阵乘法的过程,可以得到:

[a imes f_i + c imes f_{i-1} = f_{i+1} ]

[b imes f_i + d imes f_{i-1} = f_{i} ]

显然,(a=1,c=1,b=1,d=0).

于是,转移矩阵为:

[left[egin{array}{ll}{1} & {1} \ {1} & {0} end{array} ight] ]

初始矩阵(emmmm...),不就是这个嘛

[left[egin{array}{l} {1} & {1} end{array} ight] ]

一次转移就乘一次转移矩阵,自行判断幂次即可.

矩阵加速递推的扩展

(updated:)

扩展情况并不多,一般只是两个递推的组合,常数项的累加以及前缀和,其实本质都是递推组合.

就都以 (Fibonacci) 数列为例好了.

扩展 1 带常数项

定义数列 (F_n) 的递推式为 (F_n=F_{n-1}+F_{n-2}+1) , 其中 (F_1 = F_2 = 1).

没有常数项的部分就是一个普通的 (Fibonacci) 数列.

我们令 (g_n) 表示常数项的递推式,显然, (g_n=g_{n-1}).

于是我们的初始矩阵就是 (:)

[left[egin{array}{llll} F_{n} & F_{n-1} & g_n end{array} ight] ]

目标矩阵就是 (:)

[left[egin{array}{llll} F_{n+1} & F_n & g_{n+1}end{array} ight] ]

那么我们要的转移矩阵应该满足 (:)

[left[egin{array}{llll} F_{n} & F_{n-1} & g_n end{array} ight] imes left[egin{array}{llll} ? & ? & ? \ ? & ? & ? \ ? & ? & ? end{array} ight] = left[egin{array}{llll} F_{n+1} & F_n & g_{n+1}end{array} ight] ]

根据上面构造矩阵的方法,可以得到,转移矩阵长这个样子 (:)

[left[egin{array}{llll} 1 & 1 & 0 \ 1 & 0 & 0 \ 1 & 0 & 1 end{array} ight] ]

(因为这些扩展的意义就在于构造不同的矩阵,所以只讲如何构造矩阵).

扩展 2 带未知项递推

定义数列 (F_n) 的递推式为 (F_n=F_{n-1}+F_{n-2}+n) , 其中 (F_1=F_2=1).

没有常数项的部分就是一个普通的 (Fibonacci) 数列.

(g_n) 表示未知项的递推式,显然, (g_n=g_{n-1}+1).

于是我们的初始矩阵就是 (:)

[left[egin{array}{llll} F_{n} & F_{n-1} & g_n & 1 end{array} ight] ]

目标矩阵就是 (:)

[left[egin{array}{llll} F_{n+1} & F_n & g_{n+1} & 1 end{array} ight] ]

那么我们要的转移矩阵应该满足 (:)

[left[egin{array}{llll} F_{n} & F_{n-1} & g_n & 1 end{array} ight] imes left[egin{array}{llll} ? & ? & ? & ? \ ? & ? & ? & ? \ ? & ? & ? & ? \ ? & ? & ? & ? end{array} ight] = left[egin{array}{llll} F_{n+1} & F_n & g_{n+1} & 1 end{array} ight] ]

根据上面构造矩阵的方法,可以得到,转移矩阵长这个样子 (:)

[left[egin{array}{llll} 1 & 1 & 0 & 0 \ 1 & 0 & 0 & 0 \ 1 & 0 & 1 & 0 \ 1 & 0 & 1 & 1 end{array} ight] ]

扩展 3 递推式的组合

令数列 (F_n) 的递推式为 (F_n=F_{n-1}+F_{n-2}+f_{n-1}+f_{n-2}),其中,(F_1=F_2=f_1=f_2=1).

这叫啥?双重 (Fibonacci) 数列?算了,还是不乱叫了.

于是我们的初始矩阵就是 (:)

[left[egin{array}{llll} F_{n} & F_{n-1} & f_n & f_{n-1} end{array} ight] ]

目标矩阵就是 (:)

[left[egin{array}{llll} F_{n+1} & F_n & f_{n+1} & f_n end{array} ight] ]

那么我们要的转移矩阵应该满足 (:)

[left[egin{array}{llll} F_{n} & F_{n-1} & f_n & f_{n-1} end{array} ight] imes left[egin{array}{llll} ? & ? & ? & ? \ ? & ? & ? & ? \ ? & ? & ? & ? \ ? & ? & ? & ? end{array} ight] = left[egin{array}{llll} F_{n+1} & F_n & f_{n+1} & f_n end{array} ight] ]

根据上面构造矩阵的方法,可以得到,转移矩阵长这个样子 (:)

[left[egin{array}{llll} 1 & 1 & 0 & 0 \ 1 & 0 & 0 & 0 \ 1 & 0 & 1 & 1 \ 1 & 0 & 1 & 0 end{array} ight] ]

扩展 4 前缀和

令数列 (F_n) 的递推式为 (F_n=F_{n-1}+F_{n-2}) , 求 (sum_{i=1}^{n}{F_i}) , 其中 (F_1=F_2=1).

(S_i) 表示到 (i) 的前缀和.

则显然有(:)

[S_i=F_{i-1}+F_{i-2}+S_{i-1} ]

于是我们的初始矩阵就是 (:)

[left[egin{array}{llll} F_{n} & F_{n-1} & S_n end{array} ight] ]

目标矩阵就是 (:)

[left[egin{array}{llll} F_{n+1} & F_n & S_{n+1} end{array} ight] ]

那么我们要的转移矩阵应该满足 (:)

[left[egin{array}{llll} F_{n} & F_{n-1} & S_nend{array} ight] imes left[egin{array}{llll} ? & ? & ? \ ? & ? & ? \ ? & ? & ? end{array} ight] = left[egin{array}{llll} F_{n+1} & F_n & S_{n+1} end{array} ight] ]

根据上面构造矩阵的方法,可以得到,转移矩阵长这个样子 (:)

[left[egin{array}{llll} 1 & 1 & 1 \ 1 & 0 & 1 \ 0 & 0 & 1 end{array} ight] ]

扩展 5 带系数

令数列 (F_n) 的递推式为 (F_n = a imes F_{n-1} + b imes F_{n-2}),其中 (F_1,F_2) 是给定的数.

初始矩阵仍然是 (:)

[left[egin{array}{llll} F_n & F_{n-1} end{array} ight] ]

目标矩阵仍然是 (:)

[left[egin{array}{llll} F_{n+1} & F_{n} end{array} ight] ]

那么我们要的转移矩阵应该满足 (:)

[left[egin{array}{llll} F_{n} & F_{n-1} end{array} ight] imes left[egin{array}{llll} ? & ? \ ? & ? end{array} ight] = left[egin{array}{llll} F_{n+1} & F_n end{array} ight] ]

根据上面构造矩阵的方法,可以得到,转移矩阵长这个样子 (:)

[left[egin{array}{llll} a & 1 \ b & 0 end{array} ight] ]

完结撒花 (!)

你说为什么这几种扩展的框架一模一样?因为我复制的啊...(懒

矩阵乘法与邻接矩阵

矩乘结合律的证明 (:)

[egin{aligned}((mathbf{A B}) mathbf{C})[i, j] & \ &=sum_{l=1}^{c}left(sum_{k=1}^{b} mathbf{A}[i, k] mathbf{B}[k, l] ight) mathbf{C}[l, j] \ &=sum_{k=1}^{b} sum_{l=1}^{c} mathbf{A}[i, k] mathbf{B}[k, l] mathbf{C}[l, j] \ &=sum_{k=1}^{b} mathbf{A}[i, k]left(sum_{l=1}^{c} mathbf{B}[k, l] mathbf{C}[l, j] ight) \ &=(mathbf{A}(mathbf{B} mathbf{C}))[i, j] end{aligned} ]

矩阵乘法能进行快速幂运算的原因就是因为它具有结合律.

引例 (1:) [TJOI2017]可乐

相信很多人都能想出一个 (Theta(t imes m)) 的做法.(虽然我没想出来,但这只是因为我菜)

问题简化一下,如果我们没有在原地停留和自爆两个操作,那么就是问从起点出发,走 (t) 步的不同路径数.

这个问题怎么做呢?

不考虑 (Dp) .

令该图的邻接矩阵是 (G) , 那么我们考虑 (G^2) 是个什么东西.(此处的幂运算是指矩阵的幂).

我们单独考虑某一行和某一列的相关运算 (:) 令其为 (G_{a,i})(G_{i,b}) , 令 (G') 为相乘得到的矩阵,那么会有 (:)

[G'_{a,b} = sum_{i=1}^m{G_{a,i} imes G_{i,b}} ]

容易发现,当且仅当 (G_{a,i})(G_{i,b}) 都不为零,即 (i) 点可连通 (a,b) 两点的时候上式的该项才为 (1) , 否则为零.

那么所有的这些情况累加起来,就是从 (a)(b) 长度为 (2) 的路径条数.(即走 (2) 步从 (a) 走到 (b) 的方案数,长度是 (2) 是因为经过一个中间点.)

由此,我们可以得到, (G^2) 得到的矩阵其实表示了任意两点间长度为 (2) 的路径条数.

那么 (G^3) 是否就表示任意两点间长度为 (3) 的路径条数呢?

(G'=G^2) , (G'')(G^3). 那么有:

[G''=G' imes G ]

[G''_{a,b}=sum_{i=1}^nsum_{j=1}^n{G_{a,i} imes G_{i,j} imes G_{j,b}} ]

分析方法与上面相同,于是我们归纳结论如下:

(G) 表示一张图的邻接矩阵表示,那么 (G^i) 表示任意两点间长度为 (i) 的路径条数.

那么我们就解决了引例的简化问题.

那么怎么处理引例中的自爆和原地不动呢?

很简单,原地不动视为自环,自爆就额外建一个虚点,表示自爆,这里要注意的是,不需要从虚点连回原图,因为自爆之后就不能再走了.

于是我们解决了引例.

那么矩乘是否仅仅只有这一个用处呢?

引例 (2:) USACO07NOV Cow Relays

题目大意 (:) 求从 (s)(t) 经过 (k) 条边的最短路.

这个问题乍一看很眼熟,似乎就是上一个问题在细节上做一下变换得到.

但你仔细思考会发现,最短路这个看似平凡的条件竟然不能用加法和乘法解决.

但其实这也合理,因为我们知道最短路的求法都是以类似于 (Dp) 的松弛操作为核心的,也就是说有一个核心运算 (: min!)

那么是否可以用矩阵解决这个运算呢?

考虑 (Floyd) 的过程,其核心代码是 (f_{i,j}=min(f_{i,j},f_{i,k}+f_{k,j}))

这给了我们一定启发,因为 (Floyd) 的过程和矩乘的过程十分相似.( (Floyd) 的本质是滚掉一维的三维 (Dp))

于是,我们大胆定义新的矩乘 (:)

令矩阵 (A) 和 矩阵 (B) 相乘的结果为矩阵 (C) .

则定义:

[C_{a,b}=min_{i=1}^m{min(C_{a,b},A_{a,i}+B_{i,b})} ]

容易发现,这个矩乘同样具有结合律.(可以从 (min) 运算是和 (+) 运算具有同样性质的二元运算符考虑,证明与普通矩乘相同).

那么这样,我们直接应用引例 (1) 中的结论即可解决该题.

引例 (3:) 最小最大边问题

找不到题目了,国集论文没给题目来源,找不到.

最小最大边问题 (:) 给定一张有向图,求某两点间通过边数恰好为 (k) 的路径,使得最大边最小.

同样的熟悉,同样的问题.

考虑如果没有长度恰好为 (k) 的做法,那么就是把 (Floyd) 的核心代码换成 (:)

[f_{i,j}=max(f_{i,j},min(f_{i,k},f_{k,j})) ]

能否采用与上面相同的方式重定义矩乘呢?答案是肯定的.

令矩阵 (A) 和矩阵 (B) 相乘的结果为矩阵 (C).

则定义 (:)

[C_{a,b}=max_{i=1}^mmin(A_{x,i},B_{i,y}) ]

直接套用上面的结论即可.

参考文献 (:) 2008年国集论文(ACM Paper):矩阵乘法在信息学中的应用--余华程

原文地址:https://www.cnblogs.com/Equinox-Flower/p/12134640.html