跳轉到

8.1 迷宮繞線 Lee 演算法#

概念說明#

Lee 演算法(1961,貝爾實驗室)是繞線的開山之作,本質是 BFS 波前擴展(wavefront expansion):把版圖切成網格,每格有成本(障礙 = ∞,金屬層、通道各有成本),從起點以同心圓方式向外擴散標記距離,直到抵達終點,再沿數字遞減回溯得到路徑。它保證:若存在路徑必能找到,且找到的是最短(最少成本)路徑——完備且最優,這是它至今仍在教學與小規模工具中存活的原因。

缺點同樣經典:BFS 探索的格子數與「起終點距離的平方」成正比,大版圖上記憶體與時間都吃不消;且每次只繞一條 net,先繞者占住資源,後繞者可能失敗(順序依賴),需要 rip-up & reroute(拆線重繞)輔助。

Lee 演算法的演進家族:

  1. Soukup 演算法:波前擴展時偏向目標方向加速(不保證最短,但快很多)。
  2. Hadlock 演算法:以「偏離曼哈頓方向的次數」為成本,保證最短且探索格數較少。
  3. Maze routing with costs:為不同層/資源賦不同成本,可處理擁塞與設計規則近似。

Lee 的三階段:擴展(expansion)、回溯(retrace)、清理(clean-up,把路徑標記為障礙)。擴展時用兩個佇列交替(現在波前/下一波前),實現 BFS 的層次擴散。

演算法步驟#

  1. 初始化網格:障礙格(已用走線、元件)標記為不可用;起點距離 0。
  2. 擴展:維護當前波前佇列,取出格 g,對四鄰居若為空且未標記,標記 dist(g)+1 並加入下一波前。
  3. 重複直到終點被標記(找到)或佇列空(無路徑,回報失敗)。
  4. 回溯:從終點出發,每步找「標號恰好小 1」的鄰居,直到回到起點。
  5. 把路徑格標為障礙,處理下一條 net。

Python 程式碼範例#

from collections import deque

def lee_route(grid, start, goal):
    """
    grid: 2D list,0=空、1=障礙
    start, goal: (r, c);回傳路徑或 None
    """
    R, C = len(grid), len(grid[0])
    if grid[goal[0]][goal[1]] == 1:
        return None
    dist = [[None] * C for _ in range(R)]
    dist[start[0]][start[1]] = 0
    cur, nxt = deque([start]), deque()
    depth = 0
    found = False
    while cur and not found:
        depth += 1
        while cur:
            r, c = cur.popleft()
            for dr, dc in ((1,0),(-1,0),(0,1),(0,-1)):
                nr, nc = r+dr, c+dc
                if not (0 <= nr < R and 0 <= nc < C):
                    continue
                if grid[nr][nc] == 1 or dist[nr][nc] is not None:
                    continue
                dist[nr][nc] = depth
                if (nr, nc) == goal:
                    found = True
                    break
                nxt.append((nr, nc))
            if found:
                break
        cur, nxt = nxt, deque()
    if not found:
        return None
    # 回溯階段:沿遞減標號走回起點
    path, cell = [goal], goal
    d = dist[goal[0]][goal[1]]
    while cell != start:
        for dr, dc in ((1,0),(-1,0),(0,1),(0,-1)):
            nr, nc = cell[0]+dr, cell[1]+dc
            if 0 <= nr < R and 0 <= nc < C and dist[nr][nc] == d - 1:
                path.append((nr, nc))
                cell = (nr, nc)
                d -= 1
                break
    return path[::-1]

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)

# 版圖:10x10,中間一道帶缺口的障礙牆
grid = [[0]*10 for _ in range(10)]
for c in range(10):
    if c != 4:                       # c=4 是牆的缺口
        grid[5][c] = 1               # 橫牆在第 5 列
start, goal = (0, 0), (9, 9)
path = lee_route(grid, start, goal)
print(f"繞線成功,路徑長 = {len(path)-1} 步")
print_grid(grid, set(path), start, goal)

# 展示 Lee 演算法的「無路徑」偵測:封死缺口
grid2 = [row[:] for row in grid]
grid2[5][4] = 1
print("\n封死缺口後:", lee_route(grid2, start, goal))

輸出:

繞線成功,路徑長 = 18 步
  S.........
  .*........
  .*........
  .*........
  .*........
  .*...#####
  .*.*/.....
  ..*.*.....
  ....*..*..
  .......*G

封死缺口後: None

路徑先向下、穿過缺口、再折向右下——BFS 保證這是 18 步的最短繞線;當缺口被封死,演算法正確回報無路徑(None),體現其完備性。

複雜度分析#

  • 時間:最壞 O(R·C)(探索全網格);典型情況與「起終點包圍的面積」成正比,約 O(d²),d 為曼哈頓距離。
  • 空間:距離陣列 O(R·C)——大版圖的主要瓶頸,催生了 A* 與區域繞線。
  • 與 A* 比較:Lee 無啟發資訊;A* 以 h(n) 引導可減少探索格數數十倍,且在 h 可容許(admissible)時仍保證最短。

練習#

  1. 修改成本模型:垂直走線成本 1、水平成本 2(模擬 preferred direction),重跑並觀察路徑形狀變化。
  2. 實作 Soukup 演算法(擴展時優先沿「朝向目標」的方向直進),比較與 Lee 在 50×50 網格上的探索格數。
  3. 兩條 net 依序繞線時,第二條可能失敗。實作簡單的 rip-up & reroute:失敗時移除相關走線再試,並討論其收斂性。