晴天小猪历险记
题目描述
在很久很久以前,有一个动物村庄,那里是猪的乐园(^_^),村民们勤劳、勇敢、善良、团结…… 不过有一天,最小的小小猪生病了,而这种病是极其罕见的,因此大家都没有储存这种药物。所以晴天小猪自告奋勇,要去采取这种药草。于是,晴天小猪的传奇故事便由此展开…… 这一天,他来到了一座深山的山脚下,因为只有这座深山中的一位隐者才知道这种药草的所在。但是上山的路错综复杂,由于小小猪的病情,晴天小猪想找一条需时最少的路到达山顶,但现在它一头雾水,所以向你求助。 山用一个三角形表示,从山顶依次向下有1段、2段、3段等山路,每一段用一个数字T(1<=T<=100)表示,代表晴天小猪在这一段山路上需要爬的时间,每一次它都可以朝左、右、左上、右上四个方向走(**注意**:在任意一层的第一段也可以走到本层的最后一段或上一层的最后一段)。 晴天小猪从山的左下角出发,目的地为山顶,即隐者的小屋。
输入
第一行有一个数n(2<=n<=1000),表示山的高度。 从第二行至第n+1行,第i+1行有i个数,每个数表示晴天小猪在这一段山路上需要爬的时间。
输出
一个数,即晴天小猪所需要的最短时间。
样例输入
5 1 2 3 4 5 6 10 1 7 8 1 1 4 5 6
样例输出
10
提示
个人认为题目的描述是有一定问题的,在这里给出vijos的题目。
同一层内连续的两个位置相邻,特别的有每一层第一个位置与最后一个位置相邻。
对于位置(i,j),它与(i-1,j-1)以及(i-1,j)相邻,特别的(i,1)与(i-1,i-1)相邻,且(i,i)与(i-1,1)相邻。
解题思路
了解到了它各个点的关系,相信很多人都可以很轻松地打出dfs的代码,于是我也这样打了,但对于这道题,肯定不可能过,于是,我便用记忆化。
就有了下面的这一段代码
#include<cstdio>
#include<cstring>
#include<iostream>
#include<queue>
#include<cstdlib>
#include<cmath>
#include<vector>
#define N 1005
using namespace std;
int n,a[N][N],dp[N][N],mov[N][N];
int dfs(int x,int y){
if (mov[x][y] >= 2 || y > x || x < 0 || y < 0)
return 0x3f3f3f3f;
if (dp[x][y] < 1000000)
return dp[x][y];
int x1 = x - 1;
int y1 = (y == x ? 1 : y);
mov[x1][y1] ++;
dp[x][y] = min (dfs(x1, y1) + a[x][y] , dp[x][y]);
mov[x1][y1] --;
x1 = x - 1;
y1 = (y == 1 ? x1 : y - 1);
mov[x1][y1] ++;
dp[x][y] = min (dfs(x1, y1) + a[x][y] , dp[x][y]);
mov[x1][y1] --;
x1 = x ;
y1 = (y == 1 ? x1 : y - 1);
mov[x1][y1] ++;
dp[x][y] = min (dfs(x1 , y1) + a[x][y] , dp[x][y]);
mov[x1][y1] --;
x1 = x;
y1 = (y == x ? 1 : y + 1);
mov[x1][y1] ++;
dp[x][y] = min (dfs(x1 , y1) + a[x][y] , dp[x][y]);
mov[x1][y1] -- ;
return dp[x][y];
}
int main(){
memset(dp,0x3f,sizeof(dp));
scanf ("%d",&n);
for (int i = 1 ;i <= n;i ++ )
for (int j = 1;j <= i ;j ++)
scanf ("%d",&a[i][j]);
dp[1][1] = a[1][1];
mov[n][1] = 1;
printf("%d",dfs(n,1));
}
乍一看很长很复杂的样子,其实dp[i][j]就表示从i,j到1的最小花费。mov[i][j]统计遍历了i,j几次,因为同一层可以说是一个环,因此从i,j开始是可能回到这个地方的。于是我们就需要标记一下,回到原点我们就要返回。
看起来是没有什么问题的,这样做也并不会超时,但这样的做法是错误的,我当时也很疑惑,这种方法为什么会是错的。
直到我对拍弄出了一组数据,才找到我这个程序的一个隐秘问题。
首先我们就仅仅看第i行 如果有一组数据 (1 0 3 3 3)。从1开始搜索的话是会更新dp[i][2]的,我们再思考一下,如果从1走到5来这样到2的话,那肯定不是最优,但。我们会沿着这条路一直搜下去,知道(1,1)。于是,我们的dp[i][2]并不是最优的。
如果改变搜索顺序,先顺着来,也会有反例,也有可能不是最优。
于是,这种方法是完全错误的。那么我们便只能用DP来进行思考了。
首先,状态转移方程应该来说是比较简单的。
其中i和1进行特殊讨论,它们则可能是由5个状态转移而来。
但肯定不能够直接这样套。或者说做完一次就求出了最优,这里的dp[i][j]的四个状态我们还有可能并未对其进行赋值,也就是说dp[i][j + 1]此类的状态是空的。因此,如果我们只弄一次,很可能求不出最优情况,因此要多做几次,多次更新dp[i][j]的值,那么这意味着一行当中的dp需要多次更新,我们可以直接暴力,做他个10遍,20遍。相对来说要多跑一些时间,但是还能过。
其实我们再想一想,如果一行中,dp的值都没有发生改变,其实就说明了最优情况都已经达到了,再做已经没有意义。我们就可以退出了。
#include<cstdio>
#include<cstring>
#include<iostream>
#include<queue>
#include<cstdlib>
#include<cmath>
#include<vector>
#define N 1005
using namespace std;
int n,a[N][N],dp[N][N];
int main(){
memset(dp,0x3f,sizeof(dp));
scanf ("%d",&n);
for (int i = 1 ;i <= n;i ++ )
for (int j = 1;j <= i ;j ++)
scanf ("%d",&a[i][j]);
dp[n][1] = a[n][1];
for (int i = n ;i >= 1 ;i --){
for (int k = 1 ;k <= 10 ;k ++){
bool flag = 0;
for (int j = 1 ;j <= i ;j ++){
int t = dp[i][j];
if (j == 1){
dp[i][j] = min(dp[i][j] , dp[i + 1][j] + a[i][j]);
dp[i][j] = min(dp[i][j] , dp[i + 1][i + 1] + a[i][j]);
dp[i][j] = min(dp[i][j] , dp[i + 1][j + 1] + a[i][j]);
dp[i][j] = min(dp[i][j] , dp[i][i] + a[i][j]);
dp[i][j] = min(dp[i][j] , dp[i][j + 1] + a[i][j]);
}
else if (j == i){
dp[i][j] = min(dp[i][j] , dp[i + 1][j] + a[i][j]);
dp[i][j] = min(dp[i][j] , dp[i + 1][1] + a[i][j]);
dp[i][j] = min(dp[i][j] , dp[i + 1][j + 1] + a[i][j]);
dp[i][j] = min(dp[i][j] , dp[i][j - 1] + a[i][j]);
dp[i][j] = min(dp[i][j] , dp[i][1] + a[i][j]);
}
else{
dp[i][j] = min(dp[i][j] , dp[i + 1][j] + a[i][j]);
dp[i][j] = min(dp[i][j] , dp[i + 1][j + 1] + a[i][j]);
dp[i][j] = min(dp[i][j] , dp[i][j - 1] + a[i][j]);
dp[i][j] = min(dp[i][j] , dp[i][j + 1] + a[i][j]);
}
if (dp[i][j] != t)
flag = 1;
}
if (!flag)
break;
}
}
printf("%d",dp[1][1]);
}
相较于搜索代码,这份代码更为好理解。其实挺暴力的。
总结
现在这样的一个阶段,错误可以说是非常隐秘的,但又是非常致命的。这个时候就需要反例的,可以自己弄,但我觉得对拍相对更为高效。做题还是要多看看。查找代码最本质的错误即是关键。