• 经典计算机基础数据结构:图


    1. 图的表示

    大家都知道图有两种标准的表示方法:邻接表或者邻接矩阵。可是它们分别有什么样的好处呢?答案是:

    • 邻接表适合稀疏图,而邻接矩阵时候稠密图;
    • 要确定图中边(u,v)是否存在,只能在定点u的邻接表中搜索v,效率不高,而这时邻接矩阵就要方便的多了;

    理论上知道了邻接表和邻接矩阵,如何用代码来实现呢?

    #include <stdlib.h>
    #include <queue>
    using namespace std;
    
    #define MAX_VERTEX_NUM 20
    typedef enum _VisitedColor
    {
        Black, White, Grey
    }VisitedColor;
    typedef struct _VertexNode
    {
        int data;  //assume the data in the node is int typed;
        struct _ArcNode *firstArc;
        //below fields are for BFS or DFS, in real scenarios, we should not put them here, but it is just for demo the algorithm;
        VisitedColor visited; //for BFS and DFS
        struct _VertexNode * parent; //for BFS and DFS
        int distance; // the distance from source point to this node, for BFS and DFS 
    }VertexNode;
    
    typedef struct _ArcNode // This class represent a arc
    {
        //VertexNode *from; //In Adjacency list, the ArcNode is to represent the arc, whose starting Node info is saved in the list, so we do not need define it here; 
        VertexNode *to;
        struct _ArcNode *next;
    }ArcNode;
    
    typedef struct _Graph
    {
        int verNum, arcNum;
        VertexNode** vertices; 
    }Graph;

    再来一个C++的,它们是非常类似的,不过就是实现时可以show一下数据封装:

    #include <iostream>
    #include <list>
    using namespace std;
    
    class ArcNode;
    class VertexNode
    {
    public:
        VertexNode(int _data)
        {
            data = _data;
        }
        void setFirstArc(ArcNode * arc)
        {
            firstArc = arc;
        }
    private:
        int data;
        ArcNode *firstArc;
    };
    
    class ArcNode
    {
    public:
        ArcNode(VertexNode *_pointingto)
        {
            pointingto = _pointingto;
        }
    private:
        VertexNode *pointingto;
    };
    
    class Graph
    {
    public:
        void AddOneArc(VertexNode *node, ArcNode *arc)
        {
    
        }
    private:
        int arcNum, verNum;
        list<VertexNode *> vertices; 
    };

    应该来说邻接表是比较常用的,但邻接矩阵也是比较重要的,不过这里我就不贴代码了。

    2. 广度优先搜索

    这就不用多说了,breadth-first-search是最简单、最基本的图搜索算法之一,必须掌握的。在贴代码之前,先说一下注意点吧:1. 我们需要区分某个定点是否已经被发现了、是否其所有相邻节点都已经被发现了,在《算法导论》中使用颜色来区分这个状态,白色表示该节点没有被发现,灰色表示这个节点被发现了,而其相邻节点还没有完全被发现;而黑色表示这个节点极其相邻节点都已经被发现。 为什么要这么做呢?因为图的结构相对复杂,可能有多个边指向一个节点(也就是这个节点有多个路径可达,我们需要避免重复,因为重复就意味着路径可能是错的),可能有环(如果有环,而我们不区分是否已经被发现,就会进入死循环了);2. 灰色节点是边缘节点,为了保证是按照BFS的顺序进行搜索,需要使用队列来管理灰色节点。

    VertexNode * BFS(Graph *g, int pattern)
    {
        int i = 0;
        for(;i<g->verNum;i++)
        {
            VertexNode *t = g->vertices[i];
            t->visited = White;
            t->parent = NULL;
            t->distance = -1;
        }
        VertexNode *s = g->vertices[0];
        s->visited = Grey;
        s->distance = 0;
        queue<VertexNode *> _queue;
        _queue.push(s);
        while(!_queue.empty())
        {
            s = _queue.front();
            if(s->data == pattern)
                return s;
            _queue.pop();
            ArcNode *arc = s->firstArc;
            for(;arc!=NULL;)
            {
                if(arc->to->visited == White)
                {
                    if(arc->to->data == pattern)
                        return arc->to;
                    arc->to->visited = Grey;//在这里将to的颜色改为grey才是对的,否则有可能被重复加入到队列中,
                    arc->to->distance = s->distance + 1;
                    _queue.push(arc->to);
                }
                arc = arc->next;
            }
            s->visited = Black;//for 结束后,也就是所有的孩子都已经被遍历过了,所以这时可以把其颜色设为black
        }
        return NULL;
    }

    3. 深度优先搜索

    这就不用多说了,breadth-first-search是最简单、最基本的图搜索算法之一,必须掌握的。

    VertexNode * DFS(Graph *g, int pattern)
    {
        int i = 0;
        for(;i<g->verNum;i++)
        {
            VertexNode *t = g->vertices[i];
            t->visited = White;
            t->parent = NULL;
            t->distance = -1;
        }
        for(i = 0;i<g->verNum;i++)
        {
            VertexNode *t = g->vertices[i];
            if(t->visited == White)
            {
                t->distance=0;
                VertexNode *ret = DFS_visit(t, pattern);
                if(ret != NULL)
                    return ret;
            }
        }


        return NULL;
    }

    VertexNode * DFS_visit(VertexNode *starting, int pattern)
    {
        if(starting->data == pattern) return starting;
        starting->visited = Grey;
        ArcNode *arc = starting->firstArc;
        for(;arc!=NULL;)
        {
            if(arc->to->visited == White)
            {
                if(arc->to->data == pattern)
                    return arc->to;
                arc->to->distance = starting->distance+1;
                DFS_visit(arc->to,pattern);
            }
            arc = arc->to->firstArc;
        }
        starting->visited = Black;
    }

    4. 拓扑排序

    使用深度优先搜索,计算节点的发现开始和结束时间,然后按照结束时间排序,就是拓扑排序的结果。一个类似于拓扑排序的问题是,有若干需要执行的task,其中的某些task之间有依赖关系,求一个这些task的执行顺序。 输入的数据是若干task,然后每个task知道自己依赖于哪个task。如果使用典型的拓扑排序算法,要把task的依赖关系反转,使用另外一种数据结构temptask,temptask知道有哪些task依赖自己,也就是自己执行完后,哪些task可以开始执行。还有一个办法就是在原来数据结构的基础上,找到不依赖任何task的task,然后从其他task的依赖列表中将这个task删除,然后再重复这个过程,就可以得到task的一个执行顺序了。

    5. 有向图的强连通分支

    强连通分支的生成利用了强联通分支的重要性质:

    6. 最小生成树

    最小生成树是指在无向连通图中,一个边的子集,它连接了所有的顶点,并且其边的权值之和最小,这样的边形成的树称为最小生成树。最小生成树算法是用贪心算法来实现的,其算法核心是寻找生成树的安全边。根据定理,设割(S, V-S)是G的任意一个不妨害A的割,且边(u,v)是通过该割的一条轻边,则边(u,v)对集合A来说是安全的。证明过程看书吧。

    Kruskal算法:基于不相交集合的算法,从全局中选择边。步骤如下:

    1. 使用各个顶点为节点构造森林;
    2. 将所有的边按照权值排序;
    3. 对排序后的边分别检查边的两个端点是否在同一个集合中,如果不在,就将这条边加到森林中,并将其所连接的两个集合合并;

    下面是《算法导论》中给的图示:

    image

    Prim算法:从某个顶点出发选择边,步骤如下:

    1. 将所有顶点放到一个最小优先级队列中;
    2. 从这个队列中选择一个点u作为起点,计算这个点的边的权值,并把key(v)设置为边(u,v)的权值;
    3. 从优先级队列中拿出key值最小的节点,放入最小生成树中;(由于其他节点尚未计算,所以此时拿出的节点就是跟u相连的节点中边权值最小的那个)
    4. 不断重复2-3,使得最小优先级队列为空;

    7. 单源最短路径

    在这里我们只看Dijkstra算法,并且需要先了解松弛技术。

    松弛技术是指对图中的每个顶点设置一个属性d[v],用来描述从原点到v的最短路径上权值的上界。松弛一条边(u,v)的操作是这样的步骤,测试是否可以通过u对迄今为止找到的到v的最短路径进行改进, 如果可以,就更新d[v]和v的前驱,其伪代码如下:

    image

    Dijkstra算法的步骤:

    1. 初始化单源;
    2. 将所有定点加入到最小优先级队列中;
    3. 从中取出第一个元素u(d[u]最小的那个,第一次调用时返回值是单源);
    4. 将这个元素u放入最短路径集合中,并对所有u的邻边进行松弛,更新邻居节点的d值;
    5. 这时,回到第3步;
  • 相关阅读:
    作业四:结对编程项目---四则运算
    作业三: 代码规范、代码复审、PSP
    自动生成四则运算题目
    源程序版本管理软件和项目管理软件的优缺点
    学习进度表
    第一周随笔
    对之前问题的回答
    结对编程——四则运算
    PSP
    代码复审
  • 原文地址:https://www.cnblogs.com/whyandinside/p/2764902.html
Copyright © 2020-2023  润新知