图论-BFS解无权有向图最短路径距离

概述

本篇博客主要内容:

  1. 对广度优先搜索算法(Breadth-First-Search)进行介绍;
  2. 介绍用邻接表的存储结构实现一个图(附C++实现源代码);
  3. 介绍用BFS算法求解无权有向图(附C++实现源代码)。

广度优先搜索算法(Breadth-First-Search)又被翻译为宽度优先搜索或横向优先搜索,简称BFS。

BFS是一种盲目搜索法。其系统地展开并检查图中的所有顶点。BFS也是最简便的图搜索算法之中的一个,其相似思想被Dijkstra算法和Prim算法採用。
通过一个样例来介绍BFS的思想,例如以下图所看到的有一张由六个顶点组成的有向图,当中图中的每条边的权值都是1(也能够看做无权图)。求解从一个顶点(源点)出发到图中所有顶点的距离。

这里写图片描写叙述

  • 选择V2作为源点。寻找距离V2距离为2的顶点。找到V2;
  • 寻找距离V2距离为1的顶点,找V2的出边,找到V0和V5;
  • 寻找距离V2距离为2的顶点。因为顶点V5的出度是0,所以找V0的出边,找到V1;
  • 寻找距离V2距离为3的顶点,找V1的出边,找到V3和V4
  • 寻找距离V2距离为4的顶点,找V3的出边,找到V5和V6
  • 于是图中所有的顶点都被訪问过一次
    总结一下搜索的顺序就是:V2–>V0–>V5–>V1–>V3–V4–>V5–>V6

这样的搜索图的方法被称为广度优先搜索(Breadth-First-Search)。该方法按层处理顶点。距离源点近期的那些顶点首先被求值,而最远的那些顶点最后被求职。这非常像树的层序遍历(level-order-traversal)。

图的邻接表实现

用邻接表实现一张图。这里採用的是两个两层链表的结构,当中一层链表存储顶点信息。每个顶点上有一个链表存储该顶点的出边。例如以下列伪代码所看到的:

#include <list>
class Edge {
    // ...
};

class VertexNode {
    std::list<Edge*> edgeAdj;    /* 顶点的邻接表 */
    // ...
};
class Graph {
    // 顶点集合
    std::list<VertexNode*> vertexSet;
    //...

以下的代码给出边的定义。
边有三个属性和三个方法:
- 属性:权重、弧(有向边)的起点和终点指针;
- 方法:获取弧的起点、终点和权值

typedef class VertexNode _VertexNode;
// 边的定义
class Edge {

    int weight;         /* 边的权值 */
    _VertexNode * ori;  /* 弧的起点*/
    _VertexNode * des;  /* 弧的终点*/

public:

    Edge(int _weight,_VertexNode *_ori,_VertexNode *_des): weight(_weight),ori(_ori),des(_des){};
    ~Edge() {};

    // 获取弧的起点
    _VertexNode* getOri() {return ori;};

    // 获取弧的终点
    _VertexNode* getDes() {return des;};

    // 获取弧的权值
    int getWeight() {return weight;};

};

以下代码给出了顶点的定义。顶点具有五个属性和十个方法。

属性的含义和方法的功能在代码的凝视中都有说明,这里有两点说明:

  • VISIT_STATUS状态表示该顶点是否被訪问过。解决顶点反复訪问的去重;
  • 顶点定义key属性,是顶点的标号。方便给大家演示。

typedef enum  _VISIT_STATUS{
    NO_VISIT,       /* 未訪问过此点 */
    VISIT_NO_ADJ,   /* 訪问过此点,但未訪问过其邻接表 */
    VISIT_ADJ,      /* 訪问过此点和其邻接表 */
} VISIT_STATUS;

class VertexNode {
    long key;               /* 顶点的关键字,用于标识该顶点*/
    int value;              /* 顶点附着的信息 */
    std::list<Edge*> edgeAdj;/* 顶点的邻接表 */
    VISIT_STATUS status;    /* 标识此顶点的訪问状态 */
    int distence;           /* 此顶点与搜索顶点的距离 */
    std::list<Edge*>::iterator iter_edge;/* 用于遍历邻接表 */
public:

