图-最短路径

274 阅读2分钟

最短路径是指两个顶点之间权值之和最小的路径,有向图,无向图均适用,不能有负权环. 最短路径有权图

最短路径-无权图

无权图相当于是全部边的权值为1. 最短路径-无权图

最短路径-负权边

有负权边,但是没有负权环时,存在最短路径. 最短路径-负权边

最短路径-负权环

有负权环时,不存在最短路径 最短路径负权环 通过负权环,A到E的路径可以无限短 A->E->D->F->E->D->F->E......

求解最短路径3个经典算法

单源最短路径:

  • Dijkstra
  • Bellman-Ford

多源最短路径:

  • Floyd

Dijkstra

属于单源最短路径算法,用于计算一个顶点到其他所有顶点的最短路径. 使用前提:不能有负权边

等价思考

Dijkstra的原理其实跟生活中的一些自然现象完全一样.

  • 把每个顶点想象成一个小石头
  • 每一条边想象成是一条绳子,每一条绳子都连着2块石头,边的权值就是绳子的长度
  • 将小石头和绳子平放在一张桌子上
  • 接下来想象一下,手拽着小石头A,慢慢地向上提起来,远离桌面
  • B,D,C,E会依次离开桌面
  • 最后绷直的绳子就是A到其他小石头的最短距离

红色的线会被拉直.

执行过程

Dijkstra流程 松弛操作:更新2个顶点之间的最短距离,这里一般是指,更新源点到另一个点的最短距离,尝试找出更短的最短路径. 上图中,确定A到D的最短路径之后,对DC,DE边进行松弛操作,更新A到C,A到E的最短距离.

private Map<V, PathInfo<V, E>> dijkstra(V begin) {
		Vertex<V, E> beginVertex = vertices.get(begin);
		if (beginVertex == null) {
			return null;
		}
		//选择的边信息
		Map<V, PathInfo<V, E>> selectedPaths = new HashMap<>();
		//所有的path信息
		Map<Vertex<V, E>, PathInfo<V, E>> paths = new HashMap<>();
		//初始化paths
		for (Edge<V, E> edge : beginVertex.outEdges) {
			PathInfo<V, E> path = new PathInfo<>();
			path.weight = edge.weight;
			path.edgeInfos.add(edge.info());
			paths.put(edge.to, path);
		}
		while (!paths.isEmpty()) {
			//获取paths里权值最小的边
			Map.Entry<Vertex<V, E>, PathInfo<V, E>> minEntry = getMinPath(paths);
			//离开桌面的顶点
			Vertex<V, E> minVertex = minEntry.getKey();
			PathInfo<V, E> minPath = minEntry.getValue();
			selectedPaths.put(minVertex.value, minPath);
			//从paths里移除
			paths.remove(minVertex);
			//minVertex的outEdges进行松弛操作
			for (Edge<V, E> edge : minVertex.outEdges) {
				//如果edge.to已经离开桌面,就没必要进行松弛操作
				if (selectedPaths.containsKey(edge.to.value)) {
					continue;
				}
				relaxForDijkstra(edge, minPath, paths);
			}
		}
		return selectedPaths;
	}

	/**
	 * 松弛操作
	 *
	 * @param edge     需要松弛的边
	 * @param fromPath edge的from的最短路径
	 * @param paths    存放着其他点(对于dijkstra来说,就是还没有离开桌面的点)的最短路径信息
	 */
	private void relaxForDijkstra(Edge<V, E> edge, PathInfo<V, E> fromPath, Map<Vertex<V, E>, PathInfo<V, E>> paths) {
		//新的可选择的最短路径:beginVertex到edge.from的最短路径+edge.weight
		E newWeight = weightManager.add(fromPath.weight, edge.weight);
		//以前的最短路径:beginVertex到edge.to的最短路径
		PathInfo<V, E> oldPath = paths.get(edge.to);
		if (oldPath != null && weightManager.compare(newWeight, oldPath.weight) >= 0) {
			return;
		}
		if (oldPath == null) {
			oldPath = new PathInfo<>();
			paths.put(edge.to, oldPath);
		} else {
			oldPath.edgeInfos.clear();
		}
		oldPath.weight = newWeight;
		oldPath.edgeInfos.addAll(fromPath.edgeInfos);
		oldPath.edgeInfos.add(edge.info());
	}

	private Map.Entry<Vertex<V, E>, PathInfo<V, E>> getMinPath(Map<Vertex<V, E>, PathInfo<V, E>> paths) {
		Iterator<Map.Entry<Vertex<V, E>, PathInfo<V, E>>> iterator = paths.entrySet().iterator();
		Map.Entry<Vertex<V, E>, PathInfo<V, E>> minEntry = iterator.next();
		while (iterator.hasNext()) {
			Map.Entry<Vertex<V, E>, PathInfo<V, E>> entry = iterator.next();
			if (weightManager.compare(entry.getValue().weight, minEntry.getValue().weight) < 0) {
				minEntry = entry;
			}
		}
		return minEntry;
	}

源码这里

Bellman-Ford

也属于单源最短路径算法,支持负权边,还能检测出是否有负权环. 对所有的边进行V-1次松弛操作(V是节点数量),得到所有可能的最短路径. 最好情况下对所有边仅需1次松弛操作就能计算出A到其他所有顶点的最短路径,如图: 最好情况 最坏情况是恰好每次都从右到左的顺序对边进行松弛操作,对所有的边需进行V-1次松弛操作才能计算出A到其他所有顶点的最短距离.如图: 最坏情况 正常情况示例,一共8条边,每次松弛操作的边为:DC,DF,BC,ED,EF,BE,AE,AB,经过4次松弛操作之后,已经计算出A到其他所有顶点的最短路径,如图: 正常情况 代码:

private Map<V, PathInfo<V, E>> bellmanFord(V begin) {
		Vertex<V, E> beginVertex = vertices.get(begin);
		if (beginVertex == null) return null;
		
		Map<V, PathInfo<V, E>> selectedPaths = new HashMap<>();
		selectedPaths.put(begin, new PathInfo<>(weightManager.zero()));
		
		int count = vertices.size() - 1;
		for (int i = 0; i < count; i++) { // v - 1 次
			for (Edge<V, E> edge : edges) {
				PathInfo<V, E> fromPath = selectedPaths.get(edge.from.value);
				if (fromPath == null) continue;
				relax(edge, fromPath, selectedPaths);
			}
		}
		
		for (Edge<V, E> edge : edges) {
			PathInfo<V, E> fromPath = selectedPaths.get(edge.from.value);
			if (fromPath == null) continue;
			if (relax(edge, fromPath, selectedPaths)) {
				System.out.println("有负权环");
				return null;
			}
		}
		
		selectedPaths.remove(begin);
		return selectedPaths;
	}