图论---最短路问题

  • 单源最短路 n: 点的数量 m: 边的数量

    • 所有边权都是正数

      • (1)朴素Dijkstra算法 O(n^2)

      • (2)堆优化版的Dijkstra算法 O(mlogn)

    • 存在负权边

      • (1)Bellmax-Fold O(nm) (让选择不超过k条边的时候使用)

      • (2)SPFA 一般O(m),最坏O(nm)

  • 多源汇最短路

    • Floyd算法 O(n^3)

边权非负

朴素Dijkstra

每次找到距离起点最近的点,然后用这个点去更新其他点,时间复杂度为O(n^2)

Dijkstra求最短路 I

import java.io.*;
import java.util.*;

class Main{
    static final int N = 510;
    static boolean[] st = new boolean[N];
    static int[] dist = new int[N];
    static int[][] g = new int[N][N];
    static int n, m;
    
    public static void Dijkstra(){
        Arrays.fill(dist, 0x3f3f3f3f);
        dist[1] = 0;
        
        for (int i = 0; i < n; i++) {
            int t = -1;
            for(int j = 1; j <= n; j++) {
                if (!st[j] && (t == -1 || dist[j] < dist[t]))
                    t = j;
            }
            
            st[t] = true;
            for (int j = 1; j <= n; j++)
                dist[j] = Math.min(dist[j], dist[t] + g[t][j]);
        }
        
    }
    public static void main(String[] args) throws IOException{
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String[] str = br.readLine().split(" ");
        n = Integer.parseInt(str[0]);
        m = Integer.parseInt(str[1]);
        
        for (int i = 0; i <= n; i++){
            Arrays.fill(g[i], 0x3f3f3f3f);
            g[i][i] = 0;
        }
        for (int i = 0; i < m; i++) {
            str = br.readLine().split(" ");
            int x = Integer.parseInt(str[0]);
            int y = Integer.parseInt(str[1]);
            int z = Integer.parseInt(str[2]);
            g[x][y] = Math.min(g[x][y], z);
        }
        
        Dijkstra();
        if (dist[n] == 0x3f3f3f3f) System.out.println(-1);
        else System.out.println(dist[n]);
    }
}


堆优化版Dijkstra

朴素Dijkstra算法,找最近的点需要n次,更新边需要m次,所以总体是O(n^2)

如果用堆优化,找最短的点是O(1),找n次; 更新边是O(logn),更新m次(外层循环是n个点,每次更新的是这个点的连边, 所有点的连边加起来一共m条),时间复杂度为O(mlogn)

不用手写堆,可以用优先队列实现,优先队列不能修改元素,因此需要修改元素就直接插入,因此会有冗余元素存在。手写堆的话,在排序的时候还要交换下标

Dijkstra求最短路 II

import java.io.*;
import java.util.*;

class Main {
    static final int N = 150010;
    static int[] h = new int[N], e = new int[N], ne = new int[N], w = new int[N];
    static int[] dist = new int[N];
    static boolean[] st = new boolean[N];
    static int n, m, idx;
    
    public static void add(int a, int b, int c) {
        e[idx] = b;
        w[idx] = c;
        ne[idx] = h[a];
        h[a] = idx++;
    }
    
    public static void Dijkstra(){
        Arrays.fill(dist, 0x3f3f3f3f);
        dist[1] = 0;
        PriorityQueue<PII> q = new PriorityQueue<>((o1, o2) -> o1.d - o2.d);
        
        q.offer(new PII(0, 1));
        while (!q.isEmpty()){
            PII t = q.poll();
            int dis = t.d;
            int ver = t.u;
            if (st[ver]) continue;
            st[ver] = true;
            
            // 用点ver更新距离
            for (int i = h[ver]; i != -1; i = ne[i]) {
                int u = e[i];
                if (dist[u] > dis + w[i]) {
                    dist[u] = dis + w[i];
                    q.offer(new PII(dist[u], u));
                }
            }
        }
    }
    
    public static void main(String[] args) throws IOException{
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String[] str = br.readLine().split(" ");
        n = Integer.parseInt(str[0]);
        m = Integer.parseInt(str[1]);
        
        Arrays.fill(h, -1);
        for (int i = 0; i < m; i++) {
            str = br.readLine().split(" ");
            int x = Integer.parseInt(str[0]);
            int y = Integer.parseInt(str[1]);
            int z = Integer.parseInt(str[2]);
            add(x, y, z);
        }
        
        Dijkstra();
        if (dist[n] == 0x3f3f3f3f) System.out.println(-1);
        else System.out.println(dist[n]);
        
    }
}

