第9章 图论算法
拓扑排序是对有向无圈图的顶点的一种排序,使得如果存在一条从Vi到Vj的路径,那么在排序中Vj就出现在Vi的后面。
一个简单的求拓扑排序的算法是先找出任意一个没有入边的顶点。然后显示出该顶点,并将它及其边一起从图中删除。然后,我们对图的其余部分同样应用这样的方法处理。
简单拓扑排序的伪代码
1 /** 2 * void topsort() throws CycleFoundException 3 * { 4 * for(int counter = 0; counter < NUM_VERTICES; counter++) 5 * { 6 * Vertex v = findNewVertexOfIndegreeZero(); 7 * if(v = null) 8 * throw new CycleFoundException(); 9 * v.topNum = counter; 10 * for each Vertex w adjacent to v 11 * w.indegree--; 12 * } 13 * } 14 */
入度的计算由下面的代码实现,同理可见此计算的花销也是O(|E| + |V|),虽然其中有嵌套的循环。
1 /** 2 * for each Vertex v 3 * v.indegree = 0; 4 * 5 * for each Vertex v 6 * for each Vertex w adjacent to v 7 * w.indegree++; 8 */
我们可以通过将所有(未分配拓扑编号)的入度为0的顶点放在一个特殊的盒子中而消除这种无效的劳动。此时findNewVertexOfIndegreeZero方法返回(并删除)的是该盒子中的任一项点。当我们降低它的邻接顶点的入度时,检查每一个顶点并在它的入度降为0时把它放入盒子中。
为实现这个盒子,我们可以使用一个栈或一个队列。首先,对每个顶点计算它的入度。然后,将所有入度为0的顶点放入一个初始为空的队列中。当队列不空时,删除一个顶点v,并将v邻接的所有顶点的入度均减1。只要一个顶点的入度降为0,就把该顶点放入队列中。此时,拓扑排序就是顶点出队的顺序。
1 /** 2 * void topsort() throws CycleFoundException 3 * { 4 * Queue<Vertex> q = new Queue<Vertex>( ); 5 * int count = 0; 6 * 7 * for each Vertex v 8 * if(v.indegree == 0) 9 * q.enqueue(v); 10 * 11 * while(!q.isEmpty( )) 12 * { 13 * Vertex v = q.dequeue();l 14 * v.topNum = ++counter; 15 * 16 * for each Vertex w adjacent to v 17 * if(--w.indegree == 0) 18 * q.enqueue(w); 19 * } 20 * if(counter != NUM_VERTICES) 21 * throw new CycleFoundException; 22 * } 23 */
广度优先搜索
无权最短路径算法的伪代码1
1 /** 2 * void unweighted(Vertex s) 3 * { 4 * for each Vertex v 5 * { 6 * v.dist = INFINITY; 7 * v.known = false; 8 * } 9 * 10 * s.dist = 0; 11 * 12 * for(int currDist = 0; currDist < NUM_VERTICES; currDist++) 13 * for each Vertex x 14 * if(!v.known && v.dist == currDist) 15 * { 16 * v.known = true; 17 * for each Vertex w adjacent to v 18 * if(w.dist == INFINITY) 19 * { 20 * w.dist = currDist + 1; 21 * w.path = x; 22 * } 23 * } 24 * } 25 */
无权最短路径算法的伪代码2
在迭代开始的时候,队列只含有距离为currDist的那些顶点。当添加距离为currDist + 1的那些邻接顶点时,由于它们自队尾入队,因此这就保证它们直到所有距离为currDist的顶点都被处理之后才被处理。在距离currDist处的最后一个顶点出队并被处理之后,队列只含有距离为currDist + 1的顶点,因此该过程将不断进行下去。
1 /** 2 * void unweighted(Vertex s) 3 * { 4 * Queue<Vertex> q = new Queue<Vertex>; 5 * 6 * for each Vertex v 7 * v.dist = INFINITY; 8 * 9 * s.dist = 0; 10 * q.enqueue(s); 11 * 12 * while(!q.isEmpty()) 13 * { 14 * Vertex v = q.dequeue(); 15 * 16 * for each Vertex w adjacent to v 17 * if(w.dist == INFINITY) 18 * { 19 * w.dist = v.dist + 1; 20 * w.path = v; 21 * q.enqueue(w); 22 * } 23 * } 24 * } 25 */
使用与对拓扑排序进行同样的分析,我们看到,只要使用邻接表,则运行时间就是O(|E| + |V|)。
Dijkstra算法
Dijkstra算法中的Vertex类
1 /** 2 * class Vertex 3 * { 4 * public List adj; // Adjacency List 5 * public boolean known; 6 * public DistType dist; // DistType is probably int 7 * public Vertex path; 8 * // Other fields and methods as needed 9 * } 10 */
显示实际最短路径的例程
1 /** 2 * void printPath(Vertex v) 3 * { 4 * if(v != null) 5 * { 6 * printPath(v.path) 7 * System.out.print("to"); 8 * } 9 * System.out.print(v); 10 * } 11 */
Dijkstra算法的伪代码
1 /** 2 * void dijkstra(Vertex s) 3 * { 4 * for each Vertex v 5 * { 6 * v.dist = INFINITY; 7 * v.known = false; 8 * } 9 * 10 * s.dist = 0; 11 * 12 * while(there is an unknown distance vertex) 13 * { 14 * Vertex v = smallest unknown distance vertex; 15 * 16 * v.known = true; 17 * 18 * for each Vertex w adjacent to v 19 * if(!w.known) 20 * { 21 * DistType cvw = cost of edge from v to w; 22 * 23 * if(v.dist + cvw < w.dist) 24 * { 25 * // Update w 26 * decrease(w.dist to v.dist + cvw); 27 * w.path = v; 28 * } 29 * } 30 * } 31 * } 32 */
具有负边值的赋权最短路径算法的伪代码
1 /** 2 * void weightedNegative(Vertex v) 3 * { 4 * Queue<Vertex> q = new Queue<Vertex>( ); 5 * 6 * for each Vertex v 7 * v.dist = INFINITY; 8 * 9 * s.dist = 0; 10 * q.enqueue(s); 11 * 12 * while(!q.isEmpty()) 13 * { 14 * Vertex v = q.dequeue(); 15 * 16 * for each Vertex w adjacent to v 17 * if(v.dist + cvw < w.dist) 18 * { 19 * //Update w 20 * w.dist = v.dist + cvw; 21 * w.path = v; 22 * if(w is not already in q) 23 * q.enqueue(w); 24 * } 25 * } 26 * } 27 */
事件节点图
为了找出方案的最早完成时间,我们只要找出从第一个事件到最后一个事件的最长路径的长。
借助顶点的拓扑顺序计算它们的最早完成时间,而最晚完成时间则通过倒转它们的拓扑顺序来计算。
事件节点图中每条边的松弛时间代表对应动作可以被延迟而又不至于推迟整体的完成的时间量。
松弛时间 = 最晚完成时间 - 最早完成时间。
某些动作的松弛时间为零,这些动作是关键性的动作,它们必须按计划结束。至少存在一种完全由零-松弛边组成的路径,这样的路径是关键路径
最短路径,求词梯的Java例程
第一个例程是findChain,它利用Map表示邻接表和两个要被连接的单词,同时返回一个Map,在该Map中,关键字是单词,而相应的值是位于从first开始的最短词梯上的关键字前面的那个单词。
getChainFromPreviousMap使用prev Map和second,它是Map中的一个关键字并返回用于形成词梯的那些单词,通过prev向后工作。通过使用LinkedList并在前头插入,我们得到以正确顺序排列的词梯。
1 public static List<String> 2 findChain(Map<String, List<String>>adjacentWords, String first, String second) 3 { 4 Map<String, String> previousWord = new HashMap<>(); 5 LinkedList<String> q = new LinkedList<>(); 6 7 q.addLast(first); 8 while (!q.isEmpty()) 9 { 10 String current = q.removeFirst(); 11 List<String> adj = adjacentWords.get(current); 12 13 if (adj != null) 14 for (String adjWord : adj) 15 if (previousWord.get(adjWord) == null) 16 { 17 previousWord.put(adjWord, current); 18 q.addLast(adjWord); 19 } 20 } 21 previousWord.put(first, null); 22 23 return getChainFromPreviousMap(previousWord, first, second); 24 } 25 26 public static List<String> getChainFromPreviousMap(Map<String, String> prev, String first, String second) 27 { 28 LinkedList<String>result = null; 29 30 if (prev.get(second) != null) 31 { 32 result = new LinkedList<>(); 33 for (String str = second; str != null; str = prev.get(str)) 34 result.addLast(str); 35 } 36 return result; 37 }
9.5.2 Kruskal算法
1 /** 2 * ArrayList<Edge>kruskal(List<Edge>edges, int numVertices) 3 * { 4 * DisjSets ds = new DisjSets(numVertices); 5 * PriorityQueue<Edge>pq = new Priority<edges>; 6 * List<Edge> mst = new ArrayList<>(); 7 * 8 * while(mst.size() != numVertices - 1) 9 * { 10 * Edge e = pq.deleteMin(); 11 * SetType uset = ds.find(e.getu()); 12 * SetType vset = ds.dind(e.getv()); 13 * 14 * if(uset != vset) 15 * { 16 * // Accept the edge 17 * mst.add(e); 18 * ds.union(uset, vset); 19 * } 20 * } 21 * return mst; 22 * } 23 */
9.6 深度优先搜索模板(伪代码)
1 /** 2 * void dfs(Vertex v) 3 * { 4 * v.visited = true; 5 * for each Vertex w adjacent to v 6 * if(!w.visited) 7 * dfs(w); 8 * } 9 */
一个连通的无向图如果不存在被删除之后使得剩下的图不再连通的顶点,那么这样的无向连通图就称为是双连通的
对顶点的Num赋值的例程(伪代码)
1 /** 2 * void assignNum(Vertex v) 3 * { 4 * v.num = counter++; 5 * v.visited = true; 6 * for each Vertex w adjacent to v 7 * if(!w.visited) 8 * { 9 * w.parent = v; 10 * assignNum(w); 11 * } 12 * } 13 */
计算Low并检验是否割点的伪代码(忽略对根的检验)
1 /** 2 * // Assign low; also check for articulation points. 3 * void assignLow(Vertex v) 4 * { 5 * v.low = v.num; 6 * for each Vertex w adjacent to v 7 * { 8 * if(w.num > v.num) 9 * { 10 * assignLow(w); 11 * if(w.low >= v.num) 12 * System.out.println(v + "is an articulation point"); 13 * v.low = min(v.low, w.low); 14 * } 15 * else 16 * if(v.parent != w) 17 * v.low = min(v.low, w.num); 18 * } 19 * } 20 */
不存在一个遍历必须是先序遍历或后序遍历的法则。在递归调用前和递归调用后都有可能进行处理
在一次深度优先搜索(忽略对根的测试)中对割点的检测(伪代码)
1 /** 2 * void findArt(Vertex v) 3 * { 4 * v.visited = true; 5 * v.low = v.num = counter++; 6 * for each Vertex w adjacent to v 7 * { 8 * if(!w.visited) 9 * { 10 * w.parent = v; 11 * findArt(w); 12 * if(w.low >= v.num) 13 * System.out.println(v + " is an articulation point"); 14 * v.low = min(v.low, w.low); 15 * } 16 * else 17 * if(v.parent != w) 18 * v.low = min(v.low, w.num); 19 * } 20 * } 21 */
9.49 编写一个程序计算其单字母替换取值为1,而单字母添加或删除取值p > 0的词梯,取值由用户指定。
1 import java.io.File; 2 import java.util.ArrayList; 3 import java.util.Scanner; 4 5 public class WordLadder 6 { 7 static int INFINITY = 99999; 8 9 private static class Vertex 10 { 11 Vertex() 12 { 13 adj = new ArrayList<>(); 14 weight = new ArrayList<>(); 15 } 16 public ArrayList<Integer> adj; // Adjacent list 17 public ArrayList<Integer> weight; // Weight list 18 public boolean known; 19 public int dist; 20 public String name; 21 public int path; 22 } 23 24 /** 25 * Print shortest path to v after dijkstra has run. 26 * Assume that the path exists. 27 */ 28 static void printPath(int vIndex, ArrayList<Vertex>V) 29 { 30 if (vIndex >= 0 && V.get(vIndex).path > -1) 31 { 32 printPath(V.get(vIndex).path, V); 33 System.out.println(" to "); 34 } 35 System.out.println(V.get(vIndex).name); 36 } 37 38 static void dijkstra(int sIndex, int tIndex, ArrayList<Vertex>Vertices) 39 { 40 int smallestDist; 41 int smallestVertex; 42 Vertex s, v, t; 43 int n = Vertices.size(); 44 45 for ( ; ; ) 46 { 47 smallestDist = INFINITY; 48 smallestVertex = -1; 49 50 for (int i = 0; i < n; i++) 51 { 52 if (!Vertices.get(i).known && Vertices.get(i).dist < smallestDist) 53 { 54 smallestDist = Vertices.get(i).dist; 55 smallestVertex = i; 56 } 57 } 58 59 if (smallestVertex < 0 || smallestVertex == tIndex) 60 break; 61 62 Vertices.get(smallestVertex).known = true; 63 v = Vertices.get(smallestVertex); 64 65 for (int j = 0; j < v.adj.size(); j++) 66 { 67 if (!(Vertices.get(v.adj.get(j))).known) 68 { 69 if (v.dist + v.weight.get(j) < Vertices.get(v.adj.get(j)).dist) 70 { 71 // Update w 72 Vertices.get(v.adj.get(j)).dist = v.dist + v.weight.get(j); 73 Vertices.get(v.adj.get(j)).path = smallestVertex; 74 } 75 } 76 } 77 } 78 } 79 80 static ArrayList<Vertex>readWords(Scanner in) 81 { 82 String oneLine = new String(); 83 ArrayList<Vertex>v = new ArrayList<>(); 84 while (in.hasNext()) 85 { 86 oneLine = in.next(); 87 Vertex w = new Vertex(); 88 w.name = oneLine; 89 w.known = false; 90 w.path = -1; 91 w.dist = INFINITY; 92 v.add(w); 93 } 94 return v; 95 } 96 97 static int oneCharOff(String word1, String word2, int p) 98 { 99 String big, small, shrink; 100 int cost; 101 102 if (Math.abs((int)(word1.length() - word2.length())) > 1) 103 return 0; 104 else 105 if (word1.length() == word2.length()) 106 { 107 int diffs = 0; 108 109 for (int i = 0; i < word1.length(); i++) 110 if (word1.charAt(i) != word2.charAt(i)) 111 if (++diffs > 1) 112 return 0; 113 if (diffs == 1) 114 return 1; 115 } 116 117 if (word2.length() > word1.length()) 118 { 119 big = word2; 120 small = word1; 121 } 122 else 123 { 124 big = word1; 125 small = word2; 126 } 127 128 for (int i = 0; i < big.length(); i++) 129 { 130 shrink = big.substring(0, i) + big.substring(i + 1,big.length()); 131 if (shrink.compareTo(small) == 0) 132 return p; 133 } 134 135 return 0; 136 } 137 138 // fills the adjacency lists for all words in the dictionary 139 static void fillAdjacencies(ArrayList<Vertex>words, int p) 140 { 141 int cost; 142 143 for (int i = 0; i < words.size(); i++) 144 for (int j = i + 1; j < words.size(); j++) 145 { 146 cost = oneCharOff(words.get(i).name, words.get(j).name, p); 147 if (cost > 0) 148 { 149 words.get(i).adj.add(i); 150 words.get(i).weight.add(cost); 151 words.get(j).adj.add(j); 152 words.get(j).weight.add(cost); 153 } 154 } 155 } 156 157 public static void main(String[] args) 158 { 159 int p = 0; 160 int w1Index, w2Index; 161 String w1 = new String(); 162 String w2 = new String(); 163 Scanner input = new Scanner(System.in); 164 Scanner dict; 165 try 166 { 167 dict = new Scanner(new File("dict.txt")); 168 } 169 catch (Exception e) 170 { 171 System.out.println("Error opening dictionary file"); 172 return; 173 } 174 175 System.out.println("What is the cost of single char deletions: "); 176 p = input.nextInt(); 177 178 ArrayList<Vertex>words = readWords(dict); 179 180 do 181 { 182 System.out.println("Enter two words in the dictionary: "); 183 w1 = input.next(); 184 w2 = input.next(); 185 // Find their indices(here is where a map would be superior) 186 // However all other accesses are now in O(1) time 187 for (w1Index = 0; w1Index < words.size() && (words.get(w1Index).name).compareTo(w1) != 0; w1Index++); 188 for (w2Index = 0; w2Index < words.size() && (words.get(w2Index).name).compareTo(w2) != 0; w2Index++); 189 }while (w1Index >= words.size() || w2Index >= words.size()); 190 191 fillAdjacencies(words,p); // make the adjacency list 192 dijkstra(w1Index, w2Index, words); // use dijkstra's algorithm 193 System.out.println(); 194 printPath(w2Index,words); // print the result 195 System.out.println(); 196 } 197 }