10.25 a.m.题解

T1:问题 B: [SHOI2013]超级跳马

题目描述

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

 

试求跳法种数对 30011 取模的结果。

输入

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

输出

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

样例输入

3 5

样例输出

10

提示

 对于 10% 的数据,1≤n≤10,2≤m≤10;

对于 50% 的数据,1≤n≤10,2≤m≤10^5;

对于 80% 的数据,1≤n≤10,2≤m≤10^9;

对于 100%的数据,1≤n≤50,2≤m≤10^9。

题解

 

        这道题题意就是走“特殊的马”,然后从左上角走到右下角(废话)。有两个关键点:一、一个马只能走本行或者相邻行。二、每次移动的水平距离必须为奇数。由于参考之前走兵的做法,可以考虑给棋盘上每一个位置赋一个值,dp[i][j]表示到达(i,j)这个点一共有多少种方案。自然所求的答案为:dp[n][m]。由于马只能挑奇数,所以不能够同列相跳(0算是偶数)。故每一列可以作为一个阶段,从左到右转移。初始自然是dp[1][1]是1,其他全是0。现在考虑转移方程,一个点,能由上下三行之前相距为奇数的点转移答案,显然转移点和当前点的横坐标寄偶性恰好相反(举个例子就好理解)。由于前面任意一个奇偶性相反的点都能作为答案,显然可以维护一个前缀和,分奇偶性存,这样直接累加三行前缀和(也可能只有2行,上下封顶的情况),就可以得到当前的dp值。

        自然,该算法虽然用了前缀和优化(不然只会更慢),但是效率显然是m*n的,根本无法过后面的数据。又发现,n非常小,用状态压缩?2的50次方也不行,而且也不好压。自然就想到了矩阵加速递推。由于每次求的量都很确定,一定程度上是相似的,所以可以定义g(x)=(sum[1][0],sum[2][0],\cdots,sum[n][0],sum[1][1],sum[2][1],\cdots ,sum[n][1]),sum代表前缀和,第二位0/1表示偶/奇。其实方便理解,还可以加上dp,但是后来发现dp无用,也就省却了。转移方式很简单,原本有的到了下一列还有。同时根据奇偶性的不同,需要分别转移。由于n=1不用计算,所以n从2开始,故顺序为偶->奇->偶->奇……不如把一组偶->奇作为一个过程,一起转移,就可以用快速幂很快解决(自然,分开处理也不错,只是考试时没想到)。最后注意一点,不要算第m列,算到m-1就行,最后直接计算最后两行的前缀和(分情况讨论奇偶),累计答案即可。

参考代码

#include<cstdio>
#include<cstring>
#define mod 30011
using namespace std;
int n,m;
struct matrix
{
	int num[105][105];
	void init() { memset(num,0,sizeof(num)); }
	void print()//调试小技巧
	{
		for(int i=1;i<=n*2;i++)
		{
			for(int j=1;j<=n*2;j++)
			{
				printf("%d ",num[i][j]);
			}
			printf("\n");
		}
		printf("\n");
	}
	matrix operator * (const matrix x)
	{
		matrix c;
		c.init();
		for(int i=1;i<=n*2;i++)
		{
			for(int j=1;j<=n*2;j++)
			{
				for(int k=1;k<=n*2;k++)
				{
					c.num[i][j]=(c.num[i][j]+num[i][k]*x.num[k][j])%mod;
				}
			}
		}
		return c;
	}
};
matrix trans,trans1,trans2,ret;
void prep()
{
	trans.init();
	trans1.init();
	trans2.init();
	ret.init();
	for(int i=1;i<=n;i++)
	{
		for(int j=-1;j<=1;j++)
		{
			if((i+j>0)&&(i+j<=n))
			{
				trans1.num[i+j+n][i]=1;
			}
		}
	}
	for(int i=n+1;i<=2*n;i++)
	{
		for(int j=-1;j<=1;j++)
		{
			if((i+j>n)&&(i+j<=n*2))
			{
				trans2.num[i+j-n][i]=1;
			}
		}
	}
	for(int i=1;i<=n*2;i++)
	ret.num[i][i]=trans1.num[i][i]=trans2.num[i][i]=1;
	trans=trans1*trans2;
}
void qpow()
{
	int y=m/2-1;
	while(y)
	{
		if(y&1) ret=ret*trans;
		trans=trans*trans;
		y/=2;
	}
	if(m%2==1) ret=ret*trans1;
}
void getans()
{
	if(m%2==1) printf("%d",(ret.num[n+1][n-1]+ret.num[n+1][n])%mod);
	else printf("%d",(ret.num[n+1][n*2-1]+ret.num[n+1][n*2])%mod);
}
int main()
{
	scanf("%d%d",&n,&m);
	prep();
	qpow();
	getans();
	return 0;
}

T2:问题 C: 阶乘字符串

题目描述

给定一个由前n个小写字母组成的串S。串S是阶乘字符串当且仅当前n 个小写字母的全排列(共n!种)都作为的子序列(可以不连续)出现。

