最短路问题模板整理

129 阅读7分钟

最短路问题

图的存储

  1. 邻接矩阵存储: 用二维数组存图,g[i][j] 代表节点i到节点j有一条有向边,其元素值为边的权重
  2. 邻接表存储(vector实现):使用C++的STL库中的vector与结构体实现
struct edge{
    int from, to, w; //边:起点 from, 重点 to, 权值 w
    edge(){}
    edge(int a, int b, int c){from = a; to = b; w = c;}
};
vector<edge> e[N]; //e[i] 存第i个节点连接的所有边

//边的插入操作 from, to, w
e[from].push_back(edge(from, to, w));

//访问节点a的所有出边
for (int i = 0; i < e[a].size(); i ++ ){
    int from = e[a][i].from;
    int to = e[a][i].to;
    int w = e[a][i].w;
}
  1. 邻接表存储(数组模拟链表)
int h[N], e[N], w[N], ne[N], idx;

// 初始化
memset(h, -1, sizeof h); //为h数组赋值为1
idx = 1;

//插入边 a, b, c (a到b的一条有向边,权重为c)
e[idx] = b; ne[idx] = h[a]; h[a] = idx; w[idx ++ ] = c;

//访问节点a的所有出边
for (int i = h[a]; ~i; i = ne[i]){
    int b = e[i], c = w[i]; //b为a指向的节点,c为权重
}

单源最短路(无负权)

解决无负权的单源最短路通常使用dijkstra算法,其思想为维护一个最短路节点集合S,和一个距源点的最短距离数组dis[N]dis数组元素初始化为无穷大.

  1. 设源点为x,并标记dis[x] = 0
  2. 选择不在S中,且其dis值最小的节点a放入S
  3. 更新节点a的所有出边指向的节点的dis值:dis[b] = min(dis[b], dis[a] + weight(a, b))
  4. 重复步骤2、3,直到所有节点都在S中

朴素版dijkatra模板,时间复杂度O(n2)O(n^2)

int dis[N], g[N][N]; //dis[i]记录节点i距离目标节点的最短距离, g[N][N]存图
bool st[N]; //st[i]为true代表节点i在集合S中

void dijkstra(){ //计算节点n到节点1的最短路
    memset(dis, 0x3f, sizeof dis); //初始化距离为无穷大
    dis[1] = 0;
    
    for (int i = 1; i <= n; i ++ ){ //n次循环,每次将一个节点加入集合S
        int t = -1;
        for (int j = 1; j <= n; j ++ ){ // 找到dis值最小的节点(即距离集合S末端节点最近的节点)
            if (!st[j] && (t == -1 || dis[t] > dis[j])) t = j; 
        }
        
        st[t] = true;  //将节点t放入集合S中
        
        for (int j = 1; j <= n; j ++ ){ //用dis[t]更新t连接的节点的最短路距离
            if (g[t][j]) dis[j] = min(dis[j], dis[t] + g[t][j]);
        }
    }
    
    return dis[n];
}

优先队列优化版dijkstra模板O(mlogn)O(mlogn)

typedef pair<int, int> pii;

int h[N], e[N], ne[N], w[N], idx;
bool st[N];

void dijkstra(){ //计算节点n到节点1的最短距离
    memset(dis, 0x3f, sizeof dis); //初始化距离为无穷大
    dis[1] = 0;
    
    priority_queue<pii, vector<pii>, greater<pii> > q; //定义优先队列,存放节点的dis值与节点编号,按dis值升序排序
    q.push({dis[1], 1});
    
    while(!q.empty()){
        pii u = q.top(); //取出队头元素
        q.pop();
        int a = u.first, b = u.second; // 取出距离与节点编号
        if (st[b]) continue; //若节点最短路已经确定,则继续循环
        st[b] = true; //标记节点,代表节点的最短路已经确定
        
        for (int i = h[b]; ~i; i = ne[i]){ //遍历节点b的所有出边,更新最短路
            int x = e[i];
            if (dis[x] > a + w[i]){ //若节点x的最短路距离可以被b的最短距离更新,则执行更新与入队操作
                dis[x] = a + w[i];
                q.push({dis[x], x});
            }
        }
    }
    
    if (dis[n] == 0x3f3f3f3f) return -1; //若节点的dis值仍为无穷大,则不存在最短路径
    return dis[n];
}

