算法基础部分5-广度优先搜索

算法部分 基础5

一、广度优先搜索的简述

  广度优先搜索,用队列保存待扩展的节点。从队首取出节点,扩展出的新节点放入队尾直到出现目标节点。广度优先搜索框架如下

Bfs(){
	初始化队列
	while(队列不为空未能找到目标节点){
		取队首节点扩展,并将扩展出的非重复节点放入队尾;
			必要时要记住每个节点的父节点;
	}
}

  新扩展出的节点如果和以前扩展出的节点相同,则新节点就不必再考虑。如何判重?状态节点数目巨大,如何存储?怎么才能较快判断一个状态是否重复?这些都是要通过实际问题权衡分析的。

广度优先搜索引入 – 抓住那头牛

  农夫知道一头牛的位置,想要抓住它。农夫和牛都位于数轴上,农夫起始位于点 N (0 <= N <= 100000) , 牛位于点 K (0 <= K <= 100000) 。农夫有两种移动方式:
  1. 从 X 移动到 X - 1 或者 X + 1 , 每次移动花费一分钟
  2. 从 X 移动到 2 * X , 每次移动花费一分钟
  假设牛没有意识到农夫的行动,站在原地不动。农夫最少要花多少时间才能抓住牛?

用有向图的方式描述农夫和牛的位置关系,
假设农夫起始位于点 3 , 牛位于 5 , N = 3 , K = 5 , 最右边是 6 .
如何搜索到一条走到 5 的路径?

在这里插入图片描述
  策略如下

策略 1 深度优先搜索:从起点出发,随机挑选一个方向,能往前走就往前走(扩展),走不
动就回溯。不能走已经走过的点 (要判重) . 
运气好的话:
3 -> 4 -> 5   或者,
3 -> 6 -> 5
运气不太好:
3 -> 2 -> 4 -> 5   或者
3 -> 2 -> 1 -> 0 -> 4 -> 5
要想求解最优()解,则要遍历所有走法。可以用各种手段优化,比如,若已经找到路径
长度为 n 的解,则所有长度大于 n 的走法就不必尝试。
运算过程中需要存储路径上的节点,数量较少。用栈存节点。

策略 2 广度优先搜索:
a. 给节点分层。起点是 0 层。从点最少只需要 n 步就能达到的点属于第 n 层次, 比
如
第一层: 2, 4, 6
第二层: 1, 5
第三层: 0
b. 然后依层次顺序,从小到大扩展节点。把层次低的点全部扩展出来后,才会扩招层次
高的点。
搜索过程 (节点扩展过程)3
2 4 6
1 5
问题就解决了。注意:扩展时,不能扩展出已经走过的节点 (要判重) .
可以确保找到最优解,但是因为扩展出来的节点较多,且多数节点都需要保存,因此需要
的存储空间比较大。用队列存节点。

接着具体是队列变化的过程,看着图了解
在这里插入图片描述

两个队列变化情况
Closed 是进入的节点, Open 是扩展的节点
-------------------------- // 从初始节点开始
                    Closed
3                   Open
-------------------------- // 扩展 3 节点
3                   Closed
  2 4 6             Open
-------------------------- // 扩展 2 节点(会判重) 新节点 1 入扩展队列
3 2                 Closed
    4 6 1           Open
-------------------------- // 扩展 4 节点(会判重) 新节点 5 入扩展队列
3 2 4               Closed
      6 1 5         Open
-------------------------- // 扩展 6 节点(会判重) 都重复了
3 2 4 6             Closed
        1 5         Open 
-------------------------- // 扩展 1 节点(会判重) 新节点 0 入扩展队列
3 2 4 6 1           Closed
          5 0       Open 
-------------------------- // 目标节点 5 出列,问题解决!
3 2 4 6 1 5         Closed
            0       Open 
找到的第一个目标节点就是最优节点
这样通过 5 节点的指针或者层次号,就可以找到节点的路径关系。

