zoukankan      html  css  js  c++  java
  • 图论 最短路 基础

    图论基础 , 最短路

    图的简单概念

    顶点 (Vertex), 边 (Edge)

    有向图 , 无向图 , 无向图是一种特殊的有向图

    ,有向图分为出度 和 入度,无向图的度,代表 连出去的边

    顶点都可以具有属性,称为权重,顶点称为 点权,边 称为 边权

    稠密图 边很多,大约是 顶点的平方

    **稀疏图 ** 边很少 ,

    重边(平行边),自环,

    路径:从一给顶点到达另一个顶点称为一条路径

    路径中边的数量称为路径长度,如果路径中的顶点均不重复,称为简单路径

    如果路径中的第一个顶点 (v_i) 和最后一个顶点 (v_j) 是同一个顶点,则称这条路径为回路

    连通 : 两个点连通,指 u 和 v 相互可达

    图连通 :无向图 任意两个节点 相互可达 ,

    强连通 : 有向图 任意 两个节点 相互可达

    图的存储结构

    • 邻接矩阵 (二维数组) 对于 n 个顶点,需要 (O(n^2)) 空间

      • 严重浪费空间,适用于 稠密图 , 或者是 ,题目的输入 已矩阵的方式给出

      • G[N] [N]

        #include <iostream>
        #include <cstring>
        using namespace std;
        const int N = 1e3 + 233,INF = 0x3f3f3f3f; //INF 最大值
        int g[N][N];
        int main(){
            int n,m,x,y,w;
            memset(g,0x3f,sizeof g); // 初始化图,全都不联通
            cin >> n >> m;
            while(m--){
                cin >> x >> y >> w;
                g[x][y] = w; // 单向边,x to y ,权重 为 w
                g[y][x] = w; // 两条都写,等于双向边, 图中都为双向边,就成无向图了。
            }
            for(int i = 1;i <= n; ++i){
                for(int j = 1;j <= n; ++j){
                    cout << g[i][j] <<" ";
                }
                cout << endl;
            }
            return 0;
        }
        
    • 邻接表

      对图中的每个顶点都建立一个单链表,存储这个顶点的连出的点

      • vector Adj[N];

      • vector 自身底层函数的缺陷, 每次扩大空间,都会copy一边,然后扩大原来空间的两倍

      • vector G[]:
        优点:
        1.写起来比链式前向星快(大概
        2.每个顶点的出边都是用vector存储的,方便执行一些STL中的函数(比如排序)
        缺点
        1.STL会略慢一些
        2.浪费空间,由于vector申请空间的方式是两倍扩容,遇到卡空间的题目的时候会跪

        #include <iostream>
        #include <cstring>
        #include <vector>
        using namespace std;
        const int N = 1e3 + 233,INF = 0x3f3f3f3f;
        struct edge
        {
            int node ,weight;
            edge(int node_,int weight_)://构造函数,让我们可以直接给结构体赋值
                node(node_),weight(weight_){}
        };
        vector<edge> v[N];
        int n,m;
        int main()
        {
            cin >> n >> m;
          while(m--)
            {
                int x,y,w;
                cin >> x >> y >> w;
                v[x].push_back(edge(y,w));//模拟链表,存边
                //v[y].push_back(edge(x,w));//双向
            }
            for(int i = 1;i <= n; ++i){
                for(int j = 0;j < v[i].size(); ++j){
                    cout << i << " " << v[i][j].node <<" " << v[i][j].weight <<"  ";
                }
                cout << endl;
            }
            return 0;
        }
        
    • 链式前向星 (数组模拟邻接表)

      • 先学会数组模拟链表,就会数组模拟邻接表了
      • 避免了copy时间的浪费,非常快速
      • 主要使用这种存图结构

      前置知识,数组模拟链表(头插法)

      #include <iostream>
      #include <cstring>
      #include <vector>
      using namespace std;
      const int N = 1e3 + 233,INF = 0x3f3f3f3f;
      int e[N],ne[N],h,idx; 
      // head 表示头结点的下标
      // e[i] 表示节点i的值
      // ne[i] 表示节点i的next指针是多少
      // idx 存储当前已经用到了哪个点
      void init(){ // 初始化为头节点 为 -1
          h = -1,idx = 0;
      }
      void add(int x){
          e[idx] = x;// 保存x的值 到 idx 这个指针的位置
          ne[idx] = h;// idx 的下一个指针 为 头节点
          h = idx++; // 头节点 = idx 指针  , idx ++
      }
      int main(){
          int n ,x;
          cin >> n;
          init();
          while(n--){
              cin >> x;
              add(x);
          }
          for(int i = h;i != -1; i = ne[i]){
              cout << e[i] << " ";
          }
          return 0;
      }
      
      #include <iostream>
      #include <cstring>
      #include <vector>
      using namespace std;
      const int N = 1e3 + 233,INF = 0x3f3f3f3f;
      int e[N],ne[N],h[N],w[N],idx;//数组模拟邻接表, h代表 n个头节点
      // e存每条边,ne指向下一条边,h是链表头,w是每个顶点的权重,idx是索引值
      void add(int a,int b,int c){// 采用头插法
          e[idx] = b;
          ne[idx] = h[a];
          w[b] = c;
          h[a] = idx++;
      }
      int n,m;
      int main()
      {
          memset(h,-1,sizeof h);//初始化头节点,全为-1
          int a,b,c;
          cin>>n>>m;
          while(m--)
          {
              cin >> a >> b >> c;
              add(a,b,c); // 单向边
              //add(b,a,c) 双向
          }
          for(int i = 0;i < n; ++i){
              for(int j = h[i];j != -1;j = ne[j]){
                  int v = e[j];
                  cout << i <<" " <<v<<" "<<w[v]<<" ";
              }
              cout << endl;
          }
          return 0;
      }
      

    图的遍历

    DFS (求连通块),求欧拉回路 与 哈密顿回路

    例题 : https://vjudge.net/problem/UVA-572

    • 用 dfs 朝 八个方向 搜 ,每找到一个 ,做一次标记,最后输出连通块的数目
    #include <iostream>
    #include <cstring>
    using namespace std;
    const int N = 233;
    char g[N][N];
    int n,m,ans;
    int dx[8] = {0, 0,1,1, 1,-1,-1,-1}; // 方向数组 
    int dy[8] = {1,-1,0,1,-1, 0, 1,-1};
    bool pd(int x,int y){ // 判断是否越界
        if(x >= 1 && x <= m && y >= 1 && y <= n) return 1;
        else return 0;
    }
    void dfs(int x,int y){ // dfs求连通块
        for(int i = 0;i < 8; ++i){
            int nx = x + dx[i];
            int ny = y + dy[i];
            if(pd(nx,ny) && g[nx][ny] == '@'){
                g[nx][ny] = '*';
                dfs(nx,ny);
            }
        }
    }
    int main(){
        ios::sync_with_stdio(0);
        cin.tie(0),cout.tie(0);
        while(cin >> m >> n){
            if(m == 0 && n == 0) break;
            memset(g,0,sizeof g);
            ans = 0;
            for(int i = 1;i <= m; ++i){
                for(int j = 1;j <= n; ++j)
                    cin >> g[i][j];
            }
            for(int i = 1;i <= m; ++i){
                for(int j = 1;j <= n; ++j){
                    if(g[i][j] == '@'){
                        ans++; // 连通块个数++
                        g[i][j] = '*';
                        dfs(i,j);
                    }
                }
            }
            cout << ans << endl;
        }
        return 0;
    }
    

    BFS (边权为 1 的最短路算法 ,队列实现 )

    例题 : https://vjudge.net/problem/OpenJ_Bailian-3752

    • 用队列 扩展 ,如果找到 出口,直接跳出即可
    #include <iostream>
    #include <cstring>
    #include <queue>
    using namespace std;
    const int N = 50;
    char g[N][N];
    int r,c,ans;
    int dx[4] = {0,0,1,-1};
    int dy[4] = {1,-1,0,0};
    bool vis[N][N]; // 判断是否访问过 点
    struct Ponit{
        int x,y,t; // x,y 坐标, t 代表当前的步数
        Ponit(int x,int y,int t):x(x),y(y),t(t){} // 构造函数
    };
    bool pd(int x,int y){
        if(x >= 1 && x <= r && y >= 1 && y <= c) return 1;
        else return 0;
    }
    void bfs(){
        queue<Ponit> q;
        q.push({1,1,1}); // 初始化 队列
        vis[1][1] = 1;
        while(q.size()){
            Ponit t = q.front();
            q.pop();
            for(int i = 0;i < 4; ++i){
                int nx = t.x + dx[i];
                int ny = t.y + dy[i];
                if(nx == r && ny == c) {
                    ans = t.t + 1;
                    return ;
                }
                if(pd(nx,ny) && g[nx][ny] == '.' && !vis[nx][ny]){
                    vis[nx][ny] = 1;
                    q.push({nx,ny,t.t + 1});
                }
            }
        }
    }
    int main(){
        ios::sync_with_stdio(0);
        cin.tie(0),cout.tie(0);
        cin >> r >> c;
        for(int i = 1;i <= r; ++i){
            for(int j = 1;j <= c; ++j)
                cin >> g[i][j];
        }
        bfs();
        cout << ans;
    
    
        return 0;
    }
    

    最短路径算法

    由BFS启发 得到的最短路算法

    因为,BFS采用的是队列的思想,因此,可以想出一种基于队列的算法来处理,边权为任意值的算法

    单源最短路

    Dijkstra O(n^2) 只能处理正权边

    算法主要特点:以起点为核心,逐层向外扩展,每次都会取一个最近点继续扩展,直到取完所有点为止。

    算法步骤

    初始化距离 dist[1] = 0, dist[i] = +INF,从起点开始。

    循环迭代 n 次,

    集合 s 代表,已经确定最短路的点

    for i 0 ~ n { // 每次循环,可以确定一个 点的距离,n次 ,确定n个点

    ​ 在dist 中 找到不在 s 中的距离源点最近的点 , 设为 t // 基于贪心实现

    ​ 把 t 加到 s里面去

    ​ 用 t 来更新 ,其他所有点的距离 ( 更新 t 的出边 ) dist [x] > dist[t] + w;

    }

    例题 :

    HDU 2544

    acwing 849. Dijkstra求最短路 I

    习题推荐:

    HDU 1874,2066,2112,2680,

    POJ 1797

    #include <bits/stdc++.h>
    using namespace std;
    const int N = 510;
    int g[N][N],n,m,dist[N];
    bool st[N];
    // dist[i] 代表 1 到 i的最短路
    int dijkstra(){
        memset(dist,0x3f,sizeof dist);
        dist[1] = 0;
        // 循环 n 次,每次确定一个最短路的值
        for(int i = 0;i < n; ++i){
            int t = -1;
            // 在所有 st 为 false的点中,找到 dist最小的点
            for(int j = 1;j <= n; ++j){
                if(!st[j] && (t == -1 || dist[t] > dist[j])){
                    t = j;
                }
            }
            st[t] = 1;
            for(int j = 1;j <= n; ++j){
                dist[j] = min(dist[j],dist[t] + g[t][j]); 
                // dist[t] + g[t][j] 表示, 1 - t的最短路 加上 t 到 j的距离,
                // 等价于 1 - j 的距离
            }
        }
        if(dist[n] == 0x3f3f3f3f) return -1;
        else return dist[n];
    
    }
    int main(){
        ios::sync_with_stdio(0);
        cin.tie(0),cout.tie(0);
        cin >> n >> m;
        memset(g,0x3f,sizeof g);
        while(m--){
            int a,b,c;
            cin >> a >> b >> c;
            g[a][b] = min(g[a][b],c); // a 和 b之间可能有重边,取最小的边为权值
        }
    
        cout << dijkstra();
    
    
        return 0;
    }
    

    堆优化dij

    #include <bits/stdc++.h>
    using namespace std;
    typedef long long LL;
    typedef pair<int,int> PII;
    const int N = 1e4 + 10,M = 5e5 + 10;
    const int INF = 2147483647;
    int h[N],e[M],ne[M],w[M],dist[N],idx;
    int n,m;
    bool st[N];
    inline void add(int a,int b,int c){
    	e[idx] = b,w[idx] = c,ne[idx] = h[a],h[a] = idx++;
    }
    void dij(int s){
    	for(int i = 1;i <= n; ++i) dist[i] = INF;
    	dist[s] = 0;
    	priority_queue<PII,vector<PII>,greater<PII> > q;
    	q.push(make_pair(0,s));
    	while(q.size()){
    		auto ver = q.top().second,distance = q.top().first;
    		q.pop();
    		if(st[ver]) continue;
    		st[ver] = 1;
    		for(int i = h[ver];i != -1; i = ne[i]){
    			int j = e[i];
    			if(dist[j] > dist[ver] + w[i]){
    				dist[j] = dist[ver] + w[i];
    				q.push(make_pair(dist[j],j));
    			}
    		}
    	}
    
    }
    int main(){
    	ios::sync_with_stdio(0);
    	cin.tie(0),cout.tie(0);
    	int s,a,b,c;
    	memset(h,-1,sizeof h);
    	cin >> n >> m >> s;
    	for(int i = 0;i < m; ++i){
    		cin >> a >> b >> c;
    		add(a,b,c);
    	}
    	dij(s);
    	for(int i = 1;i <= n; ++i) cout << dist[i] <<' ';
    	return 0;
    }
    

    Bell-Ford O(ne) 可以处理负权边,不能处理负权回路,负权回路没有最短路

    // 只能存边

    算法步骤:

    进行n-1次松弛操作

    每次循环 m 次 ,更新每条边的距离

    #include <bits/stdc++.h>
    using namespace std;
    const int N = 1e5 + 23;
    int dist[510],st[510];
    int n , m;
    struct edge{
        int from,to,w;
    }e[N];
    void bf(){
        memset(dist,0x3f,sizeof dist);
        dist[1] = 0;
        for(int i = 0;i < n-1; ++i){
            for(int j = 0;j < m; ++j){
                int a = e[i].from,b = e[i].to, t = e[i].w;
                dist[b] = min(dist[b],dist[a] + t);
            }
        }
    }
    int main(){
        cin >> n >> m;
        for(int i = 0;i < m; ++i){
            cin >> e[i].from >> e[i].to >> e[i].w;
        }
        bf();
        for(int i = 1;i <= n; ++i){
            cout << dist[i] <<" ";
        }
        return 0;;
    }
    

    SPFA (队列优化版本的 Bell-Ford) 可以判断负环,容易被网格图卡,

    dist[b] = min(dist[b],dist[a] + t);  // 优化 dist a
    
    while(队列不空 )  , {
     	取出队头  t
        删除队头
        删掉队头的标记
        更新遍历  t 的所有出边 {
            更新每个点的距离,如果更新成功,并且当前的点没有入队,
                那么就加入队列
            	一个点只能入队一次,不能多次入队。
        }
        
    }
    
    #include <bits/stdc++.h>
    using namespace std;
    const int N = 1e5 + 23;
    int dist[510],st[510];
    int e[N],ne[N],w[N],h[N],idx,n,m;
    int a, b,c;
    void add(int a,int b,int c){
        e[idx] = b;
        w[idx] = c;
        ne[idx] = h[a];
        h[a] = idx++;
    }
    void spfa(){
        memset(dist,0x3f,sizeof dist);
        dist[1] = 0;
        st[1] = 1;
        queue<int> q; // 存所有变小了的节点
        q.push(1);
        while(q.size()){
            int t = q.front();
            q.pop();
            st[t] = 0;
            for(int i = h[t];i != -1; i = ne[i]){
                int j = e[i];
                if(dist[j] > dist[t] + w[i]){
                    
                    dist[j] = dist[t] + w[i];
                    //cnt[j] = cnt[t] + 1;  判断负环
                    //if (cnt[j] >= n) return true;  直接跳出
                    if(st[j] == 0){
                        st[j] = 1;
                        q.push(j);
                    }
                }
            }
        }
    
    }
    int main(){
        cin >> n >> m;
        memset(h,-1,sizeof h);
        for(int i = 0;i < m; ++i){
            cin >> a >> b >> c;
            add(a,b,c);
        }
        spfa();
        for(int i = 1;i <= n; ++i){
            cout << dist[i] <<" ";
        }
        return 0;;
    }
    

    spfa https://blog.csdn.net/u011644423/article/details/38345631

    https://skywt.cn/posts/spfasummary/

    POJ 1511,3259

    多源汇最短路 DP思想,暴力 ,只能用 邻接矩阵存图

    Floyed O(n^3), 还可以用来判断,图中的两个点是否相连

    void floyd(){
        for(int k = 1;k <= n; ++i){
            for(int i = 1;i <= n; ++i){
                for(int j = 1;j <= n; ++j){
                    g[i][j] = min(g[i][j],g[i][k] + g[k][j]);
                }
            }
        }
    }
    

    习题推荐:

    HDU 1869,3665,1596,1734

    POJ 1125

    最短路的应用 : 差分约束系统(不等式求解)

    习题:

    kuangbin最短路专题 ,https://vjudge.net/contest/324762

    推荐阅读:

    • 洛谷日报 :

    https://www.luogu.org/blog/wym483739/xue-tu-lun-ni-zhen-di-liao-xie-zui-duan-lu-ma-post

    https://studyingfather.blog.luogu.org/some-coding-tips-for-oiers some tips for oiers

    https://www.luogu.org/blog/chengni5673/tu-lun-di-xiao-ji-qiao-yi-ji-kuo-zhan 图论小技巧

    https://www.luogu.org/blog/little-sun/dijkstra dij详解

    https://www.luogu.org/blog/encore/io-you-hua-nei-suo-shi oi 优化

    https://oi-wiki.org/graph oi-wiki-图论专题

  • 相关阅读:
    angular读书笔记(三)
    angular读书笔记(二)
    angularjs读书笔记(一)
    angular和web前端的一些细节
    angular的service
    angular学习之directive
    最近学的twig
    最近学的grunt
    今天学的angularJS
    android即时通讯开发笔记(一)绪论~实现用户登录,自动登录,注销功能
  • 原文地址:https://www.cnblogs.com/lukelmouse/p/11485681.html
Copyright © 2011-2022 走看看