图论数据结构和基础算法

图论数据结构和基础算法

1、数据结构(以下文章中V代表顶点数,E代表边数)

图常用的两种数据结构
  1. 邻接矩阵

    使用矩阵表示图的连接情况,对于无权图,我们可以用0代表两点之间不连接,1代表两点之间连接。对于有权图,我们可以定义一个极大值INF,来代表两个点不连接,使用权值代表两点连接,这样既存储了权值又存储了两点是否连接。

    //定义一个二维数组  用fill函数赋值
    int adj[MAX_V][MAX_V];
    fill(adj,adj+MAX_V*MAX_V,INF);
    

    优点: 可以在常数时间O(1)内判断两点时间是否有边存在

    缺点: 花费O(V^2)的存储空间,对于边很少的稀疏图空间有极大的浪费

    注意:如果图中某两点之间有重边或者某个顶点有环需要特殊处理;在无权图中,只需要设

    //adj[i][j]为顶点i到顶点j的边数即可
    

    有权图中,我们只能保存权值最大或权值最小的边即可(根据实际情况选择保存最大还是最小)。必须保存所有边时,可以采取邻接表

  2. 邻接表

    在邻接表中,是通过使用链表数组结构,把每个顶点相邻的边存储起来。所使用空间为O(V+E);

    //c++中可以用vector数组来表示邻接表
    vector<int> Graph[MAX_V];  //注意!!!这个相当于二维数组
    //当边上有属性情况下
    struct edge { int to, cost; }; //to代表与当前顶点相邻的顶点 cost代表权值
    vector<edge> Graph[MAX_V];
    //第三种结构
    struct vertex {
        vector<vertx*> edge; //指向与之相邻顶点的指针
        /*
        顶点属性
        */
    };
    vertex Graph[MAX_V];
    

