算法题-蛇和梯子
实验十二 蛇和梯子
[实验目的]
- 掌握深度优先和广度优先搜索算法的基本思想和设计方法;
- 理解贪心算法的局限性;
- 提高分析与解决问题的能力。
[预习要求]
1. 认真阅读教材或参考书, 掌握贪心算法、深度优先和广度优先搜索算法的基本思想;
2. 写出求解“蛇和梯子”的程序;
3. 设计好测试用例。
[实验题]
“蛇和梯子”是一个在N×N(0<N<=20)的方格棋盘上进行的游戏。(见下图)
| |
| -- |
| | |
方格从1到N的平方编号。除了第1号和最后编号的方格,其它的格子都有可能有蛇或梯子存在(蛇和梯子的数量及具体位置由输入确定,它们的数量都在100之内并且蛇和梯子不能临近放置,也就是在任何了放置两者首尾的方格之间至少还有一个未放置任何东西的格子)。开始的时候玩家把他们的标志物放在1号格子中。玩家轮流以扔骰子的方式移动他们的指示物。如果一个指示物到达了一条蛇的嘴部,则把它移回蛇的尾部。如果一个指示物到达了一个梯子的底部则将它移动到梯子的顶部。如果你是一个可以自由控制骰子的高手,现在请求出你至少需要扔几次骰子才能到达标为N^2的格子。(比如在上图所示例一中,你的方案应该是走4步到达5并由梯子上升到16,再走4步到达20并由梯子上升到33,然后走3步。这样,你一共需要扔3次骰子。而在例二中,你的方案应该是连扔4个6。)
[实验提示]
从上面的实验题中很容易看出,这个问题是不能用贪心算法解决的,因为你不能保证在这步到达一个数码比较大的格子就意味着最好的效率(即使是通过梯子到达了一个这步所能到达的最大号码的格子),也就是说,号码的大小并不能代表从这个格子到达终点所需要的步数上的多少,这就带给我们一个启发:蛇和梯子真的需要看成是两个东西来分别处理么?实际上确实是不需要的,蛇和梯子的本质就是我们经常在游戏中说的“单向传送点”,只不过梯子的底部是入口而顶部是出口,蛇的嘴部是入口而尾部是出口罢了,对于他们的描述完全可以选择相同的结构:
struct SnakeAndLadder{
int from,to;
};
接下来要考虑的是解决问题的方法。贪心算法被否定之后,我们的选择可能会是搜索,对于本题所采用的搜索显然应该以广度优先的方式进行,但是稍加分析我们就会发现如果单纯地采用广度优先搜索会产生许多重复的结点,现在我们将指示物处于某格的结点简称为结点X,那么比如在例1中,第1步过后,队列中存放的结点是2,23,4,16,6,7,在第二步时,当结点2成为扩展结点时将生成结点23、4、16、6、7、8,其中只有8不存在于当前活结点队列中,即使加以判断,不把重复的结点再次加入队列中,那至少也需要对活结点队列进行搜索。实际上我们完全有更好的方法。
应该意识到,采用树状结构和搜索的方法处理问题其重要的一点是利用祖先结点的差异性来对儿子结点做不同的处理。然而在本实验中,儿子结点的生成只依赖于父结点的信息而与其它祖先结点无关,所以采用树来描述这个过程其实是多余的。在走了若干步之后,对于一个特定的格子实际上只有两种状态的区分:
1、在走了这些步数之后存在一种方案使指示物位于此格中;
2、不存在这样的一种方案。
所以我们可以用一个N×N大小的数组来描述若干步之后可以到达的格子的集合,其中每一个元素描述一个格子的状态,0表示不存在一种方案到达,1表示存在至少一种方案到达。这样,我们从表示第n步状态的数组,完全可以推出表示第n+1步状态的数组,而且在第n+1步状态的数组得到之后,表示第n步状态的数组也就不再存在利用价值了。一旦数组中表示最后一个格子的元素成为1,就表示可以通过这个步数完成任务了。
比如在例1中,描述棋盘状态的数组其变化过程应该为:
描述状态 | 数组元素的内容(从表示第1个格的元素排列到表示最后一个格的元素) |
---|---|
起始状态 | 100000000000000000000000000000000000 |
第1步之后 | 010101100000000100000010000000000000 |
第2步之后 | 000101111111100111101111111110001000 |
第3步之后 | 000001111111111111101111111111111101 |
到第3步之后,数组的最后一个元素已经变为了1,这即表明存在一种方案,使得我们扔3次骰子就可以完成任务。以下是实现此算法的主要部分代码,数组下标0——size*size-1的元素分别表示了从第1格到最后一格的状态,step记录步数,obstacle是struct SnakeAndLadder的向量,描述了蛇和梯子的传送入口和出口:
//初始化状态数组和步数记录
for (j=1;j<size*size;j++)
grid[j]=0;
grid[0]=1;
step=0;
while (grid[size*size-1]==0)
{ ** for** (j=0;j<size*size;j++)
gridbak[j]=grid[j];
for (j=0;j<size*size;j++)
grid[j]=0;
**for** (j=0;j<size*size-1;j++)
//搜索所有的格子(最后一格不用搜索因为在过程结束前它一定为0)
{
**if ** (gridbak[j]==0)//若在上一步无法到达此格则跳出
**continue** ;
** for** (k=1;k<=6;k++)
{
test=0;
** if** (j+k>size*size-1)
** break** ;
** for** (l=0;l<obstacle.size();l++)
//如果此格是一个传送入口,将传送出口的格子可达
{
**if** (obstacle[l].from==j+k)
{
grid[obstacle[l].to]=1;
test=1;
**break** ;
}
}
**if** (test==0 && grid[j+k]==0)
grid[j+k]=1;
}
}
step++;//步数加一
}
过程执行完毕后,输出step即可。
另外,此题规定棋盘为N*N大小只是为了输入方便,举例画图方便,实际上多大的棋盘都是一样可以看作一条直线,数组用一维的便可以了。
[实验步骤]
1.先用广度优先的方式求解该问题,并测试你的程序,直至正确为止;
2.针对问题实例,实录运行时的输入、输出界面;
3.将你的程序和实录的界面存盘备用。
[实验报告要求]
1阐述实验目的和实验内容;
2.提交模块化的实验程序源代码;
3.简述程序的测试过程,提交实录的输入、输出文件;
4鼓励对实验内容展开讨论,鼓励提交思考与练习题的代码和测试结果。
[思考与练习]
试针对该问题,用深度优先的方式求解,并分析求解时存在的问题。