【图论】Day01_最短路算法之「Dijkstra」

一、什么是 Dijkstra 算法?

特点

  • 解决有向图或者无向图的单源最短路径问题。
  • 图中不能有负权边。
  • 复杂度: O ( E   l o g V ) O(E\ logV)

算法思路

在这里插入图片描述
从顶点 2 的三条临边中选择最短的边,显然是边 0->2,它的花费是 2.
在这里插入图片描述
接下来,从顶点 2 开始继续选择边 2->1,总花费是 2 + 1 = 3,这样我们就找到一条从 0->1 比直接从边 0->1 花费 5 要短的路径。
在这里插入图片描述
以此类推,整个过程可总结为:

  • 使用贪心的思想进行 n-1 次查找。
  • 从一个起点结点 v 开始,找出距离点 v 最近的点,假设为 v1,标记为 visited;
  • 下一次查找时,从上述结点 v1 开始,又查找未访问结点中找出距离点 v1 最近的点 v2,判断 dist[v->v1] + W[v1->v2] 是否小于 dist[v->v2],是则更新 dist[v2]
  • 重复上述过程。

二、题解

方法一:朴素版 Dijkstra

算法中的变量介绍

  • 因为每次都要找到花费最小的边对应的顶点,所以使用最小堆较适合。
  • int[] dist[] d i s t [ V i ] dist[V_i] 表示从 V 0 V_0 走到 V i V_i 的当前最短路径长度。
  • String[] path p a t h [ V i ] path[V_i] 表示 V i V_i 的前一个顶点,即当前可确认最短路上的倒数第 2 结点。
  • boolean vis[Vi]:记录访问过的最短路经中顶点 V i V_i
  • int[][] w w [ i ] [ j ] w[i][j] 表示从结点 i 到结点 j 的边的权值。
    • 如果结点 i 到结点 j 不直通,则表示为 I N F INF

算法大致流程

  • 迭代 N 次,每次可确定一个顶点 V i V_i 的到其点的最短距离:
    • 每一次找出没有访问过,并且距离源点距离比当前点要小的点的编号 mini。
    • 遍历每一个点 1~V,每次更新从 0->j 的最短路,具体为;
      • dist[j] = min(dist[j], dist[mini] + edges[mini][j])
  • 遍历 N 次后,即可得到 0~N 的最短路径。

存在的疑惑:

  • Q1:为什么一开始 vis[1] 不用标记为 true
    A1:因为,我们就是需要从第 1 个点开始找与第 1 个点的最短距离边。
import java.util.*;
import java.math.*;
import java.io.*;
public class Main{
	static int V, E;
	static int[][] edges;
	static int[] dist;
	static boolean[] vis;
	final static int MAX_VALUE = 10000000;
	
	private static int dijkstra() {
		dist[1] = 0;
		for (int i = 0; i < V; i++) {			//跌打n次,表示从每一个结点开始一次查找
			int mini = -1;
			for (int j = 1; j <= V; j++) {		//枚举每一个点
				if (vis[j] == false && (mini == -1 || dist[j] < dist[mini])) //从所有未访问的点钟找到dist最小的点
					mini = j;
			}
			vis[mini] = true;
			for (int j = 1; j <= V; j++) {
				dist[j] = Math.min(dist[j], dist[mini] + edges[mini][j]);
			}
		}
		return dist[V] == MAX_VALUE ? -1 : dist[V];
	}
    public static void main(String[] args) throws IOException {  
        Scanner sc = new Scanner(new BufferedInputStream(System.in));
        BufferedWriter w = new BufferedWriter(new OutputStreamWriter(System.out));
		V = sc.nextInt();
		E = sc.nextInt();
		edges = new int[V+1][V+1];
		vis = new boolean[V+1];
		dist = new int[V+1];
		Arrays.fill(dist, MAX_VALUE);
		
		for (int i = 1; i <= V; i++)
		for (int j = 1; j <= V; j++) {
			edges[i][j] = MAX_VALUE;
		}
		
		for (int i = 0; i < E; i++) {
			int x = sc.nextInt();
			int y = sc.nextInt();
			int z = sc.nextInt();
			edges[x][y] = z;
		}
		System.out.println(dijkstra());
    }
}

复杂度分析

  • 时间复杂度: O ( n 2 ) O(n^2)
  • 空间复杂度: O ( n 2 ) O(n^2)

方法二:堆优化

  • 朴素版的 dijkstra 每次要用 O ( n ) O(n) 的时间去寻找距离源点的最短边,使用 pq 将本次遍历的距离最短的边的结点入队,下次取的时候直接用 O ( l o g V ) O(logV) 时间取。
  • 为了防止重复点入队,假如 boolean[] vis 先剪枝。

数据结构说明

  • Edge{...}:包含该边的末尾节点的编号以及边的权重。
import java.util.*;
import java.math.*;
import java.io.*;
public class Main{
	static int V, E;
	static int[] dist;
	static boolean[] vis;
	final static int MAX_VALUE = 0x7fffffff;
	static ArrayList<Edge>[] edges;
	
