矩阵快速幂

前言

神仙算法
前置知识:矩阵

1. 矩阵怎么快速幂

矩阵的幂与数字的幂一样,就是自己和自己相乘。求一个矩阵的 \(n\) 次幂同样可以用快速幂加速。
首先我们打上数字快速幂的模板:

ll ksm(ll n,ll k){
    ll ans=1,f=n;  
    while(k){
        if((k&1)==1)ans=ans*f;
        f=f*f;
        k=k>>1;
    }
    return ans;
}

在加入一个重载过 * 运算符的支持乘法运算的矩阵结构体:

struct matrix{
	ll data[105][105];ll n,m;
	matrix()memset(data,0,sizeof data);
	matrix(ll line,ll row,ll k){
		n=line;m=row;
		if(m!=n){
			cout<<"invaild initializing"<<'\n';
			return;
		}
		for(int i=1;i<=n;i++)data[i][i]=k;
	}
	matrix operator*(const matrix &x)const{//重载 * 操作 
		if(m!=x.n){
			cout<<"invaild multi operator"<<'\n';
			matrix res(1,1,1);
			return res;
		}
		matrix c;
		c.n=n;
		c.m=x.m;
		for(int i=1;i<=n;i++)
			for(int j=1;j<=m;j++)
				for(int k=1;k<=x.m;k++)
					c.data[i][k]+=data[i][j]*x.data[j][k];
		return c;
	}
};

将原来快速幂中的数据类型改成矩阵就可以了。
模板代码:

#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
	ll x=0,f=1;
	char c=getchar();
	while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
	while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
struct matrix{
	ll data[105][105];ll n,m;
	matrix()memset(data,0,sizeof data);
	matrix(ll line,ll row,ll k){
		n=line;m=row;
		if(m!=n){
			cout<<"invaild initializing"<<'\n';
			return;
		}
		for(int i=1;i<=n;i++)data[i][i]=k;
	}
	matrix operator*(const matrix &x)const{//重载 * 操作 
		if(m!=x.n){
			cout<<"invaild multi operator"<<'\n';
			matrix res(1,1,1);
			return res;
		}
		matrix c;
		c.n=n;
		c.m=x.m;
		for(int i=1;i<=n;i++)
			for(int j=1;j<=m;j++)
				for(int k=1;k<=x.m;k++)
					c.data[i][k]+=data[i][j]*x.data[j][k];
		return c;
	}
};
matrix ksm(matrix n,ll k){
    matrix ans(n.n,n.m,1),f=n;  
    while(k){
        if((k&1)==1)ans=ans*f;
        f=f*f;
        k=k>>1;
    }
    return ans;
}
int main(){

	ll n;
	cin>>n;
	matrix bc(2,2,1);
	bc.data[1][1]=1;bc.data[1][2]=1;
    bc.data[2][1]=1;bc.data[2][2]=0;
	matrix res=ksm(bc,n);//计算矩阵 bc 的 n 次方 

	return 0;
}

2. 矩阵快速幂有什么用

仅是计算一个矩阵的幂有点太简单了,我们常用矩阵快速幂来加速递推。
考虑一个经典的递推:斐波那契数列。

\[F_n=F_{n-1}+F_{n-2} \]

使用递推计算斐波那契数列的第 \(n\) 项的时间复杂度为 \(O(n)\)
但是使用矩阵快速幂,我们可以把时间优化到 \(O(\log n)\)
当然,一般这样的斐波那契数列的项数非常大,题目中会让对答案取模。
如何用矩阵递推呢?
我们知道,计算斐波那契数列的下一项需要前两项的值,因此先把 \(F_n\)\(F_{n-1}\) 放到一个一行两列的矩阵里:

\[\begin{pmatrix} F_n&F_{n-1} \end{pmatrix}\]

此时我们如果可以用某种操作让矩阵变成:

\[\begin{pmatrix} F_{n+1}&F_{n} \end{pmatrix}\]

就可以完成一次递推。
我们想到矩阵乘法的一些特点:结果是矩阵的每一行列上的积的和。
因此,我们不妨让这个矩阵乘以:

\[\begin{pmatrix} 1&1\\ 1&0 \end{pmatrix}\]

为什么这样乘,我们一列一列考虑:
首先对于结果矩阵第一行第一列,它等于 \(F_n\times 1+F_{n-1}\times 1\),即 \(F_n+F_{n-1}\),也就是 \(F_{n+1}\)
对于结果矩阵第一行第二列,它等于 \(F_n\times 1\),也就是 \(F_n\)
于是计算结果为:

