高级算法指北——矩阵乘法、矩阵快速幂及其应用

I 走近矩阵乘法

i 定义

当矩阵 \(A\) 和矩阵 \(B\) 满足矩阵 \(A\) 的列数等于矩阵 \(B\) 的行数时,两个矩阵可以相乘。设 \(A\) 是一个 \(n\times m\) 的矩阵,\(B\) 是一个 \(m \times k\) 的矩阵,若 \(C=A\times B\),则 \(C\) 是一个 \(n\times k\) 的矩阵。

具体地,矩阵 \(C\) 满足:

\[C_{i,j}=\sum_{k=1}^mA_{i,k}\times B_{k,j} \]

即矩阵 \(C\) 的第 \(i\)\(j\) 列为 \(A\) 的第 \(i\) 行与 \(B\) 的第 \(j\) 列对应位置一一相乘所得值的和。简记为“前行乘后列,对应相乘再求和”。

代码很简单,直接 \(n^3\) 暴力求和即可。我们可以把乘法操作封装在一个结构体里面。

struct matrix{
	int a[N][N];
	friend matrix operator*(matrix x,matrix y){
		matrix res;
		rep(i,1,n){
			rep(j,1,n)
			    res.a[i][j]=0;
		}
		rep(i,1,n){
			rep(j,1,n){
				rep(k,1,n)
				    res.a[i][j]+=x.a[i][k]*y.a[k][j],res.a[i][j]%=mo;
			}
		}
		return res;
	}
}

特别地,有一类 \(n\times n\) 的矩阵,满足它乘上任何矩阵(或任何矩阵乘上它)后的得到的矩阵不变。我们称之为单位矩阵。单位矩阵除位于左上至右下的对角线上的值为 \(1\) 以外,其余的值均为 \(0\)

ii 矩阵乘法的运算律与矩阵快速幂

矩阵乘法不满足交换律,由定义中行和列的差异可以明显地看出。

矩阵乘法满足结合律。因此,我们可以使用快速幂来优化矩阵的乘方计算,其代码逻辑与普通的快速幂相同,只需要将初始值设为单位矩阵即可。

matrix quick_power(matrix base,int x){
	matrix ret;
	rep(i,1,n){//初始化为单位矩阵
		rep(j,1,n){
			if(i==j)ret.a[i][j]=1;
			else ret.a[i][j]=0;
		}
	}
	while(x){
		if(x&1)ret=ret*base;
		base=base*base;
		x>>=1;
	}
	return ret;
}

II 矩阵快速幂的应用

i 矩阵快速幂优化 dp

对于一类转移来源单一且相似、某一维状态范围极大的 dp,我们可以根据转移式构造方阵,使用矩阵乘法表示其转移过程,并使用矩阵快速幂加速转移即可。

已知一个数列 \(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\) 取余的值。

【解题】
考虑到每一次转移涉及到前三个 \(a\) 的值,考虑将它们放入一个 \(1\times 3\) 的矩阵中去。

\[\begin{bmatrix} a_{i-3} & a_{i-2} & a_{i-1} \end{bmatrix} \]

转移时,我们显然要将其变为下面这个矩阵:

\[\begin{bmatrix} a_{i-2} & a_{i-1} & a_{i} \end{bmatrix} \]

\[\begin{bmatrix} a_{i-2} & a_{i-1} & a_{i-1}+a_{i-3} \end{bmatrix} \]

考虑构造一个 \(3\times 3\) 的方阵,使得我们可以通过让原来的矩阵乘上这个方阵,得到新的矩阵。使用让一个原来的矩阵中的元素与方阵中一列对应,通过待定系数的方式,我们可以轻易求出转移方阵 \(G\) 如下:

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

此外,不难构造出初始矩阵 \(S_1\)

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

则有 \(S_i=S_1\times G^{i-1}\)。用矩阵快速幂即可快速求出 \(S_i\) 的值。对本题而言,求出 \(S_{n-2}\) 即可。

小Z所在的城市有N个公交车站,排列在一条长(N-1)km的直线上,从左到右依次编号为1到N,相邻公交车站间的距离均为1km。 作为公交车线路的规划者,小Z调查了市民的需求,决定按下述规则设计线路:

  1. 设共K辆公交车,则1到K号站作为始发站,N-K+1到N号台作为终点站。
  2. 每个车站必须被一辆且仅一辆公交车经过(始发站和终点站也算被经过)。
  3. 公交车只能从编号较小的站台驶往编号较大的站台。
  4. 一辆公交车经过的相邻两个站台间距离不得超过Pkm。