class PII{
    public int d;
    public int u;
    public PII(int d, int u) {
        this.d = d;
        this.u = u;
    }
}




边权有负值

Bellman-ford

该算法可来求经过k条边的最短路问题,边权可以为负。也可以用来求是否存在负环,但是求负环一般用spfa,Bellman-ford的时间复杂度为O(mn)

算法流程
1、for n 次 ; 迭代k次表示:从1号点经过了不超过k条边到其他点的距离;如果第n次更新时,
dist还发生变化(经过了n条边,有n+1个点,则一定有两个点一样),说明存在负权边的环

2、for 所有边 a->b--w dist[b]=min(dist[b],dist[a]+w)

last数组存的是dist上一次更新的备份,假设1->2=1,2->3=1,初始dist[2]=+00,
第一次更新dist[2]=1,更新dist[3]时,就用dist[2]来更新了,所以应该用上一次的结果更新

有边数限制的最短路

import java.io.*;
import java.util.*;

class Main {
    static final int N = 510, M = 10010;
    static int[] dist = new int[N];
    static int[] last = new int[N];
    static node[] g = new node[M];
    static int n, m, k;
    
    public static void bellman_fold(){
        Arrays.fill(dist, 0x3f3f3f3f);
        dist[1] = 0;
        for (int i = 0; i < k; i++) {
            last = Arrays.copyOf(dist, dist.length);
            for (int j = 0; j < m; j++) {
                int a = g[j].a;
                int b = g[j].b;
                int c = g[j].c;
                if (dist[b] > last[a] + c){
                    dist[b] = last[a] + c;
                }
            }
        }
    }
    
    public static void main(String[] args) throws IOException{
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String[] str = br.readLine().split(" ");
        n = Integer.parseInt(str[0]);
        m = Integer.parseInt(str[1]);
        k = Integer.parseInt(str[2]);
        
        for (int i = 0; i < m; i++) {
            str = br.readLine().split(" ");
            int a = Integer.parseInt(str[0]);
            int b = Integer.parseInt(str[1]);
            int c = Integer.parseInt(str[2]);
            g[i] = new node(a, b, c);
        }
        
        bellman_fold();
        if (dist[n] > 0x3f3f3f3f / 2) System.out.println("impossible");
        else System.out.println(dist[n]);
    }
}

class node{
    int a;
    int b;
    int c;
    public node(int a, int b, int c){
        this.a = a;
        this.b = b;
        this.c = c;
    }
}


spfa

spfa是对bellman-Fold进行优化,bellman-Fold每次对所有边进行迭代,

dist[y] = min(dist[y], last[x] + w); 如果last[x]没有变化,那么dist[y]也不需要更新

因此每次只需要将变化过的点放入队列,每次从队列取来更新后边的点即可

这里的st数组标记的是该点是否在队列中

spfa时间复杂度一般是O(m), 最坏是O(nm)

spfa求最短路

import java.io.*;
import java.util.*;

class Main{
    static final int N = 100010;
    static int[] dist = new int[N];
    static boolean[] st = new boolean[N];
    static int[] h = new int[N], e = new int[N], ne = new int[N], w = new int[N];
    static int n, m, idx;
    
    public static void add(int a, int b, int c) {
        e[idx] = b;
        w[idx] = c;
        ne[idx] = h[a];
        h[a] = idx++;
    }
    
    public static void spfa(){
        Arrays.fill(dist, 0x3f3f3f3f);
        dist[1] = 0;
        Queue<Integer> q = new LinkedList<>();
        q.offer(1);
        st[1] = true;
        
        while (!q.isEmpty()){
            int t = q.poll();
            st[t] = false;
            
            for (int i = h[t]; i != -1; i = ne[i]) {
                int u = e[i];
                if (dist[u] > dist[t] + w[i]){
                    dist[u] = Math.min(dist[u], dist[t] + w[i]);
                    if (!st[u]) {
                        q.offer(u);
                        st[u] = true;
                    }
                }
            }
        }
    }
    
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String[] str = br.readLine().split(" ");
        n = Integer.parseInt(str[0]);
        m = Integer.parseInt(str[1]);
        
        Arrays.fill(h, -1);
        for (int i = 0; i < m; i++) {
            str = br.readLine().split(" ");
            int x = Integer.parseInt(str[0]);
            int y = Integer.parseInt(str[1]);
            int z = Integer.parseInt(str[2]);
            add(x, y, z);
        }
        
        spfa();
        if (dist[n] == 0x3f3f3f3f) System.out.println("impossible");
        else System.out.println(dist[n]);
        
    }
}


spfa判断负环