程序如下,

#include <iostream>
#include <cstring>
#include <queue>

using namespace std;

int N, K;
const int MAXN = 100000;
// 判重标记,visited[i] = true 表示 i 已经扩展过
int visited[MAXN + 10];

// 类可以用结构体表示
struct Step{
    int pos; // 位置
    int steps; // 到达 x 所需要的步数
    Step( int xx, int s ): pos(xx), steps(s){}
};
queue < Step > q; // 队列,即 Open 表

int main(){
    cin >> N >> K; // 输入起点和终点
    memset( visited, 0, sizeof(visited) );
    q.push(Step(N, 0));
    visited[N] = 1;

    while(!q.empty()){
        Step s = q.front(); // 队列头元素
        if(s.pos == K){ // 找到目标
            cout << s.steps << endl;
            return 0;
        }
        else{
            // 往左边走
            if(s.pos - 1 >= 0 && ! visited[s.pos - 1]){
                q.push(Step(s.pos - 1, s.steps + 1));
                visited[s.pos - 1] = 1;
            }
            // 往右边走
            if(s.pos + 1 <= MAXN && ! visited[s.pos + 1] ){
                q.push(Step(s.pos + 1, s.steps + 1));
                visited[s.pos + 1] = 1;
            }
            // 跳着走  s.pos 移动到 2 * s.pos , 感觉是飞着走
            if(s.pos * 2 <= MAXN && ! visited[s.pos * 2]){
                q.push(Step(s.pos * 2, s.steps + 1));
                visited[s.pos * 2] = 1;
            }
            q.pop();
        }
    }
    return 0;
}

可以这样理解,先对三种方式满足要求进行入队列,依次出队列。在出的队列中以元素为起始,再进行三种方式入队列,并且判断重复,这样遍历找到目标位置。但是这道题不是特别典型。

二、广度优先搜索的例子

1. 迷宫问题

  问题描述:定义一个矩阵

0 1 0 0 0
0 1 0 1 0
0 0 0 0 0 
0 1 1 1 0
0 0 0 1 0
它表示一个迷宫,其中 1 表示墙壁,0 表示可以走的路,只能横着走或者竖着走,不能
斜着走,要求编程序找出从左上角到左下角最短路线。

  问题分析

基础的广搜问题。首先将起始位置入队列

每次从队列拿出一个元素,扩展其相邻的 4 个元素的队列(要判重)
直到队头元素为终点为止。队列里的元素记录了指向父节点(上一步)的指针

队列元素:
struct {
	int r, c; // 坐标
	int f; // 父节点在队列中的下标
}

队列不能用 STL 的 queue 和 deque , 要自己写,因为要留存数据。用一维数组实
现,维护一个队头指针 head 和队尾 tail 指针。 head = tail 就是空的

程序如下,感觉理解还不是很清楚,自己写不出来程序。以后学习完成补充上。

2. 八数码问题

  八数码问题是人工智能中的经典问题,问题描述如下
  有一个 3 * 3 的棋盘,其中有 0 - 8 共 9 个数字,0 表示空格,其他的数字可以和 0 交换位置,求由初始状态到达目标状态的步数最少的解。

8 2 3          1 2 3
1 4 6   --->   4 5 6
5 7 0          7 8 0

  分析如下

用合理的编码表示“状态”,减小存储
方案一:
每个状态用一个字符串存储,要 9 个字节,浪费空间,注意是每个状态!

方案二:
. 每个状态对应一个 9 位数,则该 9 位数最大为 876,543,210, 小于 2^31 ,int 就能表示一个状态。
. 判重需要一个标志位序列,每个状态对应于标志位序列中的 1 位,标志位为 0 , 表
示该状态尚未扩展,为 1 则说明已经扩展过了。
. 标志位序列可以用字符数组 a 存放。a 的每个元素存放 8 个状态的标志位。最多需
要 876,543,210 / 8 + 1 个元素,即是 109,567,902 字节
. 如果某个状态对应于数 x , 则标志位就是 a[x / 8] 的第 x % 8. 但是这个空间还是太大了

