[SHOI2013]超级跳马

[SHOI2013]超级跳马

又是一道紫题欸!

题目描述

现有一个 n n n m m m 列的棋盘,一只马欲从棋盘的左上角跳到右下角。每一步它向右跳奇数列,且跳到本行或相邻行。跳越期间,马不能离开棋盘。例如,当 n = 3 n = 3 n=3 m = 10 m = 10 m=10 时,下图是一种可行的跳法。

试求跳法种数对 30   011 30\,011 30011 取模的结果。

输入格式

仅有一行,包含两个正整数 n , m n, m n,m,表示棋盘的规模。

输出格式

仅有一行,包含一个整数,即跳法种数模 30   011 30\,011 30011 后的结果。

样例 #1

样例输入 #1

3 5

样例输出 #1

10

提示

  • 对于 10 % 10\% 10% 的数据, 1 ≤ n ≤ 10 1 \leq n \leq 10 1n10 2 ≤ m ≤ 10 2 \leq m \leq 10 2m10
  • 对于 50 % 50\% 50% 的数据, 1 ≤ n ≤ 10 1 \leq n \leq 10 1n10 2 ≤ m ≤ 1 0 5 2 ≤ m ≤ 10^5 2m105
  • 对于 80 % 80\% 80% 的数据, 1 ≤ n ≤ 10 1 \leq n \leq 10 1n10 2 ≤ m ≤ 1 0 9 2 \leq m \leq 10^9 2m109
  • 对于 100 % 100\% 100% 的数据, 1 ≤ n ≤ 50 1 \leq n \leq 50 1n50 2 ≤ m ≤ 1 0 9 2 \leq m \leq 10^9 2m109

思路部分

第一眼

思路1

是一道超级简单的dp题欸

对于(2,5)这个点(蓝色方框)来说,它可以从图中黄色方框内数字的和转移而来(黄色方框的位置上的马可以跳到蓝色方框),而黄色方框则离蓝色方框差了奇数列,那么我们可以想到一个最朴素的dp转移方程

附上一份会超时的代码
#include<bits/stdc++.h>
using namespace std;
int n,m,a[60][1000000];
int mod=3011;
int main(){
//	freopen("jump.in","r",stdin);
	freopen("jump1.out","w",stdout);
	scanf("%d%d",&n,&m);
	a[1][1]=1;
	for(int j=2;j<=m;j++){
		for(int i=1;i<=n;i++){
			for(int k=1;k<j;k+=2){
				a[i][j]=(a[i][j]+a[i+1][j-k]);
				a[i][j]=(a[i][j]+a[i][j-k]);
				a[i][j]=(a[i][j]+a[i-1][j-k]);
			}
		}
	}
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			cout<<setw(7)<<a[i][j];
		}
		cout<<endl;
	}
	return 0;
}

然而,你会发现一个很严重的问题。貌似数组不能开那么大,而且,另外一个严重问题是,它会超时(复杂度太高了)!!!完了,这算法是没救了

于是乎,这份代码沦为了对拍工具……

第二眼

思路2

我们来先解决一下复杂度问题。最简单的办法的当然是前缀和。

前缀和其实很容易想到的,因为每次我们得到一个点的答案都会向前找到相隔奇数行的答案来计算,我们只要将前面奇数行的答案统计前缀和就可以省去这个步骤了

我们每次计算出新的答案后,就将答案累加到前缀和中,每次只用访问前缀和中相邻行就可以了

再次附上会超时的代码

(我考试就写的这个,然而它TLE了)

