图分无向图(简单连接)、有向图(连接有方向)、加权图(连接带权值)、加权有向图。
一:无向图
通常将含有平行便的图称为多重图,而将没有平行边和自环的图称为简单图。如果从任意一个顶点都存在一条路径到达任意顶点,称这副图是连通图。要处理一张图就需要一个个地处理它的连通分量(子图)。
当一幅图含有V个结点的图G满足以下5个条件之一,那它就是一棵树:
①G有V-1条边且无环。
②G有V-1条边且是连通的。
③G是连通的,当删除任意一条边都会使它不连通。
④G是无环图,当添加任意一条边都会产生一条环。
⑤G中的任意一对顶点之间仅存在一条简单路径。
二分图能够将所有结点分为两部分的图,且其中的每条边所连接的两个顶点都分别属于不同的部分,可以通过染色法来判断是不是二分图。
使用邻接表数组来表示图,使用的空间和V+E成正比,添加一条边所需的时间为常数,遍历顶点v的所有相邻顶点所需的时间和v的度数成正比。
public class Graph { private int V;//顶点数目; private int E;//边的数目 private Fundamental.Bag<Integer>[] adj;//邻接表 //创建一个含有V个顶点但不含有边的图 Graph(int v) { V = v; this.E = 0; adj = new Fundamental.Bag[v];//创建邻接表 for (int i = 0; i < v; i++) { adj[i] = new Fundamental.Bag<Integer>(); } } //读入一幅图 Graph() { Scanner scanner = new Scanner(System.in); int E = scanner.nextInt();//边数 for (int i = 0; i < E; i++) { //读入两个顶点 int v = scanner.nextInt(); int w = scanner.nextInt(); addEdge(v,w);//向图添加这两个顶点构成的边 } } //顶点数 int V() { return V; } //边数 int E() { return E; } //向图添加一条边 void addEdge(int v, int w) { adj[v].add(w); adj[w].add(v); E++; } //和v相邻的所有顶点,不考虑顺序 Iterable<Integer> adj(int v) { return adj[v]; } //计算顶点v的度数 public static int degree(Graph G, int v) { int degree = 0; for (Integer integer : G.adj(v)) { degree++; } return degree; } //计算所有顶点的最大度数 public static int maxDegree(Graph G) { int max = 0; for (int i = 0; i < G.V(); i++) { if (degree(G, i) > max) { max = degree(G, i); } } return max; } //计算所有顶点的平均度数 == 所有边乘以2得到所有顶点度数之和,再除以顶点数 public static double avgDegree(Graph G){ return 2.0 * G.E() / G.V(); } //计算自环的个数 public static int numberOfSelfLoops(Graph G){ int count = 0 ; for (int i = 0; i < G.V(); i++) { for (int w : G.adj(i)) { if (i == w){ count++; } } } return count/2; } //图的邻接表的字符串表示 @Override public String toString() { return super.toString(); } }
1.1 深度优先搜索
走迷宫:①选择一条没有标记过的通道,在你走过的路上铺一条绳子。②标记所有你第一次路过的路口和通道。③当来到一个标记过的路口时用绳子回退到上一个路口。④当回退到的路口已没有可走的通道时继续回退。
深度优先搜索在访问一个顶点时,将它标记为已访问,递归地访问它的所有没有被标记过的邻居顶点。深度优先搜索只会访问和起点连通的顶点,且每条边都会被访问两次,在第二次访问时会发现这个顶点已经被标记过。它解决了从s到给定目的顶点v是否存在一条路径的问题。
//深度优先搜索算法 public class DepthSearch { //记录和起点连通的所有顶点 private boolean[] marked; private int count; public DepthSearch(Graph G,int s){ marked = new boolean[G.V()]; dfs(G,s); } private void dfs(Graph G,int v){ marked[v] = true; count++; for (int w : G.adj(v)) { if (!marked[w]){ //递归的访问v点的还没有被标记过的相邻顶点,如果图连通的所有顶点都会被访问。 dfs(G,w); } } } public boolean marked(int w){ return marked[w]; } public int count(){ return count; } }1.1.1 寻找路径-->点与点的连通性
从起点s到它连通的所有顶点的路径。
public class DepthSearchPaths { private boolean[] marked;//这个顶点上调用过dfs()了吗 //记住每个顶点到起点的路径,是一颗由父链接表示的树 //(记录每个顶点的值是它的父节点) private int[] edgeTo;//从起点到一个顶点的已知路径上--的最后一个顶点 private final int s;//起点 public DepthSearchPaths(Graph G, int s) { marked = new boolean[G.V()]; edgeTo = new int[G.V()]; this.s = s; dfs(G,s); } private void dfs(Graph G, int v){ marked[v] = true; for (int w : G.adj(v)) { if (!marked[w]){ //由边v-w第一次访问任意w时,将edgeTo[w]设为v来记住这条路径 edgeTo[w] = v; dfs(G,w); } } } public boolean hasPathTo(int v){ return marked[v]; } //在v到达起点s之前,将遇到的所有顶点都压入栈中 public Iterable<Integer> pathTo(int v){ if (!hasPathTo(v)){ return null; } Stack<Integer> path = new Stack<>(); for (int x= v; x!=s; x=edgeTo[x]){ path.push(x); } path.push(s); return path; } }1.1.2 找出图的连通分量(子图)
使用一个以顶点作为索引的数组id[],将同一个连通分量中的顶点和连通分量的标识符关联起来(int值),标识符0会被赋予第一个连通分量中的所有顶点,1会被赋予第二个连通分量的所有顶点,依此类推。
//深度优先搜索-->找出图中的所有连通分量 public class CC { private boolean[] marked;//这个顶点上调用过dfs()了吗 private int[] id; private int count; public CC(Graph G){ marked = new boolean[G.V()];//初始化marked id = new int[G.V()];//初始化id for (int s = 0; s < G.V(); s++) { if (!marked[s]){ dfs(G,s); count++; } } } private void dfs(Graph G,int v){ marked[v] = true; id[v] = count; for (int w : G.adj(v)) { if (!marked[w]){ dfs(G,w); } } } //顶点v和顶点w是否连通 public boolean connected(int v,int w){ return id[v] == id[w]; } //顶点v属于哪个连通分量(标识符) public int id(int v){ return id[v]; } //连通分量数(从0开始) public int count(){ return count++; } } 使用CC来解决图连通性问题与union-find算法相比,后者不需要完整地构造并表示一幅图和对图进行预处理,而且还是一种动态算法(任何时候只需要常数时间来检查两个顶点是否连通),更适合那些只需要判断连通性或是需要完成大量连通性查询和插入操作混合等类似任务的应用。而深度优先搜索更适合实现图的抽象数据类型,更有效的利用已有的数据结构。1.1.3 图G是无环图吗?(假设不存在自环或平行边)
//深度优先搜索--->判断图G是否为无环图 public class CycleGraph { private boolean[] marked;//这个顶点上调用过dfs()了吗 private boolean hasCycle; public CycleGraph(Graph G){ marked = new boolean[G.V()]; for (int s = 0; s < G.V(); s++) { if (!marked[s]){ dfs(G,s,s); } } } private void dfs(Graph G,int v,int u){ marked[v] = true; for (int w : G.adj(v)) { if (!marked[w]){ dfs(G,w,v); } else if (w != u) { hasCycle = true; } } } public boolean isHasCycle(){ return hasCycle; } }1.1.4 图G是二分图吗?(双色问题-->染色法)
//深度优先搜索-->判断图G是否为二分图(双色问题)-->染色法解决 public class TwoGraph { private boolean[] marked;//这个顶点上调用过dfs()了吗 private boolean[] color; private boolean twoGraph = true; public TwoGraph(Graph G){ marked = new boolean[G.V()]; color = new boolean[G.V()]; for (int s = 0; s < G.V(); s++) { if (!marked[s]){ dfs(G,s); } } } private void dfs(Graph G,int v){ marked[v] = true; for (int w : G.adj(v)) { if (!marked[w]){ color[w] = !color[v]; dfs(G,w); } else if (color[w] == color[v]) { twoGraph = false; } } } public boolean isTwoGraph(){ return twoGraph; } }1.2 广度优先搜索
深度优先搜索时一个人在走迷宫,而广度优先搜索是一组人在各个方向上走迷宫,当遇到路口时,这个人会分裂出路口的各个方向的多个人去搜索,相遇时就合二为一。
广度优先搜索的实现:
①先将起点加入队列。
②取队列中的下一个顶点v并标记它。
③将与v相邻的所有未被标记的顶点加入队列。
1.2.1 寻找最短路径-->点到点的最短路径
//广度优先搜索-->寻找起点到它连通的所有顶点的最短路径 public class BreadthSearchPaths { private boolean[] marked;//到达该顶点的最短路径已知吗? //记住每个顶点到起点的路径,是一颗由父链接表示的树(记录每个顶点的值是它的父节点) private int[] edgeTo;//从起点到一个顶点的已知路径上--的最后一个顶点 private final int s;//起点 public BreadthSearchPaths(Graph G, int s) { marked = new boolean[G.V()]; edgeTo = new int[G.V()]; this.s = s; bfs(G,s); } private void bfs(Graph G,int s){ Queue<Integer> queue = new SynchronousQueue<Integer>(); marked[s] = true;//标记起点 queue.add(s);//加入队列 while (!queue.isEmpty()){ int v = queue.poll();//从队列中删去下一顶点 for (int w : G.adj(v)) { if (!marked[w]){ edgeTo[w] = v;//保存最短路径的最后一条边 marked[w] = true;//标记它,因为最短路径已知 queue.add(w);//并将它添加到队列中 } } } } public boolean hasPathTo(int v){ return marked[v]; } //在v到达起点s之前,将遇到的所有顶点都压入栈中 public Iterable<Integer> pathTo(int v){ if (!hasPathTo(v)){ return null; } Stack<Integer> path = new Stack<>(); for (int x= v; x!=s; x=edgeTo[x]){ path.push(x); } path.push(s); return path; } }1.3 符号图
①顶点名为字符串。
②用指定的分隔符来隔开顶点名(允许含有空格)。
③每一行都表示一组边的集合,每一条边都连着这一行的第一个名称表示的顶点和其他
名称所表示的顶点。
④顶点总数V和边的总数E都是隐式定义。
符号图的实现:
①一个符号表st,键的类型为String(顶点名),值的类型为int(索引)。
②一个数组keys[],用作反向索引,保存每个顶点索引所对应的顶点名。
③一个Graph对象G,它使用索引来引用图中顶点。
//符号图 public class SymbolGraph { private HashMap<String, Integer> st;//符号名 -- 索引 private String[] keys; //索引 -- 符号名 private Graph G;//符号图(数组邻接表) //stream 数据流,sp分隔符 public SymbolGraph(String stream, String sp) { st = new HashMap<String, Integer>(); /** * 遍历两遍数据来构造以上数据结构,st和keys */ Scanner scanner = new Scanner(System.in); while (scanner.hasNextLine()) { String[] a = scanner.nextLine().split(sp);//读取字符串 //为每个不同的字符串关联一个索引 for (int i = 0; i < a.length; i++) { if (!st.containsKey(a[i])) { st.put(a[i], st.size()); } } } //构造反向索引 keys = new String[st.size()]; for (String name : st.keySet()) { keys[st.get(name)] = name; } //构造图--第二遍 G = new Graph(st.size()); while (scanner.hasNextLine()) { String[] a = scanner.nextLine().split(sp); int v = st.get(a[0]); for (int i = 0; i < a.length; i++) { G.addEdge(v,st.get(a[i])); } } } public boolean contains(String s){ return st.containsKey(s); } public int index(String s){ return st.get(s); } public String name(int v){ return keys[v]; } //返回数组邻接表 public Graph G(){ return G; } }1.3.1 最短间隔数-->符号表+广度优先搜索
public static void main(String[] args) { //使用符号表+广度优先搜索 -->实现最短间隔数 Scanner scanner = new Scanner(System.in); SymbolGraph sg = new SymbolGraph(scanner.next(), scanner.next()); Graph G = sg.G(); String source = scanner.next(); if (!sg.contains(source)){ System.out.println(source +" not in database."); return; } int s = sg.index(source); BreadthSearchPaths bfs = new BreadthSearchPaths(G, s); while (!scanner.next().isEmpty()){ String sink = scanner.nextLine(); if (sg.contains(sink)){ int t = sg.index(sink); if (bfs.hasPathTo(t)){ for (int v : bfs.pathTo(t)) { System.out.println(" "+sg.name(v)); } }else { System.out.println("not in database."); } } } }
二:有向图
public class Digraph { private int V;//顶点数目; private int E;//边的数目 private Fundamental.Bag<Integer>[] adj;//邻接表数组 //创建一幅含有V个顶点但没有边的有向图 public Digraph(int v){ this.V = v; this.E = 0; adj = new Fundamental.Bag[v];//创建邻接表 for (int i = 0; i < v; i++) { adj[i] = new Fundamental.Bag<Integer>(); } } //从输入流中读取一幅有向图 Digraph(InputStream inputStream){} //顶点数 int V() { return V; } //边数 int E() { return E; } //向有向图中添加一条边v->w void addEdge(int v,int w){ adj[v].add(w); E++; } //由v指出的所有顶点 Iterable<Integer> adj(int v) { return adj[v]; } //该图1的方向图 Digraph reverse(){ Digraph R = new Digraph(V); for (int v = 0; v < V; v++) { for (int w : adj(v)) { R.addEdge(w,v); } } return R; } @Override public String toString() { return super.toString(); } }2.1 深度优先搜索-->有向图的可达性
//深度优先搜索-->有向图的可达性(给定一个或多个顶点能到达哪些其他顶点的问题) public class DirectedDFS { private boolean[] marked; public DirectedDFS(Digraph G,int s){ marked = new boolean[G.V()]; dfs(G,s); } public DirectedDFS(Digraph G,Fundamental.Bag<Integer> source){ marked = new boolean[G.V()]; for (int s : source) { if (!marked[s]){ dfs(G,s); } } } private void dfs(Digraph G,int v){ marked[v] = true; for (int w : G.adj(v)) { if (!marked[w]){ dfs(G,w); } } } public boolean marked(int v){ return marked[v]; } public static void main(String[] args) { Scanner scanner = new Scanner(System.in); Digraph G = new Digraph(scanner.nextInt()); Fundamental.Bag<Integer> sources = new Fundamental.Bag<>(); for (int i = 1; i < scanner.nextLine().length(); i++) { sources.add(i); } DirectedDFS reachable = new DirectedDFS(G, sources); for (int v = 0; v < G.V(); v++) { if (reachable.marked(v)){ System.out.println(v+" "); } } } } 多点可达性一个重要应用就是在内存管理系统中,如java的使用的标记-清除的垃圾收集,一个顶点表示一个对象,一条边表示一个对象对另外一个对象的引用。它会为每个对象保留一个位做垃圾收集之用,定期标记所有可以被访问到的对象,然后回收没有被标记的对象来腾出内存空间。2.2 深度优先搜索之拓扑排序-->优先级限制下的任务调度
拓扑排序:条件是有向图是无环图。原理是将所有的顶点进行排序,使得所有的有向边均从排在前面的元素指向排在后面的元素。所有边都是向下的。
2.2.1 深度优先搜索-->顶点排序
//深度优先搜索-->s所有顶点排序 public class DepthSearchOrder { private boolean[] marked; private SynchronousQueue<Integer> pre;//所有顶点的前序排列 private SynchronousQueue<Integer> post;//所有顶点的后序排列 private Stack<Integer> reversePost;//所有顶点的逆后序排列 public DepthSearchOrder(Digraph G){ pre = new SynchronousQueue<>(); post = new SynchronousQueue<>(); reversePost = new Stack<>(); marked = new boolean[G.V()]; for (int v = 0; v < G.V(); v++) { if (!marked[v]){ dfs(G,v); } } } private void dfs(Digraph G, int v){ //前序排列在递归调用之前将顶点加入队列 pre.add(v); marked[v] = true; for (int w : G.adj(v)) { if (!marked[w]){ dfs(G,w); } } //后序排列在递归调用之后将顶点加入队列 post.add(v); //逆后序排列在递归调用之后将顶点加入队列 reversePost.push(v); } public Iterable<Integer> pre(){ return pre; } public Iterable<Integer> post(){ return post; } public Iterable<Integer> reversePost(){ return reversePost; } }2.2.2 拓扑排序-->顶点的逆后序排列
//拓扑排序 public class Topological { private Iterable<Integer> order; //顶点的拓扑排序 public Topological(Digraph G){ DirectedCycle findCycle = new DirectedCycle(G); //第一遍深度遍历保证不存环 if (!findCycle.hasCycle()){ //第二遍深度遍历产生顶点的逆后续排列 DepthSearchOrder dfs = new DepthSearchOrder(G); //顶点的逆后序排序 order = dfs.reversePost(); } } public Iterable<Integer> order(){ return order; } public boolean isDAG(){ return order!=null; } public static void main(String[] args) { Scanner scanner = new Scanner(System.in); String filename = scanner.nextLine(); String separator = scanner.nextLine(); //SymbolDigraph与SymbolGraph一样代码 SymbolDigraph sg = new SymbolDigraph(filename, separator); Topological top = new Topological(sg.G()); //如果给定的图包含环则order=null for (int v : top.order()) { System.out.println(sg.name(v)); } } }2.3 深度优先搜索-->寻找有向环
一旦找到一条有向边v->w且w已经存在栈中,则就找到了一个环。因为栈表示一条由w到v的有向路径。
//深度优先搜索-->判断有向图是否为有环图 public class DirectedCycle { private boolean[] marked; private int[] edgeTo; private Stack<Integer> cycle;//有向环中的所有顶点(如果存在有向环) private boolean[] onStack;//递归调用栈上的所有顶点 public DirectedCycle(Digraph G){ onStack = new boolean[G.V()]; edgeTo = new int[G.V()]; for (int v = 0; v < G.V(); v++) { if (!marked[v]){ dfs(G,v); } } } private void dfs(Digraph G,int v){ onStack[v] = true; marked[v] = true; for (int w : G.adj(v)) { if (this.hasCycle()){ return; } else if (!marked[w]) { edgeTo[w] = v; dfs(G,w); }else { cycle = new Stack<Integer>(); for (int x = v; x != w ; x = edgeTo[x]) { cycle.push(x); } cycle.push(w); cycle.push(v); } onStack[v] = false; } } public boolean hasCycle(){ return cycle!=null; } public Iterable<Integer> cycle(){ return cycle; } }2.4 强连通分量
//深度优先搜索的顶点逆后序排列-->计算强连通分量(在CC的基础上实现的) public class SCC { private boolean[] marked;//已访问过的顶点 private int[] id;//强连通分量的标识符 private int count;//强连通分量的数量 public SCC(Digraph G){ marked = new boolean[G.V()];//初始化marked id = new int[G.V()];//初始化id DepthSearchOrder order = new DepthSearchOrder(G.reverse()); for (int s : order.reversePost()) { if (!marked[s]){ dfs(G,s); count++; } } } private void dfs(Digraph G, int v){ marked[v] = true; id[v] = count; for (int w : G.adj(v)) { if (!marked[w]){ dfs(G,w); } } } //顶点v和顶点w是否强连通 public boolean stronglyConnected(int v,int w){ return id[v] == id[w]; } //顶点v属于哪个强连通分量(标识符) public int id(int v){ return id[v]; } //强连通分量数(从0开始) public int count(){ return count++; } }2.5 有向图总结
三:最小生成树(加权图)
以下算法的实现都基于以下要求:
①连通图;②边的权值可能是0或负数;③所有边的权值都各不相同。
切分定理:
切分定理将会把加权图中的所有顶点分为两个集合、检查横跨两个集合的所有边并识别
哪条边应属于图的最小生成树。
图的切分是将图的所有顶点分为两个非空且不重叠的两个集合,横切边是一条连接两个
属于不同集合的顶点的边,切分定理也表明了对于每一次切分,权值最小的横切边必然
属于最小生成树,不断切分直到找到所有边。
3.1 加权无向图的数据类型
允许存在平行边,允许存在自环。
//带权值的边的数据类型 public class Edge implements Comparable<Edge> { private int v;//顶点之一 private int w;//另一个顶点 private double weight;//边的权值 public Edge(int v,int w,int weight){ this.v = v; this.w = w; this.weight =weight; } //边的权值 public double weight(){ return weight; } //边两端的顶点之一 public int either(){ return v; } //得到另一个顶点 public int other(int vertex){ if (vertex == v){ return w; } else if (vertex == w) { return v; }else { throw new RuntimeException("没有意义的边"); } } @Override public int compareTo(Edge that) { if (this.weight()<that.weight()){ return -1; } else if (this.weight()> that.weight()) { return +1; }else { return 0; } } }//加权无向图的数据类型 public class EdgeWeightGraph { private int V;//顶点总数 private int E;//边的总数 private Fundamental.Bag<Edge>[] adj;//邻接表 public EdgeWeightGraph(int V){ this.V = V; this.E = 0; adj = new Fundamental.Bag[V]; for (int v = 0; v < V; v++) { adj[v] = new Fundamental.Bag<Edge>(); } } public int V(){ return V; } public int E(){ return E; } public void addEdge(Edge e){ int v = e.either(); int w = e.other(v); adj[v].add(e); adj[w].add(e); E++; } public Iterable<Edge> adj(int v){ return adj[v]; } //返回加权无向图中的所有边 public Iterable<Edge> edges(){ Fundamental.Bag<Edge> b = new Fundamental.Bag<>(); for (int v = 0; v < V; v++) { for (Edge e : adj[v]) { if (e.other(v)>v){ b.add(e); } } } return b; } }3.2 最小生成树之Prim(Dijkstra)算法(贪心算法)
prim算法的主要思想是每一步都会为一颗生长中的树添加一条边,一开始这个树只有一个顶点,然后会向它添加v-1条边,每次总会是将下一条连接树中的顶点与不在树中的顶点且权值最小的边加入树中。(每次从子问题中找最小值加入树中,处理不了有向图)。
//最小生成树之prim算法-->延时实现 public class LazyPrim { private boolean[] marked;//顶点索引数组标记最小生成树的顶点 private SynchronousQueue<Edge> mst;//队列保存最小生成树的边 private MinHeapIndexPriorityQueue<Edge> pq;//优先队列保存横切边(包括失效边) public LazyPrim(EdgeWeightGraph G) { pq = new MinHeapIndexPriorityQueue<Edge>(G.V()); marked = new boolean[G.V()]; mst = new SynchronousQueue<Edge>(); visit(G, 0);//假设G是连通 while (!pq.isEmpty()) { Edge e = pq.deleteMin(); int v = e.either(); int w = e.other(v); if (marked[v] && marked[w]) { continue; } mst.add(e); if (!marked[v]) { visit(G, v); } if (!marked[w]) { visit(G, w); } } } private void visit(EdgeWeightGraph G, int v) { //标记顶点v并将所有连接v和未被标记顶点的边加入pq marked[v] = true; for (Edge e : G.adj(v)) { if (!marked[e.other(v)]) { pq.insert(e.other(v), e); } } } public Iterable<Edge> edges(){return mst;} public double weight(){ double w = 0; for (Edge edge : mst) { w += edge.weight(); } return w; } public static void main(String[] args) { EdgeWeightGraph G = new EdgeWeightGraph(10); LazyPrim mst = new LazyPrim(G); for (Edge e : mst.edges()) { System.out.println(e); } System.out.println(mst.weight()); } }//最小生成树之prim算法-->即时实现(只需要保存权值最小的那条边) public class PrimMST { private Edge[] edgeTo;//距离树最近(权值最小)的边 private double[] distTo;//distTo[w] = edgeTo[w].weight private boolean[] mared;//如果v在树中则为true private MinHeapIndexPriorityQueue<Double> pq;//保存有效的横向边 public PrimMST(EdgeWeightGraph G){ edgeTo = new Edge[G.V()]; distTo = new double[G.V()]; mared = new boolean[G.V()]; for (int v = 0; v < G.V(); v++) { distTo[v] = Double.POSITIVE_INFINITY; } pq = new MinHeapIndexPriorityQueue<Double>(G.V()); distTo[0] = 0.0; pq.insert(0,0.0);//用顶点0和权值为0初始化pq while (!pq.isEmpty()){ visit(G,pq.deleteMin());//将权值最小的顶点添加到树中 } } private void visit(EdgeWeightGraph G,int v){ //将顶点v添加到树中,更新数据 mared[v] = true; for (Edge e : G.adj(v)) { int w = e.other(v); if (mared[w]){ continue;//v-w失效 } if (e.weight()<distTo[w]){ //连接w和树的最佳边Edge变为e edgeTo[w] = e; distTo[w] = e.weight(); if (pq.contains(w)){ pq.change(w,distTo[[w]]); }else { pq.insert(w,distTo[w]); } } } } }3.3 最小生成树之Kruskal算法(贪心算法)
kruskal算法的主要思想是按照边的权值顺序(小到大)处理它们,将权值最小的边加入最小生成树,加入的边不会与已经加入的边构成环,直到树中含有v-1条边为止就完成最小生成树。(比prim算法慢,因为在完成优先队列操作之外,还需要一次connect()判断是否连接,处理不了有向图)。
//最小生成树之kruskal算法 public class KruskalMST { private SynchronousQueue<Edge> mst;//保存最小生成树的所有边 public KruskalMST(EdgeWeightGraph G){ mst =new SynchronousQueue<Edge>(); //保存还未被检查的边 MinHeapIndexPriorityQueue<Edge> pq; pq = new MinHeapIndexPriorityQueue<Edge>(G.V()); for (Edge e : G.edges()) { pq.insert(e.either(),e); } //用uf判度无效的边 union_find uf = new union_find(G.V()); while (!pq.isEmpty() && mst.size()<G.V() -1){ Edge e = pq.deleteMin();//得到权值最小的边和他的顶点 int v = e.either(); int w = e.other(v); if (uf.connected(v,w)){ continue;//忽略失效的边 } uf.union(v,w);//合并分量 mst.add(e);//保存最小生成树的边 } } public Iterable<Edge> edges(){ return mst; } }3.4 最小生成树总结
四:最短路径
以下算法的实现都基于以下要求:
①路径是有向的;②并不是所有顶点都是可达的;③可以负权值;④最短路径不唯一;⑤可能存在平行边和自环。
4.1 加权有向图的数据类型
//加权有向边的数据类型 public class DirectedEdge { private int v;//边的起点 private int w;//边的终点 private double weight;//边的权值 public DirectedEdge(int v,int w,double weight){ this.v = v; this.w = w; this.weight = weight; } public double weight(){ return weight;} //指出这条边的顶点 public int from(){ return v;} //这条边指向的顶点 public int to(){ return w;} }//加权有向图的数据类型 public class EdgeWeightDigraph { private int v;//顶点总数 private int E;//边的总数 //由顶点索引的Bag对象数组 private Fundamental.Bag<DirectedEdge>[] adj;//邻接表 //v个顶点的空边有向图 public EdgeWeightDigraph(int v){ this.v = v; this.E = 0; adj = new Fundamental.Bag[v]; for (int i = 0; i < v; i++) { adj[i] = new Fundamental.Bag<DirectedEdge>(); } } public int V(){ return v;} public int E(){ return E;} //将e边添加到该有向图中 public void addEdge(DirectedEdge e){ adj[e.from()].add(e); E++; } //从v指出的边 public Iterable<DirectedEdge> adj(int v){ return adj[v]; } //该有向图中的所有边 public Iterable<DirectedEdge> edges(){ Fundamental.Bag<DirectedEdge> bag = new Fundamental.Bag<>(); for (int i = 0; i < v; i++) { for (DirectedEdge e : adj[v]) { bag.add(e); } } return bag; } }4.2 无负权值有环的加权有向图中的最短路径之Dijkstra算法
边的松弛技术:
放松边v-->w意味着检查从s到w的最短路径是否是先从s到v,然后再由v到w。如果是,
则根据这个情况更新数据结构的内容。
dijkstra算法主要思想是在不考虑负权值和环下将顶点索引的数组distTo[s](起点)初始化为0,distTo[ ]中的其他元素初始化为正无穷,然后将distTo[ ]最小的非树顶点放松并加入树中,一直这样操作下去,直到所有顶点都在树中或者所有的非树顶点的distTo[ ]值均为无穷大。它解决了权值非负的加权有向图的单起点最短路径问题。
dijkstra算法每次都会为最短路径树添加一条边,该边由一个树中的顶点指向非树顶点w,且w它是到起点s最近的顶点。
//最短路径的dijkstra算法 public class DijkstraSP { private DirectedEdge[] edgeTo; private double[] distTo; private MinHeapIndexPriorityQueue<Double> pq; public DijkstraSP(EdgeWeightDigraph G,int s){ edgeTo = new DirectedEdge[G.V()]; distTo = new double[G.V()]; pq = new MinHeapIndexPriorityQueue<>(G.V()); for (int v = 0; v < G.V(); v++) { distTo[v] = Double.POSITIVE_INFINITY; distTo[s] = 0.0; pq.insert(s,0.0); while (!pq.isEmpty()){ relax(G,pq.deleteMin()); } } } //顶点的松弛 private void relax(EdgeWeightDigraph G,int v){ for (DirectedEdge e : G.adj(v)) { int w = e.to(); if (distTo[w]>distTo[v] +e.weight()){ distTo[w] = distTo[v] + e.weight(); edgeTo[w] = e; if (pq.contains(w)){ pq.change(w,distTo[w]); }else { pq.insert(w,distTo[w]); } } } } //起点s到v的距离,如果不存在则路径无穷大 public double distTo(int v){ return distTo[v]; } //是否存在从起点s到v的路径 public boolean hasPathTo(int v){ return distTo[v] < Double.POSITIVE_INFINITY; } //从起点s到v的路径,如果不存在则为null public Iterable<DirectedEdge> pathTo(int v){ if (!hasPathTo(v)){return null;} Stack<DirectedEdge> path = new Stack<>(); for (DirectedEdge e=edgeTo[v];e!=null;e=edgeTo[e.from()]){ path.push(e); } return path; } public static void main(String[] args) { EdgeWeightDigraph G = new EdgeWeightDigraph(10); DijkstraSP sp = new DijkstraSP(G,0); for (int t = 0; t < G.V(); t++) { if(sp.hasPathTo(t)){ for (DirectedEdge e : sp.pathTo(t)) { System.out.println(e); } } } } }4.2.1 任意顶点对之间的最短路径
//任意顶点对之间的最短路径 public class DijkstraAllSP { private DijkstraSP[] all; DijkstraAllSP(EdgeWeightDigraph G){ all = new DijkstraSP[G.V()]; for (int v = 0; v < G.V(); v++) { all[v] = new DijkstraSP(G,v); } } Iterable<DirectedEdge> path(int s,int t){ return all[s].pathTo(t); } double dist(int s, int t){ return all[s].distTo(t); } }4.2.2 欧几里得图中的最短路径
欧几里得图中顶点都在平面上且边的权值与顶点欧几里得间距成正比,解决单点、给定两点和任意定点对之间的最短路径问题,也大大提高了Dijkstra算法的运行速度。
4.3 有负权值无环的加权有向图中的最短路径算法
因为图是无环的特点,那么就出现了一种比Dijkstra算法更快、更简单的最短路径算法。此算法能够在线性时间内解决单点最短路径问题、能够处理负权值问题、能够找出最长的路径等问题。它简单扩展了无环有向图的拓扑排序算法,并结合顶点的松弛技术,通过一个一个地按照拓扑顺序放松所有顶点来实现的。
4.3.1 最短路径:
//无环加权有向图的最短路径算法 public class AcyclicSP { private DirectedEdge[] edgeTo; private double[] distTo; public AcyclicSP(EdgeWeightDigraph G,int s){ edgeTo = new DirectedEdge[G.V()]; distTo = new double[G.V()]; for (int v = 0; v < G.V(); v++) { distTo[v] = Double.POSITIVE_INFINITY; } distTo[s] = 0.0; Topological top = new Topological(G);//深度优先搜索得到拓扑排序结果 for (int v : top.order()) { relax(G,v); } } //顶点的松弛 private void relax(EdgeWeightDigraph G,int v){ for (DirectedEdge e : G.adj(v)) { int w = e.to(); if (distTo[w]>distTo[v] +e.weight()){ distTo[w] = distTo[v] + e.weight(); edgeTo[w] = e; } } } //边的松弛 private void relax(DirectedEdge e){ int v = e.from(), w = e.to(); if (distTo[w]>distTo[v]+e.weight()){ distTo[w] = distTo[v] +e.weight(); edgeTo[w] =e; } } //起点s到v的距离,如果不存在则路径无穷大 public double distTo(int v){ return distTo[v]; } //是否存在从起点s到v的路径 public boolean hasPathTo(int v){ return distTo[v] < Double.POSITIVE_INFINITY; } //从起点s到v的路径,如果不存在则为null public Iterable<DirectedEdge> pathTo(int v){ if (!hasPathTo(v)){return null;} Stack<DirectedEdge> path = new Stack<>(); for (DirectedEdge e=edgeTo[v];e!=null;e=edgeTo[e.from()]){ path.push(e); } return path; } }4.3.2 最长路径:
边的权值可负,通过复制原始无环有向图得到一个副本并将副本中的所有边的负权值都取相反数,这样副本中的最短路径即为原图中的最长路径。
4.3.3 优先级限制下的并行任务调度
在之前在单个处理器限制下使用拓扑排序完成一组关于任务完成的先后次序的优先级限制问题,完成任务的总耗时就是所有任务所需要的总时间。
现在假设有足够多的处理器并能够同时处理任意多的任务,那么现在受到的只有优先级的限制,因此需要一个高效的算法,正好存在一种线性时间的算法叫做“关键路径”,此算法和无环加权有向图中的最长路径问题是等价的。
//优先级限制下的并行任务调度问题的关键路径(无环加权有向图的最长路径( public class CPM { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); int N = scanner.nextInt(); EdgeWeightDigraph G = new EdgeWeightDigraph(2 * N + 2); int s = 2 * N; int t = 2 * N + 1; for (int i = 0; i < N; i++) { String[] a = scanner.nextLine().split("\\s+"); double duration = Double.parseDouble(a[0]); G.addEdge(new DirectedEdge(i, i + N, duration)); G.addEdge(new DirectedEdge(s, i, 0.0)); G.addEdge(new DirectedEdge(i + N, t, 0.0)); for (int j = 0; j < a.length; j++) { int successor = Integer.parseInt(a[j]); G.addEdge(new DirectedEdge(i + N, successor, 0.0)); } } AcyclicSP lp = new AcyclicSP(G, s); for (int i = 0; i < N; i++) { System.out.println(i + ":" + lp.distTo(i)); } System.out.println("finish time:" + lp.distTo(t)); } }4.4 有负权值有环的加权有向图中的最短路径算法
在含有环且有负权值的的加权有向图中寻找最短路径是一个没有意义的问题,也无法在这种图中高效找出一种有效的最短路径算法。但是仍然需要能够识别出图中的负权值环(环的所有边的权值之和为负),那就是Bellman-ford算法。这个算法非常通用,它没有指定边的放松顺序,也能解决单点最短路径问题。
Bellman-ford算法最多遍历V轮,如果V轮之后还是需要放松的话就说明存在负权值环,只要在V轮之内有一轮不放松一条边就解决了有负权值有环的加权有向图的单点最短路径问题。
4.4.1 负权值环检测-->Bellman-ford算法(单点最短路径问题)
//检查图是否含有负权值环,如果没有此算法也解决了单点最短路径问题 public class BellmanFordSP { private double[] distTo;//从起点到某个顶点的路径长度 private DirectedEdge[] edgeTo;//从起点到某个顶点的最后一条边 private boolean[] onQ;//该顶点是否存在队列中 private SynchronousQueue<Integer> queue;//正在被放松的顶点 private int cost;//relax()的调用次数 private Iterable<DirectedEdge> cycle;//edgeTo[]中的是否存在负权值环 public BellmanFordSP(EdgeWeightDigraph G,int s){ distTo = new double[G.V()]; edgeTo = new DirectedEdge[G.V()]; onQ = new boolean[G.V()]; queue = new SynchronousQueue<Integer>(); for (int v = 0; v < G.V(); v++) { distTo[v] = Double.POSITIVE_INFINITY; } distTo[s] = 0.0; queue.add(s); onQ[s] = true; while (!queue.isEmpty() && !hasNegativeCycle()){ int v = queue.poll(); onQ[v] = false; relax(G,v); } } //是否含有负权值环 private boolean hasNegativeCycle(){ return cycle != null; } private void relax(EdgeWeightDigraph G,int v){ for (DirectedEdge e : G.adj(v)) { int w = e.to(); if (distTo[w] > distTo[v] + e.weight()){ distTo[w] = distTo[v] + e.weight(); edgeTo[w] = e; if (!onQ[w]){ queue.add(w); onQ[w] = true; } } if (cost++ % G.V() ==0){ findNegativeCycle(); } } } //查找环 private void findNegativeCycle(){ int V = edgeTo.length; EdgeWeightDigraph spt = new EdgeWeightDigraph(V); for (int v = 0; v < V; v++) { if (edgeTo[v]!=null){ spt.addEdge(edgeTo[v]); } } //检查环 DirectedCycle cf = new DirectedCycle(spt); cycle = cf.cycle(); } //得到负权值环,没有null public Iterable<Edge> negativeCycle(){ return cycle; } //起点s到v的距离,如果不存在则路径无穷大 public double distTo(int v){ return distTo[v]; } //是否存在从起点s到v的路径 public boolean hasPathTo(int v){ return distTo[v] < Double.POSITIVE_INFINITY; } //从起点s到v的路径,如果不存在则为null public Iterable<DirectedEdge> pathTo(int v){ if (!hasPathTo(v)){return null;} Stack<DirectedEdge> path = new Stack<>(); for (DirectedEdge e=edgeTo[v];e!=null;e=edgeTo[e.from()]){ path.push(e); } return path; } }4.4.2 套汇
套汇问题等价于加权有向图中的负权值环的检测问题,取边权值的自然对数并取反,这样原始问题中所有边的权值之积的计算就转化为新图中所有边的权值之和的计算了。当边的权值之和最小时汇率之积正好最大。如果存在负权值环,那么权值之和是无穷小。
//套汇-->负权值环的检测 public class Arbitrage { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); int V = scanner.nextInt(); String[] name = new String[V]; EdgeWeightDigraph G = new EdgeWeightDigraph(V); for (int v = 0; v < V; v++) { name[v] = scanner.nextLine(); for (int w = 0; w < V; w++) { double rate = scanner.nextDouble(); DirectedEdge e = new DirectedEdge(v, w, -Math.log(rate)); G.addEdge(e); } } BellmanFordSP spt = new BellmanFordSP(G, 0); if (spt.hasNegativeCycle()) { double stake = 1000.0; for (DirectedEdge e : spt.negativeCycle()) { System.out.println(stake + ":" + name[e.from()]); stake *= Math.exp(-e.weight()); System.out.println(stake + ":" + name[e.to()]); } }else { System.out.println("没有套汇机会"); } } }4.5. 总结
Dijkstra算法是按照顶点距离起点的远近顺序构造最短路径树的算法,该算法也可以用来计算最小生成树。