方案三:
. 将每个状态的字符串形式看作一个 9 进制数,则该 9 位数最大为 876543210(9),
即是 381367044(10) 需要的标志位数目也降为 381367044(10) 比特,即是47,670,881 
字节。
. 如果某个状态对应于数 x , 则其标志位就是 a[x/8] 的第 x % 8. 空间要求还是大。

方案四:
. 把每个状态都看做 ’0-8‘ 的一个排列,以此排列在全部排列中的位置作为其序
号。状态用其排列序号来表示。
. 012345678 是第 0 个排列,876543210 是第 9! - 1. 状态总数即排列总数是:9! = 362880
. 判重用的标志数组 a 只需要 362880 比特即可。
. 如果某个状态的序号为 x , 则其标志位就是 a[x / 8] 的第 x % 8----
 
在进行状态空间转移,把字符串移动转化为 int 类型存储,再把 int 型转化为 字符串
形式移动。但是函数运行会消耗时间,相当于用时间交换空间。

方案5. 还是用一个状态看作一个数的 10 进制表示形式
. 用 set<int> 进行判重。每入队一个状态,就能将其加入到 set 里面,判重时,查
找该 set , 看能否找到状态。

  输入数据情况

输入数据:
2 3 4 1 5 x 7 6 8
输出数据:
ullddrurdllurdruldr

这里序列中
u 表示使空格上移
d 表示使空格下移
l 表示使空格左移
r 表示使空格右移

相当于
8 2 3          1 2 3
1 4 6   --->   4 5 6
5 7 0          7 8 0

程序如下

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <set>

using namespace std;

int goalStatus = 123456780; // 目标状态
const int MAXS = 400000;
char result[MAXS]; // 输出要移动状态
struct Node{
    int status; // 状态
    int father; // 父结点指针,即 myQueue 的下标
    char Move;  // 父结点到本结点的移动方式:u/d/r/l
    Node(int s, int f, char m): status(s), father(f), Move(m){ }
    Node(){ }
};
Node myQueue[MAXS]; // 状态队列,状态总数为 362880
int qHead = 0; // 队头指针
int qTail = 1; // 队尾指针
char moves[] = "udrl"; // 4 种移动

void InitStatusToStrStatus(int n, char * strStatus){
    sprintf(strStatus, "%09d", n); // 需要保留前 9 个
}
int NewStatus(int status, char cMove){
    // 求从 status 经过 cMove 移动后得到的新状态。若状态不可行,则返回 -1
    char tmp[20];
    int zeroPos; // 字符 '0' 的位置
    InitStatusToStrStatus(status, tmp);
    for(int i = 0; i < 9; i++){
        if(tmp[i] == '0'){
            zeroPos = i;
            break;
        } // 返回空格位置
    }
    switch(cMove){
        case 'u':
            if(zeroPos - 3 < 0)
                return -1; // 空格在第一行
            else{
                tmp[zeroPos] = tmp[zeroPos - 3];
                tmp[zeroPos - 3] = '0';
            }
            break;
        case 'd':
            if(zeroPos + 3 > 8)
                return -1; // 空格在第三行
            else{
                tmp[zeroPos] = tmp[zeroPos + 3];
                tmp[zeroPos + 3] = '0';
            }
            break;
        case 'l':
            if(zeroPos % 3 == 0)
                return -1; // 空格在第一列
            else{
                tmp[zeroPos] = tmp[zeroPos - 1];
                tmp[zeroPos - 1] = '0';
            }
            break;
        case 'r':
            if(zeroPos % 3 == 2)
                return -1; // 空格在第三列
            else{
                tmp[zeroPos] = tmp[zeroPos + 1];
                tmp[zeroPos + 1] = '0';
            }
            break;
    }
    return atoi(tmp);
}