在最终设计线路之前,小Z想知道有多少种满足要求的方案。由于答案可能很大,你只需求出答案对30031取模的结果。

N<=10^9,1<P<=10,K<N,1<K<=P,K<=8

【解题】
不难想到设 \(dp_{i,j}\) 表示考虑到第 \(i\) 个车站,包含 \(i\) 的后 \(p\) 个公交车站停靠的那个车目前是否在那里停最后一个站的状态为 \(j\) 时的方案总数。初始位置为 \(dp_{k,2^k-1}\),终止位置为 \(dp_{n,2^k-1}\)。考虑转移时,有 \(dp_{i,nw}=\sum dp_{i-1,st}\),其中 \(nw\) 状态能由 \(st\) 状态左移 \(1\) 位后移动一个车到最低位而得到。不难发现能转移到 \(nw\) 状态种类是固定的,与 \(i\) 无关,故考虑构造方阵帮助转移。

我们可以将所有状态下的 \(dp\) 值排成一横排,形成一个矩阵。对于方阵的第 \(i\) 行第 \(j\) 列上的数,由于它会和原矩阵中第 \(i\) 个数(原矩阵和新矩阵均只有 \(1\) 行)相乘,贡献累加到新矩阵中第 \(j\) 个位置上去,所以若状态 \(i\) 能转移到状态 \(j\),则值为 \(1\),否则为 \(0\)。初始矩阵中仅有 \(2^k-1\) 那个状态对应位置上的值为 \(1\),最终答案即为乘出来的矩阵上 \(2^k-1\) 那个状态对应位置上的值。

int n,k,p,m;//状态总数为m 
struct matrix{
	int a[N][N]; 
	friend matrix operator *(matrix x,matrix y){
		matrix ret;
		rep(i,1,m){
			rep(j,1,m)
			    ret.a[i][j]=0;
		}
		rep(i,1,m){
			rep(j,1,m){
				rep(l,1,m){
					ret.a[i][j]+=x.a[i][l]*y.a[l][j];
					ret.a[i][j]%=mo;
				}
			}
		}
		return ret;
	}
}mat,pre;
int sit[N],id[(1<<11)+5];//有效状态 为了左移一位后不爆炸,多开一倍 
matrix quick_power(matrix base,int x){
	matrix ret;
	rep(i,1,m){
		rep(j,1,m){
			if(i!=j)ret.a[i][j]=0;
		    else ret.a[i][j]=1;
		} 
	}
	while(x){
		if(x&1)ret=ret*base;
		base=base*base;
		x>>=1;
	}
	
	return ret;
}
signed main(){
	read(n),read(k),read(p);
	rep(i,1,(1<<p)-1){//统计有效状态 
		if(!(i&1))continue;
		if(__builtin_popcount(i)!=k)continue;
		sit[++m]=i,id[i]=m;
	}
	rep(i,1,m){
		rep(j,1,m)
			pre.a[i][j]=mat.a[i][j]=0;
	}
	pre.a[1][1]=1;
	rep(i,1,m){//初始时所有1排在最后,是最小的(最靠前的)那个可行状态. 
	    int st=sit[i];
	    rep(j,1,p){
	    	if(!((st>>(j-1))&1))continue;
	    	int nw=((st^(1<<(j-1)))<<1)|1;
	    	if(!id[nw])continue;
	    	mat.a[i][id[nw]]=1;
		}
	}
	pre=pre*quick_power(mat,n-k);
	printf("%lld\n",pre.a[1][1]);//最终状态的所有1排在最后,仍然是最小的那个可行状态.
	return 0; 
}

ii 邻接矩阵乘法解决图论问题

豆豆先生酷爱冒险,他一听说这个消息,立马赶到了池塘,想做第一个在桥上旅游的人。池塘里有食人鱼,食人鱼的行进路线有周期性,这个周期只可能是 \(2\)\(3\) 或者 \(4\) 个单位时间。每个单位时间里,食人鱼可以从一个石墩游到另一个石墩。每到一个石墩,如果上面有人它就会实施攻击,否则继续它的周期运动。如果没有到石墩,它是不会攻击人的。

