public class Graph<T> {

	private Map<T, List<Edge>> map;

	public Graph() {
		map = new HashMap<>();
	}

	public void insert(T a, T b, int weight, boolean isDirected) {
		insert(a, b, weight);
		if (!isDirected)
			insert(b, a, weight);
	}

	private void insert(T a, T b, int weight) {
		List<Edge> edges = map.getOrDefault(a, new LinkedList<>());
		edges.add(new Edge(a, b, weight));
		map.put(a, edges);
		if (!map.containsKey(b))
			map.put(b, new LinkedList<>());
	}

	class Edge {
		T a;
		T b;
		int weight;

		Edge(T a, T b, int weight) {
			this.a = a;
			this.b = b;
			this.weight = weight;
		}
	}
}

概念

  • 无向图:边是顶点的无序对
  • 有向图:边是顶点的有序对
  • 弧:有向图的边
  • 网:边带权的图
  • 子图:边和顶点都是某图的子集的图为该图的子集
  • 完全图:所有顶点互连的无向图
  • 稀疏图:边或弧的个数 $e < nlogn $
  • 稠密图:边或弧的个数 \(e >= nlogn\)
  • 度:顶点关联的边数
    • 入度:指向该顶点的边数
    • 出度:该顶点指出的边数
  • 简单路径:顶点不重复出现
  • 连通图:无向图任两个顶点间有路径相通
  • 强连通图:有向图任两个顶点间有一条有向路径
  • 连通分量:非连通图中各个极大连通子图
  • 生成树:\(n\) 个顶点的连通图,其中 \(n-1\) 条边和 \(n\) 个顶点构成的级小连通子图

深度优先

public void dfs(T node) {
	dfs(node, new HashSet<T>());
}

private void dfs(T node, Set<T> visited) {
	visited.add(node);
	System.out.print(node + " ");
	List<Edge> edges = map.get(node);
	for (Edge e : edges) {
		if (visited.contains(e.b))
			continue;
		dfs(e.b, visited);
	}
}

广度优先

// 类似于树的层次遍历
// 多一步判断顶点是否访问过
public void bfs(T node) {
	Queue<T> queue = new LinkedList<>();
	Set<T> visited = new HashSet<>();
	queue.add(node);
	visited.add(node);
	System.out.print(node + " ");
	while (!queue.isEmpty()) {
		node = queue.poll();
		List<Edge> edges = map.get(node);
		for (Edge e : edges) {
			if (visited.contains(e.b))
				continue;
			queue.add(e.b);
			visited.add(e.b);
			System.out.print(e.b + " ");
		}
	}
	System.out.println();
}

最小生成树

最小生成树(MST):各边 权值总和最小 的生成树

性质
\(G = (V, E, W)\) 为一个带权连通图,\(T\)\(G\) 的最小生成树。
对任一不在 \(T\) 中的边 \(uv\),如果将 \(uv\) 加入 \(T\) 中会产生一回路,使得 \(uv\) 是回路中权值最大的边。

Prim 普里姆算法

  1. 初始:选一顶点作为生成 MST 的起始点,加到 MST 中。
  2. 迭代:顶点分为两类,MST 中的,和非 MST 中的。选取一条边,要求:
    • 一端连接 MST 中的顶点,一端连接非 MST 中的顶点
    • 权值最小
      将该边和对应顶点添加到 MST 中。
  3. 终止:循环 n - 1 次,找到 MST 的 n 个顶点和 n - 1 条边。

Prim 利用的是 贪心 的思想。

:Prim 算法 不适用 于有向图。

   1       2
a ---> b <--- c  
< -------------
       3              

如上所示,假如选取弧 a-->b 算法将无法继续进行。

public Graph<T> prim(T start) {
	Graph<T> mst = new Graph<T>();

	// 初始化 map 保存非 MST 顶点到 MST 顶点的最短路径
	Map<T, Edge> lowestCostMap = new HashMap<>();
	for (T node : map.keySet()) {
		lowestCostMap.put(node, new Edge(node, null, Integer.MAX_VALUE));
	}
	lowestCostMap.remove(start);

	while (lowestCostMap.size() > 0) {
		// start 为 MST 中新加入的顶点 更新最短路径
		updateLowestCost(lowestCostMap, start);
		// 寻找最短路径
		Edge edge = new Edge(null, null, Integer.MAX_VALUE);
		for (T n : lowestCostMap.keySet()) {
			Edge e = lowestCostMap.get(n);
			if (e.weight < edge.weight) {
				edge = e;
			}
		}
		// 非连通图
		if (edge.b == null)
			break;
		mst.insert(edge.a, edge.b, edge.weight, false);
		lowestCostMap.remove(edge.b);
		start = edge.b;
	}

	return lowestCostMap.size() > 0 ? null : mst;
}