单源最短路(有负权)

Bellman-ford算法O(nm)O(nm)

Bellman-ford算法主要用于解决有边权限制的最短路算法,如到目标节点做多不超过kk步的最短路径,其算法思想为,对所有边进行kk次松弛,用dis[N]dis[N]矩阵存储每次松弛后每个节点到达目标节点的最短距离,在每次松弛操作时,用上一次的disdis矩阵更新当前边缘末端节点的最短距离。

const int N = 1e5 + 10, M = 2e5 + 10;

int n, m, k, dis[N], st[N];

struct edge{ //结构体存储图
    int a, b, c;
}e[M];

void bellman-ford(){
    memset(dis, 0x3f, sizeof dis);
    dis[1] = 0;
    for (int i = 0; i < k; i ++ ){ //进行k次松弛
        memcpy(st, dis, sizeof dis); // 拷贝上次松弛的结果
        
        for (int j = 0; j < m; j ++ ) { // 遍历每条边,对其进行松弛操作
            int a = e[j].a, b = e[j].b, c = e[j].c;
            dis[b] = min(dis[b], st[b] + c); //松弛操作
         }
    }
    
    if (dis[n] > 0x3f3f3f3f / 2) return -1; //判断最短路是否存在
    else return dis[n];
}

int main(){
    cin >> n >> m >> k;
    int a, b, c;
    for (int i = 0; i < m; i ++ ){
        cin >> a >> b >> c;
        e[i] = edge({a, b, c});
    }
    
    cout << bellman-ford() << endl;
}

SPFA算法 一般O(m)O(m),最坏O(nm)O(nm)

spfa算法是bellman-ford算法的优化,其思想为,在bellman-ford算法对每条边进行松弛的过程中,并不是所有节点距目标节点的最短距离都会发生变化,但只有最短距离变小的节点才会对其他节点的最短距离产生影响。spfa算法与dijkstra算法类似,具体步骤如下:

  1. 声明队列Q,最短距离列表dis[N]初始化为无穷大,节点状态列表st[N],判断节点是否在队列中。将起始节点s入队,并设置dis[s] = 0st[i] = true,若队列不为空,则执行以下操作.
  2. 队头元素u出队,并标记st[u] = false.
  3. 遍历节点u的所有出边所指向的节点x判断节点x的最短距离是否能被节点u更新,若能更新,则将节点u入队,并标记st[x]及更新dis[x]
const int N = 1e5 + 10, M = 2 * N;
int h[N], e[M], ne[M], w[M], idx;
int n, m, st[N], dis[N];

int spfa(){ //计算1号节点到n号节点的最短距离
    queue<int> q;
    dis[1] = 0;  //存储1号节点到所有节点的最短距离
    st[1] = 1; //标记1号节点在队列中
    q.push(1);
    
    while(!q.empty()){
        int u = q.fornt();  //取出队头元素
        q.pop();
        st[u] = 0;
        
        for (int i = h[u]; ~i; i = ne[i]){ //遍历节点u的所有邻居
            int x = e[i]; //取出邻居x
           
            if (dis[x] > dis[u] + w[i]){  //若1号点到x的最短距离可以被更新,则更新并将x入队
                dis[x] = dis[u] + w[i];
                
                if (!st[x]){ //若x不在队列中
                    q.push(x);
                    st[x] = 1;
                }
            }
        }
    }
    
    if (dis[n] > 0x3f3f3f3f / 2) return -1;  // 判断最短路是否存在
    else return dis[n];
}

void addedge(int a, int b, int c){ //节点的插入操作
    e[idx] = b; ne[idx] = h[a]; w[idx] = c; h[a] = idx ++;
}

void init(){ //初始化
    memset(h, -1, sizeof h);
    memset(dis, 0x3f, sizeof dis);
    idx = 1;
}