2、基础算法

  1. 图的深度优先搜索(DFS)

    int adj[MAX_N][MAX_N];
    int vexs[n]; //存顶点向量信息
    void GraphDFS() {
        bool flag[n]; //标记这个顶点有没有访问过
        for(int i=0;i<V;i++) {
            flag[i] = false;
        }
        for(int i=0;i<V;i++) {
            if(!flag[i]) { //未访问过
                DFS(i);
            }
        }
    }
    
    void DFS(int i) {
        cout<<vexs[i]<<" "; //输出搜索顺序
        for(int j=0;j<V;j++) {
            if(adj[i][j] && !flag[i]) DFS(j);  //找到一个与之相邻并且未使用的顶点继续DFS
        }
    }
    
  2. 图的广度优先搜索(BFS)

    int que[V+2]; //用数组充当队列 节省空间
    int head,tail=; //head充当头结点,tail充当尾节点
    void GraphBFS() {
        bool flag[V]; //标志数组作用同上
        fill(flag,flag+V,false);
        for(int i=0;i<V;i++) {
            if(!flag[i]) { //元素未访问过
                cout<<vexs[i]<<" ";
                flag[i] = true;
                que[++tail] = i; //入队
                while(head<=tail) { //队列不空
                    int j=que[head++]; //出队
                    for(int k=0;k<V;k++) {
                        if(adj[j][k] && !flag[k]) { //与j相邻的元素且未访问过的顶点进行访问
                            cout<<vexs[i]<<" ";
                            flag[k] = 1;
                            que[++tail] = k; //k入队
                        }
                    }
                }
            }
        }
    }
    
  3. 单源最短路径(Bellman-Ford算法)存在负圈无法处理

    优点: 可以处理存在边的权值为负的图

    从一个点出发求到其他所有顶点的最短路径

    Bellman-Ford算法流程分为三个阶段:(这三个阶段摘自某位大佬的文章 由于过去时间久了忘了链接 实在抱歉)
    (1) 初始化:将除源点外的所有顶点的最短距离估计值 d[v] ←+∞, d[s] ←0;

    (2) 迭代求解:反复对边集E中的每条边进行松弛操作,使得顶点集V中的每个顶点v的最短距离估计值逐步逼近其最短距离;(运行|v|-1次)

    (3) 检验负权回路:判断边集E中的每一条边的两个端点是否收敛。如果存在未收敛的顶点,则算法返回false,表明问题无解;否则算法返回true,并且从源点可达的顶点v的最短距离保存在 d[v]中。

    详细说明:

    首先指出,图的任意一条最短路径既不能包含负权回路,也不会包含正权回路,因此它最多包含|v|-1条边。

    其次,从源点s可达的所有顶点如果 存在最短路径,则这些最短路径构成一个以s为根的最短路径树。Bellman-Ford算法的迭代松弛操作,实际上就是按顶点距离s的层次,逐层生成这棵最短路径树的过程。

    在对每条边进行1遍松弛的时候,生成了从s出发,层次至多为1的那些树枝。也就是说,找到了与s至多有1条边相联的那些顶点的最短路径;对每条边进行第2遍松弛的时候,生成了第2层次的树枝,就是说找到了经过2条边相连的那些顶点的最短路径……。因为最短路径最多只包含|v|-1 条边,所以,只需要循环|v|-1 次。

    每实施一次松弛操作,最短路径树上就会有一层顶点达到其最短距离,此后这层顶点的最短距离值就会一直保持不变,不再受后续松弛操作的影响。(但是,每次还要判断松弛,这里浪费了大量的时间,怎么优化?单纯的优化是否可行?)

    如果没有负权回路,由于最短路径树的高度最多只能是|v|-1,所以最多经过|v|-1遍松弛操作后,所有从s可达的顶点必将求出最短距离。如果 d[v]仍保持 +∞,则表明从s到v不可达。

    如果有负权回路,那么第 |v|-1 遍松弛操作仍然会成功,这时,负权回路上的顶点不会收敛。

    /*
    算法核心思想:先存入图中所有边的信息包括{起点,终点,权值},在定义一个存最短距离的数组d[MAX_V],初始化为INF代表从固定点到第i个顶点无路。
    之后进入一个死循环中,在死循环中(该死循环最多执行n-1次),对所有边进行遍历使最短距离数组d[MAX_N]不断更新最短距离(不断加入新的路径,不断更新最短距离),直到不再更新退出死循环,程序结束。  时间复杂度为O(V*E)
    */
    struct edge { int from, to, cost; };
    edge es[MAX_V]; 
    int d[MAX_V];
    int V,E; //V是顶点数,E是边数
    
    void find_shortest_path(int s) { //s是源点
        fill(d,d+MAX_V,INF);
        d[s] = 0; //一定要让从源点到源点最短距离为0 不然下面将进入循环无法跳出
        while(1) {
            bool updata = false; //作为d[MAX_N]是否更新的标记
            for(int i=0;i<E;i++) {
                edge e = es[i];  
                //d[e.from]!=INF 表示的是 从源点s到e.from这条边的起点有路
                //d[e.to] > d[e.from] + e.cost 代表 走e这条边比之前到达顶点e.to
                //更近  即需要更新d[MAX_N]
                if(d[e.from]!=INF && d[e.to] > d[e.from] + e.cost){
                    d[e.to] = d[e.from] + e.cost;
                    updata = true;
                }
            }
            if(!updata) break; //如果不再更新代表已经找好了最短路径或者无路可以到达 跳出死循环
        }
    }
    
    //还可以修改这个算法判断图中是否存在负圈(即权值为负的圈)
    //如果图中不存在从s可达的负圈,那么最短路径不会经过同一个顶点两次(也就是说,最多通过V-1条边,while最多执行V-1次) 如果执行了超过V-1次代表存在负圈
    
    bool find_negative_loop() { //存在负圈返回true
        fill(d,d+MAX_V,0); //最短距离数组初始化为0
        
        for(int i=0;i<V;i++) {
            for(int j=0;j<E;j++) {
                edge e = es[j];
                if(d[e.to] > d[e.from] + e.cost) {
                    d[e.to] = d[e.from] + e.cost;
                    
                    if(i == V-1) return true; //第V次更新了 存在负圈
                }
            }
        }
        return false;
    }
    
  4. SPFA(堆优化的Bellman-Ford算法)(国际和国内并不怎么认可这个算法,仅供了解不详细介绍)

    #include<bits/stdc++.h>
    using namespace std;
    const int MAX_N = 9999;
    const int INF = 9999;
    int adj[MAX_N][MAX_N]; //SPFA用邻接表存储图求最短路径
    bool flag[MAX_N]; //记录顶点是在队列中
    int d[MAX_N]; //最短路径
    int num[MAX_N]; 
    //记录每个点的入队次数 如果有一个点入队次数超过n次(就是顶点数)那么证明图中存在负圈,SPFA就无法求解
    
    
    bool SPFA(int s,int n) {
        //先进行基本的初始化工作  使用从大到小排序的优先队列
        priority_queue<int,vector<int>,greater<int>>  q;
        fill(flag,flag+n,false);
        fill(num,num+n,0);
        fill(d,d+n,INF);
        d[s] = 0;
        q.push(s);
        flag[s] = true; //s在队列中
        num[s]++;
        while(!q.empty()) {
            int p = q.top(); q.pop(); //出队
            for(int i=0;i<n;i++) {
                if(d[i] > d[p] + adj[p][i]) { //存在更短的路径
                    d[i] = d[p] + adj[p][i];
                    if(!flag[i]) { //如果i顶点不在队列中
                        q.push(i); //入队
                        num[i]++;  //入队次数增加
                        if(num[i]>n) {  //存在负环 无法求解
                            return false;
                        }
                        flag[i] = true;
                    }
                }
            }
            flag[p] = false; //p顶点出队了 不在队列中
        }
        return true;
    }
    
  5. Dijkstra算法(求单源最短路径问题)无法对存在负权值边的图求解 时间复杂度为O(V^2) 时间复杂度只与顶点数量有关 适合稠密图(边多的图 V~E^2)

    /*
    算法核心思想:
    (1) 找到最短距离已经确定的顶点,更新与这个节点相邻的顶点的最短路径
    (2) 此后就不再关心(1)中已经确定最短距离的顶点
    */
    int adj[MAX_N][MAX_N];  //使用邻接矩阵
    bool used[MAX_N];
    int d[MAX_N];
    
    void Dijkstra(int s) {
        fill(d,d+n,INF); //初始化
        fill(used,used+n,false); 
        d[s] = 0;
        while(true) { //可以改写为for(int k=0;k<V-1;k++) 不存在负圈至多执行V-1次
            int v = -1;  //用于寻找used为false且最短距离最小的点
            for(int i=0;i<n;i++) {
                if(!used[i] && (v==-1 || d[i] < d[v])) v=i;
            }
            if(v==-1) break;  //所有顶点都找完了
            used[v] = true;
            for(int j=0;j<n;j++) {  //用新确定的最短路径的点更新其余最短路径
                d[j] = min(d[j],d[v]+adj[v][j]);
            }
        }
    }  
    //if(d[n] == INF) 说明两点不连通 做题时要考虑进去
    
  6. Dijkstra算法(堆优化优先队列)时间复杂度为O(E log|V|) 适合稀疏图 V ~E同级 使用优先队列实现由于其本身优先队列采取的是冗余 会使时间复杂度变为O(E log|E|) 手写堆 则时间复杂度不变 但由于E和V同级 所以优先使用STL的优先队列

    struct edge {int to, cost;};  //边的结构
    typedef pair<int,int> P; 
    int V; 
    vector<edge> Graph[MAX_N];
    int d[MAX_N];
    void Dijkstraqueue(int s) {
        fill(d,d+V,INF); //初始化
        d[s] = 0;
        priority_queue< P,vector<P>,greater<P> > que;
        que.push(P(0,s)); //让起点入队
        while(!que.empty()) { //只要队列不空就循环
            P p=que.top(); que.pop();
            int v = p.second; //记录路径最短的顶点
            if(d[v]<p.first) continue; //如果这个点已经更新过最短路径 那么继续下一次循环就好
            for(int i=0;i<Graph[v].size();i++) {   //更新v点对邻接点的影响
                edge e = Graph[v][i];
                if(d[e.to] > d[v] + e.cost) {
                    d[e.to] = d[v] + e.cost;
                    que.push(P(d[e.to],e.to));
                }
            }
        }
    }
    
  7. Floyd-Warshall算法(多源最短路径求解 时间复杂度为O(V^3))

    //简单的动态规划
    int d[MAX_V][MAX_V]; //存图的邻接矩阵
    int V;  //顶点数
    
    void Floyd_Warshall() {
        for(int k=0;k<V;k++)  //k是dp的中间状态所以需要先循环   
            for(int i=0;i<V;i++)
                for(int j=0;j<V;j++) {
                    d[i][j] = min(d[i][j], d[i][k]+d[k][j]);
                } //循环结束后 d[i][j] 存放的就是i-->j最短路径
    }
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值