由这个定义出发,可以得到一个简单的枚举法去验证,但是它实在太慢了。所以现在请你设计一个算法,在1 秒内判断出给定的串是否是阶乘字符串。

 

输入

输入第1 行一个整数T,表示这个文件中会有T组数据。

接下来分T个块,每块2 行:

第1 行一个正整数n,表示S 由前n个小写字母组成。

第2 行一个字符串s。

 

输出

对于每组数据,分别输出一行。每行是YES 或者NO,表示该数据对应的串S是否是阶乘字符串。

样例输入

2

2

bbaa

2

aba

样例输出

NO

YES

提示


第一组数据中,ab 这个串没有作为子序列出现。

 
 


 

 


1

 


2

 


3

 


4

 


5

 


6

 


7

 


8

 


9

 


10

 


N<=

 


3

 


5

 


20

 


7

 


20

 


20

 


20

 


26

 


26

 


26

 


T=

 


4

 


5

 


5

 


5

 


5

 


5

 


5

 


5

 


5

 


5

 


|S|<=

 


10

 


350

 


400

 


300

 


450

 


450

 


450

 


450

 


450

 


450

题解

        这道题要用到序列自动机

        序列自动机能够以比较优的效率(约等于暴力)求出第i个字符以后第一个字符为j的位置。实现方式就是倒序更新nxt数组。nxt[i][j]表示第i位以后第一次出现j的位置(不存在则为无穷大)。依次把每一位当做转移的元素,改变的是j,此时需要给nxt[i][s[i]-'a']直接赋值成i(写法也许不同),表示更新了一个点。

        下面是实现这个的代码。

for(int i=1;i<=n;i++) nxt[len+1][i]=INF;
for(int i=len;i>=0;i--)
{
	for(int j=1;j<=n;j++) nxt[i][j]=nxt[i+1][j];
	if(i<len) nxt[i][s[i]-'a'+1]=i+1;
}

        其实很容易想到dp的意义(某大佬说的)。dp[i]表示状态为i的最小的前缀位置即dp[i]表示前dp[i]位能找到满足条件的所有前缀。此处讲一讲状态,状态是由一个二进制数来表示,从0开始记位,分别表示从a开始一直到‘a’+n-1的小写字母,如果此二进制位上为1,则表示这一个小写字母已经在状态中,也就是说计算的全排列数要加上这个小写字母。

        dp转移比较难想,但是想通了就很简单。假设此时的状态为i,那么自然可以是由任意一个子状态转移而来。子状态表示状态i的二进制位中任意少一个1的状态。如何理解,可以想象,假设第5位就满足状态k,同时k|(1<<3)=i,那么只需要找到第5位后的第一个代表(1<<3)的小写字母的位置。如果找到,就意味着以这个小写字母为结尾的状态为i的排列已经找到。由于状态为i,所以可能不止需要找一个结尾。但是一旦全部找到,那么就意味着状态i的所有全排列都能找到。而一旦有一步找不到,就会返回极大值。因此只需要判断最后的全部都是1的状态(1<<n)-1是不是极大值,依此判断是不是符合条件。

        但是1<<26,开空间肯定炸了。因此考虑其他优化,先枚举几个n比较小的。容易发现,把abcdefg……一直到要求的'a'+n-1,作为一个循环,如此循环n次一定能找到全排列。(只需要关注结尾,然后往回看,发现每一个小写字符都能够按序拍找到适合的排位,即可理解)于是发现n^2来判断,21已经是极限。因此猜想n太大应该也不行,姑且推论在23上下。洛谷的题解给出了n^2-n+1的构造,因此大致而言21已经是能够得到YES的最大范围了(非常抱歉,此处我无法给出非常严谨的证明,但是极限能开22、23,可以卡一下空间更保险)。针对数据而言,确实应该没有出到卡21以上的数据。

参考代码

#include<cstdio>
#include<cstring>
#define INF 1<<30
using namespace std;
int t,n,len,nxt[460][30],dp[5001000];
char s[2000];
int max1(int p,int q) { return p>q?p:q; }
int main()
{
	scanf("%d",&t);
	while(t--)
	{
		scanf("%d",&n);
		scanf("%s",s);
		if(n>21)
		{
			printf("NO\n");
			continue;
		}
		len=strlen(s);
		for(int i=1;i<=n;i++) nxt[len+1][i]=INF;
		for(int i=len;i>=0;i--)
		{
			for(int j=1;j<=n;j++) nxt[i][j]=nxt[i+1][j];
			if(i<len) nxt[i][s[i]-'a'+1]=i+1;
		}
		memset(dp,0,sizeof(dp));
		for(int i=1;i<(1<<n);i++)
		{

			for(int j=1;j<=n;j++)
			{
				if((i&(1<<(j-1)))!=0)
				{
					if(dp[(i^(1<<(j-1)))]==INF) dp[i]=INF; 
					else dp[i]=max1(dp[i],nxt[dp[(i^(1<<(j-1)))]][j]); 
				}
			}
		}
		if(dp[(1<<n)-1]<INF) printf("YES\n");
		else printf("NO\n");
	}
	return 0;
}