#include<bits/stdc++.h>
using namespace std;
long long n,m;//a[60][1000000];
long long a1[60],a2[60],now[60];
int mod=30011;
int main(){
//	freopen("jump.in","r",stdin);
	freopen("jump.out","w",stdout);
	scanf("%d%d",&n,&m);
	a1[1]=a2[1]=a2[2]=1;
	for(int j=3;j<=m;j++){
		if(j%2==1){//from a2
			for(int i=1;i<=n;i++){
				now[i]=(a2[i-1]+a2[i]+a2[i+1])%mod;
				a1[i]=(a1[i]+now[i])%mod;//putin a1
				// cout<<setw(10)<<now[i];
			}
			// cout<<endl;
		}
		else{//from a1
			for(int i=1;i<=n;i++){
				now[i]=(a1[i-1]+a1[i]+a1[i+1])%mod;
				a2[i]=(a2[i]+now[i])%mod;//putin a2
				// cout<<setw(10)<<now[i];
			}
			// cout<<endl;
		}
	}
	printf("%d",now[n]);
	return 0;
}

因为每次都是从相隔奇数的列中得到答案,所以我们计算前缀和需要记录两个,分别是奇数列和偶数列的(数组a1和数组a2)

然而,残酷的事实是,它又超时了,算法它又沦为了对拍工具……

第三眼-它终于是正解了!

思路

我们除了这样统计前缀和,还有其他办法吗?

那肯定啊(那不就是正解吗)

我们将转移方程式改为这样:

画个图来解释一下:

有了这幅图就一目了然了。首先,前一列的相邻三个数是需要相加的,然后呢,前面相隔三列,五列,甚至更远的列都被同一行相隔两列的那个数包含在内了,所以我们就显而易见的得到了这个转移方程。

说完了转移方程,那我们又该怎么写呢?那当然是矩阵加速了啊~~(说实话开始时我完全没想到)~~

对于我们新的一列,我们需要从前面的两行转移过来,那么初始矩阵就得到了:

这是对于n等于4时的情况(前4个元素为第二列的,后4个元素为第一列的):

前面n个元素为相隔一列的元素,n+1至2*n个元素为相隔两列的元素

这时候就该构建转移矩阵了:(矩阵应该为2n*2n大小的方阵)

我们要将这个初始矩阵变为存放第三列和第二列元素的矩阵:

很容易从之上推出这个转移矩阵(上图是对于n等于4的情况),我们对于第三列的每一个元素只需要按照上面已经给出的转移矩阵来算就行了(前提是你已经会了矩阵乘法)

貌似这道题已经做出来了!然而,显示却很残酷,你会发现,你的答案是错的。

错误的地方

你认真的debug后,你会神奇的发现,在第三列的第一行的答案貌似不对(错误的答案会是这样(如下图))

这是因为当我们转移第三列第一行这个元素时,会将1,1上的元素也加上去,然而按照题意,这个点是不会被算在1,3这个点中的

所以这是一个特殊情况(网上大多数做法是减去一个什么东西~~(貌似是什么前缀和,但我没看懂)~~),我在这里就非常直白地直接将初始矩阵从第4列和第3列开始,直接避免了这个神奇的问题:

然后我们只要特判m小于等于4和n小于等于4的情况(后来看别人的讨论才知道,这样特判还可以避免一些神奇的错误(比如50,2会输出神奇的1))

这样的话,我们就只用快速幂m-4次就可以了

最终代码

