图论–基础知识

在数学的分支图论中,(Graph)用于表示物件与物件之间的关系,是图论的基本研究对象。一张图由一些小圆点(称为顶点结点)和连结这些圆点的直线或曲线(称为)组成 -- 维基百科

图

  • 有向图&无向图:图中的边是否有方向。

  • 入度与出度(有向图的顶点),入度:以当前顶点为终点的边的数量;出度:以当前顶点为起点的边的数量

  • 路径:从u到v的一条路径是指一个序列v0,e1,v1,e2,v2,...ek,vk,其中ei的顶点为vi及vi - 1,k称作路径的长度。如果它的起止顶点相同,该路径是“闭”的,反之,则称为“开”的。一条路径称为一简单路径(simple path),如果路径中除起始与终止顶点可以重合外,所有顶点两两不等。边的序列

  • 环:起点与终点重合的路径;自环:一条边的两顶点相同

  • DAG:有向无环图

  • 图的存储

    • 邻接矩阵:使用一个二维数组g(i,j)表示图中各个顶点的联系。若边存在权值,则g(i,j)记录i -> j的相应权值;否则赋值为1,表示i - > j,存在边。

    • 邻接表:链式前向星(速度较vector实现更快)

      const int kN = 1010; 
      struct node{
          int e,ne,w;//终点,与当前边同起点的下一条边的索引,权值  
      }g[kN];
      int idx,n,h[kN];//idx 边的索引 h数组,存储各个头结点中 -- 索引最大的边(数组模拟链表,头插法) 
      void add(int a,int b,int c){//a - b,权值w 
          g[idx].e = b;
          g[idx].ne = h[a];
          g[idx].w = c;
          h[a] = idx++;//更新当前起始节点中边的最大索引 
      }
      int main(void){
          ios::sync_with_stdio(false);
          cin.tie(0);
          int a,b,w;
          memset(h, -1, sizeof h);
          cin >> n;
          for(int i = 0; i < n; i++){
              cin >> a >> b >> w;
              add(a,b,w);//有向图 
              //add(b,a,w);
          }
          for(int i = 1; i <= n; i++){//遍历图
              for(int j = h[i]; j != -1; j = g[j].ne){//遍历以i为起点所有边
                  cout << i << "->" << g[j].e << ' ' << g[j].w << endl; 
              }
          }
          return 0;
      } 
      
    • 十字链表

      • 在邻接表中存储了节点和它的出度信息,对于遍历某一点能到达的所有点,可以快速方便的求出。但是,对于到达这个点的所有点的计算不太方便
      • 结合逆邻接表,每个节点记录它的入度边和出度边
      • 顶点结点:data域:相关顶点信息;指向第一条以该顶点为终点的弧的指针;指向第一条以该顶点为起点的弧的指针。
      • 弧结点:tailvex,headvex:存储弧尾和弧头索引;taillink,headlink:存储下一条同弧尾和弧头的弧的索引,其它的如权值w....
  • DFS&BFS

    //dfs求最短路,遍历所有路径,维护min
    int g[kN][kN],ans = 0x3f3f3f3f;
    bool vis[kN][kN];
    void dfs(int s,int dist){//当前节点编号,所走过的路径
        if(dist > ans) return;
        if(s == n){
            if(ans > dist) ans = dist;
            return;
        }
        for(int i = 1; i <= n; i++){
            if(g[s][i] != -1 && !vis[s][i]){//s,i间未到达的边
                vis[s][i] = true;
                dfs(i, dist + g[s][i]);
                vis[s][i] = false;
            }
        }
    }
    //bfs,起始节点入队,按层搜索相邻节点 -- end
    #define first x//节点编号
    #define second dist//到此节点的最短路
    void vfs(){
        pair<int,int> cur,top;
        queue<pair<int,int> > q;
        q.push(make_pair(sx,sy));
        vis[sx] = true;
        while(!q.empty()){
            top = q.front();
            q.pop();
            if(top.x == end){
                cout << top.dist << endl;
                return;
            }
            cur.dist = top.dist + 1;
            for(int i = 1; i <= n; i++){
                if(g[top.x][i] != -1 && !vis[i]){
                    cur.x = i;
                    q.push(cur);
                    vis[i] = true;
                }
            }
        }
    } 
    
  • 最短路:

    a->b节点的最短路:1. 直接到达(相邻);2.借助其他顶点中转到达;

    • 边权为正的图,任意两顶点的最短路,不会经过重复的节点 or 边,路径中的节点数不会超过n,边不会超过n -1条。

    • 松弛:通过方法2,使路径变短的操作

    • 单源最短路:定点到其它顶点的最短路

    • Dijkstra算法(不能处理负权边 -- 有负环,贪心不成立 ):先选择一个距起始点最近的顶点加标记,通过这两点的最短路径去更新起点到达其它顶点的最短路(dist数组)。之后不断的选择当前未标记的距起点路径最短的点去松弛其他的点.--贪心:每次使用当前最短路径的点去松弛与它相邻的点。

    const int inf = 0x3f3f3f3f;
    int n,g[kN][kN],dist[kN];//dist数组存储定点到其它各点的最短路
    bool vis[kN];
    void dijkstra(int s,int e){
        memset(dist,0,sizeof dist);
        memset(vis,0,sizeof vis);
        dist[s] = 0;
        for(int i=0;i<n;i++){ // n 次循环
            int t = -1;
            for(遍历以i为起始节点的所有边,另一个结点j){  
                //找到当前未访问过的距定点路径最短的点
                //在未到达的点上 若有更小的dist 修改t 
                if(!vis[j] && (t == -1 || dist[t] > dist[j])) t = j;
            }
            vis[t] = true;
            //通过t点松弛它的相邻顶点
            for(int j=1;j<=n;j++){
                if(dist[j] < (dist[t]+g[t][j])) dist[j] = dist[t]+g[t][j];
            }
        } 
    }
    
    • Floyd算法(任意两点间的最短路,边权正负均可,借助每一个顶点去松弛其它的路径) -- 不能有负环
    void floyd(){
        for(int k=0;k<n;k++){//借助所有点中转 
            for(int i=0;i<n;i++){
                for(int j=0;j<n;j++){
                    g[i][j] = min(g[i][j],g[i][k] + g[k][j]);
                }
            }
        }
    }
    
    • Bellman-Ford(单源最短路 + 可以处理负权边)
      • 检测图中是否存在负权回路,若在进行了n - 1轮松弛之后,依然存在dis[v[i]] > dis[u[i]] + w[i],说明存在回路。n个顶点,n - 1条边。
    int u[kN],v[kN],W[kN],dist[kN];//U -> v 权值为w
    for(int k = 1; k <= n - 1; k++){
        for(int i = 1; i <= m; i++){//遍历m条边,可否通过u->v这条边缩短dist[v[i]] -- 松弛
            if(dist[v[i]] > dist[u[i]] + w[i]) dist[v[i]] = dist[u[i]] + w[i];
        }
    }
    
    • 一个假算法 -- SPFA(Bellman-Ford队列优化,可使用优先队列)
      • Bellman-Ford当前轮中松弛的点连接的边下一轮中才会有松弛操作 -- 使用队列维护这些引起松弛操作的结点
    struct node{
        int e,ne,w;
    }edge[kN];
    int head[kN],idx,n,m,t;
    //bfs判负环,入队次数大于n,复杂度有点高...
    bool spfa(){
        queue<int> q;
        q.push(s);
        vis[s] = true;//入队加标记,出队取消标记
        while(!q.empty()){
            int top = q.front();
            q.pop();
            vis(top) = false;
            for(int v = head[top]; v != -1; v= edge[v].ne){//遍历以top为起点的所有边
                if(relax(u,v) && !vis[v]){
                    q.push(v);
                    vis[v] = true;
                }
            }
        }
    }
    //SLF优化:优先队列优化的SPFA,权值高的元素排在队首 -- 有负权边:复杂度会卡到2的指数级
    //dfs版 判断(多次入队)及之后的退出上更快
    
  • 各算法对比 -- 《啊哈 !算法》