    VertexNode(long _key,int _value) : key(_key), \
        value(_value) { distence = 0xFFFF;status = NO_VISIT; };
    ~VertexNode() {};

    // 获取该顶点的关键字
    long getKey() { return key;};

    // 设置此顶点的关键字
    void setKey(long _key) { key = _key;};

    // 在顶点上加入一条弧
    void addEdge(Edge* _edge);

    // 在顶点上删除一条弧
    void removeEdge(Edge* _edge);

    // 获取邻接表第一条边
    Edge * getEdgeHeader() {
        iter_edge = edgeAdj.begin();
        return *iter_edge;
    };

    bool nextEdge(Edge **_edge) {
        iter_edge++;
        if (iter_edge != edgeAdj.end()) {
            *_edge = *iter_edge;
            return true;
        }
        return false;
    };

    // 获取顶点的訪问状态
    VISIT_STATUS getStatus() { return status;};

    // 设置顶点的訪问状态
    void setStatus(VISIT_STATUS _status) {status = _status;};

    // 获取出发点到此点的最小距离
    int getDistence() {return distence;};

    // 设置出发点到此点的最小距离
    void setDistence(int _distence) {distence = _distence;};

};

OKay,看过了边和顶点的定义,以下代码给出图的定义。主要有一个属性vertexSet和三个方法组成。当中vertexSet属性主要表示图中顶点的集合。

typedef std::map<long, int>  DistenceOfGraph;

class Graph {
    // 顶点集合
    std::list<VertexNode*> vertexSet;

public:

    Graph() {};
    ~Graph() {};

    /* 功能找出key == _searchKey的顶点到图中所有其他顶点的距离。注:不可达的顶点距离为0xFFFF
     * Input <long _searchKey> 查找关键字
     * Output map<long, int>&dis 输出结果。long 为关键字。int 为输出结果
     */
    void bfs(long _searchKey,DistenceOfGraph &dis);

    // 打印图中的所有顶点
    void printNode();

    // 打印顶点键值为key的边
    void printEdge(long key);

    // 寻找顶点关键字为key的顶点,若找到由_node变量返回
    bool findNode(long key,VertexNode **_node);

    // 向图中添加一个顶点
    VertexNode* addNode(long key,int value);