// 更新每个顶点的最短路径
private void updateLowestCost(Map<T, Edge> lowestCostMap, T v) {
	for (Edge e : map.get(v)) {
		if (!lowestCostMap.containsKey(e.b))
			continue;
		int newLowestCost = e.weight;
		if (newLowestCost < lowestCostMap.get(e.b).weight) {
			lowestCostMap.put(e.b, e);
		}
	}
}

Prim 以 顶点 为主导,因此更适用于 稠密图,时间复杂度为 \(O(V^2) \quad V 为顶点数\)

Kruskal 克鲁斯卡尔算法

将所有顶点加入到 MST 中,将原图中的边按权值由小到大排序,考察各条边:

  • 若边的两个顶点属于 MST 两个不同的连通分量,则将此边加入 MST,同时把两个连通分量连接为一个连通分量
  • 若边的两个顶点属于 MST 同一个连通分量,则舍去,以免造成回路

如此下去,直到连通分量数为 1。

Kruskal 利用的是 贪心 的思想。

:与 Prim 类似,Kruskal 同样不能用于有向图。

public Graph<T> kruskal() {
	Graph<T> mst = new Graph<T>();
	// 预处理:对边排序 将顶点转换成数字 初始化并查集的 id
	List<Edge> edgeList = new LinkedList<>();
	Set<T> visited = new HashSet<>();
	Map<T, Integer> convertMap = new HashMap<>();
	for (T n : map.keySet()) {
		List<Edge> edges = map.get(n);
		for (Edge e : edges) {
			if (visited.contains(e.b))
				continue;
			edgeList.add(e);
		}
		visited.add(n);
		convertMap.put(n, convertMap.size());
	}
	int num = convertMap.size();
	int[] id = new int[num];
	for (int i = 0; i < num; i++) {
		id[i] = i;
	}
	Collections.sort(edgeList, new Comparator<Edge>() {
		@Override
		public int compare(Edge o1, Edge o2) {
			return o1.weight - o2.weight;
		}
	});

	for (Edge e : edgeList) {
		if (num == 1)	// 连通分量数为 1 即已经连通
			break;
		int p = findId(id, convertMap.get(e.a));
		int q = findId(id, convertMap.get(e.b));
		if (p == q)
			continue;
		mst.insert(e.a, e.b, e.weight);
		id[p] = q;
		num--;
	}

	return mst;
}

private int findId(int[] id, int i) {
	while (id[i] != i)
		i = id[i];
	return i;
}

Kruskal 以 为主导,因此更适用于 稀疏图,时间复杂度为 \(ElogE \quad E 为边数\),来源于对边排序的时间。

拓扑排序

可用于判断有向图是否为 无环图,安排活动的先后顺序(如先修课的安排)等。

  1. 选一个入度为 0 的顶点,放到拓扑排序序列中
  2. 删除该顶点以及由它出发的所有弧
  3. 重复1,2,直到没有入度为 0 的顶点
  4. 若图中还有剩余顶点,说明有回路
public List<T> topSort() {
	Map<T, Integer> inDegreeMap = new HashMap<>();
	Queue<T> queue = new LinkedList<>();	// 保存入度为 0 的顶点
	for (T node : map.keySet()) {
		inDegreeMap.put(node, 0);
	}
	for (T node : map.keySet()) {
		List<Edge> edges = map.get(node);
		for (Edge edge : edges) {
			inDegreeMap.put(edge.b, inDegreeMap.get(edge.b) + 1);
		}
	}

	for (T node : map.keySet()) {
		if (inDegreeMap.get(node) == 0)
			queue.add(node);
	}

	List<T> res = new ArrayList<>();
	while (queue.size() > 0) {
		T cur = queue.poll();
		res.add(cur);
		for (Edge edge : map.get(cur)) {
			int num = inDegreeMap.get(edge.b) - 1;
			inDegreeMap.put(edge.b, num);
			if (num == 0)
				queue.add(edge.b);
		}
	}

	return res.size() == map.size() ? res : null;
}

时间复杂度:

  • 计算所有顶点的入度 O(E)
  • 初始建立入度为 0 的顶点队列 O(V)
  • 每个顶点入、出队各一次 O(V)
  • 每条边执行一次入度减 1 操作 O(E)

总的时间复杂度为 O(V + E)

最短路径

Dijkstra 迪杰斯特拉算法

Dijkstra 求的是从一个 源点 到其它各点的最短路径。

  1. 初始:集合 \(S\) 为已求得最短路径的顶点,初始只包含源点 \(v_0\),集合 \(T\) 为未求得最短路径的顶点。
  2. 迭代:\(S\) 中每加入一个新的顶点 \(u\),更新 \(v_0\)\(T\) 中顶点的最短路径长度,并选择其中最小值,加入到 \(S\) 中。
    \(最短路径长度_{new} = Math.min(最短路径长度_{old}, 顶点 u 的最短路径长度 + u 到该顶点的路径长度)\)
  3. 终止:循环 \(n - 1\) 次。

