🌌 L4: 冥王星邊界

D1: 冥王星星際網絡:圖論與搜索演算法

⏱️ 預計時間:6-8 小時 🎯 目標級分:5 級分 📊 難度:⭐⭐⭐⭐⭐

🚀 任務背景

恭喜!你已經抵達太陽系的邊界——冥王星區域。這裡有複雜的星際航道網絡,連接著各個太空站和小行星帶。要在這個網絡中找到最佳路徑,你需要掌握「圖論」——電腦科學中最強大的工具之一。

圖可以表示任何「物件之間的關係」:
  • 太空站之間的航道
  • 社交網絡中的朋友關係
  • 網頁之間的超連結
  • 城市之間的道路

📚 知識點說明

什麼是圖(Graph)?

圖由兩部分組成:
  • 頂點(Vertices/Nodes):物件本身
  • 邊(Edges):物件之間的連接
    A --- B
    |     |
    C --- D

圖的類型

  1. 無向圖 vs 有向圖
  2. 無向圖:邊沒有方向(雙向道路)
  3. 有向圖:邊有方向(單行道)
  1. 加權圖 vs 無權圖
  2. 無權圖:所有邊的「成本」相同
  3. 加權圖:每條邊有不同的權重(距離、時間等)

圖的表示法

1. 鄰接矩陣(Adjacency Matrix)
# 適合稠密圖,查詢快但佔空間
graph = [[0, 1, 1, 0],
         [1, 0, 0, 1],
         [1, 0, 0, 1],
         [0, 1, 1, 0]]
2. 鄰接串列(Adjacency List) ✅ 推薦
# 適合稀疏圖,節省空間
graph = {
    'A': ['B', 'C'],
    'B': ['A', 'D'],
    'C': ['A', 'D'],
    'D': ['B', 'C']
}

圖的遍歷

廣度優先搜尋(BFS):一層一層探索
  • 使用佇列(Queue)
  • 找最短路徑(無權圖)
深度優先搜尋(DFS):一條路走到底
  • 使用堆疊(Stack)或遞迴
  • 探索所有路徑

💻 範例程式碼

範例 1:建立星際航道網絡

import sys
from collections import defaultdict

# 任務:建立太空站網絡 n = int(sys.stdin.readline()) # 太空站數量 m = int(sys.stdin.readline()) # 航道數量

# 使用鄰接串列 graph = defaultdict(list)

print("建立航道:") for _ in range(m): a, b = map(int, sys.stdin.readline().strip().split()) graph[a].append(b) graph[b].append(a) # 無向圖 print(f"太空站 {a} ↔ 太空站 {b}")

print("\n網絡結構:") for station in sorted(graph.keys()): neighbors = sorted(graph[station]) print(f"太空站 {station}: 連接到 {neighbors}")

範例 2:BFS - 最短路徑搜索

import sys
from collections import deque, defaultdict

def bfs_shortest_path(graph, start, end): """使用 BFS 找出最短路徑""" if start == end: return [start] # 佇列:儲存 (當前節點, 路徑) queue = deque([(start, [start])]) visited = {start} while queue: current, path = queue.popleft() # 探索所有鄰居 for neighbor in graph[current]: if neighbor in visited: continue new_path = path + [neighbor] # 找到目標 if neighbor == end: return new_path visited.add(neighbor) queue.append((neighbor, new_path)) return None # 無法到達

# 任務:找出從地球到冥王星的最短航道 n = int(sys.stdin.readline()) m = int(sys.stdin.readline())

graph = defaultdict(list) for _ in range(m): a, b = map(int, sys.stdin.readline().strip().split()) graph[a].append(b) graph[b].append(a)

start = int(sys.stdin.readline()) end = int(sys.stdin.readline())

path = bfs_shortest_path(graph, start, end)

if path: print(f"最短路徑:{' → '.join(map(str, path))}") print(f"需要經過 {len(path) - 1} 個航道") else: print("無法到達目標!")

範例 3:DFS - 探索所有路徑

import sys
from collections import defaultdict

def dfs_all_paths(graph, start, end, path=[]): """使用 DFS 找出所有路徑""" path = path + [start] # 到達目標 if start == end: return [path] # 沒有鄰居 if start not in graph: return [] paths = [] for neighbor in graph[start]: # 避免循環 if neighbor not in path: new_paths = dfs_all_paths(graph, neighbor, end, path) paths.extend(new_paths) return paths

