用“农夫过河“5分钟学会队列和BFS

写在前面

说到搜索算法,就不得不提到深度优先搜索(DFS)广度优先搜索(BFS),在前面的几篇文章中,我给DFS的主要原理实现,和简单问题解决做了介绍。

如果是DFS是基于栈(Stack)来实现,那么BFS就是基于队列(Queue)来实现,在数据结构中,栈和队列是相对的,栈后进先出,而队列先进先出。

问题引入

这是一个很经典的问题,我们现在有一条河,河的一侧有农夫,羊,狼和菜,现在我们要设计一种方案,让他们到另外一边去,规则如下:

(1)在没有农夫的情况下,羊会被狼吃掉。

(2)在没有农夫的情况下,菜会被羊吃掉。

(3)农夫一次至多带一件物品。

(4)物品不能在没有农夫的情况下移动

(5)被移动的物品必须与农夫在同一侧。

尝试解决

基本定义

如果我们要表示他们的位置,实际上大家很容易就能想到使用二进制,比如在原来的一侧用0,在新的一侧用1

我们用四位二进制分别表示:农夫、羊、狼、菜。

则初始状态为:0000,结束状态为:1111

如何移动

如果说农夫带着羊移动,那状态会变成1100,带着狼就是1010,菜就是1001,不带任何物品就是1000,我们就写出了四种可能的情况的二进制表示。

但是,我们如何将这个“操作”,实施到“状态”上呢?

答案就是:异或运算(XOR)   

                                              NewPos = OldPos\bigoplus Movement

只要让原来的位置 XOR 我们的操作,就可以得到:新的位置。

