c语言实现农夫过河问题,傻瓜式讲解,看不明白来打我

农夫过河问题本站可以搜索到很多博客,笔者是初学c语言,功底不深,没能力创造源代码,只是看了一篇大佬的文章对该问题进行描述之后,产生了一点自己的想法,我想以我能理解的方式分享给许多初学c语言朋友们,笔者自认为智商不高,但是我既然都能把这个算法看懂,相信各位肯定没问题。这位大佬巧妙使用二进制位的方法来解决该问题,非常之秒,废话多说下面开始讲解:

过河问题的题目场景我就简单描述一下了:农夫F,狼W,养S,白菜C,这4样东西要过河,也可以理解成农夫带着其他三样东西过河,但是每次农夫只能携带一样物体,并且有些物体在农夫不在场的情况下不能和谐共处。这里笔者把问题再次简单化:让农夫站在河上岸的最左边,接下来依次是狼W,羊S,白菜C。这样可以用4个二进制位代表他们的位置,最初状态把每个二进制位分别赋上0,表示这4样东西都在河上岸,只要把任意一样物体所对应的二进制位都置为1,说明该物体到达了河的下岸。所以我们就可以找到该问题的实质就是让你把一个4位的二进制数“0000”按照一定的步骤和规则,变化到另外一个4位的二进制数“1111”。

接下来我把这个4位二进制数的所有表示状态写下来:

0000(十进制为0):表示农夫,狼,养,白菜在上岸,
0001(十进制为1):表示农夫,狼,羊在上岸,白菜在下岸
0010(十进制为2):表示农夫,狼在上岸,羊在下岸,白菜在上岸
0011(十进制为3),-----------------------------------------------------------
0100(十进制为4),-----------------------------------------------------------
0101(十进制为5),-----------------------------------------------------------
0110(十进制为6),-----------------------------------------------------------
0111(十进制为7),-----------------------------------------------------------
1000(十进制为8),-----------------------------------------------------------
1001(十进制为9),-----------------------------------------------------------
1010(十进制为10),-----------------------------------------------------------
1011(十进制为11),-----------------------------------------------------------
1100(十进制为12),-----------------------------------------------------------
1101(十进制为13),-----------------------------------------------------------
1110(十进制为14),-----------------------------------------------------------
1111(十进制为15),表示四样物体都在下岸

当然,整个程序的实现过程不可能是按照顺序从0000,0001,0010,0011,---到1111的,因为这16个状态当中必定会有不符合条件的危险状态(比如1100,就是农夫和狼在下岸,羊和白菜在上岸,这样白菜就被羊吃了),而且从前一个状态变化到下一个状态也必须遵循一定的逻辑,比如不可能从0000直接变化到0001,解释如下:农夫,狼,羊不可能只呆在上岸不动,而白菜自己单独从上岸走到下岸,这个合理的可能变化应该是从0000变化到1001,代表农夫带着白菜一起从上岸走到了下岸,但是这里会出现危险状态1001表示农夫和白菜到了下岸,而把狼和羊单独留在了上岸,这样狼就把羊给吃了,所以复合规则的可能变化应该是0000到1010,代表农夫把羊带去了下岸,把狼和白菜留在了上岸,然后1010变化到0010,代表农夫从下岸又回到了上岸来进行下一个物体的运输过程------,至于下一个物体的运输过程,我这里就不再描述了,毕竟这个事情可以留给电脑去遍历。

现在我们遇到一个问题就是如果把这个4位的二进制数的任意一个状态给到电脑,比如1010,电脑只能读取到1010,也可以认为就是读取到了一个十进制数10,我们如何通过10这个十进制数来告知电脑这4样物体分别是在河的哪一边呢?我们可以通过定义一个枚举变量
enum fwsc 
{
    farmer= 8, wolf = 4, sheep = 2, cabbage = 1
}; 大家看farmer的这个十进制值8,写成四位二进制的形式就是1000, wolf的十进制值4写成四位二进制形式就是0100,sheep的十进制值2写成四位二进制形式就是0010,cabbage的十进制值1写成四位二进制形式就是0001。  现在我们把上面提到的16种状态中的任意一种比如1010这个状态,我们把这个状态同枚举变量里farmer给定的十进制值8(1000)进行按位与操作:1010&1000,这个按位与操作只能得到两个结果:十进制数0或者是8. 大家可以注意到不管是给到本文一开始说明的16种状态中的任意一种,进行同farmer的按位与操作,我们能得到,并且只能得到两个十进制数0(0000)或者8(1000),到底是0还是8只取决于1010的农夫这个二进制位,如果是1,则得到8,如果是0,则得到0,所以,我们让电脑进行这个按位与操作就能判定农夫到底是在上岸还是下岸。