# 任務:找出所有可能的探索路線 n = int(sys.stdin.readline()) m = int(sys.stdin.readline())

graph = defaultdict(list) for _ in range(m): a, b = map(int, sys.stdin.readline().strip().split()) graph[a].append(b) graph[b].append(a)

start = int(sys.stdin.readline()) end = int(sys.stdin.readline())

all_paths = dfs_all_paths(graph, start, end)

print(f"找到 {len(all_paths)} 條路徑:\n") for i, path in enumerate(all_paths, 1): print(f"路徑 {i}: {' → '.join(map(str, path))}")

範例 4:迷宮探索(網格圖)

import sys
from collections import deque

def maze_bfs(maze, start, end): """在迷宮中找最短路徑""" rows = len(maze) cols = len(maze[0]) # 四個方向:上下左右 directions = [(-1, 0), (1, 0), (0, -1), (0, 1)] direction_names = ['上', '下', '左', '右'] queue = deque([(start, [start])]) visited = {start} while queue: (r, c), path = queue.popleft() # 到達終點 if (r, c) == end: return path # 探索四個方向 for (dr, dc), name in zip(directions, direction_names): nr, nc = r + dr, c + dc # 檢查邊界 if 0 <= nr < rows and 0 <= nc < cols: # 檢查是否可通行且未訪問 if maze[nr][nc] != '#' and (nr, nc) not in visited: visited.add((nr, nc)) queue.append(((nr, nc), path + [(nr, nc)])) return None

# 任務:在小行星帶中找到安全路徑 # '.' = 安全區域, '#' = 隕石, 'S' = 起點, 'E' = 終點 rows = int(sys.stdin.readline()) cols = int(sys.stdin.readline())

maze = [] start = end = None

for i in range(rows): row = list(sys.stdin.readline().strip()) maze.append(row) for j in range(cols): if row[j] == 'S': start = (i, j) maze[i][j] = '.' elif row[j] == 'E': end = (i, j) maze[i][j] = '.'

path = maze_bfs(maze, start, end)

if path: print(f"找到安全路徑!長度:{len(path)}") print("\n迷宮(標記路徑):") # 標記路徑 for r, c in path: if (r, c) != start and (r, c) != end: maze[r][c] = '*' maze[start[0]][start[1]] = 'S' maze[end[0]][end[1]] = 'E' for row in maze: print(''.join(row)) else: print("無法找到安全路徑!")

範例 5:連通分量檢測

import sys
from collections import defaultdict

def find_connected_components(graph, n): """找出圖中的所有連通分量""" visited = set() components = [] def dfs(node, component): visited.add(node) component.append(node) for neighbor in graph[node]: if neighbor not in visited: dfs(neighbor, component) for node in range(1, n + 1): if node not in visited: component = [] dfs(node, component) components.append(sorted(component)) return components

# 任務:識別獨立的星系群 n = int(sys.stdin.readline()) # 太空站數量 m = int(sys.stdin.readline()) # 航道數量

graph = defaultdict(list) for _ in range(m): a, b = map(int, sys.stdin.readline().strip().split()) graph[a].append(b) graph[b].append(a)

components = find_connected_components(graph, n)

print(f"發現 {len(components)} 個獨立星系群:\n") for i, comp in enumerate(components, 1): print(f"星系群 {i}: {comp}") print(f" 包含 {len(comp)} 個太空站")

🔍 程式碼解說

關鍵技術點

  1. BFS vs DFS 的選擇
| 特性 | BFS | DFS | |------|-----|-----| | 數據結構 | 佇列 | 堆疊/遞迴 | | 最短路徑 | ✅ 保證 | ❌ 不保證 | | 記憶體 | 較多 | 較少 | | 適用場景 | 最短路徑 | 路徑存在性 |
  1. visited 集合的重要性
   visited = set()  # 防止重複訪問
   
   # 在有環的圖中,沒有 visited 會無限循環!
   
  1. 網格圖的技巧
   # 四個方向的移動
   directions = [(-1,0), (1,0), (0,-1), (0,1)]
   
   for dr, dc in directions:
       new_r, new_c = r + dr, c + dc
       # 檢查邊界
       if 0 <= new_r < rows and 0 <= new_c < cols:
           # 處理
   
  1. defaultdict 的妙用
   from collections import defaultdict
   
   # 自動創建空列表
   graph = defaultdict(list)
   graph[1].append(2)  # 不需要先初始化 graph[1]
   

