图论 最短路 基础

时间:2019-09-08
本文章向大家介绍图论 最短路 基础,主要包括图论 最短路 基础使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

图论基础 , 最短路

图的简单概念

顶点 (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 <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 1e5 + 233;
typedef pair<int,int> PII;
int dist[N],e[N],w[N],ne[N],h[N],idx,n,m,x,y,z;
bool st[N];
void add(int a,int b,int c){
    e[idx] = b,w[idx] = c,ne[idx] = h[a],h[a] = idx ++;
}
int dij(){
    memset(dist,0x3f,sizeof dist);
    priority_queue<PII,vector<PII>,greater<PII>> heap; // 初始化堆
    dist[1] = 0;
    heap.push({0,1});
    while(heap.size()){
        auto t = heap.top();// log n 找到最小的距离
        heap.pop();
        int distance = t.first,ver = t.second;
        if(st[ver]) continue;
        st[ver] = true;
        for(int i = h[ver];i != -1;i = ne[i]){
            int j = e[i];
            if(dist[j] > distance + w[i]){
                dist[j] = distance + w[i];
                heap.push({dist[j],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(h,-1,sizeof h);
    while(m--){
        cin >> x >> y >> z;
        add(x,y,z);
    }
    cout << dij();
    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-图论专题

原文地址:https://www.cnblogs.com/317zhang/p/11485681.html