【杂题总汇】UVa-10618 Tango Tango Insurrection

【UVa-10618】 Tango Tango Insurrection


 

◇ 题目 +vjudge 链接+

(以下选自《算法竞赛入门经典》-刘汝佳,有删改)

<题目描述>

你想学着玩跳舞机。跳舞机的踏板上有4个箭头:上、下、下、右。当舞曲开始时,屏幕上会有一些箭头往上移动。当向上移动箭头与顶部的箭头模板重合时,你需要用脚踩一下踏板上的相同箭头。不需要踩箭头时,踩箭头并不会受到惩罚,但当需要踩箭头时,必须踩一下,哪怕已经有一只脚放在了该箭头上。很多舞曲的速度快,需要来回倒腾步子,所以最好写一个程序来帮助你选择一个轻松的踩踏方式,使得能量消耗最少。为了简单起见,将一个八分音符作为一个基本时间单位,每个时间单位要么需要踩一个箭头(不会同时需要踩两个箭头),要么什么都不需要踩。在任意时刻,你的左右脚应放在
不同的两个箭头上,且每个时间单位内只有一只脚能动(移动和/或踩箭头),不能跳跃。另外,你必须面朝前方以看到屏幕(即:你不能把左脚放到右箭头上,并且右脚放到左箭头上)。
当你执行一个动作(移动和/或踩)时,消耗的能量这样计算:

■ 如果这只脚上个时间单位没有任何动作,消耗1单位能量。
■ 如果这只脚上个时间单位没有移动,消耗3单位能量。
■ 如果这只脚上个时间单位移动到相邻箭头,消耗5单位能量。
■ 如果这只脚上个时间单位移动到相对箭头(上到下,或者左到右),消耗7单位能量。

正常情况下,你的左脚不能放到右箭头上(或者反之),但有一种情况例外:如果你的左脚在上箭头或者下箭头,你可以临时扭着身子用右脚踩左箭头,但是在你的右脚移出左箭头之前,你的左脚都不能移到另一个箭头上。类似地,右脚在上箭头或者下箭头时,你也可以临时用左脚踩右箭头。一开始,你的左脚在左箭头上,右脚在右箭头上。

 <输入输出>

多组数据(不超过100组),每组数据包含一行——一个由'L','R','D','U'以及'.'组成的字符串(长度不超过70),第i个字符表示时间为i时需要踩下的键。

对于每组数据输出一个串,第i个字符表示时间为i时移动哪只脚,不移动输出'.'。


 

◇ 解析

由于本题影响答案的因素非常多,但是每一个因素的范围都不算大!所以我们选择DP,又因为本题不合法的条件也很多,我们选择记忆化搜索,能够简单地屏蔽所有不合法情况

首先给四个方向编号:上下左右依次编为0~3,空('.')编为4。方便计算、表示。顺便就把原来的字符串转换成int数组dir[]。

影响答案的因素如下:

  ■ 已经完成的箭头个数,设为pos,作为第一维状态;也就是说现在需要匹配第pos个箭头;

  ■ 当前左右脚分别所在的箭头编号,分别设为l,r,作为第二、三维状态;

  ■ 上一步是哪只脚移动/踩下,记为 pre,左脚操作为1,右脚为2,若没有移动/踩下,pre=0,作为第四维;

那么状态就像这样:dp[pos][l][r][pre]表示已经完成了前pos个箭头,且左右脚分别在l,r的位置,上一步左/右脚完成了一个箭头

很容易想到——初始的左右脚在左,右两键上,所以初始状态是 dp[0][2][3][0],即一个箭头都没有匹配,上一步没有操作。

而终止状态是当pos=n时(在这里数组从0开始)返回值为0。

在转移状态之前,我们还需要再处理几个函数,便于转移时值的计算:

  ■ 判断状态是否合法 bool Allowed(int l,int r,int fl,int fr)

参数表包含原来左右脚的位置l,r,以及下一步将要到达的位置fl,fr。

