【noip 2009】 乌龟棋 记忆化搜索&动规
题目背景
小明过生日的时候,爸爸送给他一副乌龟棋当作礼物。
题目描述
乌龟棋的棋盘是一行N个格子,每个格子上一个分数(非负整数)。棋盘第1格是唯一的起点,第N格是终点,游戏要求玩家控制一个乌龟棋子从起点出发走到终点。
乌龟棋中M张爬行卡片,分成4种不同的类型(M张卡片中不一定包含所有4种类型的卡片,见样例),每种类型的卡片上分别标有1、2、3、4四个数字之一,表示使用这种卡片后,乌龟棋子将向前爬行相应的格子数。游戏中,玩家每次需要从所有的爬行卡片中选择一张之前没有使用过的爬行卡片,控制乌龟棋子前进相应的格子数,每张卡片只能使用一次。
游戏中,乌龟棋子自动获得起点格子的分数,并且在后续的爬行中每到达一个格子,就得到该格子相应的分数。玩家最终游戏得分就是乌龟棋子从起点到终点过程中到过的所有格子的分数总和。
很明显,用不同的爬行卡片使用顺序会使得最终游戏的得分不同,小明想要找到一种卡片使用顺序使得最终游戏得分最多。
现在,告诉你棋盘上每个格子的分数和所有的爬行卡片,你能告诉小明,他最多能得到多少分吗?
输入输出格式
输入格式:
输入文件的每行中两个数之间用一个空格隔开。
第1行2个正整数N和M,分别表示棋盘格子数和爬行卡片数。
第2行N个非负整数,a1a2……aN,其中ai表示棋盘第i个格子上的分数。
第3行M个整数,b1b2……bM,表示M张爬行卡片上的数字。
输入数据保证到达终点时刚好用光M张爬行卡片。
输出格式:
输出只有1行,1个整数,表示小明最多能得到的分数。
输入输出样例
输入样例#1: 输出样例#1:
9 5 73 6 10 14 2 8 8 18 5 17 1 3 1 2 1说明
每个测试点1s
小明使用爬行卡片顺序为1,1,3,1,2,得到的分数为6+10+14+8+18+17=73。注意,由于起点是1,所以自动获得第1格的分数6。
对于30%的数据有1≤N≤30,1≤M≤12。
对于50%的数据有1≤N≤120,1≤M≤50,且4种爬行卡片,每种卡片的张数不会超过20。
对于100%的数据有1≤N≤350,1≤M≤120,且4种爬行卡片,每种卡片的张数不会超过40;0≤ai≤100,1≤i≤N;1≤bi≤4,1≤i≤M。
虽然说,暴力出奇迹,但是,暴力终归不是最优解,那么就要用到一些形形色色奇奇怪怪的带有技巧的搜索,就比如记忆化搜索。
基本搜索方式
就是一个dfs,非常简单,此处略。
记忆化搜索
记忆化搜索,官方解释为:算法上依然是搜索的流程,但是搜索到的一些解用动态规划的那种思想和模式作一些保存。
说白了就是把以前搜过的记住,以后就不用搜了,直接调用数组中保存的答案,下面代码中用dp数组进行记录选 i 张1,j 张2,k 张3,l 张4 能得到的最优解。
1 #include <iostream> 2 #include <cstdio> 3 #include <cstdlib> 4 #include <algorithm> 5 #include <cmath> 6 using namespace std; 7 int scr[360],crd[5],m,n,dp[41][41][41][41]; 8 int dfs(int i,int j,int k,int l,int now) 9 { 10 if(now==n) return 0; //到达边界返回0 11 12 if(i) //如果第一张卡片有剩余,下同 13 { 14 if(dp[i-1][j][k][l]) //如果以前搜过,直接调用,下同 15 dp[i][j][k][l]=max(dp[i][j][k][l],dp[i-1][j][k][l]+scr[now+1]); 16 else //如果没搜过,那就dfs搜 17 dp[i][j][k][l]=max(dp[i][j][k][l],dfs(i-1,j,k,l,now+1)+scr[now+1]); 18 } 19 20 if(j) 21 { 22 if(dp[i][j-1][k][l]) 23 dp[i][j][k][l]=max(dp[i][j][k][l],dp[i][j-1][k][l]+scr[now+2]); 24 else 25 dp[i][j][k][l]=max(dp[i][j][k][l],dfs(i,j-1,k,l,now+2)+scr[now+2]); 26 } 27 28 if(k) 29 { 30 if(dp[i][j][k-1][l]) 31 dp[i][j][k][l]=max(dp[i][j][k][l],dp[i][j][k-1][l]+scr[now+3]); 32 else 33 dp[i][j][k][l]=max(dp[i][j][k][l],dfs(i,j,k-1,l,now+3)+scr[now+3]); 34 } 35 36 if(l) 37 { 38 if(dp[i][j][k][l-1]) 39 dp[i][j][k][l]=max(dp[i][j][k][l],dp[i][j][k][l-1]+scr[now+4]); 40 else 41 dp[i][j][k][l]=max(dp[i][j][k][l],dfs(i,j,k,l-1,now+4)+scr[now+4]); 42 } 43 return dp[i][j][k][l]; //将dp中存的结果带回 44 } 45 int main() 46 { 47 cin>>n>>m; 48 for(int i=1;i<=n;i++) 49 cin>>scr[i]; 50 for(int i=1;i<=m;i++) 51 { 52 int x; 53 cin>>x; 54 crd[x]++; 55 } 56 57 cout<<dfs(crd[1],crd[2],crd[3],crd[4],1)+scr[1]; //注意要加上第一个格子的得分 58 return 0; 59 }
用记忆化搜索的代码在洛谷上评测 十个点的总时间为 239ms
动态规划
动态规划 即 dp,官方解释为:动态规划(dynamic programming)是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法。
说白了就是分阶段的解决问题,对于相同的题目来说,耗时极短,是比赛中的重要解题方法之一,但是动态转移方程的推出也是令许多选手头疼的一大难题,如果写好了记忆化搜索的话,推出动态转移方程也就很简单了。
1 #include <iostream> 2 #include <cstdio> 3 using namespace std; 4 int now,scr[360],crd[5],maxn,m,n,dp[41][41][41][41]; 5 int main() 6 { 7 cin>>n>>m; 8 for(int i=1;i<=n;i++) 9 scanf("%d",&scr[i]); 10 for(int i=1;i<=m;i++) 11 { 12 int x; 13 scanf("%d",&x); 14 crd[x]++; 15 } 16 for(int i=0;i<=crd[1];i++) 17 for(int j=0;j<=crd[2];j++) 18 for(int k=0;k<=crd[3];k++) 19 for(int l=0;l<=crd[4];l++) 20 { 21 if(i) dp[i][j][k][l]=max(dp[i][j][k][l],dp[i-1][j][k][l]); 22 if(j) dp[i][j][k][l]=max(dp[i][j][k][l],dp[i][j-1][k][l]); 23 if(k) dp[i][j][k][l]=max(dp[i][j][k][l],dp[i][j][k-1][l]); 24 if(l) dp[i][j][k][l]=max(dp[i][j][k][l],dp[i][j][k][l-1]); 25 //以上四行是由上阶段开始枚举每一张牌来选出目标阶段的最优解 26 dp[i][j][k][l]+=scr[i+j*2+k*3+l*4+1]; //加上当前格子的得分 27 } 28 cout<<dp[crd[1]][crd[2]][crd[3]][crd[4]]; 29 return 0; 30 }
用动规写的代码在洛谷上评测 十个点的总时间为203ms。
可见动规和记忆化搜索的运行时间基本相同,所以如果能推出动态转移方程并且非常自信的话,当然是用动规好一些,但是如果没有推出动态转移方程 或者你并不确定的话,还是用记忆化搜索好一些。