比如现在位置:1100(羊和农夫在对岸),我们执行操作:1100(农夫运羊),就得到1100 XOR 1100 = 0000(农夫和羊都回到原侧

我们来看下代码是如何描述的:

int step[4] = { 0b1000, 0b1100, 0b1010, 0b1001 }; // 农夫四种操作
int ways[16]; // 保存每个状态的上一个状态
int start = 0b0000; // 起始状态,农夫、羊、狼、菜都在左岸
int endd = 0b1111; // 目标状态,农夫、羊、狼、菜都在右岸

如果你不了解异或运算,我可以简单讲解一下,异或(XOR) 实际上也叫半加运算,也就是“不进位加法”,即1+1=0(正常的或运算应该是1),1+0=1,0+1=1,0+0=0

如何判断位置

如何从我们的四位二进制位置信息中获取我们想要目标的位置?我们可以使用与运算来进行,比如1010这种情况,只要求1010&1000,就可以得到1000,知道农夫在目标侧。

而只要求1010&0100,就可以得到0000,知道羊还在原来那一侧。

// 每个角色的当前状态
int Farmer(int pos)
{ 
    if ((pos & 0b1000) == 0b1000)return 1;
    else return 0; 
}
int Sheep(int pos)
{
    if ((pos & 0b0100) == 0b0100)return 1;
    else return 0;
}
int Wolf(int pos)
{ 
    if ((pos & 0b0010) == 0b0010)return 1;
    else return 0; 
}
int Vege(int pos)
{
    if ((pos & 0b0001) == 0b0001)return 1;
    else return 0;
}

在判断位置的这一小段,实际上我们还需要判断位置是否安全,还记得刚刚题目的要求吗?

// 状态是否安全
int Safe(int pos) 
{
    if ((Sheep(pos) == Wolf(pos)) && Farmer(pos) != Sheep(pos)) return 0; // 羊和狼在一起,农夫不在
    if ((Sheep(pos) == Vege(pos)) && Farmer(pos) != Sheep(pos)) return 0; // 羊和菜在一起,农夫不在
    return 1;
}

广度优先搜索(BFS)

实际上,对于每种状态,我们都有至多四种可做出的变动。注意这里说的四种,是指XOR数学运算上的可能性,在不考虑限制条件和实际情况下,我们可以简单画出一个这样的树:

在这里我们引入深度优先和广度优先的区别,在DFS中,我们会挑选一个路径,一直走到头,比如在最左边,0000->1000->0000,然后返回0000,再去探索1100 

而在BFS中,我们会逐行探索,从0000到第一次层的1000,1100,1010,1001然后再从1000开始,继续探索其下的0000,0100,0010,0001

可以发现,与DFS的逐列探索,正好相反。你可以用矩阵这样理解:

                                                   \begin{pmatrix} D &D &D \\ F& F & F\\ S & S &S \end{pmatrix}\begin{pmatrix} B & F & S\\ B& F&S \\ B& F& S \end{pmatrix}

代码实现

我们可以先将起始位置压入队列,然后取出,在这个起始位置(含约束条件的情况下)去推算所有可能的下一个位置,并将他们全压入队列中,这样一直取出,直到队空。

由于队列先进先出,我们可以保证:在第n行的位置没算完前,第n+1行的位置不会被计算。

只有当第n行的位置全部出队(被计算完),才会轮到第n+1行参与计算。

void BFS()
{
    q.push(start); // 将起始状态入队
    ways[start] = start; // 起始状态的上一个状态是它自己

    while (!q.empty()) 
    {
        int curr = q.front();
        q.pop();

        if (curr == endd) return;

        if (Safe(curr ^ step[0]) && ways[curr ^ step[0]] == -1)
        {
            q.push(curr ^ step[0]); // 将下一个状态入队
            ways[curr ^ step[0]] = curr; // 标记当前状态为下一个状态的来源
        }
        
        if ((Farmer(curr) == Sheep(curr)) && Safe(curr ^ step[1]) && ways[curr ^ step[1]] == -1)
        {
            q.push(curr ^ step[1]); // 将下一个状态入队
            ways[curr ^ step[1]] = curr; // 标记当前状态为下一个状态的来源
        }

        if ((Farmer(curr) == Wolf(curr)) && Safe(curr ^ step[2]) && ways[curr ^ step[2]] == -1)
        {
            q.push(curr ^ step[2]); // 将下一个状态入队
            ways[curr ^ step[2]] = curr; // 标记当前状态为下一个状态的来源
        }

        if ((Farmer(curr) == Vege(curr)) && Safe(curr ^ step[3]) && ways[curr ^ step[3]] == -1)
        {
            q.push(curr ^ step[3]); // 将下一个状态入队
            ways[curr ^ step[3]] = curr; // 标记当前状态为下一个状态的来源
        }
    }
}

在这里,我们使用ways数组来保存状态和路径,确保路径不会重复(数组初始置-1,如果不是-1说明走过了,就不能再走这种状态)

而if语句中那一大长段则先判断可行性,然后再移动。比如只有当羊和农夫在同一侧,羊和农夫移动完状态安全,移动的位置是第一次出现。如下:

if ((Farmer(curr) == Sheep(curr)) && Safe(curr ^ step[1]) && ways[curr ^ step[1]] == -1)

最后,我给出全部代码:

#include<iostream>
#include<queue>
using namespace std;

queue<int> q;

int step[4] = { 0b1000, 0b1100, 0b1010, 0b1001 }; // 农夫分别带羊、狼、菜过河的四种操作
int ways[16]; // 保存每个状态的上一个状态(用于回溯路径)
int start = 0b0000; // 起始状态,农夫、羊、狼、菜都在左岸
int endd = 0b1111; // 目标状态,农夫、羊、狼、菜都在右岸

// 每个角色的当前状态
int Farmer(int pos)
{ 
    if ((pos & 0b1000) == 0b1000)return 1;
    else return 0; 
}
int Sheep(int pos)
{
    if ((pos & 0b0100) == 0b0100)return 1;
    else return 0;
}
int Wolf(int pos)
{ 
    if ((pos & 0b0010) == 0b0010)return 1;
    else return 0; 
}
int Vege(int pos)
{
    if ((pos & 0b0001) == 0b0001)return 1;
    else return 0;
}

// 状态是否安全
int Safe(int pos) 
{
    if ((Sheep(pos) == Wolf(pos)) && Farmer(pos) != Sheep(pos)) return 0; // 羊和狼在一起,农夫不在
    if ((Sheep(pos) == Vege(pos)) && Farmer(pos) != Sheep(pos)) return 0; // 羊和菜在一起,农夫不在
    return 1;
}

void BFS()
{
    q.push(start); // 将起始状态入队
    ways[start] = start; // 起始状态的上一个状态是它自己

    while (!q.empty()) 
    {
        int curr = q.front(); // 取队头状态为当前状态
        q.pop(); // 队头出队

        if (curr == endd) return; //到达目标,结束

        if (Safe(curr ^ step[0]) && ways[curr ^ step[0]] == -1)
        {
            q.push(curr ^ step[0]); // 将下一个状态入队
            ways[curr ^ step[0]] = curr; // 标记当前状态为下一个状态的来源
        }
        
        if ((Farmer(curr) == Sheep(curr)) && Safe(curr ^ step[1]) && ways[curr ^ step[1]] == -1)
        {
            q.push(curr ^ step[1]);
            ways[curr ^ step[1]] = curr;
        }

        if ((Farmer(curr) == Wolf(curr)) && Safe(curr ^ step[2]) && ways[curr ^ step[2]] == -1)
        {
            q.push(curr ^ step[2]);
            ways[curr ^ step[2]] = curr;
        }

        if ((Farmer(curr) == Vege(curr)) && Safe(curr ^ step[3]) && ways[curr ^ step[3]] == -1)
        {
            q.push(curr ^ step[3]);
            ways[curr ^ step[3]] = curr;
        }
    }
}

// 打印路径
void printPath() 
{
    for (int i = 0; i < 16; i++)
    {
        cout << ways[i]<<" ";
    }
}

int main() 
{
    for (int i = 0; i < 16; i++) ways[i] = -1;
    BFS(); // 执行广度优先搜索
    printPath(); // 输出
    return 0;
}

输出结果:

我们从最后一个(15)逐步往前推,然后写出全部过程(十进制) :

0 -> 12 -> 4 -> 14 -> 2 -> 11 -> 3 -> 15

二进制表示:

0000 -> 1100 -> 0100 -> 1110 -> 0010 -> 1011 -> 0011 -> 1111

自然语言描述:

农夫先带着羊去对岸,农夫自己回到原岸,农夫带着狼去对岸,农夫把羊带回原岸,农夫带着菜去对岸,农夫自己回到原岸,农夫带上羊去对岸。

优化输出(拓展)

引入bitset头文件后,我们可以将输出进一步优化,请读者自行阅读理解,不再做讲解。

// 打印路径,从终点回溯到起点
void printPath() 
{
    if (ways[endd] == -1) {
        cout << "无解!" << endl;
        return;
    }

    int path[16]; // 用来存储路径
    int stepCount = 0;
    int curr = endd;

    // 回溯路径
    while (curr != start) {
        path[stepCount++] = curr;
        curr = ways[curr];
    }
    path[stepCount++] = start;

    // 反向输出路径
    for (int i = stepCount - 1; i >= 0; i--) {
        cout << "状态: " << bitset<4>(path[i]) << endl;
    }
}

输出如下:(更加直观)

感谢阅读!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值