首先若左右脚都没有移动,则此时的状态是合法的,返回true。如果发现右脚没动(就是左脚动了)如果左脚将要到达的位置fl不是右脚的位置r,且右脚不在左箭头上(题目要求右脚只能临时在左箭头上,左脚也只能临时在右箭头上,所以如果右脚在左箭头上,是不能移动左脚的!),返回true。左脚与右脚判法相同。其余情况均为false。

■ 计算从当前位置到目标位置的花费 int Energy(int from,int to,inr last,int now)

参数表包含移动的脚的初始位置from和目标位置to,上一步进行操作的是哪只脚last,当前进行操作的是哪只脚now。

若当前进行操作的脚和上一次进行操作的脚不同,即last!=now时,根据题目第一条法则,我们可以判断这步操作花费为1。

若仅仅是没有移动,即from=to,则根据题目第二条法则,判断花费为3。

若移动的是相邻的两个箭头,判断花费为5。这里有一个小操作😐:因为相对两个箭头就是 0,1 和 2,3,1^1=0而2^1=3,所以若from^1=to,就可以判断是相对的箭头,其余是相邻箭头……(的确方便了很多)

接下来就是状态转移了!

首先判断当前“箭头”是否是真的箭头。

若是空("."),则分两种情况——左右脚都不移动,左脚移动或右脚移动。对于左右脚都不移动,对应的状态是 DP(pos+1,l,r,0),因为没有任何操作,下一步的pre=0。然后枚举4个箭头,让左右脚分别尝试移动到该箭头,用Allowed判断状态是否合法,再用Energy计算花费,最后取min,注意更新下一步的pre

若是一个箭头,则让左右脚都去尝试移动到该箭头,操作和之前相同,判断合法,计算花费,再取min。

状态转移式就像下面这样:

 

en……代码很长,真的很长……😱

最后我们不是输出花费……是输出方案,所以我们还要存一个数组 oh my god

记录和 dp[pos][l][r][pre] 有关的输出值为 pri[pos][l][r][pre][0~2] ,因为我们要存储三个值,这里的第五维大小是3。

我们存储pri的目的是能够从第一个状态(或者已知的唯一终止状态,但本题终止状态不唯一,就不能够从终止状态开始)开始,递推或递归输出解。在这里我们需要三个值,也就是DP状态的后三个值——因为pos每一次都加一,可以直接递推完成。由于最后一个值 pre 存储的是上一步是哪只脚在操作,也就是我们的答案,我们只需要输出pre就行了。但是pre存储的是上一步的操作,所以第i步的答案其实存储在第i+1个pre里……既然如此,我们可以确定答案就是pos=1~n的pre。根据pri[0][2][3][0],可以直接得到pos=1时的三个值l,r,pre。从pos=1的三个值开始递推。

(其实我是个蒟蒻)这个递推输出真的不知道怎么讲了,还是看下面代码的注释吧😥


 

◇ 源代码