可见,Dijkstra 与 Prim 非常相像,区别在于最短路径的选取:

  • Prim:到 MST 中所有顶点中长度最短的路径
  • Dijkstra:到源点长度最短的路径
// 整体思路与 Prim 基本一致
public Map<T, Integer> dijkstra(T start) {
	Map<T, Integer> res = new HashMap<>();
	res.put(start, 0);

	// 初始化 map 保存源点到未计算最短路径的顶点的最短距离
	Map<T, Integer> lowestCostMap = new HashMap<>();
	for (T n : map.keySet()) {
		lowestCostMap.put(n, Integer.MAX_VALUE);
	}
	lowestCostMap.remove(start);

	while (lowestCostMap.size() > 0) {
		// start 为新计算最短路径的顶点 更新最短路径长度
		updateLowestCost(lowestCostMap, start, res.get(start));
		// 选取其中最小值
		int minCost = Integer.MAX_VALUE;
		T node = null;
		for (T n : lowestCostMap.keySet()) {
			int cost = lowestCostMap.get(n);
			if (cost <= minCost) { // 这里是 <= 因为有可能不存在路径 即 cost = Integer.MAX_VALUE
				minCost = cost;
				node = n;
			}
		}
		lowestCostMap.remove(node);
		res.put(node, minCost);
		start = node;
	}

	return res;
}

// 与 Prim 的区别在于 newLowestCost 的计算
private void updateLowestCost(Map<T, Integer> lowestCostMap, T v, int cost) {
	for (Edge e : map.get(v)) {
		if (!lowestCostMap.containsKey(e.b))
			continue;
		int newLowestCost = e.weight + cost;
		if (newLowestCost < lowestCostMap.get(e.b)) {
			lowestCostMap.put(e.b, newLowestCost);
		}
	}
}

时间复杂度也与 Prim 相同,为 \(O(V^2)\)

:Dijkstra 算法 不适用 于权值有负数的图。
Dijkstra 每次迭代将一个新的顶点加入到集合 \(S\) 中,即已经求出了源点到该顶点的最短路径。
加入后,只更新与该顶点相连的顶点的最短路径信息。
如果存在权为负数的边,那么就可能存在一条比当前得出的最短路径还要短的路径。这就产生了矛盾。

Floyd 弗洛伊德算法

Floyd 求的是 每一对 顶点的最短路径。

  1. 初始:从顶点 i 到顶点 j 的最短路径中间可能有 n 个顶点作为桥梁。先初始化一个二维矩阵 d 保存路径长度,路径中间没有其他顶点,即 d[i][j] = 连接 i 和 j 的边的长度。
  2. 迭代:对于每一个顶点 \(k\),将 \(k\) 作为桥梁,检查 d[i][k] + d[k][j] 和 d[i][j] 的大小关系。每循环一次,代表以前 \(k\) 个顶点作为桥梁的路径已经检查过。
  3. 终止:\(k = 1, 2, 3, ... n\),进行 \(n\) 次试探,代表所有顶点作为桥梁的路径已经全部被检索过。

Floyd 利用的是 动态规划 的思想。

public Map<T, Map<T, Integer>> floyd() {
	Map<T, Map<T, Integer>> res = new HashMap<>();
	// 预处理:将顶点转换成数字 初始化矩阵
	Map<T, Integer> convertMap = new HashMap<>();
	int n = map.size();
	int[][] d = new int[n][n];
	for (int i = 0; i < n; i++) {
		Arrays.fill(d[i], Integer.MAX_VALUE);
		d[i][i] = 0;
	}
	for (T node : map.keySet()) {
		res.put(node, new HashMap<>());
		convertMap.put(node, convertMap.size());
	}
	for (T node : map.keySet()) {
		for (Edge edge : map.get(node)) {
			d[convertMap.get(edge.a)][convertMap.get(edge.b)] = edge.weight;
		}
	}

	for (int k = 0; k < n; k++) {
		for (int i = 0; i < n; i++) {
			for (int j = 0; j < n; j++) {
				if (d[i][k] == Integer.MAX_VALUE || d[k][j] == Integer.MAX_VALUE)
					continue;
				d[i][j] = Math.min(d[i][k] + d[k][j], d[i][j]);
			}
		}
	}

	for (T i : map.keySet()) {
		for (T j : map.keySet()) {
			Map<T, Integer> tmp = res.get(i);
			tmp.put(j, d[convertMap.get(i)][convertMap.get(j)]);
		}
	}

	return res;
}

时间复杂度:\(O(V^3)\)

posted @ 2020-03-31 17:05  JL916  阅读(140)  评论(0)    收藏  举报