#include<bits/stdc++.h>
using namespace std;
struct matrix{
	long long h,l;//h代表行,l代表列
	int a[110][110];
}ans,wns;
long long n,m;
int mod=30011;
matrix times(matrix r,matrix c){
	matrix timesans;
	memset(timesans.a,0,sizeof(timesans.a));
	timesans.h=r.h;
	timesans.l=c.l;
	for(int i=1;i<=r.h;i++){
		for(int j=1;j<=c.l;j++){
			for(int q=1;q<=r.l;q++){
				timesans.a[i][j]=(timesans.a[i][j]+r.a[i][q]%mod*c.a[q][j]%mod)%mod;
			}
		}
	}
	return timesans;
}	
matrix mul(matrix x,long long y){
	matrix res;
	res.h=res.l=x.l;
	memset(res.a,0,sizeof(res.a));
	for(int i=1;i<=res.l;i++){
		res.a[i][i]=1;
	}
	for(;y;y>>=1){
		if(y&1) res=times(x,res);
		x=times(x,x);
	}
	return res;
}
matrix buildansup4(long long n){
	matrix temp;
	memset(temp.a,0,sizeof(temp.a));
	temp.h=1;
	temp.l=2*n;
	temp.a[1][1]=5;
	temp.a[1][2]=6;
	temp.a[1][3]=3;
	temp.a[1][4]=1;
	temp.a[1][n+1]=2;
	temp.a[1][n+2]=2;
	temp.a[1][n+3]=1;
	return temp;
}
matrix buildans3(){
	matrix temp;
	memset(temp.a,0,sizeof(temp.a));
	temp.h=1;
	temp.l=6;
	temp.a[1][1]=5;
	temp.a[1][2]=6;
	temp.a[1][3]=3;
	temp.a[1][4]=2;
	temp.a[1][5]=2;
	temp.a[1][6]=1;
	return temp;
}
matrix buildans2(){
	matrix temp;
	memset(temp.a,0,sizeof(temp.a));
	temp.h=1;
	temp.l=4;
	temp.a[1][1]=5;
	temp.a[1][2]=5;
	temp.a[1][3]=2;
	temp.a[1][4]=2;
	return temp;
}
matrix buildans1(){
	matrix temp;
	memset(temp.a,0,sizeof(temp.a));
	temp.h=1;
	temp.l=2;
	temp.a[1][1]=2;
	temp.a[1][2]=1;
	return temp;
}
matrix buildwns(long long n){
	matrix temp;
	temp.l=temp.h=2*n;
	memset(temp.a,0,sizeof(temp.a));
	for(int i=1;i<=n;i++){
		temp.a[i][i]=1;
		if(i-1>=1){
			temp.a[i][i-1]=1;
		}
		if(i+1<=n){
			temp.a[i][i+1]=1;
		}
	}
	for(int i=1;i<=n;i++){
		temp.a[i][n+i]=1;
	}
	for(int i=n+1;i<=2*n;i++){
		temp.a[i][i-n]=1;
	}
	return temp;
}
void printmatrix(matrix p){
	for(int i=1;i<=p.h;i++){
		for(int j=1;j<=p.l;j++){
			cout<<p.a[i][j]<<" ";
		}
		cout<<endl;
	}
} 
int main(){
	cin>>n>>m;
	if(m<=4){
		if(m==4){
			if(n==1){
				cout<<5;
				return 0;
			}
			if(n==2){
				cout<<6;
				return 0;
			}
			if(n==3){
				cout<<3;
				return 0;
			}
			if(n==4){
				cout<<1;
				return 0;
			}
		}
		if(m==3){
			if(n==1){
				cout<<2;
				return 0;
			}
			if(n==2){
				cout<<2;
				return 0;
			}
			if(n==3){
				cout<<1;
				return 0;
			}
		}
		if(m==2){
			if(n==1){
				cout<<1;
				return 0;
			}
			if(n==2){
				cout<<1;
				return 0;
			}
		}
		if(m==1){
			if(n==1){
				cout<<1;
				return 0;
			}
		}
		cout<<0;
		return 0;
	}
	if(n>=4){
		ans=buildansup4(n);
	}
	if(n==3){
		ans=buildans3();
	}
	if(n==2){
		ans=buildans2();
	}
	if(n==1){
		ans=buildans1();
	}
	wns=buildwns(n);
	// printmatrix(wns);
	// system("pause");
	wns=mul(wns,m-4);
	ans=times(ans,wns);
	printf("%lld",ans.a[1][n]%mod);
	// printmatrix(ans);
	return 0;
}

一点点小问题

我遇见了一个神奇的问题,在最后这个代码中,第5行和第8行的变量定义如果是longlong的话,他luogu评测的第11个点居然会TLE~~(我满脸疑惑)~~

不知道为什么,我把这两个变量改为int以后第11个点就过了……我真的不知道为什么~~(感觉很玄学)~~

如果有哪个大佬知道问题在哪里,欢迎在评论区留言

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值