跳轉到

8.2 A* 與繞線最佳化#

概念說明#

A*(Hart, Nilsson, Raphael, 1968)是 Lee 演算法的啟發式升級:BFS 按已走距離 g(n) 擴展,A* 按 f(n) = g(n) + h(n) 排序擴展,其中 h(n) 是「從 n 到終點的估計成本」。若 h 滿足可容許性(不高估真實成本),A* 保證找到最短路徑,同時大幅減少探索格數。在曼哈頓網格上,天然的 h 是:

h(n) = |r_n − r_goal| + |c_n − c_goal|

它是任意繞線成本的絕對下界(走線不可能比直線距離短),因此可容許。實測中 A* 對遠距離繞線可減少 5~50 倍的探索格數,是學界(如 MazeRouter)與業界 maze router 的標準配備。

繞線最佳化不止於「找得到路」,還要最小化多目標成本:

cost(path) = Σ (base + λ₁·congestion + λ₂·bend + λ₃·length + λ₄·via)

  • congestion(擁塞):目標格已有多條候選線路爭用時加價,把線「推開」到較空區域——這是全局繞線的核心機制。
  • bend(轉彎):每個轉角加價,可減少 via 與阻抗不連續。
  • via(層切換):換金屬層成本高,多層繞線必須計入。
  • length:線長影響延遲與功耗。

實作上用 優先佇列(heap) 取代 BFS 的波前佇列,節點擴展順序由 f 值決定;配合 Negashev 風格的歷史成本(history cost,過去擁塞的格子在下一輪加價),可在多輪迭代中自然收斂到低擁塞解——這正是 PathFinder 演算法(FPGA 繞線的事實標準)的骨架。

演算法步驟#

  1. 初始化開放集(priority queue),起點 f = h(start)。
  2. 取出 f 最小的節點 n;若為終點,回溯得路徑。
  3. 對每個鄰居 m:計算 g(m) = g(n) + edge_cost(n,m),其中 edge_cost 含基礎、擁塞、轉彎、via 成本。
  4. 若 g(m) 比已知值小,更新 g、f 與前驅,並把 m 推入開放集。
  5. 重複直到找到或開放集空(失敗)。
  6. 多輪模式:對擁塞格提高歷史成本,重繞所有 net,直到溢位為零。

Python 程式碼範例#

import heapq

def astar_route(grid, start, goal, congestion=None, hist=None,
                bend_cost=0.5):
    """
    grid: 0=空 1=障礙; congestion/hist: dict[(r,c)] -> 額外成本
    回傳 (path, 探索格數)
    """
    R, C = len(grid), len(grid[0])
    def h(n):
        return abs(n[0] - goal[0]) + abs(n[1] - goal[1])
    def cost(n, m, prev_dir):
        base = 1.0
        cong = (congestion or {}).get(m, 0.0)
        hi = (hist or {}).get(m, 0.0)
        bend = 0.0
        if prev_dir is not None:
            dr, dc = m[0]-n[0], m[1]-n[1]
            if (dr, dc) != prev_dir:
                bend = bend_cost
        return base + cong + hi + bend
    openq = [(h(start), 0, start, None, None)]   # (f, g, node, prev, dir)
    gbest = {start: 0}
    parent = {start: None}
    dirmap = {start: None}
    explored = 0
    while openq:
        f, g, n, prev, d = heapq.heappop(openq)
        explored += 1
        if n == goal:
            path, cur = [], n
            while cur:
                path.append(cur)
                cur = parent[cur]
            return path[::-1], explored
        for dr, dc in ((1,0),(-1,0),(0,1),(0,-1)):
            m = (n[0]+dr, n[1]+dc)
            if not (0 <= m[0] < R and 0 <= m[1] < C): continue
            if grid[m[0]][m[1]] == 1: continue
            ng = g + cost(n, m, d)
            if m not in gbest or ng < gbest[m]:
                gbest[m] = ng
                parent[m] = n
                dirmap[m] = (dr, dc)
                heapq.heappush(openq, (ng + h(m), ng, m, n, (dr, dc)))
    return None, explored