同样的道理,我们拿16种状态数中的任何一种同枚举量wolf的值同样进行按位与操作,我们可以得到相应的结论,也就是根据状态数的狼的二进制位,我们可以得到0或者是4,这两个结果,分别可以探测出在状态数中的狼的这个二进制位到底是1还是0.

通过上面定义的枚举变量的四个值farmer= 8, wolf = 4, sheep = 2, cabbage = 1分别同任意一个状态数进行按位与操作,我们可以分别探测出在任意状态数中任意物体的位置到底是在上岸还是下岸。 通过以上的操作,我们把任意一个16种状态数种的一种给到电脑,电脑可以获取到的信息量就很多了,电脑可以分别探测出任意物体在该状态下的位置。我们把这个探测过程封装到一个名为
getLocation的函数里面
int getLocation(int currentLocation, int fwsc) //fwsc为上面定义的枚举变量的4种值中的任意一种,currentLocation代表16种状态数中的任意一种 
{
    //若返回值等于0,则表示该物体在河的上岸;若返回值等于1,则表示该物体在河岸下岸.
    switch (fwsc)
    {
    case cabbage:
        if ((currentLocation & cabbage) == 0) //2进制与运算,判断物体是哪一岸 
            return 0;
        else
            return 1;
        break;
    case sheep:
        if ((currentLocation & sheep) == 0)
            return 0;
        else
            return 1;
        break;
    case wolf:
        if ((currentLocation & wolf) == 0)
            return 0;
        else
            return 1;
        break;
    case farmer:
        if ((currentLocation & farmer) == 0)
            return 0;
        else
            return 1;
        break;
    default:
        break;
    }
    return -1;
}

通过getLocation函数,我们在16种状态数中拿到任意一个就可以得到其中记录着的任意物体的位置,我们有了任意物体的位置信息之后,就可以判断该状态中各种物体是否存在上面提到的不安全状态,这样我们通过编写一个isSafe来实现该判断过程

int isSafe(int currentLocation) //返回0,表示不安全;返回1,表示安全。
{
    int f, w, s, c;

    f = getLocation(currentLocation, farmer);//通过位置判断函数单独获取农夫的位置 
    w = getLocation(currentLocation, wolf);
    s = getLocation(currentLocation, sheep);
    c = getLocation(currentLocation, cabbage);
    if (f != w && w == s) //若农夫不和狼在一侧,而狼却和羊在一侧
        return 0;
    else if (f != s && s == c) //若农夫不和羊在一侧,而羊却和白菜在一侧
        return 0;
    return 1;
}
我们有了以上的安全状态判断和位置判断函数之后就可以遍历的方式来实现农夫的运输过程了,这个是最核心也是最难理解的一个函数,笔者在读大佬的文章也是领悟了许久才懂的,我把函数当中比较难理解的语句单独说明:

首先函数的返回值大佬定义为int型的,其实有没有返回值都无所谓,只要实现了移动过程就行。函数的形参有两个,一个是一个有16个int类型元素的数组,一个是一个int类型的变量。
这个int类型的数组我们在main函数里给它初始化,第一个元素的值我们初始化为-2,其他元素的值我们初始化为-1. 这里这个数组就是用来记录本文一开始提到的16种状态,要实现过河的过程无非就是从状态0000经过一系列的步骤变化到最后的状态1111,我们知道最开始的状态是0000,如果不考虑过河的游戏规则,从状态0000变化到下一个状态可以说有15种可能,然后再次变化又有15种可能,两次变化就有15乘以15种排列可能,这些变化当中有可能又变回以前的状态了,比如从0000-1010-0000,那么这样的变化就是无效变化了,我们必须剔除掉,不然程序的循环遍历过程就会陷入死循环,那么如何让程序规避这种回到以前状态的变化呢,我们在main函数里定义并且初始化这个数组就起到了这么一个规避作用,我们让数组赋予初始值,假如我们从状态0000可以变化到0001(当然这个变化不符合游戏规则,我这里只是举个例子),那么我就把0000这个状态的值赋值到数组对应的0001下班位置去,比如数组名如果为route,代码表示为route[0001]=0000,十进制表示为route[1]=0; 这样这个数组的第二个元素的初始值就被改为了0,就不再是初始值-1了,如果下一个变化从0001变化到1001,那么代码表示为route[1001]=0001;十进制表示为route[9]=1;也就是数组下标为9的元素的初始值被改为了变化到这个状态(这个状态用数组对应的下标表示)之前的状态值。这样的话,我们从一个状态变化到另外一个状态,数组的值就被赋成了由什么状态变化而来的这个状态值,所以我们只要控制每次变化后的状态所对应的值不为初始值-1即可,因为16个状态对应的十进制数为0~15,不含-1,所以我们索性就把数组的初始值都赋成-1用来表示该状态没有被遍历过,而一旦被遍历过了,就被赋值成变化之前的状态值,这样的话,只要我们的数组下标为15(二进制为1111)的元素被遍历赋值过了,就表明什么??就表明数组下标为15的这个元素被赋上的这个值代表一种变化前的状态,该状态能够直接一步就变化到1111这个状态,也就是成功过河的状态,那么表明过河成功了,循环停止,而且数组元素里记录的非-1和-2这些值就依次记录着从状态0000变化到状态1111的中间状态,route[x]=y,(x和y的取值范围0~15)表示状态x是由状态y一步变化而来的。

 有了这个数组的记录,我们就可以让程序的遍历过程不会发生无意义的返回到以前的状态的这种情况,保证每一次位置状态的变化都异于以往任何一种状态,那么可能的变化排列就变成了14*13*12----*1也就是,对了这里时间复杂度怎么计算??本菜鸟不会哈哈,是不是O(N!)?
当然以上情况笔者描述的是不考虑游戏规则的情况下的一个遍历过程,这里需要对状态变化过程加以限制(不能变化成不安全状态,以及不能没有农夫的陪伴而让其他任意东西自己单独过一次河,而且还要考虑到只有当农夫与物体在同一岸,才能同该物体一起发生过河行为),接下来依次讲解这些限制条件的代码语句:
只有当农夫与物体在同一岸,才能同该物体一起发生过河行为:
((currentLocation & farmer) == 0) == ((currentLocation & mover) == 0)
currentLocation & farmer用16种状态值的任意一种值同farmer进行按位与得到农夫是在哪一岸,
currentLocation & mover用16种状态值的任意一种值同mover进行按位与(这里的mover代表狼wolf,羊sheep,白菜cabbage的枚举变量值的任意值)得到了狼或羊或白菜在哪一岸
如果农夫和物体在同一岸,假设在下岸,那么上诉表达式就是  (1==0)==(1==0),假==假,整体表达式为真,如果农夫和物体在同一岸,假设在上岸,那么上诉表达式就是 
(0==0)==(0==0),真==真,整体表达式为真,如果农夫和物体在不同岸,那么就是(1==0)==(0==0)或者(0==0)==(1==0),那么就是,假==真,真==假,整体表达式为假。综上所需,这个老长的表达式如果结果为真代表农夫和物体在同一岸,如果为假,代表农夫和物体不在同一岸。
接下来解释nextLocation = currentLocation ^ (farmer | mover),这条语句是笔者认为该代码段里最精髓的部分,它能够表示农夫携带一个物体,移动到对岸,当然前提是该物体必须与农夫在同一岸,所以执行该语句的前提是必须符合上面的那条判断在同一岸的前提下。farmer | mover代表用枚举变量里的farmer的取值同其他任意物体的枚举变量取值进行按位与,这里farmer的值为8(1000),假设mover为sheep的值,这里就是1000|0010结果为1010,假设当前状态农夫和羊都在0岸,其他两样物体在哪里无所谓,所以用x,y表示,所以当前状态currentlocation的取值为
0x0y,0x0y^ 1010,那么这个异或的结果农夫和羊的二进制位就由原来的0变成了1,假如原来农夫和羊都在1岸,那么就是1x1y^ 1010,那么这个异或的结果农夫和羊的二进制位就由原来的1变成了0,而其他两样东西的二进制位不管你x,y取0还是1,都会保持原值不变,这意味着什么??意味着这个表达式把当前状态的值经过农夫携带任意在同一侧的物体到对岸而其他两样物体任然在原岸这么一个变化过程,变化到了新的值nextLocation,同志们,这不就是题目要求的意思么,船一次只能坐两样,而且农夫必须在船上,这个表达式太精髓了同志们。
运输函数过程中的一个循环表达式for (mover = 1; mover <= 8; mover <<= 1) 值得一提,我相信大多数新人都没见到过,以前我们熟悉的循环表达式例如for(i=0,i<n,i++)这种形式可以理解为i的初值为0,循环结束条件是i等于n(i走到n就停止了,不会加到大于n),循环量的变化规则是自加1,这样i一个个增加,最终会达到i小于n的临界值而让循环终止,而这里的for循环变量mover的初值是1(0001),循环终止条件mover大于8(1000),循环量的变化规则不是自加1了,而是自身的二进制位向左移动一位,那么这个mover的变化过程可以表述为0001,0010,0100,1000,这4个值不就正好是我们上面的枚举变量里的各种物体的代表值么,说明这个循环可以遍历任何一个物体。