维护一个cnt数组即可,cnt[i]表示从1-i经过了多少条边,如果边数大于等于n,说明存在负环

spfa判断负环

import java.io.*;
import java.util.*;

class Main {
    static final int N = 2010, M = 10010;
    static int[] h = new int[N], e = new int[M], ne = new int[M], w = new int[M], cnt = new int[N];
    static int[] dist = new int[N];
    static boolean[] st = new boolean[N];
    static int n, m, idx;
    
    public static void add(int a, int b, int c) {
        e[idx] = b;
        w[idx] = c;
        ne[idx] = h[a];
        h[a] = idx++;
    }
    
    public static boolean spfa(){
        Arrays.fill(dist, 0x3f3f3f3f);
        dist[1] = 0;
        Queue<Integer> q = new LinkedList<>();
        // 因为有的负环可能从点1无法到达,所以将所有点都加入队列
        for (int i = 1; i <= n; i++){
            q.offer(i);
            st[i] = true;
        }
        
        while (!q.isEmpty()){
            int t = q.poll();
            st[t] = false;
            
            for (int i = h[t]; i != -1; i = ne[i]) {
                int u = e[i];
                if (dist[u] > dist[t] + w[i]) {
                    dist[u] = dist[t] + w[i];
                    cnt[u] = cnt[t] + 1;
                    if (cnt[u] >= n)
                        return true;
                    if (!st[u]) {
                        q.offer(u);
                        st[u] = true;
                    }
                }
            }
        }
        return false;
    }
    
    public static void main(String[] args) throws IOException{
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String str[] = br.readLine().split(" ");
        n = Integer.parseInt(str[0]);
        m = Integer.parseInt(str[1]);
        
        Arrays.fill(h, -1);
        for (int i = 0; i < m; i++) {
            str = br.readLine().split(" ");
            int x = Integer.parseInt(str[0]);
            int y = Integer.parseInt(str[1]);
            int z = Integer.parseInt(str[2]);
            add(x, y, z);
        }
        
        if (spfa()) System.out.println("Yes");
        else System.out.println("No");
        
    }
}


多源汇最短路

Floyd

Floyd实际上是一个dp问题

  • 状态表示: f[k, i, j]表示从i到j,中间点只经过1到k(不包括i和j)的所有路径最小值

  • 状态计算: f[k, i, j] = min(f[k-1, i, j], f[k-1, i, k] + f[k-1, k, j])

f[k-1, i, j]表示不经过第k个点,f[k-1, i, k] + f[k-1, k, j]表示经过第k个点

可以发现f[k, i, j]只和k-1层有关系,所以第一维可以去掉

f[i][j] = min(f[i][j], f[i][k] + f[k][j])

f[i][j]一定是属于第k-1层的,而f[i][k]不一定属于k-1层,如果k小于j,那么f[i][k]属于第k层,即f[k, i, k] + f[k-1, k, j]

如果有负环,则一定不存在最短路,但是存在最短路,说明从i到j一定存在一条路径不包括重复点且最短.所以f[k,i,k]表示从i走到k,将1到k作为中间点,如果将k作为中间点,即k到k为一个环,更新最短路时会去点这个负环,所以此时答案不会被影响

Floyd求最短路

import java.io.*;
import java.util.*;

class Main{
    static final int N = 210;
    static int[][] dist = new int[N][N];
    static int n, m, k;
    
    public static void Floyd(){
        for (int k = 1; k <= n; k++) 
            for (int i = 1; i <= n; i++) 
                for (int j = 1; j <= n; j++)
                    dist[i][j] = Math.min(dist[i][j], dist[i][k] + dist[k][j]);    
    }
    
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String str[] = br.readLine().split(" ");
        n = Integer.parseInt(str[0]);
        m = Integer.parseInt(str[1]);
        k = Integer.parseInt(str[2]);
        
        for (int i = 1; i <= n; i++){
            Arrays.fill(dist[i], 0x3f3f3f3f);
            dist[i][i] = 0;
        }
        for (int i = 0; i < m; i++) {
            str = br.readLine().split(" ");
            int x = Integer.parseInt(str[0]);
            int y = Integer.parseInt(str[1]);
            int z = Integer.parseInt(str[2]);
            dist[x][y] = Math.min(dist[x][y], z);
        }
        
        Floyd();
        
        for (int i = 0; i < k; i++) {
            str = br.readLine().split(" ");
            int x = Integer.parseInt(str[0]);
            int y = Integer.parseInt(str[1]);
            if (dist[x][y] >= 0x3f3f3f3f / 2) System.out.println("impossible");
            else System.out.println(dist[x][y]);
        }
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值