📝 Quiz: 星際救援任務

題目:多目標最短路徑

你的太空船在太空站 S,需要救援位於太空站 T1 和 T2 的兩組太空人,然後返回基地 E。

請找出一條路徑,使得總航道數最少。你可以選擇先救援 T1 或 T2。

輸入格式:
  • 第一行:N M(太空站數量,航道數量)
  • 接下來 M 行:每行兩個整數 a b,表示太空站 a 和 b 之間有航道
  • 最後一行:S T1 T2 E(起點、兩個救援點、終點)
輸入範例:
6 7
1 2
1 3
2 4
3 4
4 5
4 6
5 6
1 2 5 6
輸出範例:
最佳路徑:1 → 2 → 4 → 5 → 4 → 6
總航道數:5
救援順序:
  1. 從起點 1 到救援點 2
  2. 從救援點 2 到救援點 5
  3. 從救援點 5 到終點 6

💡 提示

  • 使用 BFS 計算任意兩點間的最短距離
  • 比較兩種順序:S→T1→T2→E 和 S→T2→T1→E
  • 選擇總距離較短的路徑

---

Quiz 解答

import sys
from collections import deque, defaultdict

def bfs_distance(graph, start, end): """計算從 start 到 end 的最短距離""" if start == end: return 0, [start] queue = deque([(start, [start])]) visited = {start} while queue: current, path = queue.popleft() for neighbor in graph[current]: if neighbor in visited: continue new_path = path + [neighbor] if neighbor == end: return len(new_path) - 1, new_path visited.add(neighbor) queue.append((neighbor, new_path)) return float('inf'), []

# 讀取輸入 n, m = map(int, sys.stdin.readline().strip().split())

graph = defaultdict(list) for _ in range(m): a, b = map(int, sys.stdin.readline().strip().split()) graph[a].append(b) graph[b].append(a)

S, T1, T2, E = map(int, sys.stdin.readline().strip().split())

# 計算所有需要的距離 dist_S_T1, path_S_T1 = bfs_distance(graph, S, T1) dist_S_T2, path_S_T2 = bfs_distance(graph, S, T2) dist_T1_T2, path_T1_T2 = bfs_distance(graph, T1, T2) dist_T2_T1, path_T2_T1 = bfs_distance(graph, T2, T1) dist_T1_E, path_T1_E = bfs_distance(graph, T1, E) dist_T2_E, path_T2_E = bfs_distance(graph, T2, E)

# 方案 1:S → T1 → T2 → E route1_dist = dist_S_T1 + dist_T1_T2 + dist_T2_E route1_path = path_S_T1[:-1] + path_T1_T2[:-1] + path_T2_E

# 方案 2:S → T2 → T1 → E route2_dist = dist_S_T2 + dist_T2_T1 + dist_T1_E route2_path = path_S_T2[:-1] + path_T2_T1[:-1] + path_T1_E

# 選擇較短的路徑 if route1_dist <= route2_dist: best_path = route1_path best_dist = route1_dist order = [T1, T2] else: best_path = route2_path best_dist = route2_dist order = [T2, T1]

print(f"最佳路徑:{' → '.join(map(str, best_path))}") print(f"總航道數:{best_dist}") print(f"\n救援順序:") print(f"1. 從起點 {S} 到救援點 {order[0]}") print(f"2. 從救援點 {order[0]} 到救援點 {order[1]}") print(f"3. 從救援點 {order[1]} 到終點 {E}")

解答說明

  1. 分段計算
  2. 將問題分解為多個兩點間最短路徑
  3. 使用 BFS 計算每段距離
  1. 比較兩種方案
   # 方案 1:S → T1 → T2 → E
   # 方案 2:S → T2 → T1 → E
   # 選擇總距離較短的
   
  1. 路徑拼接
   # 注意:拼接時要去掉中間節點的重複
   path_S_T1[:-1] + path_T1_T2[:-1] + path_T2_E
   

---

🎉

訓練完成!

恭喜你完成 星際網絡與圖論 訓練!