\[\begin{pmatrix} F_{n+1}&F_{n} \end{pmatrix}\]

变成了我们想要的样子!
而且我们发现,如果让结果矩阵再乘以那个特殊的矩阵,我们还能再完成一次递推。
也就是说,我们将一个加法递推转换为了矩阵乘法,且这个乘法是一个原始矩阵多次乘以同一个特殊矩阵的过程,即乘以这个特殊矩阵的多次幂。
求一个矩阵的多次幂,快速幂算法正好可以快速解决。
因此,我们要求斐波那契数列的第 \(n\) 项,可以先把对应特殊矩阵的多次幂求解出来,最后乘以一个给定的初始值矩阵,答案便在结果矩阵的特定位置。
我自己把那个特殊矩阵称为转移矩阵(有点像 DP 的转移方程)。
简单观察找规律可以发现,如果我们把初始矩阵定为

\[\begin{pmatrix} F_n&F_{n-1} \end{pmatrix}\]

即:

\[\begin{pmatrix} 1&1 \end{pmatrix}\]

求解斐波那契第 \(n\) 项,只需要用快速幂求解出转移矩阵的 \(n-2\) 次幂再与初始矩阵相乘,答案便在第一行,第一列。
于是可以写代码了,记得取模:
Luogu P1962 斐波那契数列

#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
	ll x=0,f=1;
	char c=getchar();
	while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
	while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
const int mod=1e9+7;
struct matrix{
	ll data[6][6];ll n,m;
	matrix()memset(data,0,sizeof data);n=0;m=0;
	matrix(int n,int m){
		memset(data,0,sizeof data);this->n=n;this->m=m;
	}
	matrix operator *(const matrix &x)const {
		matrix res(n,x.m);
		for(int i=1;i<=n;i++)
			for(int j=1;j<=m;j++)
				for(int k=1;k<=x.m;k++)
					res.data[i][k]=(res.data[i][k]+data[i][j]*x.data[j][k])%mod;
		return res;
	}
};
matrix qp(matrix m,ll k){
	matrix res=m;
	while(k){
		if(k&1)res=res*m;
		m=m*m;
		k>>=1;
	}
	return res;
}
int main(){

	ll n;
	cin>>n;
	matrix bc(2,2);
	bc.data[1][1]=1;bc.data[1][2]=1;
	bc.data[2][1]=1;bc.data[2][2]=0;//这样排列可以在视觉上方便给矩阵赋初值
	matrix m(1,2);
	m.data[1][1]=1;m.data[1][2]=1;
	if(n<=2){
		cout<<1;
		return 0;
	}
	matrix res=qp(bc,n-2);
	matrix ans=res*m;
	cout<<ans.data[1][1];
	
	return 0;
}

回顾一下我们的解题过程,在这个转移矩阵中,只有 \(1\)\(0\) 两种元素,因此我们可以看成结果矩阵 要 还是 不要 原矩阵中的某些元素。
在上面的例题中,我们对答案矩阵的每一个,对应转移矩阵的每一列应是什么样子,思考过程如下:

  1. 对于结果矩阵的第一个,由于我们要在这里存放 \(n+1\) 项斐波那契数列的值,因此我需要 \(F_{n}\)\(f_{n-1}\),也就是原矩阵的全部两个元素相加。所以,转移矩阵第一列的两个数都是 \(1\),就代表着 “都要”。
  2. 对于结果矩阵的第二个,由于我们将来还要继续向下递推 \(n+2\) 项的值,因此还需要 \(n+1\) 项和 \(n\) 项的值。所以,转移矩阵第二列从上到下的两个数分别是 \(1,0\),表示我要原矩阵中的 \(F_n\),不要 \(F_{n-1}\)

当然,以上是转移矩阵中只有 \(0,1\) 的情况。如果遇到递推方程中有系数,还可以在转移矩阵上标任何的数,可以表示不取,取了还要变为几倍等更多含义。
某些求数列带系数前缀和的题目甚至不能递推,我们还可能要用一些数学方法间接推导出值。

例题:
LibreOJ - 10221 Fibonacci 前 n 项和
LibreOJ - 10222 佳佳的 Fibonacci
OI-wiki 上带系数的递推式的转移矩阵


迁移自洛谷

posted @ 2025-02-04 13:29  hm2ns  阅读(40)  评论(0)    收藏  举报