T3:问题 D: 简单的寻路问题

题目描述

经过一年在杭州的学习,易芬飞终于到达了家乡宁波。离开宁波一年,易芬飞有很多人要见面。尤其是一个好朋友Merceki。

 

易芬飞的家在乡下,但Merceki的家在市中心。因此,易芬飞与Merceki安排在肯德基见面。宁波有很多肯德基,他们想选择一家总时间最短的肯德基。

 

现在给你一张宁波地图,易芬飞和Merceki都可以上下左右移动到相邻的道路上,只需花费11分钟。

输入

输入包含多个测试用例。
每个测试用例包括前两个整数n,m。(2<=n,m<=200)。
接下来的n行,每行包含m个字符。
‘Y’ 表示易芬飞初始位置。

‘M’ 表示Merceki初始位置。
‘#’ 无法通过的路;
‘.’ 路
‘@’ KFC

输出

对于每个测试用例,输出易芬飞和Merceki到达肯德基的最短总时间。你可以肯定,总会有一家肯德基可以让他们见面。 

样例输入

4 4
Y.#@
....
.#..
@..M
4 4
Y.#@
....
.#..
@#.M
5 5
Y..@.
.#...
.#...
@..M.
#...#

样例输出

66

88

66

题解

        这道题化简成最简式就是:需要两个人到达同一个地方,求总时间最少。可以得知,这两个人是互不干扰的。因此到达同一地点可以拆分成2个人先分别跑SFPA,然后枚举每一个规定的地点,最后输出答案即可。

参考代码

#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
struct node
{
	int x,y;
}a[50000];
int dx[5]={0,0,1,0,-1};
int dy[5]={0,1,0,-1,0};
int tot=0,ans;
int n,m,st_x,st_y,ed_x,ed_y;
char s[300];
int e_map[210][210],vis[210][210],dis1[210][210],dis2[210][210];
queue<node>q;
int min1(int a1,int b1) { return a1<b1?a1:b1; }
int max1(int a1,int b1) { return a1>b1?a1:b1; }
bool pd(int x,int y)
{
	return (x>0) && (x<=n) && (y>0) && (y<=m) ;
}
int main()
{
	while(scanf("%d%d",&n,&m)!=EOF)
	{
		ans=1<<30;
		tot=0;
		memset(dis1,127/3,sizeof(dis1));
		memset(dis2,127/3,sizeof(dis2));
		for(int i=1;i<=n;i++)
		{
			scanf("%s",s);
			for(int j=1;j<=m;j++)
			{
				e_map[i][j]=0;
				if(s[j-1]=='#') e_map[i][j]=1;
				if(s[j-1]=='Y') 
				{
					st_x=i;
					st_y=j;
				}
				if(s[j-1]=='M')
				{
					ed_x=i;
					ed_y=j;
				}
				if(s[j-1]=='@')
				{
					a[++tot].x=i;
					a[tot].y=j;
				}
			}
		}
		dis1[st_x][st_y]=0;
		node ht;
		ht.x=st_x;
		ht.y=st_y;
		q.push(ht);
		while(!q.empty())
		{
			node k=q.front();q.pop();
			vis[k.x][k.y]=0;
			for(int i=1;i<=4;i++)
			{
				int x1=k.x+dx[i];
				int y1=k.y+dy[i];
				if(!pd(x1,y1)) continue;
				if(e_map[x1][y1]==1) continue;
				if(dis1[x1][y1]>dis1[k.x][k.y]+11)
				{
					dis1[x1][y1]=dis1[k.x][k.y]+11;
					if(!vis[x1][y1])
					{
						vis[x1][y1]=1;
						node hr;
						hr.x=x1;
						hr.y=y1;
						q.push(hr);
					}
				}
			}
		}
		memset(vis,0,sizeof(vis));
		dis2[ed_x][ed_y]=0;
		ht.x=ed_x;
		ht.y=ed_y;
		q.push(ht);
		while(!q.empty())
		{
			node k=q.front();q.pop();
			vis[k.x][k.y]=0;
			for(int i=1;i<=4;i++)
			{
				int x1=k.x+dx[i];
				int y1=k.y+dy[i];
				if(!pd(x1,y1)) continue;
				if(e_map[x1][y1]==1) continue;
				if(dis2[x1][y1]>dis2[k.x][k.y]+11)
				{
					dis2[x1][y1]=dis2[k.x][k.y]+11;
					if(!vis[x1][y1])
					{
						vis[x1][y1]=1;
						node hr;
						hr.x=x1;
						hr.y=y1;
						q.push(hr);
					}
				}
			}
		}
		for(int i=1;i<=tot;i++)
		{
			ans=min1(ans,dis1[a[i].x][a[i].y]+dis2[a[i].x][a[i].y]);
		}
		printf("%d\n",ans);
	}
	return 0;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值