图论--基础知识

  • 最小生成树: 边权和最小的生成树O(mlogm)

    • 所有的顶点和边都属于图G的图称为G的子图。含有G的所有顶点的子图称为G的生成子图。
    • 生成树:包含连通图的所有顶点,任意两顶点间仅有一条通路。-- 对应到非连通图中为生成森林。
    • kruskal(贪心:从小到大加入边,如果加入这条边导致成环则不加入 --> 加入了n - 1条边)
    • 关于加入边导致成环的判断:使用并查集,查询两点是否联通 or 连接。
    • 先排序O(nlogn),并查集O(nlogm)
    int x,f[kN];
    struct edge{
        int form,to,value;
    } 
    int kruskal(edge *edges,int m){//边集 大小 
        sort(edges,edges + m,cmp);//按边权升序排列
        int r = 0;//权
        for(int i = 0; i < m; i++){
            if(find(deges[i].from) == find(edges[i].to)) continue;//成环,在同一连通块中
            r += edges[i].value;//边权加入权值和
            unionn(edges[i].from,edges[i].to);//合并连通块 
        } 
        return r;
    }
    
    • prim

      • 从一个顶点开始不断加点(与已加入的顶点间边权最小的):每次要选择距离最小的一个结点,以及用新的边更新其他结点的距离
      • 暴力,并查集维护,O(n^2):一个选择集合,一个边的集合。任一节点开始,枚举它可以到达的所有边,寻找最小权值,搜索这条边到达的节点...
      int edges[N][N],dist[N];
      bool vis[N];
      int prim(int n){
          vis[1] = true;//任一节点开始
          for(int i = 2; i <= n; i++) dist[i] = edges[1][i];
          int ret = 0;
          for(int i = 2; i <= n; i++){
              int x = 1;
              for(int j = 1; j <= n; j++){//寻找最小权值 
                  if(!vis[j] && dist[j] > dist[x]) x = j;
              }
              ret += dist[x];
              vis[x] = true;
              for(int j = 1; j <= n; j++){//更新dist x -> 其他节点 
                  if(!vis[j]) dist[j] = max(dist[j],edges[x][j]);
              }
          }
          return ret;
      } 
      
      • 其它更快的:二叉堆、fib堆,emm...这两个都不会

原文链接: https://www.cnblogs.com/honey-cat/p/12990057.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

    图论--基础知识

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/351596

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年3月2日 上午6:57
下一篇 2023年3月2日 上午6:58

相关推荐