    // 向图中添加一条边
    void addEdge(long keyOri,long keyDes,int weight);

};

okay,如今我们已经完毕了图的邻接表存储方式的定义,以上图中的各方法将在后面详细实现。


BFS算法求解无权有向图

在本文第一部分广度优先搜索那部分给出了一个求解无权有向图的问题。以下我将结合此问题运用上面介绍的图的邻接表结构存储来介绍BFS算法的实现。

广度优先搜索过程中在訪问V1顶点邻接表过程中,将V0和V5顶点后缓存,然后处理V0,再将V0顶点邻接表中的点缓存起来。这个过程用到的缓存结构事实上就是一个队列。

在遍历某一顶点的邻接表的同一时候,将邻接表中临接的顶点缓存到队列中。依次处理。
还有就是一个去除反复訪问的问题。为每个已经计算过距离的顶点。设置到达此点距离,并更新其状态由未訪问过该顶点到訪问过该顶点但未訪问过其邻接表。
OKay,说明了这两点我们能够一起看bfs方法的代码了。

// 通过输入结点关键字_searchKey,找到该顶点
// 找到该顶点到图中其他可达顶点的最小距离
void Graph::bfs(long _searchKey,DistenceOfGraph& dis) {
    queue<VertexNode*> queueCache;
    int distence = 0;
    // 若不存在此关键字,则返回
    VertexNode *node = NULL;
    if (!findNode(_searchKey, &node)) {
        return ;
    }

    // 查找点距离查找点的距离为0
    node->setDistence(0);
    node->setStatus(VISIT_NO_ADJ);
    (dis).insert(pair<long, int>(_searchKey,0));

    // 广度优先遍历图
    while (IsNotNull(node)) {
        // 若顶点的邻接表已经被訪问。则继续訪问下一个顶点
        if ( JudgeNodeStatusVisitAdj(node) ) {
            // queue next
            QueueNext();
            continue;
        }

        // 遍历顶点得所有临界顶点
        Edge * edgeTmp = node->getEdgeHeader();
        while (IsNotNull(edgeTmp)) {
            // 获取顶点的临接点
            VertexNode * nodeTmp = edgeTmp->getDes();

            // 若该顶点未訪问过,则将此点加入队列,并设置到此点的距离
            if (JudgeNodeStatusNoVisit(nodeTmp)) {
                queueCache.push(nodeTmp);
                distence = edgeTmp->getOri()->getDistence() + edgeTmp->getWeight();
                (dis).insert(pair<long, int>(GetDesNodeKey((edgeTmp)),distence));
                edgeTmp->getDes()->setDistence(distence);
                nodeTmp->setStatus(VISIT_NO_ADJ);

            }

            EdgeNext(edgeTmp);
        }

        QueueNext();
    }



    return;
}

相信到这里,bfs算法已经非常清晰了,那么我在最后给出完整的实现代码和单元測试程序。

完整代码

头文件:

#include <stdio.h>


#ifndef _GRAPH_BFS_H
#define _GRAPH_BFS_H

#include <list>
#include <map>

#define IsNotNull(a) (a)
#define IsNull(a) !(a)
#define JudgeNodeStatusVisitAdj(a) (a)->getStatus() == VISIT_ADJ
#define JudgeNodeStatusNoVisit(a) (a)->getStatus() == NO_VISIT
#define GetDistence(_edge) (_edge)->getOri()->getDistence() + (_edge)->getWeight()
#define GetOriNodeKey(_edge) (_edge)->getOri()->getKey()
#define GetDesNodeKey(_edge) (_edge)->getDes()->getKey()
#define EdgeNext(_edge) if (!node->nextEdge(&(_edge))) { break;}
#define QueueNext() if (queueCache.empty()) {break;} node = queueCache.front();queueCache.pop()
#define DistenceInsert(_dis,_edge) \
        DistenceOfGraph::iterator it = (_dis).find(GetOriNodeKey((_edge))); \
        if (it == (_dis).end() || GetDistence((_edge)) < it->second) { \
            cout<<"insert key = "<<GetOriNodeKey((_edge))<<"distence = "<<GetDistence((_edge))<<endl;\
            (_dis).insert(pair<long, int>(GetOriNodeKey((_edge)),GetDistence((_edge)))); \
        }


typedef class VertexNode _VertexNode;
typedef enum  _VISIT_STATUS{
    NO_VISIT,       /* 未訪问过此点 */
    VISIT_NO_ADJ,   /* 訪问过此点。但未訪问过其邻接表 */
    VISIT_ADJ,      /* 訪问过此点和其邻接表 */
} VISIT_STATUS;
typedef std::map<long, int>DistenceOfGraph;

// 边
class Edge {

    int weight;         /* 边的权值 */
    _VertexNode * ori;  /* 弧的起点*/
    _VertexNode * des;  /* 弧的终点*/

public:

    Edge(int _weight,_VertexNode *_ori,_VertexNode *_des): weight(_weight),ori(_ori),des(_des){};
    ~Edge() {};

    // 获取弧的起点
    _VertexNode* getOri() {return ori;};

    // 获取弧的终点
    _VertexNode* getDes() {return des;};

    // 获取弧的权值
    int getWeight() {return weight;};

};

//顶点
class VertexNode {
    long key;               /* 顶点的关键字,用于标识该顶点*/
    int value;              /* 顶点附着的信息 */
    std::list<Edge*> edgeAdj;    /* 顶点的邻接表 */
    VISIT_STATUS status;    /* 标识此顶点的訪问状态 */
    int distence;           /* 此顶点与搜索顶点的距离 */
    std::list<Edge*>::iterator iter_edge;/* 用于遍历邻接表 */
public:

    VertexNode(long _key,int _value) : key(_key), \
        value(_value) { distence = 0xFFFF;status = NO_VISIT; };
    ~VertexNode() {};