bool Bfs(int status){
    // 寻找从初始状态 status 到目标的路径,找不到则返回 false
    int newStatus;
    set <int> expanded;
    expanded.insert(status);
    myQueue[qHead] = Node(status, -1, 0);
    while(qHead != qTail){ // 队列不为空
        status = myQueue[qHead].status;
        if(status == goalStatus) // 找到目标状态
            return true;
        for(int i = 0; i < 4; i++){ // 尝试 4 种移动
            newStatus = NewStatus(status, moves[i]);
            if(newStatus == -1)
                continue; // 不可移,尝试下一种
            if(expanded.find(newStatus) != expanded.end() )
                continue; // 已经扩展过,尝试下一种
            expanded.insert(newStatus);
            myQueue[qTail++] = Node(newStatus, qHead, moves[i]);  // 新结点入队列
        }
        qHead ++;
    }
    return false;
}

int main(){
    goalStatus = atoi("123456780"); // 目标状态
    char line1[50];
    char line2[20];
    while(cin.getline(line1, 48)){
        int i, j;
        // 将输入的原始字符串变成数字的字符串
        for(i = 0, j = 0; line1[i]; i++){
            if(line1[i] != ' '){
                if(line1[i] == 'x')
                    line2[j++] = '0';
                else
                    line2[j++] = line1[i];
            }
        }
        line2[j] = 0; // 字符串形式的初始状态
        if(Bfs(atoi(line2))){
            int moves = 0;
            int pos = qHead;
            do{ // 通过 father 找到成功的状态序列,输出相应步骤
                result[moves++] = myQueue[pos].Move;
                pos = myQueue[pos].father;
            }while(pos); // pos = 0 说明已经回退到初始状态了
            for(int i = moves - 1; i >= 0; i--)
                cout << result[i];
        }
        else
            cout << "unsolvable" << endl;
    }
    return 0;
}

运行结果如下,
在这里插入图片描述
分析

主要出发点从广度优先搜索,并且算法会判断重复,使用两个队列来实现,实现的队列
通过数组。
对于Bfs的程序:
while(qHead != qTail){ // 队列不为空
    status = myQueue[qHead].status;
    if(status == goalStatus) // 找到目标状态
        return true;
    for(int i = 0; i < 4; i++){ // 尝试 4 种移动
        newStatus = NewStatus(status, moves[i]);
        if(newStatus == -1)
            continue; // 不可移,尝试下一种
        // 找到返回迭代器,没有找到则返回end()
        if(expanded.find(newStatus) != expanded.end()) // set 数据存储方式
            continue; // 已经扩展过,尝试下一种
        expanded.insert(newStatus);
        // myQueue[qTail] 的前一个节点是 qHead 作为记录的
        myQueue[qTail++] = Node(newStatus, qHead, moves[i]);  // 新结点入队列
    }
    qHead ++;
}

分析:
判断重复的方式是通过 set 来实现:
if(expanded.find(newStatus) != expanded.end()) // set 数据存储方式
    continue; // 已经扩展过,尝试下一种

这个语句的广度优先搜索的核心:
myQueue[qTail++] = Node(newStatus, qHead, moves[i]);for 循环中,相当于 myQueue[qTail] 存储的节点是可以找到它的父节点
的,通过 qHead 来实现。

  其他地方都相对好理解一些,具体的都在代码中做了标注。

三、总结

  广度优先搜索主要通过两个队列实现。具体学习还需要多看例子多理解。感觉这个章节学习的不是很透彻,在接下来练习中要继续加强练习。
  最近出来外面做实验出差了,9月19日出来的,9月30号才可以回去。一直忙着调试实验设备,今天刚把用 matlab 实时采集脑电部分弄好,准备继续调试刺激部分,从而写算法实现闭环调控。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值