	private static void dijkstra(int s) {
		dist[s] = 0;
		PriorityQueue<Edge> pq = new PriorityQueue<>((e1, e2) -> e1.w - e2.w);
		pq.add(new Edge(s, dist[s]));
		
		while (!pq.isEmpty()) {
			Edge edge = pq.poll();
			int v = edge.end;
			if (vis[v])
				continue;
			vis[v] = true;
			for (int i = 0; i < edges[v].size(); i++) {
				Edge t = edges[v].get(i);
				if (dist[t.end] > dist[v] + t.w) {
					dist[t.end] = dist[v] + t.w;
					pq.add(new Edge(t.end, dist[t.end]));
				}
			}
		}
	}
    public static void main(String[] args) throws IOException {  
        Scanner sc = new Scanner(new BufferedInputStream(System.in));
        BufferedWriter w = new BufferedWriter(new OutputStreamWriter(System.out));
		V = sc.nextInt();
		E = sc.nextInt();
		int S = sc.nextInt();
		
		vis = new boolean[V+1];
		dist = new int[V+1];
		edges = new ArrayList[V+1];
		for (int i = 0; i <= V; i++) {
			edges[i] = new ArrayList<Edge>();
		}
		Arrays.fill(dist, MAX_VALUE);
		for (int i = 0; i < E; i++) {
			int s = sc.nextInt();
			int e = sc.nextInt();
			int cost = sc.nextInt();
			edges[s].add(new Edge(e, cost));
		}
		dijkstra(S);
		for (int i = 1; i <= V; i++) 
			System.out.print(dist[i] + " ");
    }
	static class Edge {
		int end, w;
		Edge(int _end, int _w) {
			end = _end;
			w = _w;
		}
	}
}

复杂度分析

  • 时间复杂度: O ( E l o g V ) O(ElogV) ,没次取出堆顶的点都会遍历下其连接的边,比如第一次要遍历的边数为 E1,向堆里插入更新距离了的点的复杂度为 O ( l o g V ) O(logV) ,所以总的时间复杂度为 E 1 l o g V + E 2 l o g V + . . . . E i l o g V E_1logV + E_2logV + .... E_ilogV 。由于所有边数总和为 E,所以总的时间复杂度为 O ( E l o g V ) O(ElogV)
  • 空间复杂度: O ( E + V ) O(E+V)

方法三:链式前向星

qswl,算法错误:在Dijkstra 算法中,遍历某一个结点的孩子结点时,加入到队列后,不应该把它标记为访问过 vis[to] = true,不要和 SPFA 算法搞混。

洛谷的题对 Java 很不友好的,总是 TLE/MLE

import java.util.*;
import java.math.*;
import java.io.*;
public class Main{
	static int V, E;
	static int[] dist;
	static int INF = 0x3f3f3f3f;
	static boolean[] vis;
	static int[] head;
	static Edge[] edges;
	static int tot;
	static int MAXN = 1000000;
	private static void addEdge(int u, int v, int w) {
	    edges[++tot] = new Edge();
		edges[tot].to = v;
		edges[tot].w = w;
		edges[tot].next = head[u];
		head[u] = tot;
	}
	private static void dijkstra(int S) {
	    dist[S] = 0;
	    Queue<Node> q = new PriorityQueue<>((e1, e2) -> e1.cost - e2.cost);
	    q.add(new Node(S, dist[S]));
	    
	    while (!q.isEmpty()) {
	        Node now = q.poll();
			int v = now.to;
			if (vis[v])
				continue;
			vis[v] = true;
	        for (int i = head[v]; i != 0; i = edges[i].next) {
				int to = edges[i].to, w = edges[i].w;
				if (dist[to] > dist[v] + w) {
					dist[to] = dist[v] + w;
					if (!vis[to]) {
						q.add(new Node(to, dist[to]));
						// vis[to] = true; 注
					}
				}
			}
	    }
	}
    public static void main(String[] args) throws IOException {  
        Scanner sc = new Scanner(new BufferedInputStream(System.in));
        BufferedWriter w = new BufferedWriter(new OutputStreamWriter(System.out));
	
		V = sc.nextInt();
		E = sc.nextInt();
		int s = sc.nextInt();
		
		vis = new boolean[MAXN];
		dist = new int[MAXN];
		head = new int[MAXN];
		edges = new Edge[MAXN];
		Arrays.fill(dist, INF);
		
		for (int i = 0; i < E; i++) {
			int from = sc.nextInt();
			int to = sc.nextInt();
			int cost = sc.nextInt();
			addEdge(from, to, cost);
		}
		dijkstra(s);
		for (int i = 1; i <= V; i++) {
			System.out.print(dist[i] + " ");
		}
    }
	static class Node {
		int to, cost;
		Node (int to, int cost) {
			this.to = to;
			this.cost = cost;
		}
	}
	static class Edge {
		int to, w, next;
		public Edge() {		}
		public Edge(int to, int w, int next) {
			this.to = to;
			this.w = w;
			this.next = next;
		}
	}
}

三、Dijkstra 算法总结

  • 一种使用贪心的策略解决单源最短路算法。
  • 最坏时间复杂度为 O ( V 2 ) O(V^2) ,可用 pq 优化为 O ( E l o g V ) O(ElogV)
  • 不能解决负权图。
发布了691 篇原创文章 · 获赞 151 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/qq_43539599/article/details/105458608