    // 获取该顶点的关键字
    long getKey() { return key;};

    // 设置此顶点的关键字
    void setKey(long _key) { key = _key;};

    // 在顶点上加入一条弧
    void addEdge(Edge* _edge);

    // 在顶点上删除一条弧
    void removeEdge(Edge* _edge);

    // 获取邻接表第一条边
    Edge * getEdgeHeader() {
        iter_edge = edgeAdj.begin();
        return *iter_edge;
    };

    bool nextEdge(Edge **_edge) {
        iter_edge++;
        if (iter_edge != edgeAdj.end()) {
            *_edge = *iter_edge;
            return true;
        }
        return false;
    };

    // 获取顶点的訪问状态
    VISIT_STATUS getStatus() { return status;};

    // 设置顶点的訪问状态
    void setStatus(VISIT_STATUS _status) {status = _status;};

    // 获取出发点到此点的最小距离
    int getDistence() {return distence;};

    // 设置出发点到此点的最小距离
    void setDistence(int _distence) {distence = _distence;};

};

class Graph {
    // 顶点集合
    std::list<VertexNode*> vertexSet;

public:

    Graph() {};
    ~Graph() {};

    /* 功能找出key == _searchKey的顶点到图中所有其他顶点的距离。注:不可达的顶点距离为0xFFFF
     * Input <long _searchKey> 查找关键字
     * Output map<long, int>&dis 输出结果。long 为关键字。int 为输出结果
     */
    void bfs(long _searchKey,DistenceOfGraph &dis);

    // 打印图中的所有顶点
    void printNode();

    // 打印顶点键值为key的边
    void printEdge(long key);

    // 寻找顶点关键字为key的顶点。若找到由_node变量返回
    bool findNode(long key,VertexNode **_node);

    // 向图中添加一个顶点
    VertexNode* addNode(long key,int value);

