美文网首页IOS个人开发C语言程序员
算法与数据结构(七) 图论

算法与数据结构(七) 图论

作者: 天涯明月笙 | 来源:发表于2017-09-17 17:29 被阅读150次

    图论 Graph Theory

    图论并不是研究图画。而是研究由节点和边所构成的数学模型

    图论抽象模型

    万事开头难,虽然看上去很复杂,但是慢慢学习深入之后会体会到他的魅力

    很多信息之间的联系都可以使用图来表示。数据可视化

    节点 & 边
    • 交通运输
    • 社交网络
    • 互联网
    • 工作安排
    • 脑区活动
    • 程序状态执行

    每个节点是城市,边是道路。点是航站楼,边是航线.
    社交网络 - 人 & 好友关系 电影 & 电影相似程度
    互联网 域名 域名跳转
    工作安排 :先后程度
    自动机

    图的分类:

    • 无向图(Undirected Graph)
    • 有向图(Directed Graph)

    人和人认识就是边。

    自动机转移有方向

    有向图

    以无向图为主。无向图是一种特殊的有向图

    • 无权图
    • 有权图

    边:认识不认识。 好友度
    边带值:相似程度 & 路长

    图的连通性:

    三图或者一图

    可以看做是三张图。也可以视为一个。
    图不是完全连通的。

    简单图(simple Graph):

    平行边 & 自环边

    考虑最小生成树。连通性。这两种边意义不大
    简单图:不包含。

    图的表示

    对于边的表示采用什么样的数据结构:

    邻接矩阵

    n个节点 就是一个n行n列的矩阵,无向图对角线对称:1表示连通。0表示没通

    有向图的矩阵表示:

    有向图

    邻接表:

    对于每个顶点只表达与这个顶点相连接的信息

    邻接表

    每一行都是一个链表,存放了对这一行而言相连的节点。

    有向图的邻接表

    优点:存储空间小

    邻接表适合表示稀疏图 (Sparse Graph)
    邻接矩阵适合表示稠密图 (Dense Graph)

    邻接表适合表示稀疏图,邻接矩阵适合表示稠密图。
    多少:边多。

    边的个数 与 能拥有的最大边的个数对比。

    如下图就是一个稀疏图:

    稀疏图 稠密图完全图

    所有的点之间都有边。

    每个电影和其他所有电影之间的相似度。不管相似大小都是连接的边。

    编码:

    // 稠密图 - 邻接矩阵
    class DenseGraph{
    
    private:
        int n, m;       // 节点数和边数
        bool directed;  // 是否为有向图
        vector<vector<bool>> g; // 图的具体数据
    
    public:
        // 构造函数
        DenseGraph( int n , bool directed ){
            assert( n >= 0 );
            this->n = n;
            this->m = 0;    // 初始化没有任何边
            this->directed = directed;
            // g初始化为n*n的布尔矩阵, 每一个g[i][j]均为false, 表示没有任和边
            //g = vector<vector<bool>>(n, vector<bool>(n, false));
              for (int i = 0; i < n; ++i) {
                g.push_back(vector<bool>(n, false));
            }
        }
    
        ~DenseGraph(){ }
    
        int V(){ return n;} // 返回节点个数
        int E(){ return m;} // 返回边的个数
    
        // 向图中添加一个边
        void addEdge( int v , int w ){
    
            assert( v >= 0 && v < n );
            assert( w >= 0 && w < n );
    
            if( hasEdge( v , w ) )
                return;
    
            g[v][w] = true;
            if( !directed )
                g[w][v] = true;
    
            m ++;
        }
    
        // 验证图中是否有从v到w的边
        bool hasEdge( int v , int w ){
            assert( v >= 0 && v < n );
            assert( w >= 0 && w < n );
            return g[v][w];
        }
    };
    
    

    addedge的时候已经去除了平行边的概念。因为如果有边之间return
    O(1)来判断是否有边

    稀疏图编码

    // 稀疏图 - 邻接表
    class SparseGraph{
    
    private:
        int n, m;       // 节点数和边数
        bool directed;  // 是否为有向图
        vector<vector<int>> g;  // 图的具体数据
    
    public:
        // 构造函数
        SparseGraph( int n , bool directed ){
            assert( n >= 0 );
            this->n = n;
            this->m = 0;    // 初始化没有任何边
            this->directed = directed;
            // g初始化为n个空的vector, 表示每一个g[i]都为空, 即没有任和边
            //g = vector<vector<int>>(n, vector<int>());
            for (int i = 0; i < n; ++i) {
                g.push_back(vector<int>());
            }
        }
    
        ~SparseGraph(){ }
    
        int V(){ return n;} // 返回节点个数
        int E(){ return m;} // 返回边的个数
    
        // 向图中添加一个边
        void addEdge( int v, int w ){
    
            assert( v >= 0 && v < n );
            assert( w >= 0 && w < n );
    
            g[v].push_back(w);
            //处理自环边
            if( v != w && !directed )
                g[w].push_back(v);
    
            m ++;
        }
    
        // 验证图中是否有从v到w的边
        bool hasEdge( int v , int w ){
    
            assert( v >= 0 && v < n );
            assert( w >= 0 && w < n );
    
            //使用邻接表在判断解决平行边复杂度高
            for( int i = 0 ; i < g[v].size() ; i ++ )
                if( g[v][i] == w )
                    return true;
            return false;
        }
    };
    

    邻接表取消平行边复杂度度过大。

    邻接表的优势

    遍历邻边 - 图算法中最常见的操作

    遍历邻边,邻接表有优势
      // 邻边迭代器, 传入一个图和一个顶点,
        // 迭代在这个图中和这个顶点向连的所有顶点
        class adjIterator{
        private:
            SparseGraph &G; // 图G的引用
            int v;
            int index;
    
        public:
            // 构造函数
            adjIterator(SparseGraph &graph, int v): G(graph){
                this->v = v;
                this->index = 0;
            }
    
            ~adjIterator(){}
    
            // 返回图G中与顶点v相连接的第一个顶点
            int begin(){
                index = 0;
                if( G.g[v].size() )
                    return G.g[v][index];
                // 若没有顶点和v相连接, 则返回-1
                return -1;
            }
    
            // 返回图G中与顶点v相连接的下一个顶点
            int next(){
                index ++;
                if( index < G.g[v].size() )
                    return G.g[v][index];
                // 若没有顶点和v相连接, 则返回-1
                return -1;
            }
    
            // 查看是否已经迭代完了图G中与顶点v相连接的所有顶点
            bool end(){
                return index >= G.g[v].size();
            }
        };
    
         // 邻边迭代器, 传入一个图和一个顶点,
        // 迭代在这个图中和这个顶点向连的所有顶点
        class adjIterator{
        private:
            DenseGraph &G;  // 图G的引用
            int v;
            int index;
    
        public:
            // 构造函数
            adjIterator(DenseGraph &graph, int v): G(graph){
                assert( v >= 0 && v < G.n );
                this->v = v;
                this->index = -1;   // 索引从-1开始, 因为每次遍历都需要调用一次next()
            }
    
            ~adjIterator(){}
    
            // 返回图G中与顶点v相连接的第一个顶点
            int begin(){
    
                // 索引从-1开始, 因为每次遍历都需要调用一次next()
                index = -1;
                return next();
            }
    
            // 返回图G中与顶点v相连接的下一个顶点
            int next(){
    
                // 从当前index开始向后搜索, 直到找到一个g[v][index]为true
                for( index += 1 ; index < G.V() ; index ++ )
                    if( G.g[v][index] )
                        return index;
                // 若没有顶点和v相连接, 则返回-1
                return -1;
            }
    
            // 查看是否已经迭代完了图G中与顶点v相连接的所有顶点
            bool end(){
                return index >= G.V();
            }
        };
    

    算法封装成类

    图的文件表示

    用文件来表示
    第一行有多少节点,多少边。然后节点对:表示这两个节点间有边。

    template <typename Graph>
    class ReadGraph{
    
    public:
        // 从文件filename中读取图的信息, 存储进图graph中
        ReadGraph(Graph &graph, const string &filename){
    
            ifstream file(filename);
            string line;
            int V, E;
    
            assert( file.is_open() );
    
            // 第一行读取图中的节点个数和边的个数
            assert( getline(file, line) );
            stringstream ss(line);
            ss>>V>>E;
    
            assert( V == graph.V() );
    
            // 读取每一条边的信息
            for( int i = 0 ; i < E ; i ++ ){
    
                assert( getline(file, line) );
                stringstream ss(line);
    
                int a,b;
                ss>>a>>b;
                assert( a >= 0 && a < V );
                assert( b >= 0 && b < V );
                graph.addEdge( a , b );
            }
        }
    };
    

    运行结果:

    运行结果

    图的遍历

    • 深度优先遍历
    • 广度优先遍历
    图:深度优先遍历

    一个点往下试。记录是否遍历过。

    0-1-2-5-3-4-6

    连通分量

    遍历完成后看没遍历过的点

    // 求无权图的联通分量
    template <typename Graph>
    class Component{
    
    private:
        Graph &G;       // 图的引用
        bool *visited;  // 记录dfs的过程中节点是否被访问
        int ccount;     // 记录联通分量个数
        int *id;        // 每个节点所对应的联通分量标记
    
        // 图的深度优先遍历(递归方式)
        void dfs( int v ){
    
            visited[v] = true;
            id[v] = ccount;
            typename Graph::adjIterator adj(G, v);
            for( int i = adj.begin() ; !adj.end() ; i = adj.next() ){
                if( !visited[i] )
                    dfs(i);
            }
        }
    
    public:
        // 构造函数, 求出无权图的联通分量
        Component(Graph &graph): G(graph){
    
            // 算法初始化
            visited = new bool[G.V()];
            id = new int[G.V()];
            ccount = 0;
            for( int i = 0 ; i < G.V() ; i ++ ){
                visited[i] = false;
                id[i] = -1;
            }
    
            // 求图的联通分量
            for( int i = 0 ; i < G.V() ; i ++ )
                if( !visited[i] ){
                    dfs(i);
                    ccount ++;
                }
        }
    
        // 析构函数
        ~Component(){
            delete[] visited;
            delete[] id;
        }
    
        // 返回图的联通分量个数
        int count(){
            return ccount;
        }
    
        // 查询点v和点w是否联通
        bool isConnected( int v , int w ){
            assert( v >= 0 && v < G.V() );
            assert( w >= 0 && w < G.V() );
            return id[v] == id[w];
        }
    };
    

    main.cpp

    // 测试图的联通分量算法
    int main() {
    
        // TestG1.txt
        string filename1 = "testG1.txt";
        SparseGraph g1 = SparseGraph(13, false);
        ReadGraph<SparseGraph> readGraph1(g1, filename1);
        Component<SparseGraph> component1(g1);
        cout<<"TestG1.txt, Component Count: "<<component1.count()<<endl;
    
        cout<<endl;
    
        // TestG2.txt
        string filename2 = "testG2.txt";
        SparseGraph g2 = SparseGraph(7, false);
        ReadGraph<SparseGraph> readGraph2(g2, filename2);
        Component<SparseGraph> component2(g2);
        cout<<"TestG2.txt, Component Count: "<<component2.count()<<endl;
    
        return 0;
    }
    

    运行结果:

    运行结果

    求出连通分量,可以求出两个结点是否相连
    添加id来记录。

     int *id;        // 每个节点所对应的联通分量标记
    

    并查集只能让我们知道两个结点是否相连。而路径需要图论来解决

    获得两点间的一条路径。

    两点间路径

    深度优先获得一条路径。遍历的同时保存是从哪遍历过来的。

     int * from;     // 记录路径, from[i]表示查找的路径上i的上一个节点
    
    // 路径查询
    template <typename Graph>
    class Path{
    
    private:
        Graph &G;   // 图的引用
        int s;      // 起始点
        bool* visited;  // 记录dfs的过程中节点是否被访问
        int * from;     // 记录路径, from[i]表示查找的路径上i的上一个节点
    
        // 图的深度优先遍历
        void dfs( int v ){
    
            visited[v] = true;
    
            typename Graph::adjIterator adj(G, v);
            for( int i = adj.begin() ; !adj.end() ; i = adj.next() ){
                if( !visited[i] ){
                    from[i] = v;
                    dfs(i);
                }
            }
        }
    
    public:
        // 构造函数, 寻路算法, 寻找图graph从s点到其他点的路径
        Path(Graph &graph, int s):G(graph){
    
            // 算法初始化
            assert( s >= 0 && s < G.V() );
    
            visited = new bool[G.V()];
            from = new int[G.V()];
            for( int i = 0 ; i < G.V() ; i ++ ){
                visited[i] = false;
                from[i] = -1;
            }
            this->s = s;
    
            // 寻路算法
            dfs(s);
        }
    
        // 析构函数
        ~Path(){
    
            delete [] visited;
            delete [] from;
        }
    
        // 查询从s点到w点是否有路径
        bool hasPath(int w){
            assert( w >= 0 && w < G.V() );
            return visited[w];
        }
    
        // 查询从s点到w点的路径, 存放在vec中
        void path(int w, vector<int> &vec){
    
            assert( hasPath(w) );
    
            stack<int> s;
            // 通过from数组逆向查找到从s到w的路径, 存放到栈中
            int p = w;
            while( p != -1 ){
                s.push(p);
                p = from[p];
            }
    
            // 从栈中依次取出元素, 获得顺序的从s到w的路径
            vec.clear();
            while( !s.empty() ){
                vec.push_back( s.top() );
                s.pop();
            }
        }
    
        // 打印出从s点到w点的路径
        void showPath(int w){
    
            assert( hasPath(w) );
    
            vector<int> vec;
            path( w , vec );
            for( int i = 0 ; i < vec.size() ; i ++ ){
                cout<<vec[i];
                if( i == vec.size() - 1 )
                    cout<<endl;
                else
                    cout<<" -> ";
            }
        }
    };
    

    main.cpp:

    // 测试寻路算法
    int main() {
    
        string filename = "testG.txt";
        SparseGraph g = SparseGraph(7, false);
        ReadGraph<SparseGraph> readGraph(g, filename);
        g.show();
        cout<<endl;
    
        Path<SparseGraph> path(g,0);
        cout<<"Path from 0 to 6 : " << endl;
        path.showPath(6);
    
        return 0;
    }
    

    运行结果:

    运行结果

    复杂度:

    • 稀疏图(邻接表): O( V + E )
    • 稠密图(邻接矩阵):O( V^2 )

    想获得一个节点的所有相邻节点的时候,我们要将图中所有节点扫一遍

    深度优先遍历算法对有向图依然有效

    查看环:有向图

    图的广度优先遍历

    使用队列。

    队列

    队列为空。距离0节点的距离排序
    层序遍历。<=

    记录距离。from。

    广度优先遍历:求出了无权图的最短路径。

    代码实现:

    // 寻找无权图的最短路径
    template <typename Graph>
    class ShortestPath{
    
    private:
        Graph &G;       // 图的引用
        int s;          // 起始点
        bool *visited;  // 记录dfs的过程中节点是否被访问
        int *from;      // 记录路径, from[i]表示查找的路径上i的上一个节点
        int *ord;       // 记录路径中节点的次序。ord[i]表示i节点在路径中的次序。
    
    public:
        // 构造函数, 寻找无权图graph从s点到其他点的最短路径
        ShortestPath(Graph &graph, int s):G(graph){
    
            // 算法初始化
            assert( s >= 0 && s < graph.V() );
    
            visited = new bool[graph.V()];
            from = new int[graph.V()];
            ord = new int[graph.V()];
            for( int i = 0 ; i < graph.V() ; i ++ ){
                visited[i] = false;
                from[i] = -1;
                ord[i] = -1;
            }
            this->s = s;
    
            // 无向图最短路径算法, 从s开始广度优先遍历整张图
            queue<int> q;
    
            q.push( s );
            visited[s] = true;
            ord[s] = 0;
            while( !q.empty() ){
    
                int v = q.front();
                q.pop();
    
                typename Graph::adjIterator adj(G, v);
                for( int i = adj.begin() ; !adj.end() ; i = adj.next() )
                    if( !visited[i] ){
                        q.push(i);
                        visited[i] = true;
                        from[i] = v;
                        ord[i] = ord[v] + 1;
                    }
            }
    
        }
    
        // 析构函数
        ~ShortestPath(){
    
            delete [] visited;
            delete [] from;
            delete [] ord;
        }
    
        // 查询从s点到w点是否有路径
        bool hasPath(int w){
            assert( w >= 0 && w < G.V() );
            return visited[w];
        }
    
        // 查询从s点到w点的路径, 存放在vec中
        void path(int w, vector<int> &vec){
    
            assert( w >= 0 && w < G.V() );
    
            stack<int> s;
            // 通过from数组逆向查找到从s到w的路径, 存放到栈中
            int p = w;
            while( p != -1 ){
                s.push(p);
                p = from[p];
            }
    
            // 从栈中依次取出元素, 获得顺序的从s到w的路径
            vec.clear();
            while( !s.empty() ){
                vec.push_back( s.top() );
                s.pop();
            }
        }
    
        // 打印出从s点到w点的路径
        void showPath(int w){
    
            assert( w >= 0 && w < G.V() );
    
            vector<int> vec;
            path(w, vec);
            for( int i = 0 ; i < vec.size() ; i ++ ){
                cout<<vec[i];
                if( i == vec.size()-1 )
                    cout<<endl;
                else
                    cout<<" -> ";
            }
        }
    
        // 查看从s点到w点的最短路径长度
        int length(int w){
            assert( w >= 0 && w < G.V() );
            return ord[w];
        }
    };
    
    

    main.cpp:

    // 测试无权图最短路径算法
    int main() {
    
        string filename = "testG.txt";
        SparseGraph g = SparseGraph(7, false);
        ReadGraph<SparseGraph> readGraph(g, filename);
        g.show();
        cout<<endl;
    
        // 比较使用深度优先遍历和广度优先遍历获得路径的不同
        // 广度优先遍历获得的是无权图的最短路径
        Path<SparseGraph> dfs(g,0);
        cout<<"DFS : ";
        dfs.showPath(6);
    
        ShortestPath<SparseGraph> bfs(g,0);
        cout<<"BFS : ";
        bfs.showPath(6);
    
        return 0;
    }
    

    运行结果:

    广度优先一定可以找到最短无权路径

    广度优先一定可以找到最短无权路径,深度也有可能找到。
    图的存储,边加入的顺序。多条最短路径。跟遍历顺序有关。

    更多算法:

    flood fill

    抠图 图遍历

    图的最短路径。

    扫雷走迷宫本质是一颗树。能不能走出去就是两点能否连通。最短路径。

    迷宫生成 是一个生成树的过程

    选定入口与出口:选择一部分红色

    迷宫生成

    相关文章

      网友评论

        本文标题:算法与数据结构(七) 图论

        本文链接:https://www.haomeiwen.com/subject/zevpsxtx.html