int main(){
    ios::sync_with_stdio(false); //优化cin写入速度
    cin.tie(0); //优化cin写入速度
    init();
    cin >> n >> m;  
    int a, b, c;
    for (int i = 0; i < m; i ++ ){
        cin >> a >> b >> c;
        addedge(a, b, c);  //插入连边
    }
    
    int ans = spfa();
    if (ans < 0) puts("impossible");
    else cout << ans << endl;

负权回路

Bellman-ford判断负环

Bellman-ford求最短路的思想是对每条边均进行k次松弛操作,假设1号节点到n号节点存在最短路且不存在负环,则在进行n次松弛操作后,一定能求的1到n的最短路。若其路径中存在负环,则在1到n路径上的某个点,1号点距其最短距离在进行n次松弛操作后,必定能继续变小,以此来判断负环是否存在。

const int N = 1e5 + 10, M = 2e5 + 10;
int dis[N], st[N];

struct Edge{
    int a, b, c;
}e[M];

bool bellman_ford(){
    memset(dis, 0x3f, sizeof dis);
    dis[1] = 0;
    bool ans = false;
    
    for (int i = 0; i < n; i ++ ){
        memcpy(st, dis, sizeof dis); //拷贝上次的结果
         
        for (int j = 0; j < m; j ++ ){
            int a = e[j].a, b = e[j].b, c = e[j].c;
            dis[b] = min(dis[b], st[a] + c); //用上次的结构更新本次松弛
        }
    }
    
    for (int i = 0; i < m; i ++ ){  //在进行n次松弛后判断是否能继续松弛,若能,则负环存在,否则不存在
        int a = e[i].a, b = e[i].b, c = e[i].c;
        if (dis[i] > dis[i] + c) {
            ans = true; //表示存在负环
            break;
        }
    }
    
    return ans; 

spfa判断负环

由于spfabellman-ford算法的变体,因此其判断负环的方式与bellman-ford算法类似,即在进行n次松弛之后,若某节点仍可继续松弛,则存在负环。但由于spfa通过维护一个队列结构来进行松弛操作,某一节点被松弛后,即进入队列,因此可以开辟一个数组判断某个节点进入队列的次数,若大于n次,则必定存在负环。

int cnt[N]; //记录节点进入队列的次数

bool spaf(){
    queue<int> q;
    for (int i = 1; i <= n; i ++ ) {
        dis[i] = 0;
        st[i] = 1; //编辑节点是否在队列中
        cnt[i] = 1; //统计节点进入队列的次数
        q.push(i);
    }
    
    while(!q.empty()){
        int u = q.front();
        q.pop();
        st[u] = 0;
        
        for (int i = h[u]; ~i; i = ne[i]){
            int a = e[i];
            if (dis[a] > dis[u] + w[i]){
                dis[a] = dis[u] + w[i]; //松弛操作
                if (!st[a]) { //若a不在队列中,则入队
                    st[a] = 1;
                    q.push(a);
                    cnt[a] ++;
                    if (cnt[a] > n) return true; //若入队次数大于n,存在负环
                }
            }
        }
    }
    return false;
}

多源汇最短路 O(n3)O(n^3)

多源汇最短路可计算两两节点之间的最短路,通常用floyd算法解决,其思想为:

  1. 遍历每个节点k,视其为中间节点。

  2. 遍历所有节点对(i, j), 若i可指向节点k,节点k可指向j,则做如下操作

    dis(i, j) = min(dis(i, j), dis(i, k) + dis(k, j))

int g[N][N]; //邻接矩阵存储节点间的距离
const int INF = 0x3f3f3f3f;
int n, m, k;

void floyd(){
    for (int k = 1; k <= n; k ++ ){
        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]); //利用中间节点k来更新节点i与j之间的最短路
            }
        }
    }
}

int main(){
    cin >> n >> m >> k; //n表示节点数,m表示边数,k表示查询次数
    int a, b, c;
    for (int i = 1; i <= n; i ++ ){ //初始化,两两不同节点间的距离初试化为无穷大
        for (int j = 1; j <= n; j ++ ){
            g[i][j] = INF;
            if (i == j) g[i][j] = 0;
        }
    }
    for (int i = 0; i < m; i ++ ) {
        cin >> a >> b >> c;
        g[a][b] = min(g[a][b], c);
    }
    
    floyd();
    
    while(k -- ){ //k次查询,查询两个节点间的最短距离
        cin >> a >> b;
        if (g[a][b] > INF / 2) puts("impossible");
        else cout << g[a][b] << endl;
    }
}