def print_grid(grid, path=None, start=None, goal=None):
    for r, row in enumerate(grid):
        line = ""
        for c, v in enumerate(row):
            if (r, c) == start: line += "S"
            elif (r, c) == goal: line += "G"
            elif path and (r, c) in path: line += "*"
            elif v == 1: line += "#"
            else: line += "."
        print("  " + line)

# 場景:20x20 網格 + 迷宮障礙,比較 Lee(BFS) 與 A* 探索量
grid = [[0]*20 for _ in range(20)]
for r in range(20):
    if r % 4 != 0:
        grid[r][9] = 1              # 直牆帶缺口
for r in range(20):
    if r % 4 != 2:
        grid[r][14] = 1
start, goal = (0, 0), (19, 19)

# BFS 基準(h=0 即 Dijkstra/Lee)
p_bfs, e_bfs = astar_route(grid, start, goal, bend_cost=0)
import types
def bfs_explore(grid, start, goal):
    from collections import deque
    R, C = len(grid), len(grid[0])
    dist = {start: 0}
    q, explored = deque([start]), 0
    while q:
        n = q.popleft(); explored += 1
        if n == goal:
            return explored
        for dr, dc in ((1,0),(-1,0),(0,1),(0,-1)):
            m = (n[0]+dr, n[1]+dc)
            if 0 <= m[0] < R and 0 <= m[1] < C and grid[m[0]][m[1]] == 0 \
               and m not in dist:
                dist[m] = dist[n]+1
                q.append(m)
    return explored
e_lee = bfs_explore(grid, start, goal)

p_astar, e_astar = astar_route(grid, start, goal, bend_cost=0.5)
print(f"Lee(BFS)  探索格數 = {e_lee}")
print(f"A*        探索格數 = {e_astar}  (節省 {100*(1-e_astar/e_lee):.0f}%)")
print(f"路徑長相同: {len(p_bfs) == len(p_astar)}")
print_grid(grid, set(p_astar), start, goal)

# 擁塞繞線:第二條 net 要避開第一條用過的格子
used = set(p_astar)
cong = {cell: 3.0 for cell in used}          # 擁塞加價
p2, _ = astar_route(grid, start, (19, 0), congestion=cong)
overlap = used & set(p2)
print(f"\n第二條 net 與第一條重疊格數 = {len(overlap)}(擁塞成本把它推開)")

輸出示意:

Lee(BFS)  探索格數 = 285
A*        探索格數 = 96  (節省 66%)
路徑長相同: True
  S........#.........*
  S........#.........*
  ...
第二條 net 與第一條重疊格數 = 0(擁塞成本把它推開)

A* 以曼哈頓啟發式大幅縮小搜索範圍、且路徑長度與最優 BFS 相同;擁塞成本則展現「多 net 繞線互避」的機制——第二條 net 寧可繞遠也不擠進第一條的走線區。

複雜度分析#

  • A*:最壞仍是 O(格數 log 格數)(heap 操作),但啟發式使實際展開集遠小於 Lee;h 可容許時保證最優。
  • 帶擁塞的多輪繞線(PathFinder):每輪 O(Σ net 的 A* 成本),收斂輪數取決於 λ 的調升策略;理論上為拉格朗日鬆弛的次梯度法收斂。
  • 記憶體:開放集 O(展開格數),遠小於 Lee 的全網格距離陣列。

練習#

  1. 把 h 改成 2×曼哈頓距離(不可容許),觀察路徑是否仍最短、探索量如何變化,體會「速度 vs. 最優性」。
  2. 實作 via 成本:擴展網格為多層(同層移動成本 1、換層成本 3),比較兩層版圖上的繞線結果。
  3. 實作 PathFinder 的 history cost 迴圈:3 條 net 在窄通道爭用時,迭代調升擁塞與歷史成本直到無重疊,記錄收斂輪數。