/*Lucky_Glass*/
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN=70,MAXDIR=4,INF=int(1e9);
int dp[MAXN+5][MAXDIR+5][MAXDIR+5][3],dir[MAXN+5],pri[MAXN+5][MAXDIR+5][MAXDIR+5][3][3];
//dp[i][j][k][r] 表示 匹配完前i个箭头,现在左右脚分别在j,k,上一次操作是脚r完成的
//pri[i][j][k][r] 对应dp[i][j][k][r],三个值l,r,pre存储了计算dp[i][j][k][r]时,选择的最优状态是dp[i+1][l][r][pre]
//dir 是str 转换成int 的数组
char str[MAXN+5];
int n;
inline int Energy(int from,int to,int last,int now){
	if(last!=now) return 1; //没有动作
	if(from==to) return 3; //仅仅是没有移动
	if(from!=(to^1)) return 5; //不是相对箭头,也就是相邻箭头
	return 7; //剩下的就是相对箭头
}
inline bool Allowed(int l,int r,int fl,int fr){
	if(l==fl && r==fr) return true; //不移动一定正确
	if(l==fl) //移动右脚
		if(fr!=l && l!=3) //不和左脚重合,且当前不是必须移动左脚
			return true;
	if(r==fr)
		if(fl!=r && r!=2)
			return true;
	return false;
}
int DP(int pos,int l,int r,int pre){
	if(pos==n)
		return dp[pos][l][r][pre]=0;
	int &ans=dp[pos][l][r][pre];
	if(ans!=-1)
		return ans;
	ans=INF;
	if(dir[pos]==4)
	{
		ans=DP(pos+1,l,r,0); //不做移动,也不踩箭头,没有花费
		pri[pos][l][r][pre][0]=l;pri[pos][l][r][pre][1]=r;pri[pos][l][r][pre][2]=0; //记录pri
		for(int i=0;i<4;i++)
		{
			if(Allowed(l,r,i,r)) //移动左脚
			{
				int res=DP(pos+1,i,r,1)+Energy(l,i,pre,1);
				if(ans>res)
					ans=res,
					pri[pos][l][r][pre][0]=i,pri[pos][l][r][pre][1]=r,pri[pos][l][r][pre][2]=1;
			}
			if(Allowed(l,r,l,i)) //移动右脚
			{
				int res=DP(pos+1,l,i,2)+Energy(r,i,pre,2);
				if(ans>res)
					ans=res,
					pri[pos][l][r][pre][0]=l,pri[pos][l][r][pre][1]=i,pri[pos][l][r][pre][2]=2;
			}
		}
	}
	else
	{
		if(Allowed(l,r,dir[pos],r)) //移动左脚
		{
			int res=DP(pos+1,dir[pos],r,1)+Energy(l,dir[pos],pre,1);
			if(ans>res)
				ans=res,
				pri[pos][l][r][pre][0]=dir[pos],pri[pos][l][r][pre][1]=r,pri[pos][l][r][pre][2]=1;
		}
		if(Allowed(l,r,l,dir[pos])) //移动右脚
		{
			int res=DP(pos+1,l,dir[pos],2)+Energy(r,dir[pos],pre,2);
			if(ans>res)
				ans=res,
				pri[pos][l][r][pre][0]=l,pri[pos][l][r][pre][1]=dir[pos],pri[pos][l][r][pre][2]=2;
		}
	}
	return ans;
}
void Print(){
	int l=2,r=3,pre=0; //从初始状态开始
	for(int i=0;i<=n;i++){
		int nl=l,nr=r; //保存备份
		if(i>0){ //第0个位置没有答案,第1个位置存储的是第0个位置的答案
			if(pre==0)printf(".");
			else if(pre==1)printf("L");
				else printf("R");
		}
		l=pri[i][nl][nr][pre][0]; //移动到下一个位置
		r=pri[i][nl][nr][pre][1];
		pre=pri[i][nl][nr][pre][2];
	}
}
int main(){
	//freopen("in.txt","r",stdin);
	while(true)
	{
		memset(dp,-1,sizeof dp);
		memset(dir,0,sizeof dir);
		memset(pri,0,sizeof pri);
		fgets(str,MAXN+5,stdin);
		if(str[0]=='#') break;
		n=strlen(str)-1;
		for(int i=0;i<n;i++) //转换成int数组,上下左右顺序
			switch(str[i])
			{
				case 'U': dir[i]=0;break;
				case 'D': dir[i]=1;break;
				case 'L': dir[i]=2;break;
				case 'R': dir[i]=3;break;
				case '.': dir[i]=4;break;
			}
		//printf("%d\n",DP(0,2,3,0));
		DP(0,2,3,0);
		Print();
		printf("\n");
	}
	return 0;
}

  


The End

Thanks for reading!

- Lucky_Glass

(Tab:如果我有没讲清楚的地方可以直接在邮箱lucky_glass@foxmail.com email我,在周末我会尽量解答并完善博客~📃)

  

posted @ 2018-08-14 15:25  Lucky_Glass  阅读(268)  评论(0编辑  收藏  举报
TOP BOTTOM