首页 > 技术文章 > 石头游戏

spnooyseed 2020-05-10 12:57 原文

矩阵乘法-----石头游戏

我做了两个地方的石头游戏,
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\n",ans) ;
	  
	  return 0 ;
}

推荐阅读