圖的最短路徑
只是個人的總結, 防止忘記
定義: 找到一個點到另一個頂點成本最小的路徑
Dijkstra( 權重非負, 有無環都可)
能夠得到最短路徑的算法, 只要證明自己可以放松所有的邊, 直到都失效為止.
對于樸素算法: 需要最終所有節點都被放松過
對于有限隊列優化的: 優先級最小的頂點的distTo[]值就是最短路徑的權重, 它不會小于任意已經放松過的任意頂點的最短路徑的權重, 也不會大于任何還未被放松過的任意頂點的最短路徑的權重. 這樣, 所有s可以到達的頂點的最短路徑都按照最短路徑的權重順序被放松.
如果沒有環, 那可以用拓撲排序優化, 先拓撲排序, 然后按照順序放松節點, 這是無環最優化的, 且可以處理負權重邊.
可以做的leetcode題目
(https://leetcode-cn.com/problems/path-with-maximum-probability/)
(https://leetcode-cn.com/problems/path-with-minimum-effort/)
(https://leetcode-cn.com/problems/network-delay-time/)
Dijkstra像是 BFS 和 PRIM 算法.
樸素的Dijkstra:
會放松所有的邊, ans里的節點都會被訪問到. 且可以看到一個節點的距離值可以被多次修改.
// 3. dijkstra
def dijkstraSolution(times: Array[Array[Int]], n:Int, k:Int): Int = {
import scala.collection.mutable
val tu = mutable.Map[Int, mutable.Map[Int, Int]]()
(1 to n).foreach{ i => tu.put(i, mutable.Map[Int, Int]())}
times.foreach{
case Array(uid, vid, weight) => {
tu(uid).put(vid, weight)
}
}
val ans = mutable.Map[Int, Int]((1 to n).map( _ -> Integer.MAX_VALUE):_*) // 這個, ans的元素綜合為n個和88行, 保證了全部能被訪問到
ans.update(k, 0)
val visitedSet = mutable.Set[Int]()
while(visitedSet.size != n) {
val _@(miniNode, miniWeight): (Int, Int) = ans.filterNot(e => visitedSet.contains(e._1)).minBy(_._2)
visitedSet.add(miniNode)
// println("visited: " + visitedSet.map(_.toString))
// println("ans" + ans.toMap)
val adjs: mutable.Map[Int, Int] = tu(miniNode)
adjs.foreach{
case (adjNode, adjValue) => {
ans(adjNode) = math.min(adjValue + miniWeight, ans(adjNode))
// ans.update(adjNode, math.min(adjValue + miniWeight, ans(adjNode)))
}
}
}
if(ans.values.exists( _ == Integer.MAX_VALUE)) {
-1
}else{
ans.values.max
}
}
優先隊列優化的
在設置一個點的距離前, 會將所有之前放松其他邊得到的可能最小距離值, 放到優先隊列中.
另外, 優先隊列里的元素, 是修改它的值, 還是重新壓入,使得隊列中存在失效的數據. 這兩種方式, 前者是即時實現, 后者是lazy的延時實現.(我沒太明白算法4里的這個)
// 4. optimized dijstra
def optimized_dijstra(times: Array[Array[Int]], n: Int, k: Int) = {
import scala.collection.mutable
val tu = mutable.Map[Int, mutable.Map[Int, Int]]()
(1 to n).foreach{ i => tu.put(i, mutable.Map[Int, Int]())}
times.foreach{
case Array(uid, vid, weight) => {
tu(uid).put(vid, weight)
}
}
val ans = mutable.Map[Int, Int]((1 to n).map( _ -> Integer.MAX_VALUE):_*)
implicit val tupleOrdering: Ordering[(Int, Int)] = new Ordering[(Int, Int)]{
override def compare(x: (Int, Int), y: (Int, Int)): Int = x._1 - x._2
}
val updateSignalPriorityQueue = mutable.PriorityQueue[(Int, Int)]()(tupleOrdering)
updateSignalPriorityQueue.enqueue((k,0))
while(updateSignalPriorityQueue.nonEmpty) { // method1
val _@(miniNode, updatedWeight) = updateSignalPriorityQueue.dequeue()
// 如果沒有被更新過...
// 被更新過
if(ans(miniNode) == Integer.MAX_VALUE || updatedWeight < ans(miniNode)) {
// if( ans(miniNode) == Integer.MAX_VALUE || !visitedSet.contains(miniNode)) {
ans(miniNode) = updatedWeight
// visitedSet.add(miniNode)
tu(miniNode).foreach {
case _@(adjV, adjW) => {
updateSignalPriorityQueue.enqueue(adjV -> (updatedWeight + adjW)) // 這個方法的復雜度和對有限隊列的操作成正比, updateWeight < ans(miniNOde)保證了失效的邊不會再作用, 但也是個彈出操作, 而如果在
// 這里, 不用enqueue, 如果adjV存在于有限隊列,修改它的值,并且,調整順序, 總調整次數與彈出操作次數相同
}
}
}else{
()
}
}
if(ans.values.exists( _ == Integer.MAX_VALUE)) {
-1
}else{
ans.values.max
}
}
確定無環的用拓撲排序優化
bfs 或者 dfs 的方式
效率不高 , 可以忽略
變種
最長路徑
- 轉換視角, 非負權重圖的最短路徑就是 負權重值的最長路徑 , 只要把原圖的權重全部取反, 最后得到結果后再取反就好.
- 改變relax函數中的不等式方向, 原來是新的權重如果小于舊的權重更新, 現在改成, 新的權重如果大于舊的則更新
兩種方法都是轉化了視角, 最短和最長是一對對偶.
另外, 在加權有向圖(權重可能為負數) 中尋找最長路徑, 已知最好的算法的復雜度也是指數級別的, 而若是有環, 則更復雜了.
并行任務調度
優先級限制下的并行任務調度定義:
- 一組需要完成的任務和每個任務所需時間
- 一組關于任務完成的先后次序的優先級限制
在滿足此限制條件下, 在數量不限的處理器上安排這些任務, 使得任務最快完成, 且使用的處理器資源最少(可以理解為cpu不要忙等, 即給定任務要直接執行, 而不是還要再等它的前序先完成)最少.
這個問題, 可以通過"關鍵路徑"的方法證明它和 "無環加權有向圖"的最長路徑 問題等價
將定義里的兩個條件轉換成對應的圖:
將任務轉化成圖的一條邊, 邊有兩個點, 任務的時間就是邊的權重, 而 "關鍵路徑" 即條件二: 優先級次序 轉化而來的, 關鍵路徑之間轉化的節點間也有路徑, 不過權重為0 , 此外, 開始S具有0權重到任意任務的開始節點, 任意任務的結束節點有0權重到結束節點. 第二章圖中的最長路徑, 就是每個路徑的開始節點的最長路徑權重值就是它的任務開始時間.
相對最后期限下的并行任務調度
和上個情況不同的是, 多了一個限制類型: 某個任務需要在指定的時間點之前開始, 即指定和另一個任務的開始時間的相對時間. 舉例: 任務2 必須在任務4 開始后的12個時間內啟動.
等價為; 加權有向圖(可能環以及負權重邊)的最短路徑問題