Description
有一个m×m的棋盘,棋盘上每一个格子可能是红色、黄色或没有任何颜色的。你现在要从棋盘的最左上角走到棋盘的最右下角。
任何一个时刻,你所站在的位置必须是有颜色的(不能是无色的), 你只能向上、 下、左、 右四个方向前进。当你从一个格子走向另一个格子时,如果两个格子的颜色相同,那你不需要花费金币;如果不同,则你需要花费1个金币。
另外, 你可以花费2个金币施展魔法让下一个无色格子暂时变为你指定的颜色。但这个魔法不能连续使用, 而且这个魔法的持续时间很短,也就是说,如果你使用了这个魔法,走到了这个暂时有颜色的格子上,你就不能继续使用魔法; 只有当你离开这个位置,走到一个本来就有颜色的格子上的时候,你才能继续使用这个魔法,而当你离开了这个位置(施展魔法使得变为有颜色的格子)时,这个格子恢复为无色。
现在你要从棋盘的最左上角,走到棋盘的最右下角,求花费的最少金币是多少?
Input
第一行包含两个正整数m,n以一个空格分开,分别代表棋盘的大小,棋盘上有颜色的格子的数量。
接下来的n行,每行三个正整数x, y, c分别表示坐标为(x,y)的格子有颜色c。
其中c=1代表黄色,c=0代表红色。 相邻两个数之间用一个空格隔开。 棋盘左上角的坐标为(1,1),右下角的坐标为(m,m)。
棋盘上其余的格子都是无色。保证棋盘的左上角,也就是(1,1) 一定是有颜色的。
Output
一个整数,表示花费的金币的最小值,如果无法到达,输出−1。
Sample Input 1
5 7
1 1 0
1 2 0
2 2 1
3 3 1
3 4 0
4 4 1
5 5 0
Sample Output 1
8
Sample Input 2
5 5
1 1 0
1 2 0
2 2 1
3 3 1
5 5 0
Sample Output 2
-1
Hint
对于 30%的数据, 1 ≤ m ≤ 5, 1 ≤ n ≤ 10。
对于 60%的数据, 1 ≤ m ≤ 20, 1 ≤ n ≤ 200。
对于 100%的数据, 1 ≤ m ≤ 100, 1 ≤ n ≤ 1,000。
Time Limit
1000MS
Memory Limit
256MB
写在前面:
这道题是我写深搜以来遇到第一道不用回溯的深搜,打破了我对深搜的认识:曾经我以为深搜就是要回溯,其实不然,深搜和回溯没有必然关系,深搜是深搜,回溯是回溯,深搜没了回溯还是深搜。
这道题不用回溯的原因在于剪枝方式把标记回溯的活给干了,我对于深搜又进一步思考,得出深搜和回溯的关系:当要寻找所有路径或者寻找最佳路径时,需要回溯,其实找最佳路径本质上是和找所有路径差不多,因为需要和别的路径比较才能知道最佳路径,此时深搜是试错式的,不撞南墙不回头,而且会遍历一棵树的所有结点,如果想优化树的结构使不用遍历这么多结点,可以根据题目特征剪枝,对于一些剪枝,会把标记回溯的活给干了,所以就不用标记回溯了;对于另一类深搜,是不能回溯的,但是需要标记,以免重复访问,因为这类深搜解决的问题不是走所有的路径,也不是找最佳路径,而是在走路的过程中做某些操作,且这些操作是一次性不可重复的,比如说在走遍一张网格时给特定网格计数并将计数结果标记在网格上,如果给我起名,我更想称呼此时的操作为深度周游,我现在还没做到有关的题目,日后做到了,也会写博客总结出来。
对于这道题,标记回溯不会对结果产生影响,但是是建立在标记写对位置的前提下,这里让我明白,深搜如果用回溯,不该标记的地方标记了真的就错了,运气好依然算对,运气不好就错完了。
这道题也让我明白一个道理:如果发现更优的逻辑,就用更优的逻辑写代码;因为逻辑还能再优化的代码可能是有巨大风险的,如果出错就会很难找到bug,很难解决掉;另外,解决bug的方法不止找到bug并改成正确的写法,还有优化逻辑,不必要为了不够优的逻辑去艰苦debug,往往得不偿失。
我当时不想用更高效的记忆化搜索剪枝,只想每次通过sum目前花费金币数和ans当前最优到达终点花费金币数比较达到剪枝目的,结果程序跳不出递归,编译器报段错误,oj平台报Runtime Error。我为此de了很久bug,最后放弃了,反正这又不是最优的逻辑,何必呢?至于这样写为什么不是最优的逻辑,因为这样只能更新到达终点的最优值,其他格子的最优值无法更新,更优的逻辑是使用数组记录到达棋盘每个格子的最优值,随时更新,这样比起原来写法可以剪更多枝。当数据仅为一维的时候,用原来写法剪枝还是够快,可是当数据变成二维,就慢多了。
分析:
读题知数据不保证走得到终点,所以这题是个存在性问题,因此使用深搜。不难分析:深搜的状态空间应包含格子位置、使用魔法状态、花费金币数量,其他都好办,使用魔法状态怎么解决呢?——可以用bool类型,因为就只有使用过和没使用过两种状态;或者也可以自定义状态码。
搜索本来就是暴力解法,自然要主动去想是否可以剪枝优化。这题如何剪枝呢?想象走棋盘的过程,如果我们走到一个之前走过的格子,由深搜特点显然路径是不重合的,如果我们在这条路径下走到这个格子花的金币比之前搜索的路径花的金币还要多,这条路径就必定不是最优解,因为继续走下去会比之前那条路径花更多金币;另外,如果这条路径花的金币和之前搜索的一样多,那也没必要搜索下去,因为继续走下去花的金币不会比之前那条更个少。
要实现这个剪枝,就要用数组及时更新到达棋盘上任意格子花费的最少金币,这又被称为记忆化搜索,因为记忆了搜索过程的数据。接下来思考:如此剪枝之后还需不需要再使用标记数组vis标记走过的格子,以防走回头?答案是不需要!因为这就包含在剪枝情况里,这就是这道题不用回溯的来头。
如果还是想写回溯法,也是可以的,就是千万别写错地方:要分清什么时候需要标记,什么时候不需要标记,最后再循环结束位置回溯。什么时候需要标记?在下标没越界且当前格子没访问过的前提下,有且仅有两种情况需要标记:枚举到的格子是空格并且上次没有使用魔法;枚举到的格子不是空格。除了这两种情况,都不可标记,否则会影响答案。标记的代码在写的时候不容易考虑周全,出错了也难找,我当时甚至以为剪枝就是不能和回溯一起写,还查阅了很久资料。标记代码具体写在哪,且看下方参考代码。
参考代码:
#include<stdio.h>
#include<algorithm>
//本代码可以把所有vis删去,不影响运行结果
//本代码可以把所有vis删去,不影响运行结果
//本代码可以把所有vis删去,不影响运行结果
int m,n;//棋盘大小,有色格子
int map[101][101];//棋盘数据
int step[4][2]={{1,0},{0,1},{-1,0},{0,-1}};//移动方向
int ans=0xFFFFFFF;//答案,花费最少金币数
int d[101][101];//到达某个格子花费的最少金币数
int vis[101][101];//标记走过的格子
//横、纵坐标,是否使用过魔法,花费金币总数
void dfs(int x,int y,bool magic,int sum)
{//如果这次走到(x,y)花的金币不比之前搜索的少,必定不是最优解
if(sum>=d[x][y]) return;
//否则更新最优解
d[x][y]=sum;
//判断是否到终点,是则更新答案
if(x==m && y==m){
ans=std::min(sum,ans);
return;
}
//否则开始枚举往四个方向走
int X,Y;
for(int i=0;i<4;i++)
{//计算下个格子坐标
X=x+step[i][0];
Y=y+step[i][1];
//判断下标越界,判断重复访问
if(X<1||X>m||Y<1||Y>m||vis[X][Y]) continue;
//如果是空格
if(map[X][Y]==2){
//如果上次使用过魔法,此路不通
if(magic) continue;
else {//上次没用魔法,可以用
vis[X][Y]=true;//标记
//因为要花最少金币,所以指定颜色和过来的格子一样
map[X][Y]=map[x][y];
dfs(X,Y,true,sum+2);//走到下一个格子上
map[X][Y]=2;//恢复空白
}
}
//如果不是空格
else if(map[X][Y]!=2){
vis[X][Y]=true;//标记
if(map[x][y]==map[X][Y]){//颜色相同
dfs(X,Y,false,sum);
}
else dfs(X,Y,false,sum+1);//颜色不同
}
//回溯
vis[X][Y]=false;
}
return;
}
int main()
{//初始化
for(int i=0;i<101;i++)
{
for(int j=0;j<101;j++)
{
map[i][j]=2;//全是空格
d[i][j]=0xFFFFFFF;//全是最大值
vis[i][j]=false;//全是未访问
}
}
scanf("%d%d",&m,&n);
int x,y;
while(n)
{//给指定格子涂色
scanf("%d%d",&x,&y);
scanf("%d",&map[x][y]);
--n;
}
vis[1][1]=true;//先行标记起点
dfs(1,1,false,0);//传初态
if(ans!=0xFFFFFFF) printf("%d",ans);//如果有解
else printf("-1");//如果无解
return 0;
}