借助先进的仪器,豆豆很快就摸清了所有食人鱼的运动规律,他要开始设计自己的行动路线了。每个单位时间里,他只可以沿着石桥从一个石墩走到另一个石墩,而不可以停在某座石墩上不动,因为站着不动还会有其它危险。如果豆豆和某条食人鱼在同一时刻到达了某座石墩,就会遭到食人鱼的袭击,他当然不希望发生这样的事情。

现在豆豆已经选好了两座石墩 \(\mathrm{Start}\)\(\mathrm{End}\),他想从 \(\mathrm{Start}\) 出发,经过 \(K\) 个单位时间后恰好站在石墩 \(\mathrm{End}\) 上。假设石墩可以重复经过(包括 \(\mathrm{Start}\)\(\mathrm{End}\)),他想请你帮忙算算,这样的路线共有多少种(当然不能遭到食人鱼的攻击)。

对于 \(100 \%\) 的数据,\(1 \leq N \leq 50\)\(1 \leq K \leq 2 \times 10^9\)\(1 \leq \mathrm{NFish} \leq 20\)

【解题】

首先考虑没有食人鱼的时候怎么做。

对于一个邻接矩阵 \(E\)\(E_{i,j}=1\) 表示两者之间有边可以直达。同样的,它也可以表示\(i\) 出发到 \(j\),走 \(1\) 条边的方案数。考虑我们怎么求从 \(i\) 出发到 \(j\),走 \(2\) 条边的方案数,显然可以枚举一个点 \(k\),方案数为 \(\sum E_{i,k}\times E_{k,j}\),不难发现这和矩阵乘法的式子一致。因而我们可以直接将邻接矩阵平方,得到代表\(i\) 出发到 \(j\),走 \(2\) 条边的方案数的矩阵 \(E_2\)。进一步地,设表示任意两点间走 \(x\) 条边到达的方案数的矩阵为 \(E_x\),我们不难发现 \(E_i\times E_j=E_{i+j}\),也就可以得到 \(E_i=E_1^i\)。这样,我们就可以利用矩阵快速幂来快速求解了。

在有食人鱼的情况下,我们发现食人鱼的游荡周期为 \(12\)。因此,我们可以先对于前 \(12\) 个单位时间中的每一个,处理出食人鱼在的所有的石墩。则那一个时刻的矩阵应当为在原来 \(E_1\) 的基础上,将所有有食人鱼在的石墩对应的列设为 \(0\) 之后得到的矩阵(因为有食人鱼在的石墩,即使有边连接,也到不了)。根据前面说的原理,我们将这 \(12\) 个时间的对应矩阵按顺序依次(注意矩阵乘法不符合交换律!)乘起来,便得到在前 \(12\) 分钟里,从任意一点出发到任意一点,走 \(12\) 条边的方案数矩阵。接下来的时间就是对周期的重复,直接对求出来的一个周期里的方案数矩阵做快速幂即可。最后的不满一个周期的时间可以再暴力乘。

时间复杂度 \(O(N^3\log k)\)