    // 向图中添加一条边
    void addEdge(long keyOri,long keyDes,int weight);

};

// 此測试程序測试上面Graph中的bfs方法
int testGraphBfs();
#endif

代码实现的源文件:

//
//  graph_bfs.cpp
//  100-alg-tests
//
//  Created by bobkentt on 15-8-8.
//  Copyright (c) 2015年 kedong. All rights reserved.
//
#include <iostream>
#include <queue>
#include <map>
#include <vector>
#include <list>
#include "graph_bfs.h"

using namespace std;

#define _DEBUG_ 1

void VertexNode::addEdge(Edge* _edge) {
    if (IsNull(_edge)) {
        cout<<"add an NULL edge."<<endl;
        exit(-1);
    }

#ifdef _DEBUG_
    cout<<"addEdge ori's key = "<<_edge->getOri()->getKey();
    cout<<",des's key ="<<_edge->getDes()->getKey()<<endl;;
#endif

    edgeAdj.push_back(_edge);

    return ;
}

bool Graph::findNode(long key,VertexNode **_node) {
    list<VertexNode*> &VS = vertexSet;
    list<VertexNode*>::iterator end = VS.end();
    list<VertexNode*>::iterator it;
    VertexNode *node = NULL;

    // 遍历顶点集,找到開始结点A
    for (it = VS.begin(); it != end; it++) {
        node = *it;
        if (node->getKey() == key)
        {
            break;
        }
    }
    if (it == end) {
        // 结点中
        cout<<"graph::isNodeExist cannot find key = "<<key<<"in graph."<<endl;
        _node = NULL;
        return false;
    }
    *_node = node;
    return true;
}

VertexNode* Graph::addNode(long key,int value) {
    VertexNode * node = new VertexNode(key,value);

    vertexSet.push_back(node);

    return node;
}


void Graph::addEdge(long keyOri,long keyDes,int weight) {
    VertexNode *ori = NULL;
    VertexNode *des = NULL;

    // 在图中查找这两个顶点
    if (!findNode(keyOri, &ori) || !findNode(keyDes, &des)) {
        cout<<"Graph::addEdge failed:未找到该顶点"<<endl;

        exit(-1);
    }

    // 创建此弧
    Edge * edge = new Edge(weight,ori,des);

    // 在图中弧的起点的邻接表中。加入此弧
    ori->addEdge(edge);

    return ;
}

// 通过输入结点关键字_searchKey,找到该顶点
// 找到该顶点到图中其他可达顶点的最小距离
void Graph::bfs(long _searchKey,DistenceOfGraph& dis) {
    queue<VertexNode*> queueCache;
    int distence = 0;
    // 若不存在此关键字,则返回
    VertexNode *node = NULL;
    if (!findNode(_searchKey, &node)) {
        return ;
    }

    // 查找点距离查找点的距离为0
    node->setDistence(0);
    node->setStatus(VISIT_NO_ADJ);
    (dis).insert(pair<long, int>(_searchKey,0));

    // 广度优先遍历图
    while (IsNotNull(node)) {
        // 若顶点的邻接表已经被訪问,则继续訪问下一个顶点
        if ( JudgeNodeStatusVisitAdj(node) ) {
            // queue next
            QueueNext();
            continue;
        }

        // 遍历顶点得所有临界顶点
        Edge * edgeTmp = node->getEdgeHeader();
        while (IsNotNull(edgeTmp)) {
            // 获取顶点的临接点
            VertexNode * nodeTmp = edgeTmp->getDes();

            // 若该顶点未訪问过,则将此点加入队列,并设置到此点的距离
            if (JudgeNodeStatusNoVisit(nodeTmp)) {
                queueCache.push(nodeTmp);
                distence = edgeTmp->getOri()->getDistence() + edgeTmp->getWeight();
                (dis).insert(pair<long, int>(GetDesNodeKey((edgeTmp)),distence));
                edgeTmp->getDes()->setDistence(distence);
                nodeTmp->setStatus(VISIT_NO_ADJ);

            }

            EdgeNext(edgeTmp);
        }

        QueueNext();
    }



    return;
}

void Graph::printNode() {
    list<VertexNode*>::iterator it = vertexSet.begin();
    cout<<"The nodes of Graph's keys = ";
    for (; it != vertexSet.end(); it++) {
        VertexNode * node = *it;
        cout<<node->getKey()<<" ";
    }
    cout<<endl;
    return ;
}

int testGraphBfs() {
    Graph G;

    // 画出图中所有的点
    for (int i = 0; i <= 6; i++) {
        G.addNode(i, i);
    }

    G.printNode();

    // 画出图中所有的边
    G.addEdge(0, 1, 1);/* V0-->V1 */
    G.addEdge(0, 3, 1);/* V0-->V3 */
    G.addEdge(1, 3, 1);/* V1-->V3 */
    G.addEdge(1, 4, 1);/* V1-->V4 */
    G.addEdge(2, 0, 1);/* V2-->V0 */
    G.addEdge(2, 5, 1);/* V2-->V5 */
    G.addEdge(3, 2, 1);/* V3-->V2 */
    G.addEdge(3, 4, 1);/* V3-->V4 */
    G.addEdge(3, 5, 1);/* V3-->V5 */
    G.addEdge(3, 6, 1);/* V3-->V6 */
    G.addEdge(4, 6, 1);/* V4-->V6 */
    G.addEdge(6, 5, 1);/* V6-->V5 */

    // 选择V3作为源点,求V3到其他所有的点的距离
    DistenceOfGraph dis;
    G.bfs(2, dis);

    // debug "for each dis"
    map<long,int>::iterator iter = dis.begin();
    for (; iter != dis.end(); iter++) {
        cout<<"key = "<<iter->first<<", dis = "<<iter->second<<endl;
    }


    return 0;
}

int main(int argc, const char * argv[]) {
    testGraphBfs();
    return 0;
}

Okay,今天就写到这里了。12点半了,困困哒。我要睡了,明天继续,这周把DFS、Dijkstra、Prim算法都实现一遍。
ps:这篇博文写的匆忙,有哪些不好,或者不正确的地方请朋友们指正。

posted @ 2017-06-29 20:52  clnchanpin  阅读(2106)  评论(0编辑  收藏