矩阵乘法-----石头游戏
我做了两个地方的石头游戏,
upc
Acwing
实际上我是想测试一下数据问题,
我看了网上的代码和题解,,,,,,,发现基本上都是一样的《基本上》
石头游戏在一个 n 行 m 列 (1≤n,m≤8) 的网格上进行,每个格子对应一种操作序列,操作序列至多有10种,分别用0~9这10个数字指明。
操作序列是一个长度不超过6且循环执行、每秒执行一个字符的字符串。
每秒钟,所有格子同时执行各自操作序列里的下一个字符。
序列中的每个字符是以下格式之一:
1、数字0 - 9:表示拿0~9个石头到该格子。
2、NWSE:表示把这个格子内所有的石头推到相邻的格子,N表示上方,W表示左方,S表示下方,E表示右方。
3、D:表示拿走这个格子的所有石头。
给定每种操作序列对应的字符串,以及网格中每个格子对应的操作序列,求石头游戏进行了 t 秒之后,石头最多的格子里有多少个石头。
在游戏开始时,网格是空的。
输入格式
第一行4个整数n, m, t, act。
接下来n行,每行m个字符,表示每个格子对应的操作序列。
最后act行,每行一个字符串,表示从0开始的每个操作序列。
输出格式
一个整数:游戏进行了t秒之后,所有方格中石头最多的格子有多少个石头。
输入样例:
1 6 10 3
011112
1E
E
0
输出样例:
3
样例解释
样例中给出了三组操作序列,第一个格子执行编号为0的操作序列”1E”,第二至五个格子执行编号为1的操作序列”E”,第六个格子执行编号为2的操作序列”0”。
这是另一个类似于传送带的结构,左边的设备0间隔地产生石头并向东传送。
设备1向右传送,直到设备2。
10秒后,总共产生了5个石头,2个在传送带上,3个在最右边。
样例模拟
第 1 秒 1 0 0 0 0 0
第 2 秒 0 1 0 0 0 0
第 3 秒 1 0 1 0 0 0
第 4 秒 0 1 0 1 0 0
第 5 秒 1 0 1 0 1 0
第 6 秒 0 1 0 1 0 1
第 7 秒 1 0 1 0 1 1
第 8 秒 0 1 0 1 0 2
第 9 秒 1 0 1 0 1 2
第 10 秒 0 1 0 1 0 3
题解
算法进阶指南上写的刚开始我没看懂,下面是我自己的理解加上书上写的
题目可以理解为是一个n∗m的矩阵,在里面进行操作。
不难发现,操作序列的长度不超过6,那么1~6的最小公倍数是60,(这个不需要说了吧,自己求一下就出来了)
即每经过60秒,所有操作序列都会重新处于最开始的字符处。(即又循环到了起点)
那么第k(1≤k≤60)和第k+60 秒肯定是相同的,又或者说第k秒和很多个60秒之后的第k+60*x秒是相同的
得到了这个结论,我们就可以很容易想到递推,因为这是单调的。
之后我们就可以运用矩阵乘法来加速运算。
设状态矩阵为F FF,我们用这样的方法去表示i行j列的石头数:
F[num(i , j)] , 其中呢num(i , j) = (i - 1) * m + j 注意,这只是一个构造的方法,并不一定非要是这样的,如果你有别的好的,依然可以构造其他的<我自己在上面纠结了很长时间>
根据题目定义,我们可以得到F的初始状态是[0 ,0 ,0 … 0]
因为矩阵乘法里面肯定不能全为0 , 否则你怎么乘也是0 ,所以设置设置一个1,位置就在p = n * m + 1 , 也可以再别的地方
那我们也需要一个转移矩阵A AA来转移状态吧。
对于第k(1≤k≤60)秒,构造一个转移状态A[k]行,列下标均为1 ~ n * m + 1
构造方法类似于跑图:
1、 若网格(i,j)第k秒的操作字符为“N”,且i>1 ,则令A[k][num(i,j),num(i−1,j)]=1
意思就是把石头推到上边的格子。字符"W",“S”,"E"类似。
2、 若网格(i,j)第k秒的操作字符时一个数字x,则令A[k][0,num(i,j)]=x
3、 令A[k][p][p] = 1
4、 A[k]的其它部分均赋值为0
注意石头来源其实当成了p
上述结构实际上把“状态矩阵”的第p列作为“石头来源”,A[k][p,p]=1 保证F[p]=1 ,
A[k][p,num(i,j)]=x 相当于从“石头来源”获取x个石头,放到格子(i,j)上。
另外,对于Ak[num(i,j),num(i−1,j)]=1 其实也是建边,使石头能够“流过去”。
上述文章部分参考
博客
刚开始我的矩阵快速幂是这样写的
struct node
{
ll a[N][N] ;
};
node muti(node x ,node y)
{
node res ;
memset(res.a,0,sizeof res.a);
for(int i = 0;i < N;i ++)
for(int j = 0 ;j < N;j ++)
for(int k = 0;k < N;k ++)
res.a[i][j] = (res.a[i][j] + x.a[i][k] *y.a[k][j]) % mod ;
return res ;
}
void fastm(ll b)
{
node res , c ;
memset(res.a,0,sizeof res.a);
c.a[0][1] = 1 ;
c.a[1][0] = 1 ;
c.a[1][1] = 0 ;
c.a[0][0] = 1 ;
for(int i = 0;i <N ;i ++)
res.a[i][i] = 1 ;
while(b)
{
if(b & 1) res = muti(res,c);
c =muti(c,c);
b >>= 1 ;
}
}
这个应该是最普通的矩阵快速幂了吧,但是我也只好这个,不过今天用的是另一个,但是原理是一样的,大佬们应该都是会的
void muls(ll a[66][66] , ll b[66][66])
{
ll w[66][66] ;
memset(w , 0 , sizeof w) ;
for(int i = 1;i <= p;i ++)
for(int j = 1;j <= p ;j ++)
for(int k = 1;k <= p;k ++)
w[i][j] += a[i][k] * b[k][j] ;
memcpy(a , w , sizeof w) ;
}
void mul(ll f[66] , ll a[66][66])
{
ll w[66] ;
memset(w , 0 , sizeof w) ;
for(int i = 1;i <= p;i ++)
for(int k = 1;k <= p;k ++)
w[i] += f[k] * a[k][i] ;
memcpy(f , w , sizeof w) ;
}
更多的细节代码中有注释 , 网上大部分都是这个代码,只是没有详细注释
#include <iostream>
#include <cstring>
#include <cmath>
#include <cstdio>
#include <algorithm>
using namespace std;
typedef long long ll ;
int n , m , t , act , p ;
int num(int i , int j) {return (i - 1) * m + j ;}
void muls(ll a[66][66] , ll b[66][66])
{
ll w[66][66] ;
memset(w , 0 , sizeof w) ;
for(int i = 1;i <= p;i ++)
for(int j = 1;j <= p ;j ++)
for(int k = 1;k <= p;k ++)
w[i][j] += a[i][k] * b[k][j] ;
memcpy(a , w , sizeof w) ;
}
void mul(ll f[66] , ll a[66][66])
{
ll w[66] ;
memset(w , 0 , sizeof w) ;
for(int i = 1;i <= p;i ++)
for(int k = 1;k <= p;k ++)
w[i] += f[k] * a[k][i] ;
memcpy(f , w , sizeof w) ;
}
// 以上是矩阵乘法部分,
char b[20][20] , s[100] ;
ll f[66] , e[66][66][66] , d[66][66] ;
int a[20][20] , c[20][20] ;
int main()
{
scanf("%d%d%d%d",&n , &m ,&t , &act) ;
for(int i = 1;i <= n;i ++)
{
scanf("%s",s + 1) ;
for(int j = 1;j <= m;j ++)
a[i][j] = s[j] - '0' + 1 ;
}
// 注意上面将字符串转化为数组的时候加了 1 , 这个是因为下面输入命令的时候是从一开始的
for(int i = 1;i <= act ;i ++) scanf("%s",b[i]) ;
p = n * m + 1 ; // 此时的p是 石头来源 ,
for(int k = 1;k <= 60 ;k ++) // 60秒一循环 , 处理每一秒的所有矩阵
{
for(int i = 1;i <= n;i ++)
for(int j = 1;j <= m;j ++)
{
int x = a[i][j] , y = c[i][j] ;
// x 表示上 当前格子用的是那一条命令 , y 表示每一秒使用的当前命令里面的第几个字符 , //在下面循环结束的时候再次加一取模,很是神奇,不懂的可以模拟一下,
if(isdigit(b[x][y])) // 当前命令这个该时间使用的字符
{
e[k][p][num(i , j)] = b[x][y] - '0' ;
e[k][num(i , j)][num(i , j)] = 1 ;
}
else if(b[x][y] == 'N' && i > 1) e[k][num(i , j)][num(i - 1 , j)] = 1 ;
else if(b[x][y] == 'W' && j > 1) e[k][num(i , j)][num(i , j - 1)] = 1 ;
else if(b[x][y] == 'S' && i < n) e[k][num(i , j)][num(i + 1 , j)] = 1 ;
else if(b[x][y] == 'E' && j < m) e[k][num(i , j)][num(i , j + 1)] = 1 ;
c[i][j] = (y + 1) % strlen(b[x]) ; // 此时就是加一取模,那么下一秒 也就是k + 1 秒使用的字符就是y+1 % strlen(b[x]) , 可以仔细想一想
}
e[k][p][p] = 1 ;
}
// 下面分成了两部分 ,可以想象t时间可能是很多个60组成的,然后如果还是一个一个的乘,很浪费时间,所以,//第一步就是乘一次就相当于乘了60秒的
memcpy(d , e[1] , sizeof e[1]) ;
f[p] = 1 ;
for(int k = 2;k <= 60;k ++) muls(d , e[k]) ; // 循环结束,d表示的就是60秒的矩阵
ll ans = 0 ;
int w = t / 60 ;
while(w) // 这个快速幂用d乘的 ,也就是用60秒的矩阵乘的,结果放在了f矩阵上
{
if(w & 1) mul(f , d) ;
muls(d , d) ;
w >>= 1 ;
}
// 下面的是乘的一秒的矩阵, 然后求解最大值
w = t % 60 ;
for(int i = 1;i <= w;i ++) mul(f , e[i]) ;
for(int i = 1;i < p;i ++) ans = max(ans , f[i]) ;
printf("%lld
",ans) ;
return 0 ;
}