#include<bits/stdc++.h> 
#define rep(i,j,k) for(int i=j;i<=k;i++)
#define repp(i,j,k) for(int i=j;i>=k;i--)
#define ls(x) x*2
#define rs(x) x*2+1
#define mp make_pair
#define fir first
#define sec second
#define pvv pair<vector<int>,vector<int>>
#define lowbit(x) x&-x
using namespace std;
typedef long long ll;
const int N=52,M=1e5+5,S=(1<<17)+2,mo=10000,inf=1e9+7;
void read(int &p){
	int x=0,w=1;
	char ch=0;
	while(!isdigit(ch)){
		if(ch=='-')w=-1;
		ch=getchar();
	}
	while(isdigit(ch)){
		x=(x<<1)+(x<<3)+ch-'0';
		ch=getchar();
	}
	p=x*w;
}
int n,m,q,st,ed,k;
struct matrix{
	int a[N][N];
	friend matrix operator*(matrix x,matrix y){
		matrix res;
		rep(i,1,n){
			rep(j,1,n)
			    res.a[i][j]=0;
		}
		rep(i,1,n){
			rep(j,1,n){
				rep(k,1,n)
				    res.a[i][j]+=x.a[i][k]*y.a[k][j],res.a[i][j]%=mo;
			}
		}
		return res;
	}
}e,sit[15],sum;
vector<int>shark[22];
int t[22];
matrix quick_power(matrix base,int x){
	matrix ret;
	rep(i,1,n){
		rep(j,1,n){
			if(i==j)ret.a[i][j]=1;
			else ret.a[i][j]=0;
		}
	}
	while(x){
		if(x&1)ret=ret*base;
		base=base*base;
		x>>=1;
	}
	return ret;
}
int main(){
	read(n),read(m),read(st),read(ed),read(k),st++,ed++;
	rep(i,1,m){
		int x,y;
		read(x),read(y),x++,y++;
		e.a[x][y]=e.a[y][x]=1;
	}
	read(q);
	rep(i,1,q){
		read(t[i]);
		rep(j,1,t[i]){
			int x;
			read(x),x++;
			shark[i].push_back(x);
		}
	}
	rep(i,1,12){//处理前12分钟的
		sit[i]=e;
		rep(j,1,q){
			int tms=i%t[j],p=shark[j][tms];
			rep(l,1,n)
			    sit[i].a[l][p]=0;
		}
	}
	sum=sit[1];
	rep(i,2,12)
	    sum=sum*sit[i];
	matrix ans=quick_power(sum,k/12);
	k%=12;
	rep(i,1,k)
		ans=ans*sit[i];
	printf("%d",ans.a[st][ed]);
	return 0;
}

给定一张 \(T\) 条边的无向连通图,求从 \(S\)\(E\) 经过 \(N\) 条边的最短路长度。

对于所有的数据,保证 \(1\le N\le 10^6\)\(2\le T\le 100\)

所有的边保证 \(1\le u,v\le 1000\)\(1\le w\le 1000\)

【解题】

与上面的方法类似,由于 floyd 求最短路算法的过程十分类似于矩阵乘法,只需要将乘积加和变为加和求 min。加和求 min 的操作显然也有结合律,因而我们仍然可以使用邻接矩阵做乘法,快速幂优化的方式进行求解,只需要将矩阵乘法转为加和求最小值,并在矩阵中存最短路长度即可。

注意在矩阵快速幂时,初始矩阵应当设成作为底数的那个矩阵,且幂次值同步 \(-1\);而不能用全为 INF 的矩阵替代。

int st,ed,n,m,k,p[N];
struct matrix{
	int a[N][N];
	friend matrix operator*(matrix x,matrix y){
		matrix res;
		rep(i,1,n){
			rep(j,1,n)
			    res.a[i][j]=inf;
		}
		rep(i,1,n){
			rep(j,1,n){
				rep(k,1,n)
				    res.a[i][j]=min(res.a[i][j],x.a[i][k]+y.a[k][j]);
			}
		}
		return res;
	}
}dis;
struct edge{
	int frm,to,v;
}e[M];
matrix quick_power(matrix base,int x){
	matrix ret=base;//注意这里的初始值设定,显然不能为inf. 
	x--;
	while(x){
		if(x&1)ret=ret*base;
		base=base*base;
		x>>=1;
	}
	return ret;
}
int main(){
	read(k),read(m),read(st),read(ed);
	rep(i,1,m){
		read(e[i].v),read(e[i].frm),read(e[i].to);
		p[++n]=e[i].frm,p[++n]=e[i].to;
   	}
	sort(p+1,p+n+1),n=unique(p+1,p+n+1)-p-1;
	rep(i,1,n){
		rep(j,1,n)
	        dis.a[i][j]=inf;
	}
	rep(i,1,m){
		int x,y;
		x=lower_bound(p+1,p+n+1,e[i].frm)-p,y=lower_bound(p+1,p+n+1,e[i].to)-p;
		dis.a[x][y]=dis.a[y][x]=min(dis.a[x][y],e[i].v);
	}
	st=lower_bound(p+1,p+n+1,st)-p,ed=lower_bound(p+1,p+n+1,ed)-p;
	dis=quick_power(dis,k);
	printf("%d\n",dis.a[st][ed]);
	return 0;
}
posted @ 2023-09-24 22:18  烟山嘉鸿  阅读(322)  评论(0)    收藏  举报