矩阵
欢迎来到方块世界。
definition
我们可以简单的认为其为一个二维数组,或者说就是将一个矩阵分成一个个小块,其中一个个的块内有数值而已。我们通常用大写字母和其行列来表示当前的矩阵,大概是 \(A _{n\times m}\) 这个鸭子的。 其意思就是 \(n\) 行 \(m\) 列的一个矩阵 \(A\)
给出定义矩阵的Code , 这里选用结构体
struct Matrix {
int n , m ;
int a[kmaxn][kmaxn] ;
Matrix()
{
n = m = 0 ;
memset(a , 0 , sizeof(a)) ;
}
};
Base-operational rule
运算律的英文真的是绝了。日。
加减法的运算是一样的。这里只用加法来表示 。
由于这玩意放在博客园实在是放不开了,就直接拆开了.
减法同样,无非是将上文的 + 换成了 - 而已。
几种分类。
单位矩阵:
对角线上的点全部为 \(1\)
零矩阵 :
矩阵所有的元素全部为 \(0\)
对称阵
\(……\) 其他的好像用不到。
Mul_operational rule
首先说明矩阵乘法和向量一样,支持数乘矩阵,和矩阵乘矩阵。
数乘矩阵
数乘矩阵的话就是这样的(这里不给出矩阵了,\(yy\) 一下,真的好麻烦的)
矩阵乘矩阵
首先我们点明一些东西 :
- 矩阵不满足交换律,即为 : \(A\times B\neq B\times A\)
- 矩阵不满足结合律,即为 :\(A\times(B\times C) \neq (A\times B)\times C\)
- 矩阵满足分配律,,即为: \((A+B)\times C = AC+BC\)
左分配律 :\(A\times (B+C) = AB+AC\) ,右分配律:\((A+B)\times C = AC+BC\)
因为必须满足整个式子的顺序,所以分成了左右两个分配律。
\(M_1\times M_2\) 首先点明这里的顺序是不可换的。我们需要满足的是 :\(,M_1\) 的行要等于 \(m_2\) 的列,我们在下面的计算式子可以体会到。
我们继续给出一个计算式
我们给定一个具体的矩阵 \(1\times 2\) (因为后面有提到斐波那契数列)
这里给出矩阵乘法的代码:
Matrix operator * (const Matrix &m1 , const Matrix &m2) {
Matrix m3 ; m3.n = m1.n , m3.m = m2.m ;
for(qwq int i = 1 ; i <= m3.n ; i++)
for(qwq int k = 1 ; k <= m1.m ; k++)
for(qwq int j = 1 ; j <= m3.m ; j++)
m3.a[i][j] = m3.a[i][j] + m1.a[i][k] * m2.a[j][k] ;
return m3 ;
}
枚举顺序与时间的关系 :
这个枚举顺序指的是上边代码的 \(i,j,k\) 的枚举 。
(我忘记了是不是缓存了,差不多这个意思)
在计算机中进行访问空间的时候,会首先访问缓存条,缓存是计算机对你进行的一步操作的下一步操作的预判,也就是你访问了节点 \(i\) ,计算机可能给你预判一下,你可能需要 \(i+1\) 这个节点,访问缓存往往是要更快一些的。而计算机的缓存只能取到你现在进行的相邻的数,具体什么意思,也就是计算机只能够预判一下你的这一步的周围,看一下你是否会到达 \(i+1, i-1\) 之类的 , 那么也就是很显然了,我们只要尽可能的访问连续的节点就可以保证时间的最优性了, 也就是上方的 \(k\) 的枚举必须放在最后面 (也就是枚举两个矩阵进行相乘的元素枚举)
矩阵快速幂
我们有了矩阵乘法,其他的和普通的快速幂是一样的
Matrix quick(Matrix a , int b) {
Matrix ret ;
ret.m = ret.n = 2 ;
ret.a[1][1] = 1 ; ret.a[2][2] = 1 ;
while(b)
{
if(b & 1) ret = ret * a ;
a = a * a ;
b >>= 1 ;
}
return ret ;
}
差不多就这样就行了。
应用
矩阵加速递推
满足矩阵加速递推的条件为 :
- \(1.\) 递推必然是从 \(i-1\) 的状态推导到 \(i\)
- \(2.\) 其中递推的矩阵与 \(i\) 是没有什么关系的。
1.求解Fibonacci
基础入门等级都够不到的递推: \(f_i = f_{i - 1} + f_{i - 2}\)
求解 \(f_n , n \leq 10^9\)
【solution】 :
很显然我们是有 \(O(n)\) 的解法的,但是看到 \(n\) 的这个范围,显然是不大可行的。所以我们选择优化一个。 我们是很显然的想到如果我们用 \(f_i\) 推导到 \(f_{i+1}\) 的话,我们需要 \(f_{i - 1}\) 这两个的,所以我们就聪明一下,我们经过某种变换从而达到 \(f_{i+1}\) ,这里选择用矩阵加速递推求解,我们就设递推矩阵为 \(M\) , 我们发现我们的 \(f_{i - 1} , f_{i}\) 是一个 \(1\times 2\) ,最后我们得到的 \(f_i , f_{i+1}\) 也是一个 \(1\times 2\) 的一个矩阵,从而我们就知道 \(M\) 这个矩阵为 \(2\times 2\) 的,我们设这个矩阵为
然后我们根据矩阵乘法就能够得到如下的一个式子。
所以我们最后就得到了
然后我们就那么递推就行了;
Code
/*
By : Zmonarch
知识点:
*/
#include <bits/stdc++.h>
#define int unsigned long long
#define qwq register
#define inf 2147483647
using namespace std ;
const int kmaxn = 1e6 + 10 ;
inline int read() {
int x = 0 , f = 1 ; char ch = getchar() ;
while(!isdigit(ch)) {if(ch == '-') f = - 1 ; ch = getchar() ;}
while( isdigit(ch)) {x = x * 10 + ch - '0' ; ch = getchar() ;}
return x * f ;
}
struct Matrix {
int n , m ;
int a[4][4] ;
Matrix()
{
n = m = 0 ;
memset(a , 0 , sizeof(a)) ;
}
};
Matrix operator * (const Matrix &m1 , const Matrix &m2) {
Matrix m3 ; m3.n = m1.n , m3.m = m2.m ;
for(qwq int i = 1 ; i <= m3.n ; i++)
for(qwq int k = 1 ; k <= m1.m ; k++)
for(qwq int j = 1 ; j <= m3.m ; j++)
m3.a[i][j] = m3.a[i][j] + m1.a[i][k] * m2.a[k][j] ;
return m3 ;
}
Matrix quick(Matrix a , int b) {
Matrix ret ;
ret.m = ret.n = 2 ;
ret.a[1][1] = 1 ; ret.a[2][2] = 1 ;
while(b)
{
if(b & 1) ret = ret * a ;
a = a * a ;
b >>= 1 ;
}
return ret ;
}
signed main() {
int n = read() ;
Matrix k1 , k2 ;
k1.m = 2 , k1.n = 1 ; k1.a[1][1] = 1 , k1.a[1][2] = 1 ;
k2.m = k2.n = 2 ;
k2.a[1][1] = 0 ; k2.a[1][2] = 1 ;
k2.a[2][1] = 1 ; k2.a[2][2] = 1 ;
k1 = k1 * quick(k2 , n - 1) ;
//printf("%lld\n" , k1.a[1][1]) ;
std::cout << k1.a[1][1] ;
}
矩阵加维
\(1.\) 递推式中带有常数项 \(k\)
我们需要单独为常数项 \(k\) 给他开一维,不能只取递推式中带有未知数的值而到了最后加上 \(k\)
举个例子 , 递推式为 $f_{n} = f_{n - 1} + f_{n - 2} + k $
那么我们就可以得到初始矩阵为 $$\begin{bmatrix} f_{n - 2} & f_{n - 1} & k \end{bmatrix} \times A = \begin{bmatrix} f_{n - 1} & f_{n} & k \ \end{bmatrix}$$
那么这时候我们只需要求出 \(A\) 矩阵即可。 显然矩阵是三维的,不想推了,反正我是推导出来了。
\(2.\) 递推式中带有未知数
同样的,再开一维
举个例子
\(f_{n} = f_{n-1} + f_{n-2} + n\)
首先将未知项的递推式推导出来 $(n) = (n - 1) + 1 $(我是没推出来,别问,问就是傻逼) , 得到初始矩阵
\(\begin{bmatrix} f_{n } & f_{n - 1} & n & 1 \end{bmatrix}\)
\(3.\) 求和
咕了。
其他的咕了。
超级跳马
【\(description\)】
从 \((1,1)\) 开始到 \((n,m)\) 的方案数 , 节点 \((i,j)\) 只能跳到 同行或者相邻行,且列之间的距离应为奇数 。 \(n\leq 50 , m\leq 10^9\)
【\(solution\)】:
参考文献 :题解
首先是一个非常暴力的暴力,我们明白对于节点 \(i,j\) 它只能够从 \(i\) 或者 \(i-1,i+1\) 进行转移,同时列 \(j\) 只能在奇数列进行转移
所以有一个十分暴力的三重循环
for(int j = 1 ; j <= m ; j++) //枚举列
{
for(int i = 1 ; i <= n ; i++) //枚举行
{
for(int k = j ; k >= 1 ; k-= 2) //只能跳奇数列,同时,可以从同一行进行转移,所以我们 k == i是可以的
{
(f[i][j] += f[k][j] + f[k][j - 1] + f[k][j + 1 ]) %kmod ;
}
}
}
最终答案就是 \(f_{n,m} - f_{n,m-2}\) ,这里的是一个前缀和的形式,所以我们应该减去前面的方案数,才是 \((n,m)\) 本身的 方案数。同样的, 我们可以对答案进行一下魔改,考虑一下 \(f_{n,m}\) 从何而来,它无法从 \(n+1\) 而来,所以可以从 \(f_{n - 1 , …}\) 而来, 同样的,它可以从 \(f_{n , m -1}\) 和 \(f_{n - 1 , m - 1}\) ,所以综上, \(f_{n,m} = f_{n -1 , m - 1} + f_{n , m - 1}\) 转移而来。
同样的这个状态转移也是可以进行魔改的 。
解释一下,就是模仿一下上面,得到了 \(f_{i-1,j-1} + f_{i,j-1}\) ,那么只需要解释一下 \(f_{i+1,j-1}\) 和 \(f_{i ,j -2}\) 即可了,\(f_{i+ 1 , j - 1 }\) 由于上述的 \(f(n,m)\) 是无法继续向下的,所以我们不能用 \(f_{n+1,m-1}\) 来进行标记, 然后 \(f_{i , j - 2}\) 意味,节点 \((i,j)\) 表示可以从同样的行里 , 跳奇数列而来的。
由于我们发现这个状态转移只与 \(i-1, i-2\) 有关,那么我们就可以类比上面的斐波那契数列,进行矩阵快速幂,加速转移。以 \(n = 3\) 为例 ,那么也就是
那么
\(Code\)
/*
by : Zmonarch
知识点 :
*/
#include <algorithm>
#include <iostream>
#include <cstring>
#include <vector>
#include <cmath>
#include <queue>
#include <stack>
#include <map>
#include <set>
#define int long long
#define re register
const int kmaxn = 1e6 + 10 ;
const int kmod = 30011 ;
namespace Base
{
inline int Min(int a , int b) { return a < b ? a : b ; } ;
inline int Max(int a , int b) { return a > b ? a : b ; } ;
inline int Abs(int a ) { return a < 0 ? - a : a ; } ;
};
inline int read()
{
int x = 0 , f = 1 ; char ch = getchar() ;
while(!isdigit(ch)) { if(ch == '-') f = - 1 ; ch = getchar() ; }
while( isdigit(ch)) { x = x * 10 + ch - '0' ; ch = getchar() ; }
return x * f ;
}
int n , m , len ;
struct Matrix
{
int a[110][110] ;
Matrix()
{
memset(a , 0 , sizeof(a)) ;
}
} ;
Matrix operator * (const Matrix & m1 , const Matrix & m2) //定义矩阵乘法法则
{
Matrix c ;
for(int i = 1 ; i <= len ; i++)
{
for(int j = 1 ; j <= len ; j++)
{
for(int k = 1 ; k <= len ; k++)
{
c.a[i][j] += (m1.a[i][k] * m2.a[k][j] % kmod) ;
c.a[i][j] %= kmod ;
}
}
}
return c ;
}
Matrix quick_pow(Matrix a , int k)
{
Matrix b ;
for(int i = 1 ; i <= len ; i++) b.a[i][i] = 1 ;
while(k)
{
if(k & 1) b = b * a ;
a = a * a ;
k >>= 1 ;
}
return b ;
}
signed main()
{
n = read() , m = read() ;
if(m <= 2)
{
if(n <= 2 && m <= n) printf("1\n") ;
else printf("0\n") ; return 0 ;
}
len = n << 1 ;
Matrix a ;
for(int i = 1 ; i <= n ; i++)
{
a.a[i][i - 1] = a.a[i][i] = a.a[i][i + n] = a.a[i + n][i] = 1 ;
if(i != n) a.a[i][i + 1] = 1 ;
}
Matrix s = quick_pow(a , m - 2) ;
if(n == 1)
{
printf("%lld\n" , s.a[1][1]) ;
return 0 ;
}
int s1 = ( s.a[1][len - 1] + s.a[2][len - 1] + s.a[n + 1][len - 1] ) % kmod ;
int s2 = ( s.a[1][len] + s.a[2][len] + s.a[n + 1][len]) % kmod ;
printf("%lld\n" , (s1 + s2) %kmod) ;
return 0 ;
}