雲端 Mac 上的 iOS 最短路徑驗證:非負權重、無法到達與溢位

雲端 Mac 上的 iOS 最短路徑驗證:非負權重、無法到達與溢位

雲端 Mac 上的 iOS 最短路徑驗證:非負權重、無法到達與溢位

離線導航、園區路線與應用程式內的流程規劃,都可能需要在 iOS 用戶端計算最短路徑。一般測試案例能算出距離,不代表遇到零權重環、互不連通的節點或超大權重時,仍不會傳回看似合理的錯誤結果。在雲端 Mac 上建立一份精簡、清楚的 Swift 參考實作,比只在介面上點選路線更容易找出演算法的回歸問題。

先明確定義圖與結果的契約

本文使用有向圖,頂點編號從零開始,邊權重為非負的 Int64。無向道路必須明確加入兩個方向的邊。起點到自身的距離為零,無法到達的節點傳回 nil;不能用零或最大整數作為無法到達的標記。

輸入端必須檢查起點、每條邊的終點與負權重,包括從起點無法到達的子圖。允許平行邊存在,由演算法選擇較短的一條;也允許自環,但不能因此無限更新。

Dijkstra 仰賴非負權重。不要把負數改成零來「修正」輸入,這會改變業務問題本身。

若業務以公尺、毫秒等整數單位建立圖,應在資料轉換時就決定精度。不要先用浮點數計算距離,再臨時截斷為整數,否則測試比較的就不再是同一份圖。

撰寫容易審查的參考模型

以下實作透過逐一掃描頂點來選取下一個頂點,時間複雜度為 O(V²+E),空間複雜度為 O(V)。它適合用於小型圖的驗收,不適合拿來證明正式演算法在大型地圖上的效能。將程式碼儲存為 shortest-path.swift,後續測試案例也附加到同一個檔案。

enum GraphError: Error {
    case invalidVertex
    case negativeWeight
    case overflow
}

struct Edge {
    let to: Int
    let weight: Int64
}

func distances(
    _ graph: [[Edge]],
    from source: Int
) throws -> [Int64?] {
    guard graph.indices.contains(source) else {
        throw GraphError.invalidVertex
    }

    for edges in graph {
        for edge in edges {
            guard graph.indices.contains(edge.to) else {
                throw GraphError.invalidVertex
            }
            guard edge.weight >= 0 else {
                throw GraphError.negativeWeight
            }
        }
    }

    var result = Array<Int64?>(repeating: nil, count: graph.count)
    var settled = Array(repeating: false, count: graph.count)
    result[source] = 0

    for _ in graph.indices {
        var selected: Int?
        for vertex in graph.indices where !settled[vertex] {
            guard let value = result[vertex] else { continue }
            if let current = selected {
                if value < result[current]! {
                    selected = vertex
                }
            } else {
                selected = vertex
            }
        }

        guard let vertex = selected else { break }
        settled[vertex] = true
        let base = result[vertex]!

        for edge in graph[vertex] where !settled[edge.to] {
            let (candidate, overflow) =
                base.addingReportingOverflow(edge.weight)
            guard !overflow else { throw GraphError.overflow }

            if let previous = result[edge.to] {
                if candidate < previous {
                    result[edge.to] = candidate
                }
            } else {
                result[edge.to] = candidate
            }
        }
    }

    return result
}

這個模型只傳回距離,不記錄前驅節點,避免將路徑重建問題混入距離驗收。距離相同的頂點會依編號順序處理,方便重現結果,但業務邏輯不應依賴這個順序。

用固定測試案例守住邊界

先加入一張包含平行邊、零權重環與孤立節點的小型圖。預期結果應直接寫明,不要呼叫另一份待測演算法來產生答案。

let graph: [[Edge]] = [
    [Edge(to: 1, weight: 9), Edge(to: 1, weight: 2)],
    [Edge(to: 2, weight: 0)],
    [Edge(to: 1, weight: 0), Edge(to: 3, weight: 3)],
    [],
    []
]

let actual = try distances(graph, from: 0)
precondition(actual == [0, 2, 2, 5, nil])

let singleton = try distances([[]], from: 0)
precondition(singleton == [0])

接著分別加入空圖、超出範圍的起點、負權重邊、超出範圍的終點,以及合法的最大距離。每個錯誤案例都應個別斷言具體的錯誤型別,避免把任何例外都視為測試通過。

let largest = try distances([
    [Edge(to: 1, weight: Int64.max)],
    []
], from: 0)
precondition(largest[1] == Int64.max)

do {
    _ = try distances([
        [Edge(to: 1, weight: Int64.max)],
        [Edge(to: 2, weight: 1)],
        []
    ], from: 0)
    preconditionFailure("Expected overflow")
} catch GraphError.overflow {
}

這裡採用保守策略:實際執行的鬆弛計算一旦溢位,整次查詢就失敗,即使圖中另有較短路線也不再繼續。正式實作若採用不同策略,必須另行定義契約,不能直接比較兩者的錯誤行為。

檢查性質,不只核對幾個答案

除了固定答案,還要驗證三類變換:打亂每個頂點的邊順序,距離不應改變;加入一條權重更大的重複邊,不應得到更短的距離;加入一個與原圖不連通的合法子圖,原有頂點的結果應維持不變。這些距離性質測試應限制權重範圍,確保所有相關加法都能以所用的整數型別表示。

這些性質能找出依賴輸入順序、錯誤覆寫平行邊等缺陷。若要產生隨機小型圖,應固定隨機種子,並在失敗時保存完整的鄰接串列、起點與預期結果,不能只記錄一個難以重現的失敗編號。

如果正式介面也會傳回路線,就必須額外逐邊回放:確認起點與終點正確、每條邊確實存在,而且權重總和等於距離。有平行邊時,最好傳回邊的識別碼,而不只是頂點序列。同樣短的路線可以不同,不要強制比對某一條前驅鏈。

接入雲端節點的驗收入口

在 VMFly 實體節點上,先記錄工具鏈版本,再編譯並執行。以下命令不依賴模擬器,適合放在完整應用程式建置之前。

set -eu
xcrun swiftc --version
mkdir -p .build
xcrun swiftc -Onone shortest-path.swift \
  -o .build/shortest-path-check
.build/shortest-path-check

範例使用 precondition,讓錯誤結果以非零結束狀態退出。不要改成可能被發行版最佳化移除的 assert,也不要使用會移除這些檢查的 -Ounchecked。正式專案則應將同一批測試案例接入測試目標,呼叫正式的路徑計算入口。

流水線至少要保留失敗的圖、工具鏈版本與實際距離陣列。驗收順序應維持為輸入合法性、距離結果、路徑回放、效能預算。通過正確性檢查後,再獨立量測大型圖的執行時間,避免用一次「跑得很快」掩蓋無法到達或溢位的語意錯誤。

常見問題

Dijkstra 能處理負權重邊嗎?

不適用於一般負權重圖形。應在輸入時拒絕負權重;若確實需要,請改用支援負權重的演算法,並額外偵測負環。

為何不用最大整數表示無法到達?

最大整數也可能是有效距離。請用 Optional 表示無法到達,並以 addingReportingOverflow 檢查距離加總,避免混淆無法到達、合法大值與溢位。

為 CI/CD 流程添一台實體 Mac

可選 Apple Silicon M4 與 M4 Pro,整台主機由單一客戶獨享,並非虛擬機。依照建置工作負載挑選機型,再選擇適合團隊的計費週期。

租用實體節點

將下一次建置交給專屬節點

先確認硬體規格與計費週期,再為團隊租用獨享的實體 Mac。