以下给出运输过程源代码:

int process(int route[16], int currentLocation) //农夫运送过程(核心)
{
    if (route[15] == -1) //最终状态 1111(十进制15),即成功过河 
    {
        int mover; //代表移动的哪个物体
        for (mover = 1; mover <= 8; mover <<= 1) //这里的 mover 只有四种情况:1(0001),2(0010),4(0100),8(1000)
        {
            //下面是判断农夫是否和他所要移动的物体位于同一侧
            if (((currentLocation & farmer) == 0) == ((currentLocation & mover) == 0))
            {
                int nextLocation = currentLocation ^ (farmer | mover); //预先得出下一状态
                if (isSafe(nextLocation) && route[nextLocation] == -1) //判断下一状态是否安全,并且下一状态不会是以前变化过的任何状态。
                {
                    int nextRoute[16]; //这里再次建立建立一个新数组记录所有当前数组的所有值,包括每个初始值和变化过的值 
                    for (int i = 0; i < 16; i++)
                        nextRoute[i] = route[i];//如果符合条件就把状态值变化全部记录到新数组里 
                    nextRoute[nextLocation] = currentLocation; //更新单个符合条件的状态变化值 
                    process(nextRoute, nextLocation); //递归进入下一数组
                }
            }
        }
    }
    else //若到达最终状态,即fwsc都过河了,则输出结果。
        printRoute(route, 15);

    return 1; //这里的返回值没有任何意义
}

main函数里对route数组的初始化以及调用process函数,
int main()
{
    int route[16] = { -2 }, currentLocation = 0;//送入最初的都在0岸的状态进行遍历

    for (int i = 1; i < 16; i++)
        route[i] = -1;
    process(route, currentLocation);

    system("pause");
    return 0;
}
 

打印函数是一个递归过程,也可以认为是一个倒推过程,从最后的状态1111,依次递归,每次带入其变化的前一个状态进行递归,直到倒推到最开始的0000状态,然后结束递归开始逐步打印整个过河过程,非常巧妙,时间问题,笔者不再详细描述了,看官们有不明白可以留言,今天码字码了9000多,不求点赞,只求轻喷。

int printRoute(int route[16], int status)
{
    if (status == 0)
    {
        printf("初始状态:农夫、狼、羊和白菜都在河的一侧。\n");
        return 1;
    }
    printRoute(route, route[status]);
    
    char s[200] = "";
    if ((route[status] & cabbage) == cabbage)
        strcat(s, "白菜在河岸1。");
    else
        strcat(s, "白菜在河岸0。");
    if ((route[status] & sheep) == sheep)
        strcat(s, "羊在河岸1。");
    else
        strcat(s, "羊在河岸0。");
    if ((route[status] & wolf) == wolf)
        strcat(s, "狼在河岸1。");
    else
        strcat(s, "狼在河岸0。");
    if ((route[status] & farmer) == farmer)
        strcat(s, "农夫在河岸1。");
    else
        strcat(s, "农夫在河岸0。");
    printf("%s\n", s);

    return 1;
}

以上代码来源:

农夫过河(基于C语言)_Soul0507的博客-CSDN博客_农夫过河c语言

  • 23
    点赞
  • 100
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 11
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

mathhater

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值