• 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))

  • 相关阅读:
    AndroidUI组件之ListView小技巧
    iframe属性參数
    Applet 数字签名技术全然攻略
    SoftReference
    递归算法浅谈
    VS2010 打包生成exe文件后 执行安装文件出现 TODO:&lt;文件说明&gt;已停止工作并已关闭
    创建新的Cocos2dx 3.0项目并解决一些编译问题
    ORACLE触发器具体解释
    SRM 624 D2L3: GameOfSegments, 博弈论,Sprague–Grundy theorem,Nimber
    cidaemon.exe进程cpu占用率高及关闭cidaemon.exe进程方法
  • 原文地址:https://www.cnblogs.com/JL916/p/12590624.html
Copyright © 2020-2023  润新知