这学期数据结构有这么一题,我感觉蛮有意思,就做了下。下面分享一下我的收获。
以下是我编程前做的工作:
1.弄清题目的意思
2.提出问题(做的过程中可能遇到的问题)
3.构思解决这些问题的大致方法(与编程语言(工具)联系起来),并考虑如何将这些方法在程序流程中联系起来
4.设计出程序流程图
5.优化解决方法、程序流程
6.开始编程
在编程过程中经常会冒出新的问题,或是想到更好的方法,到时候再改就可以了。不过如果编程前的构思不完备的话,不仅编程时思路不连贯,而且需要大幅度修改程序的概率大大增加,会浪费更多时间。
1.弄清题目意思
题目描述:国际象棋中,一只马在8*8格子的棋盘中走,不能重复走已经走过的格子,要求马把每个格子都走过。
(如果马处在绿色的位置,那么他可以往灰色位置上走)
2.提出问题
下面是我想到的问题:
(1)如何表示马的位置和棋盘上的格子?
(2)如何让马按照国际象棋的规则来走(隔一个格子走)?
(3)如何用C语言模拟马的行走路线(前进和退步)?
(4)如何判断下一步的位置是否出界、是否是已经踩过的位置?
(5)下一步往哪走?
(6)当马走到某个位置,发现没有可走的路了,该怎么办?
3.问题的大致解决方法
问题(1)的解决方法:
用坐标表示马和棋盘的各个位置。我的坐标系是这样建立的:
问题(2)如何让马按照国际象棋的规则来走?
对于第二个问题,教材上给了一种非常方便的方法,定义两个数组,我的程序里是
int xCoo[8] = {-2,-1,1,2,2,1,-1,-2};//xCoo 表示 x Coordinates,x坐标
int yCoo[8] = {1,2,2,1,-1,-2,-2,-1}; //y坐标
有了这两个数组,假如马当前的位置是(x,y),那它下一步可走的坐标就可以是( x+xCoo[i] , y+yCoo[i] ),i=0~7。
但是这样计算得到的坐标有可能超出棋盘的边界,或者,得到的坐标马可能已经走过,就像问题(4)所说的那样。
问题(3)如何用C语言模拟马的行走路线(前进和退步)?
马走每走一步,路线长度就加1,每退一步,路线长度减少1。而且退回的时候,路线上减少的不是马的第一步,而是退回之前路线中的最后一步,这和数据结构中的栈的规则是一致的,也就是“后进先出”的原则。所以可以用栈来模拟。那是用顺序栈呢,还是用链栈呢?顺序栈一次性分配内存,链栈可动态改变栈的大小。这里我感觉差别不大,不过我用了链栈。栈的节点暂时可以这样定义:
struct THREAD
{
int x;//当前节点的x坐标
int y;//当前节点的y坐标
struct THREAD *prev;//保存指向前一个节点的指针
}
问题(4)如何判断下一步的位置是否出界、是否是已经踩过的位置?
我打算用两个函数来解决这个问题,isOut 函数判断是否出界,isStepped 函数判断是否是走过的格子。这里又引出了一个问题:如何让马记忆已经走过的格子的坐标?我最初没有记忆,用的是遍历栈的方法判断。后来发现效率太低,便用了一个二维数组来记忆:char stepped[8][8],每个元素初始化为'n',表示没有走过。一旦马走过了某个格子,比如(x,y),则执行:stepped[ x-1 ][ y-1 ] = 'y'; 表示(x,y)已踩过。这样的话,调用isStepped函数时,想知道(x,y)是否被走过,只要
if( stepped[ x-1 ][ y-1 ] == 'y')
return 1;//表示被踩过了
else
return 0;//表示没被踩过
这样就可以了。事实上我用了一个动态二维数组,因为我做的棋盘大小是可变的
问题(5)下一步往哪走?
这个问题我最初设计的时候没有仔细考虑,用的是随机选取格子的方法。因为我当初没有想到让马选一条路有这么多种可能性,所以后来让他随机走的时候,走半个多小时都找不出一条路,于是我只好改程序,让马在选路的时候也要“思考”。
这里又有个隐含的问题:选格子的时候,选的是可以走的格子,是通过isOut、isStepped函数计算出来的,那么这些可走的格子的坐标保存在哪里呢?
由于马到了每个格子后,可走的格子都不一样,所以马每到一个位置,我就计算可走的格子的坐标,并保存到当前位置对应的节点(当前栈顶)中。可这是一组坐标,最多可能有8个坐标,全都保存到一个节点中吗?我没有这样做,因为这样显得节点中数据有点乱,我不喜欢。。。于是我就定义了一个结构体struct CANGO,结构中包含x、y坐标,保存的时候申请一块8*sizeof(CANGO)大小的堆内存,也就是一个结构体数组,然后在栈的节点中保存指向这个结构体数组首地址的指针。
struct CANGO
{
int x;
int y;
}
struct THREAD
{
int x;//当前节点的x坐标
int y;//当前节点的y坐标
struct CANGO *pCango;//保存当前节点可走的格子的坐标
struct THREAD *prev;//保存指向前一个节点的指针
}
......
struct THREAD *pNew;
pNew->pCango = (struct CANGO *)malloc( 8 * sizeof( struct CANGO ) );
......CANGO结构数组中有8个元素,如果当前节点可走的格子少于8个,比如可走的只有5个,那就把这5个格子的坐标保存到CANGO结构数组的其中5个元素中,其他3个元素的x、y成员都赋值为-1,来表示这些位置是不可走的(出界或者是已经走过)。
赋予马思考的能力
怎么降低马走错退回的概率呢?如果让马都往空旷的地方走,到后面更容易出现这种情况:没走过的格子被走过的格子包围而导致马无法跳到没走过的格子上,于是马又得退回重新找路,因此效率会很低!
所以要往这样的位置走:从这个位置上看,马可能走的位置数最少。也就是说,假如马的位置是A,下一步可走的位置有B1、B2、B3、B4,从B1出发,可走的位置有5个,从B2出发可走的位置有3个,从B3出发可走的位置有4个,从B4出发,可走的位置只有1个,那么下一步应该选择B4。
那如果还有一个B5,可走的位置也只有1个,那应该选哪个呢?我的做法是选择B4、B5中更靠近边界的那一个。更靠近边界是什么意思?比如B4的坐标是(8,1),B5的坐标是(2,5),那就选B4。因为B4离最近边界的距离是1,而B5离最近边界的距离是2。
如果从A出发还有一个可走的坐标B6:(8,5),这个坐标可走的位置也只有1个,并且到最近边界的距离也是1,那该选哪个呢?我让马选更靠近角落的那个,也就是B4(8,1)。
要是出现同样少的可走位置、同样靠近边界、同样靠近角落,那就让它随机选一个走吧。。。
以上这些想法也是受网上所说的“贪心算法”启发,这样程序的效率大大提高。
问题(6)当马走到某个位置,发现没有可走的路了,该怎么办?
很简单,退回。不过退回以后的工作就不是这么简单了,我第一次做的时候就犯了很大的错误,退回以后,马又重新走已经错过的路了,结果导致无限循环。。。。后来我改了,我跟马说,你从哪里跳回来,就不要再跳回去了。他的确这么做了,可是还是有问题,你觉得这是为什么?我来解释一下,如果马当前位置是A,他去了B,发现没路走了,退回到A,他下一步不会再去B了,然后从A跳到C,发现在C也没路可走,于是 又退回到A,照我所说的,下一步他不会去C了,然后他又 看见了B,于是又去B了,还是走了重复的路。(注意我这次的做法是:从哪里跳回来,就不要再跳回去,所以会出现这种现象)。
那应该怎么办呢?难道从A到B不可行,那应该记住永远不能从A到B吗?我认为有这种可能:马从A退回,退啊退,绕了别的路,又到了A,这时候从A到B,发现有路可以走了。
看下图中的例子,假设马走啊走走到D,沿橙色的路线D->G->H->A->B走,走到B发现没路了退回到A,到A没路又退回,一直退又退到回了D,选择了另外那条绿色的路线D->E->F->A->B,这时候从B就可以到G了。
这看上去好像有个问题,因为假如可以从绿色的路线的B到G,那么马之前沿橙色路线退回的时候,退到G就有可能的路线G->B了,因此不可能再从G退到D然后又选择绿色的路线。
但事实上还有一种情况:马确实从B退到A又退到H又退到G,然后在G发现可以去B,到了B发现只能去A,但是到了A却发现没路可走,又退回到B,然后又退到了G,如果到了G他发现没路可走,就会从G退回到D了!然后他走了绿色的路,D->E->F->A->B,到了B发现可以去G耶!不过这时候,到了G,它肯定没有路可以走了。为什么?因为能沿绿色的路走的前提是他退到了D,要退到D,那必须是在G发现没有路走了。
所以,并不是一旦发现从A到B路没有路,以后从A到B就一定没有路。所以我觉得,不能因为从A到B不行,就把这步路永久地记下来,以后都不能走。因为即使从B到了G没有路了,G也有可能是最后一步呀,这样的话,如果规定不能从A到B,马很有可能会做出误判,告诉你所有路都走遍了,没有一条可行的路,而事实上可能是有的。
前面我说过,如果只告诉马“从哪里跳回来,就不要跳回去”,会导致死循环(A->B->A->C->A->B......),又不能永久规定不能从A到B,那该怎么办?我的办法是暂时地保存A不能去的位置。比如A到B不行,那就在A的节点中保存B的位置,以后不往B走;A到C不行,在A中保存C的位置,以后不往C走。那不是成永久保存了吗?不是的。当马在A发现没路可走的时候退回,A节点被free,保存的B和C的位置也会被释放。如果下一次又走到A,重新构建A节点,这时A里没有保存任何不能走的坐标位置。
这东西太复杂了。。。不知道我分析得对不对,如果感觉我讲错了,一定要跟我说呀!!
保存的不可走的位置的方法和保存可走位置的方法一样,在节点中保存指向struct CANGO 结构体数组的指针,但是为了好区分,我做了一个宏定义:
#define CANTGO CANGO
对struct THREAD结构的修改如下:
struct THREAD
{
int x;//当前节点的x坐标
int y;//当前节点的y坐标
struct CANGO *pCango;//保存当前节点可走的格子的坐标
struct CANTGO *pCantgo//保存当前节点不可走的格子的坐标
struct THREAD *prev;//保存指向前一个节点的指针
}
4.设计程序流程
我直接上图了。。。
具体实现的话以后再整理给大家!