AI HW2 一條龍教學:未知地圖探索 Agent
2026-10-04|人工智慧導論(成大朱威達)作業二,個人作業,10/15(四)23:59 截止
一句話結論
agent.py已經寫好、測過,也經過獨立審查:老師的 3 張公開地圖全部成功、0 次撞牆,平均 94.48 分; 在 5,400 張模擬地圖上 0 失敗、0 撞牆。預估正式 9 張隱藏地圖的平均約 91 分(大概落在 88~94 之間:我模擬了兩萬次「抽 9 張考一次」,中間 90% 的結果落在 88.4~94.2;前提是隱藏地圖跟公開地圖一樣把終點放得遠。如果終點是隨便放的,預估約 88.8 分)。 你只剩「改 zip 檔名、上傳」兩個動作。
左邊點一節看一節。每節開頭都有一句「結論」,趕時間只讀那一句就好。
整份文件照「先懂問題 → 再懂背後的道理 → 再看怎麼做 → 最後看結果」的順序排,從頭讀到尾就是一條龍。
這份文件的五個部分
| 部分 | 在講什麼 | 讀完你會知道 |
|---|---|---|
| 開始 | 這份文件怎麼看、我實際做了哪些事 | 整份作業是怎麼一步一步做出來的 |
| A 作業介紹 | 老師要你做什麼、分數怎麼算 | 題目、規則、拿高分的關鍵 |
| B 邏輯框架 | 課本裡跟這份作業有關的所有觀念,從最基本的講起 | 為什麼這個作業要這樣想;考試考觀念時答得出來 |
| C 實作 | 從框架走到程式:工具、比較、最終做法、逐行讀程式、為什麼保證不出錯 | 程式在做什麼、為什麼這樣寫 |
| D 驗證與結論 | 怎麼測的、獨立審查的結果、怎麼交件、一條龍總回顧 | 這份作業有多可靠、還剩什麼風險 |
| E 考試準備 | 跟投影片的對照、可能的考題、名詞小字典 | 考前複習用 |
建議讀法(每天 90 分鐘以內)
| 天 | 讀哪幾節 |
|---|---|
| Day 1 | A1、A2 |
| Day 2 | B1~B6(邏輯框架) |
| Day 3 | C1、C2(含回放)、C2b(一步一步看)、C3、C4(用真實數字算一遍),然後用自己的話講一遍 C3 的七個步驟,講完就交件(D3) |
| Day 4 | C5(逐段讀程式)、C6(為什麼不撞牆、不卡死),自己跑一次測試(A1.15),再讀「我做了什麼」、D1、D2、D4 |
| Day 5 | E1~E3(考試準備) |
這是估計的份量。Day 1 是測試日,做完看實際花多久再調整。B 部分是這次新加的,如果 Day 2 讀不完就拆成兩天。
檔案在哪裡
| 檔案 | 是什麼 | 要交嗎 |
|---|---|---|
C:\D槽\TAICA課程\人工智慧導論\HW2\HW2\agent.py | 寫好的 Agent(本作業唯一要改的檔案) | 要(壓成 zip) |
C:\D槽\TAICA課程\人工智慧導論\HW2\學號_姓名.zip | 已經打包好的交件檔,裡面只有 agent.py。檔名要改成你的學號姓名 | 要 |
C:\D槽\TAICA課程\人工智慧導論\HW2\HW2_COOL公告.md | NTU COOL 上的作業公告原文(你貼給我的) | 不要 |
C:\D槽\TAICA課程\人工智慧導論\HW2\agent_starter_original.py | 老師原本給的空白範本(備份) | 不要 |
C:\D槽\TAICA課程\人工智慧導論\HW2\lab\ | 我做實驗用的「模擬考」工具 | 不要 |
C:\D槽\TAICA課程\人工智慧導論\HW2\HW2\ 其他檔案 | 老師給的環境、評分程式、公開地圖、README | 不要(也不能改) |
我做了什麼(完整工作紀錄)
結論:從找題目到交件檔,一共 25 個步驟。核心是「先做一個模擬考,再用模擬考比較好幾種走法,挑分數最高、而且絕對不會失敗的那個」。所有實驗的程式和數字都留在電腦裡,可以重跑。
為什麼要寫這一節
這份作業的程式是我寫的。你要交出去、也要能在考試時講出來,所以你需要知道「它是怎麼被做出來的」,而不是只看到最後的成品。
每一步我都寫了:做了什麼、為什麼要做、結果是什麼。
階段一:搞清楚題目(步驟 1~4)
| # | 做了什麼 | 為什麼 | 結果 |
|---|---|---|---|
| 1 | 到處找作業二的題目:你的電腦、課程網站作業頁(連藏在 HTML 註解裡的都查了)、課程網站的作業檔案夾、W3 上課逐字稿 | 你一開始只給我「作業二、10/15 交、跟寫程式有關」 | 都沒有。只找到老師在 W3 說「下週會在線上公告」。推測在 NTU COOL(要登入) |
| 2 | 你貼了 COOL 公告、把 HW2-1.zip 放進資料夾。我解壓縮,讀完全部 5 個檔案:README、agent.py、environment.py、public_grader.py、3 張公開地圖 | 老師說完整規格以 README 為準,所以每個字都要讀 | 搞懂規則、介面、評分公式(見 A1、A2) |
| 3 | 確認你電腦上有 Python 3.11;用老師的空白範本跑一次評分程式 | 正式評分是 Python 3.11,要用同版本測;先確認測試環境本身能動 | 有 3.11。空白範本 0/3 失敗、每步都撞牆(正常,它只會往上走) |
| 4 | 分析 3 張公開地圖:用完整地圖算出「終點離起點多遠,比幾 % 的格子遠」 | 想知道老師出題有沒有規律,可以拿來設計策略 | 發現終點都放得很遠(84%~99%)。這變成後面策略的關鍵線索 |
階段二:做模擬考工具(步驟 5)
| # | 做了什麼 | 為什麼 | 結果 |
|---|---|---|---|
| 5 | 在 HW2\lab\ 寫了一套「模擬考」:
| 正式考是 9 張沒看過的地圖。只看 3 張公開地圖,運氣成分太大,分不出哪種走法真的比較好 | 可以一次考幾百、幾千張地圖,幾秒鐘出結果 |
「終點隨便放」這個選項是保險:萬一老師的隱藏地圖不是把終點放遠,我們的策略也不能變差。
階段三:比較走法、調整參數(步驟 6~10)
| # | 做了什麼 | 結果 | 決定 |
|---|---|---|---|
| 6 | 第一輪:360 張地圖,比較 DFS、最近邊境、偏好遠處(4 種強度) | 偏好遠處約 90.4 分,最近邊境 89.1,DFS 87.5。全部 0 失敗、0 撞牆 | 偏好遠處有效,繼續往這個方向改 |
| 7 | 第二輪:900 張地圖,細調強度,再試「用迷宮實際路程算遠近」 | 強度 0.05~1.0 都差不多(約 90.5);太強(3.0)反而變差。實際路程版沒有比較好,還比較慢 | 淘汰實際路程版 |
| 8 | 第三輪:試一個更聰明的做法——估計每個邊境「後面藏了多少可能是終點的格子」 | 分數最高到 91.08,但有幾張地圖失敗(0 分) | 不能直接用。一張 0 分會拉低平均 10 分以上,遠大於多賺的 0.6 分 |
| 9 | 找失敗原因:Agent 每一步都重選目標,在兩個目標之間來回踱步,把步數用光。加上「承諾」規則:選定目標後一直走向它,直到它不再是邊境才重選 | 失敗全部歸零;平均分只差一點(次方 4 那一組:終點偏遠 91.08 → 91.01,終點隨機 88.76 → 88.80) | 保留承諾規則 |
| 10 | 調兩個參數:次方(試 0、2、4、6、8、10)和分母加多少(試 1、2、3、6) | 次方 6 最好(終點偏遠:次方 2、4、6、8、10 分別是 90.52、91.01、91.31、91.21、91.15)。分母加 1 和加 2 幾乎一樣(91.33 對 91.31,差 0.02),選 2 | 定案:次方 6、加 2 |
階段四:寫正式版、驗證(步驟 11~14)
| # | 做了什麼 | 為什麼 | 結果 |
|---|---|---|---|
| 11 | 把實驗版改寫成乾淨的正式版 agent.py:加註解、拆成小函式、外面包一層「出錯就走安全的一步」的保險。原本的空白範本先備份 | 實驗版為了方便切換寫得很亂,不適合交,也不適合你讀 | 216 行(加安全閥後是 230 行),只用 Python 內建的 deque |
| 12 | 三種驗證:
| 確認改寫沒有抄錯,也確認沒有藏著的錯誤 | 公開 3/3 成功;分數完全一樣;壓力測試 0 失敗、0 撞牆、0 出錯 |
| 13 | 算分數分布,模擬兩萬次「抽 9 張考一次」 | 想知道正式考大概會拿幾分,以及運氣好壞的範圍 | 中間值約 91.1 分;運氣差 88.3、運氣好 94.0(加安全閥後重算:91.2/88.4/94.2) |
| 14 | 打包 學號_姓名.zip,確認裡面只有一個 agent.py | 老師規定 zip 最外層只能有一份 agent.py | 完成,檔名等你改 |
階段五:教學與審查(步驟 15~17)
| # | 做了什麼 | 為什麼 | 結果 |
|---|---|---|---|
| 15 | 讀課程投影片 Ch2、Ch3、Ch4 的文字,找出跟這份作業有關的頁數 | 考試會考作業內容,要知道作業對應課本哪裡 | 發現這份作業就是 Ch4 的「線上搜尋」,評分公式就是投影片的 competitive ratio |
| 16 | 寫這份教學文件;依你的要求把 A1 擴充成 15 小段,再加上 B 部分(邏輯框架)和這一節(工作紀錄) | 你要從頭到尾都懂 | 就是你正在讀的這份 |
| 17 | 派 3 個互相獨立的 AI 審查員,各自從零開始檢查:規章合規、程式抗壓、內容正確性 | 自己檢查自己容易放水。換一個沒參與過的審查員,標準才不會降低 | 見 D2「獨立審查結果」 |
| 18 | 依審查結果修正:文件 7 個錯誤+14 個容易誤會的地方全部改掉;C6 證明重寫 | 審查員 3 指出這些會影響考試答案 | 見 D2.4、D2.5 |
| 19 | 依審查員 2 的發現,在程式裡加「安全閥」:走超過步數上限的 5% 後,改去最近的邊境。先在實驗室比較 9 種版本、用三批不同的地圖確認,再改正式版,舊版備份 | 審查員造出一張用掉 90% 步數的地圖,太接近 0 分 | 最刁鑽地圖從 90.2% 降到 29.7%,平均分沒有變差 |
| 20 | 改完程式後重跑全部驗證:公開測試、跟實驗版逐張比對、全新 5,400 張壓力測試、60 張刁鑽地圖、重新打包 zip | 程式改了,之前的驗證都要重做 | 全部通過(見 D1) |
| 21 | 在 C2 加「四種走法回放」:讓四種走法在同一張地圖上真的跑一遍,記下每一步,做成可以播放的畫面和兩張變化圖 | 你看不懂「找邊境」在說什麼,需要看它實際動起來 | C2 的回放;「邊境」改用「霧」的比喻 |
| 22 | 新增 C4「用真實數字算一遍」:用程式把最終版在公開地圖 01 第 0 步、第 12 步的每個中間數字印出來(BFS 每一輪、分地盤、寶藏分逐組相加、划算度),再算出 A、B、C 在同一個狀況會選哪個 | 你要知道寶藏分、四種走法、BFS 到底怎麼算 | C4(原本的 C4、C5 改成 C5、C6) |
| 23 | 再派一個獨立審查員,專門找「模糊、說不清楚」的句子。它找到 57 處,其中 11 處跟程式實際行為不一致(例如 DFS 其實有記憶、BFS 是從現在位置出發不是從起點)。我先驗證它說的數字,再逐條改 | 你說很多敘述太模糊 | 57 處全部改完 |
| 24 | 回放加上「每一步的解說」:讓四種走法各自錄下每一步做決定時用的資訊和計算(記錄版跟原版逐步比對,走的路完全一樣),回放時每個機器人底下顯示「這一步為什麼這樣走」,候選格子在地圖上標 ①②③;另外加「只看一個走法」放大 | 你說看數學公式對不到實際走的路,要一步一個原因 | C2 回放的解說框 |
| 25 | 新增 C2b「四種走法,一步一步看」:只用第 0 步一個時刻,每種走法拆成小步驟、一步一張圖;C4 的「分地盤」改成用一格霧當例子,從 ①、② 各畫一條路線數步數,原本的「兩隊擴散」分輪圖收進「進階」 | 你說分地盤講得太模糊,好幾個概念擠在一起,要一個一個講 | C2b;C4.1 步驟二 |
哪些是我做的、哪些要你做
| 事情 | 誰 |
|---|---|
| 寫程式、實驗、驗證、打包、寫文件、找審查員 | 我(Claude) |
| 把 zip 改成你的學號姓名、上傳到 NTU COOL | 你 |
| 讀懂這份文件,用自己的話講一遍 C3 的七個步驟 | 你(你選的 B 方案) |
課程沒有寫作業能不能用 AI 協助(只寫了期末考不能用 AI)。如果 NTU COOL 上有寫禁止,請告訴我。
A1 作業在問什麼
結論:寫一個「蒙著眼睛走迷宮」的小機器人程式。它每一步只能摸到上下左右四格,要靠自己的記憶找到終點,而且路走得越少,分數越高。你只要寫 agent.py 這一個檔案。
A1.1 用故事講一次
想像你被蒙住眼睛,丟進一個格子迷宮。
你只知道兩件事:迷宮有多大(例如 10 列 × 10 欄),還有你站在哪一格。
你不知道牆在哪,也不知道終點在哪。
每次要走之前,你可以伸手摸一摸上、下、左、右四格。每一格只會是三種東西之一:
| 摸到的 | 意思 | 能不能走進去 |
|---|---|---|
FREE | 一般的路 | 可以 |
WALL | 牆壁,或是地圖邊界外面。老師不告訴你是哪一種,兩種都叫 WALL | 不行。硬走會「撞牆」:原地不動,但還是算用掉一步,而且每撞一次扣 1 分(每張地圖最多扣 10 分) |
GOAL | 終點 | 可以,走進去就過關 |
你決定往哪走、走一步。再摸一次、再決定、再走一步。一直重複,直到走進終點。
斜對角的格子摸不到,兩格以外的也摸不到。終點只有在你隔壁時,你才會知道它在那裡。
A1.2 地圖長什麼樣
這是老師給的第 1 張公開地圖(maps/public_map_01.json),10×10。起點標成 S、終點標成 G:
col → 0 1 2 3 4 5 6 7 8 9
row ↓ 0 . . . . . . . . . G
1 . . # . # . . . . .
2 . . . . . . . . . .
3 . . . . # . . . # .
4 # . . # . # . . . .
5 . . . . . . . . # #
6 # . . . . . . . . .
7 S . . . . . . . . .
8 . # . . . # . . # .
9 . . . . . . # . . #
. 是路,# 是牆。機器人看不到這張圖。這張圖只有老師的評分程式看得到。
座標怎麼讀
每一格用 (row, col) 表示,也就是(第幾列, 第幾欄)。
row(列):從上往下數,最上面是 0。col(欄):從左往右數,最左邊是 0。- 所以左上角是
(0, 0)。這張圖的起點 S 是(7, 0):第 7 列、第 0 欄。終點 G 是(0, 9)。
| 方向 | 座標怎麼變 | 例子:從 (7, 3) 出發 |
|---|---|---|
UP 上 | row − 1 | 到 (6, 3) |
DOWN 下 | row + 1 | 到 (8, 3) |
LEFT 左 | col − 1 | 到 (7, 2) |
RIGHT 右 | col + 1 | 到 (7, 4) |
往「上」走,row 是變小,不是變大。因為 row 是從上往下數的,越上面數字越小。這跟數學課的 y 軸方向剛好相反。
A1.3 機器人一開始知道什麼、不知道什麼
| 資訊 | 知道嗎 | 怎麼拿到的 |
|---|---|---|
| 地圖大小(幾列幾欄) | 知道 | 新地圖開始時,reset() 會告訴你 |
| 起點位置 | 知道 | 同上 |
| 自己現在在哪 | 知道 | 每一步的感知裡都有 position |
| 上下左右四格是什麼 | 知道 | 每一步的感知裡都有 neighbors |
| 其他格子是路還是牆 | 不知道 | 要自己走過去摸,然後自己記住 |
| 終點在哪 | 不知道 | 只有走到終點隔壁時才會摸到 GOAL |
| 最短路線要幾步 | 不知道 | 只有評分程式知道,拿來算你的分數 |
老師另外保證了幾件事,可以放心:
- 起點到終點一定有路,不會出現走不到的題目。
- 起點不是終點,終點也不會在起點的隔壁。所以第一步一定摸不到 GOAL,一定要探索。
- 地圖只有三種大小:10×10、15×15、20×20。
- 可以重複走同一格,可以回頭。走回頭路只是多用一步,不額外扣分、也不算撞牆。
A1.4 一步裡面發生什麼事
每一步都是同樣的三拍,一直重複:
| 拍 | 誰做 | 做什麼 |
|---|---|---|
| 1. 感知 | 評分程式 | 告訴機器人:「你現在在 (7, 0),上面是牆、下面是路、左邊是牆、右邊是路。」 |
| 2. 決定 | 你寫的程式 | 想一想,回答一個方向,例如 "RIGHT"。 |
| 3. 執行 | 評分程式 | 真的往右移一格,步數 +1。如果那邊是牆,就原地不動、撞牆次數 +1。 |
第 1 拍收到的資料,在 Python 裡長這樣(這包資料叫 percept,感知):
{
"position": (7, 0), # 你現在站在第 7 列、第 0 欄
"neighbors": {
"UP": "WALL",
"DOWN": "FREE",
"LEFT": "WALL",
"RIGHT": "FREE",
},
}
四個方向每次都會給,值只會是 "FREE"、"WALL"、"GOAL" 三種之一。
A1.5 實際看一次:前三步
下面是我們的最終版機器人,在第 1 張公開地圖上真正走的前三步(從程式實際執行的紀錄抄下來):
| 第幾步 | 站在 | 摸到:上/下/左/右 | 回答 | 結果 |
|---|---|---|---|---|
| 1 | (7, 0) | WALL / FREE / WALL / FREE | RIGHT | 移到 (7, 1) |
| 2 | (7, 1) | FREE / WALL / FREE / FREE | RIGHT | 移到 (7, 2) |
| 3 | (7, 2) | FREE / FREE / FREE / FREE | RIGHT | 移到 (7, 3) |
對照上面的地圖看:
- 第 1 步摸到「左邊是 WALL」。可是地圖上 (7, 0) 左邊沒有
#,那是地圖外面。這就是「邊界也叫 WALL」的例子。 - 第 1 步摸到「上面是 WALL」,對照地圖,(6, 0) 確實是
#。 - 第 2 步摸到「下面是 WALL」,對照地圖,(8, 1) 確實是
#。 - 它沒有往下走,而是往右。為什麼?第 0 步時它算出:右邊那格後面分到 77 格霧、划算度 0.6341;下面那格後面只有 19 格霧、划算度 0.0298。右邊比較划算。(怎麼算的,C3、C4 會講。)
- 第 1、2 步走到選中的格子後,各重選一次,選到 (7, 2)、(7, 3),所以一路往右。
A1.6 整張地圖走完的樣子
同一張地圖,機器人走過的格子標成 *:
. . . . . . . * * G . . # . # . . * . . . . . . . . . * . . . . . . # . . * # . # . . # . # . * . . . . . . . . . * # # # . . . . . . * * * S * * * * * * * * * . # . . . # . . # . . . . . . . # . . #
它沿第 7 列往右走到 (7, 9),往上一格到 (6, 9)。再上面 (5, 9) 是牆,所以往左兩格到 (6, 7),沿第 7 欄一路往上到 (0, 7),最後往右兩格走進 G (0, 9)。
| 項目 | 數字 |
|---|---|
| 實際走了 | 20 步 |
| 撞牆 | 0 次 |
| 最短路線(老師用完整地圖算的) | 16 步 |
| 這張的分數 | 80 + 20 × 16/20 − 0 = 96 分 |
多走的 4 步,就是「因為看不到地圖,必須付出的探索成本」。
A1.7 每一種動作的結果
| 你回答的 | 那一格是 | 會發生什麼 |
|---|---|---|
| 一個方向 | FREE | 移過去,步數 +1 |
| 一個方向 | FREE,但之前走過 | 移過去,步數 +1。不扣分、不算撞牆,只是多用一步 |
| 一個方向 | WALL(牆或邊界) | 原地不動,步數 +1,撞牆 +1 |
| 一個方向 | GOAL | 移過去,過關,這張地圖結束 |
None、"up"(小寫)、"WAIT" 等等 | — | 這張地圖 0 分。只能是 "UP"、"DOWN"、"LEFT"、"RIGHT" 四個大寫字串 |
A1.8 步數上限
每張地圖最多可以用「4 × 列數 × 欄數」步。撞牆也算在裡面。
| 地圖大小 | 步數上限 |
|---|---|
| 10×10 | 400 |
| 15×15 | 900 |
| 20×20 | 1,600 |
步數用完還沒到終點,那張就 0 分。上限其實很寬:等於每一格可以走 4 次。加了「安全閥」(C3)之後,所有測試裡最多只用掉上限的約 30%,包含審查員專門造來刁難程式的地圖。不過這是實測結果,理論上沒辦法保證一定不會超過(原因見 C6)。
A1.9 老師給的檔案,各自在做什麼
把整個作業想成一場考試:
| 檔案 | 比喻 | 實際在做什麼 | 你要動它嗎 |
|---|---|---|---|
agent.py | 考生 | 機器人的大腦。每一步決定往哪走 | 要,唯一要寫、唯一要交的 |
environment.py | 迷宮本身+裁判 | 讀地圖、告訴機器人四周是什麼、執行移動、數步數和撞牆、判斷有沒有到終點 | 不能改 |
public_grader.py | 模擬考的考官 | 把 3 張公開地圖一張一張拿給你的機器人走,最後印出成績表 | 不能改,只用來跑 |
maps/ 資料夾 | 模擬考考卷 | 3 張公開地圖(10×10、15×15、20×20 各一張),JSON 格式 | 不能改 |
README.md | 考試規則 | 完整規格、評分公式、繳交格式 | 讀就好 |
map_example.png | 規則的插圖 | README 裡的示意圖 | 不用管 |
正式評分時,老師會換成 9 張你沒看過的隱藏地圖(README 只說三種尺寸各三張;「三種風格各一張」是我從公開地圖推測的)。公開的 3 張只是讓你練習,不算成績。
A1.10 評分程式怎麼使用你的程式
這是 public_grader.py 的核心流程,我改寫成白話:
agent = Agent() # 照你的設計圖,造出「一台」機器人
for 每一張地圖:
env = GridEnvironment(這張地圖) # 準備好迷宮和裁判
agent.reset(列數, 欄數, 起點) # 告訴機器人:新地圖開始了,忘掉上一張
while 還沒走到終點 and 步數還沒用完:
percept = env.observe() # 第 1 拍:裁判告訴機器人四周是什麼
action = agent.act(percept) # 第 2 拍:機器人回答往哪走
env.step(action) # 第 3 拍:裁判真的執行這一步
注意兩件事:
- 機器人只造一次,三張地圖都是同一台在走。所以每張新地圖開始時,
reset()一定要把舊的記憶清乾淨。不然它會拿第 1 張地圖的牆和路,來規劃第 2 張地圖的路線:可能以為某處是路結果撞牆,或以為某些地方已經探索過而跳過。 - 你的程式不用自己讀地圖、不用自己移動、不用自己算步數。那些都是裁判的事。你只負責「回答往哪走」。
A1.11 Python 小補充:為什麼記憶要放在 self 裡
先講它在做什麼:讓機器人能記住上一步發生過的事。
class Agent是機器人的設計圖。Agent()是照設計圖做出一台機器人(做出來的這一台叫「物件」)。self就是「這台機器人自己」。self.known是「這台機器人口袋裡的筆記本」。
為什麼一定要放在 self 上?
因為 act() 每一步都會被重新呼叫一次。在 act() 裡面寫的普通變數(例如 known = {}),這一步結束就消失了,下一步什麼都不記得。
放在 self 上(self.known = {})就不一樣:它掛在機器人身上,會一直留到下一步、下下一步。
README 也特別提醒:「請使用 instance variables(例如 self.internal_map、self.visited、self.path)保存資訊」。instance variable 就是「掛在 self 上的變數」。
A1.12 你要寫的三個方法
| 名稱 | 什麼時候被呼叫 | 要做什麼 | 回傳什麼 |
|---|---|---|---|
__init__() | 機器人剛造出來時,只有一次 | 把要用的變數先建好。我們的程式在這裡直接呼叫一次 reset(1, 1, (0, 0)) | 不用回傳 |
reset(rows, cols, start_position) | 每張新地圖開始前,一次 | 記下新地圖的大小和起點;把上一張的記憶全部清掉 | 不用回傳(回傳了也沒人看) |
act(percept) | 每走一步前,一次 | 看感知、更新記憶、決定方向 | 一定要回傳四個方向字串之一 |
老師給的空白範本,act() 裡只寫了 return "UP",也就是永遠往上走。我實際跑過:3 張地圖全部失敗,每一步都撞牆(10×10 那張 400 步撞了 400 次)。因為起點上面剛好是牆,它就一直撞同一面牆。你的工作就是把這一行換成聰明的決策。
A1.13 三種地圖風格
公開地圖的 JSON 檔裡有一個 style 欄位,標明風格。隱藏地圖推測也是這三種(README 說三種尺寸各三張,但沒說風格;這是我從公開地圖推測的):
| 風格 | 白話 | 公開地圖 | 難在哪 |
|---|---|---|---|
open 空曠型 | 大片空地,零星散落幾面牆,沒有外框 | public_map_01(10×10) | 到處都能走,方向選錯就會繞很遠 |
dead_ends 死路型 | 標準迷宮,走道寬一格,很多死路,只有一條正確路線 | public_map_02(15×15) | 走進死路要整段退回來 |
loops_dense 迴圈型 | 迷宮,但很多牆被打通,路會繞成圈 | public_map_03(20×20) | 同一個地方有好幾條路可以到,容易繞圈子 |
看另外兩張公開地圖長什麼樣
public_map_02(死路型,15×15):起點 S 在 (3, 9),終點 G 在 (10, 1),最短 45 步
# # # # # # # # # # # # # # # # . . . # . . . . . . . . . # # . # . # # # . # # # . # . # # . # . . . # . # S . . # . # # . # # # # # . # . # # # # # # . . . # . . . # . . . . . # # # # . # # # # # # # # # . # # . # . . . . . . . . . . . # # . # # # # # # # # # # # . # # . # . . . . . . . . . # . # # G # . # # # # # # # . # . # # . # . # . # . . . . . # . # # . # . # . # . # # # # # . # # . . . . . # . . . . . . . # # # # # # # # # # # # # # # #
public_map_03(迴圈型,20×20):起點 S 在 (3, 18),終點 G 在 (11, 2),最短 26 步
# # # # # # # # # # # # # # # # # # # # # . . . . . . . . . . . # . . . . . . . # . # . # . # . # # # . # # . # # . # # # . . . . . . . # . . . . . . . # . S . # . # # # # # . # . # # # . # . # . . . # . # . . . . . # . . . . . # . . . . . # . # . # . # . # . # # # . # . # . # . # . # . . . . . . . # . . . . . # . . . # . # . . . # . # . . . # # # # # . # . # . . . # . # . . . # . . . . . . . # . # # # . # . # . . . # . # # . . # . # . # . G . # . # . . . # . # . . . # . . . # # # . # . # . # . # # # . # . # . . . # . . . # . . . # . . . . . # . . . # . # . # . # # # . # # # . # . # # . . # . # . # . . . . . # . # . . . # . . . . . # . # # . # # # # . # . # . # . # . . # # . # . . . . . . . # . . . # . # . . . # . # # # . . . . # # . # . . . # . # . # . . . # . # . . . . . # . . . . . # .
A1.14 其他規定(有些違反會 0 分)
| 規定 | 違反會怎樣(README 原文的根據) | 我們的程式 |
|---|---|---|
只能改、只能交 agent.py | 繳交格式錯誤、導致無法開始評分 → 整份 0 分(README 第 7 節) | 只改了 agent.py |
| 只能用 Python 內建的標準函式庫 | 用了未允許的套件、導致無法載入 → 整份 0 分(README 第 7 節) | 只用了 collections.deque(內建) |
| 正式環境是 Linux+Python 3.11 | 版本不相容、導致無法載入 → 整份 0 分(README 第 7 節) | 已用你電腦上的 Python 3.11 測過 |
act() 只能回傳四個大寫方向字串 | 回傳其他值 → 那一張 0 分(README 第 5 節) | 連出錯時的備案也只回傳合法方向 |
| 不能偷看地圖檔、不能要求輸入 | README 明文禁止(README 第 7 節),但沒寫罰則;input() 會讓評分程式卡住等人打字 | 沒有 open()、沒有 input() |
| 不能跑太久 | README 只說「需在指定時間內完成」,沒寫幾秒、也沒寫罰則 | 公開地圖每張約 0.05~0.1 秒;審查員刻意做的最難 20×20 地圖,最慢一張 3.3 秒 |
A1.15 自己跑跑看
想親眼看結果,照這樣做:
- 打開終端機(VS Code 下方的 Terminal 就可以)。
- 切到作業資料夾:
cd "C:\D槽\TAICA課程\人工智慧導論\HW2\HW2" - 跑模擬考:
py -3.11 public_grader.py - 想看每一步怎麼走:
py -3.11 public_grader.py --verbose
README 寫的指令是 python3 public_grader.py,那是 Linux 的寫法。在你的 Windows 上用 py -3.11,意思是「指定用 Python 3.11 來跑」,跟正式評分環境同版本。
你應該會看到:
Map | Result | Steps | Collisions
-----------------------+------------+------------+-------------
public_map_01 | SUCCESS | 20 | 0
public_map_02 | SUCCESS | 45 | 0
public_map_03 | SUCCESS | 70 | 0
Successful maps : 3/3
| 欄位 | 意思 |
|---|---|
SUCCESS | 在步數上限內走到終點 |
FAILED | 步數用完還沒到 |
ERROR | 程式出錯了,下面會印出錯誤訊息 |
Steps | 總步數(包含撞牆) |
Collisions | 撞牆次數 |
注意:這個成績表不會印分數。分數是我用老師的公式另外算的(C2 節)。
所以對你的影響是:這份作業的重點不是「寫很多程式」,而是「想出一個聰明的走法」。程式本身大約 230 行(含註解),規則都在這一節了。接下來 A2 節講分數怎麼算,然後 B 部分講背後的邏輯框架。
A2 分數怎麼算
結論:每張地圖= 80 分基本分 + 最多 20 分效率分 − 撞牆扣分。拿高分的三個關鍵:一定要走到、永遠不撞牆、少走冤枉路。
老師的公式
沒走到終點(步數用完): 這張 0 分
走到終點:
實際移動步數 = 總步數 − 撞牆次數
效率 = min(1, 最短路線步數 / 實際移動步數)
撞牆扣分 = min(10, 撞牆次數)
這張分數 = 80 + 20 × 效率 − 撞牆扣分
作業成績 = 9 張隱藏地圖的平均
「最短路線步數」是老師用完整地圖算出來的。你的 Agent 看不到這個數字。
舉例
假設某張地圖最短要走 20 步:
| 你的表現 | 算式 | 分數 |
|---|---|---|
| 走 20 步,沒撞牆 | 80 + 20 × 20/20 | 100 |
| 走 40 步,沒撞牆 | 80 + 20 × 20/40 | 90 |
| 總共 43 步,其中撞牆 3 次(實際移動 40 步) | 80 + 20 × 20/40 − 3 | 87 |
| 步數用光還沒到 | — | 0 |
三個關鍵,一個一個看
| 關鍵 | 值多少分 | 難不難 | 我們怎麼做 |
|---|---|---|---|
| 1. 一定要走到 | 80 分(沒走到就 0) | 不難 | 步數上限很寬(每格 4 步:10×10 給 400 步)。只要不在原地打轉就有機會走到。我們的程式在 5,400 張壓力測試+60 張刁鑽地圖上 0 失敗。加了「安全閥」(C3)之後,所有測試裡最多只用掉上限的約 30%,包含審查員專門造來刁難程式的地圖。 |
| 2. 永遠不撞牆 | 最多 10 分 | 白送 | 下一步一定是走到隔壁四格之一,而這四格每一步都剛摸過。程式只會走「已知是路」的格子,或直接走進終點,所以撞牆次數永遠是 0。 |
| 3. 少走冤枉路 | 最多 20 分 | 難 | 因為一開始不知道終點在哪,一定會走一些冤枉路去「找」。這是唯一要動腦的地方,C2、C3 節在講這個。 |
一張失敗(0 分)會讓 9 張的平均掉 10 分以上。而聰明的走法能多賺多少?在模擬考上,最終版比 DFS 平均多 3.56 分(終點偏遠時)或 0.64 分(終點隨機時)。 所以設計時的優先順序是:先保證絕對不會失敗,再去追求效率。這個原則在C3 節「承諾」那段會再出現一次。
所以對你的影響是:只要程式正確、不失敗,每張至少 80 分(模擬考裡最低是 80.15)。分數高低差在「探索策略」:在老師的 3 張公開地圖上,最終版平均比 DFS 多 5.47 分,單張最多多 11.15 分(公開地圖 01:96 對 84.85)。我們的最終版預估約 91 分。
B1 Agent 是什麼
結論:Agent 就是「會看、會想、會動」的東西。課本把任何 AI 都看成一個 Agent:從環境收到感知,決定一個動作,再把動作做回環境裡。這份作業的機器人就是一個標準的 Agent。
B1.1 一個 Agent 有三個部分
想像一台掃地機器人:
- 它有感應器,可以知道地上髒不髒、前面有沒有牆。
- 它有一個大腦,根據感應到的東西決定要做什麼。
- 它有輪子和吸塵器,可以真的去做。
課本 Ch2 第 2 頁的定義就是這三件事:Agent 透過 sensors(感應器)感知環境,透過 actuators(執行器,例如輪子、手臂)對環境做動作。
| 部分 | 課本的詞 | 在這份作業裡 |
|---|---|---|
| 感應器 | sensors → 收到 percept(感知) | 摸上下左右四格,加上知道自己在哪 |
| 大腦 | agent program(Agent 程式) | 你的 agent.py |
| 執行器 | actuators → 做出 action(動作) | 回傳 "UP"/"DOWN"/"LEFT"/"RIGHT",由評分程式真的去移動 |
B1.2 Agent function:從「看過的東西」對應到「動作」
課本 Ch2 第 3 頁說:Agent 的行為,可以用一個 agent function(Agent 函數)描述。它把「到目前為止收到的所有感知」(叫 percept sequence,感知序列)對應到一個動作。
重點在「到目前為止所有的」。好的 Agent 不是只看眼前這一刻,而是參考它看過的全部東西。
所以對你的作業的影響是:機器人應該把每一步摸到的東西都記下來(這就是 self.known 筆記本),用「全部記憶」來做決定,而不是只看眼前四格。
B1.3 理性的 Agent(rational agent)
課本 Ch2 第 5~7 頁:理性的 Agent,就是在它所知道的範圍內,選擇「預期會讓表現分數最高」的動作。
注意三件事:
- 表現分數(performance measure)是外面的人定的。這份作業裡,就是老師的評分公式(A2)。
- 理性不等於全知。機器人看不到整張地圖,所以它不可能每次都走最短路。理性的意思是:用手上有的資訊,做出最好的猜測。
- 所以「多走了一些冤枉路」不代表它不理性。只要它在當時的資訊下選了最划算的路,就是理性的。
B1.4 PEAS:描述一個任務的四個面向
課本 Ch2 第 9 頁:設計 Agent 之前,先用 PEAS 把任務講清楚。PEAS 是四個英文字的開頭:
| 字母 | 英文 | 白話 | 這份作業 |
|---|---|---|---|
| P | Performance measure | 怎麼打分數 | 走到終點有 80 分;走越少步越高分(最多再加 20);撞牆一次扣 1 分(最多扣 10) |
| E | Environment | 在什麼環境裡 | 10×10、15×15 或 20×20 的格子迷宮,有牆、有一個終點 |
| A | Actuators | 能做什麼動作 | 往上、下、左、右移動一格 |
| S | Sensors | 能感知什麼 | 自己的座標;上下左右四格是 FREE、WALL 還是 GOAL |
如果考題問「請寫出 HW2 的 PEAS」,直接寫上面這張表就對了。這是課本最基本的題型。
B2 這是什麼樣的環境
結論:課本用六、七組「二選一」來分類環境。這份作業的環境是:部分可觀察、單一 Agent、確定性、序列式(sequential)、靜態、離散;known/unknown 那一項有爭議,考試建議答 unknown(見下面的提醒)。其中「部分可觀察」最重要,因為它決定了機器人一定要有記憶。
B2.1 為什麼要分類環境
不同的環境,要用不同的 Agent。就像下象棋和開車,需要的能力完全不同。
先搞清楚環境是什麼樣子,才知道 Agent 該具備哪些能力。課本 Ch2 第 11~14 頁列了這些分類。
B2.2 逐項看
| 分類 | 白話 | 這份作業是 | 為什麼 |
|---|---|---|---|
| Fully vs. partially observable (完全/部分可觀察) | 感應器能不能一次看到「做決定需要的全部資訊」 | Partially observable | 只摸得到四格,看不到整張地圖,也看不到終點 |
| Single vs. multiagent (單一/多個 Agent) | 環境裡有沒有其他會影響你分數的 Agent | Single agent | 迷宮裡只有你一個 |
| Deterministic vs. stochastic (確定性/隨機性) | 同一個狀態做同一個動作,結果是不是永遠一樣 | Deterministic | 往右就一定往右一格(或撞牆不動),沒有「有時候會滑到別格」 |
| Episodic vs. sequential (獨立回合/序列式) | 現在的決定會不會影響之後 | Sequential | 這一步走哪,決定下一步站在哪、能看到什麼 |
| Static vs. dynamic (靜態/動態) | 你在想的時候,環境會不會自己變 | Static | 牆和終點不會移動 |
| Discrete vs. continuous (離散/連續) | 狀態和動作是不是一格一格、數得出來的 | Discrete | 格子座標、四個方向 |
| Known vs. unknown (規則已知/未知) | Agent(或設計者)知不知道這個世界的「物理規則」 | 有爭議,考試建議答 Unknown | Ch4 把「迷宮地圖事先不知道」的情況叫 unknown environment。但移動規則本身是已知的,所以也有人會判 known。見下面的提醒 |
建議答:unknown,再補一句「移動規則本身已知,不知道的是地圖和每個動作的結果」。理由:
1. Ch4 第 29~35 頁的標題就叫 "Online Searching Agents with Unknown Environments",第 31 頁拿迷宮當例子:"the agent does not know that going Up from (1,1) leads to (1,2)"。這份作業就是這個情況。
2. Ch2 第 14 頁的定義是「對環境『物理規則』的了解程度」。照這個定義,也可以主張「規則已知、只是看不到地圖」=known+partially observable。所以這題有爭議。
3. 老師這門課的 Ch4 投影片明確把這種迷宮歸成 unknown,考試照老師投影片的用法答最安全。
這是我和審查員的判斷,老師沒有針對這份作業講過。
B2.3 「部分可觀察」帶來的後果
因為看不到全部,Agent 必須自己記住看過的東西,拼出一張「自己知道的地圖」。
這直接決定了下一節要講的:這份作業需要哪一種 Agent。
B3 四種 Agent,這份作業要哪一種
結論:課本由簡單到複雜介紹四種 Agent。只看眼前的「反射型」在迷宮裡會繞圈子;這份作業需要的是「有記憶(model-based)+有目標(goal-based)+會算划算度(utility-based)」的 Agent。我們的程式三樣都有。
B3.1 反射型 Agent(simple reflex agent):只看眼前
課本 Ch2 第 18~19 頁:只根據現在這一刻的感知做決定,完全不管以前看過什麼。規則像「如果怎樣,就做什麼」(condition-action rule)。
例子:「右邊是路就往右;不然下面是路就往下⋯⋯」。
課本說它只在環境「完全可觀察」時才行得通。這份作業是部分可觀察,所以反射型會出問題:
- 它不記得哪裡走過,可能在兩格之間來回走,或繞著同一圈一直轉。
- 它不記得哪裡是死路,走出死路後可能又走回去。
- 最後步數用光,那張地圖 0 分。
B3.2 有記憶的 Agent(model-based reflex agent)
課本 Ch2 第 20~21 頁:為了處理「看不到全部」,Agent 要維護一個內部狀態(internal state),記住現在看不到的那部分世界。這個內部狀態根據「過去所有的感知」來更新。
在我們的程式裡,內部狀態就是 self.known:每一格摸過的結果都寫在裡面。
B3.3 有目標的 Agent(goal-based agent)
課本 Ch2 第 22~23 頁:光知道世界長怎樣還不夠,還要知道想去哪裡。有目標的 Agent 會考慮「如果我這樣做,會發生什麼事?會不會更接近目標?」
我們的目標是「走進 GOAL」。程式會規劃路線(用 BFS),而不是只看下一步。注意:因為終點在哪裡不知道,BFS 規劃的是走到「選中的邊境」的路線,不是走到終點的路線。
B3.4 效用導向的 Agent(utility-based agent)
課本 Ch2 第 24~25 頁:當有好幾個可以追求的目標,或者結果不確定時,光有目標不夠。Agent 要對每個選擇打一個分數(叫 utility,效用),選預期效用最高的。課本特別說:效用導向的 Agent 能處理「部分可觀察」帶來的不確定性。
這正是我們的情況:
- 地圖上同時有好幾個「可以去探索的地方」(後面會叫它邊境)。
- 不知道終點在哪一個方向。
- 所以我們幫每個邊境算一個「划算度」=「估計的寶藏價值 ÷(要走的步數+2)」,選最高的。這個划算度就是效用。
- 注意:「寶藏價值」是我們自己設計的估計分數(越遠的未知格分數越高),不是真正算出來的機率。
B3.5 整理
| Agent 種類 | 核心能力 | 課本頁數 | 我們的程式 |
|---|---|---|---|
| Simple reflex | 只看當下 | Ch2 p18–19 | 不是(在迷宮會繞圈) |
| Model-based | 有記憶(內部狀態) | Ch2 p20–21 | 有:self.known |
| Goal-based | 有目標、會規劃 | Ch2 p22–23 | 有:目標是 GOAL,用 BFS 規劃路線 |
| Utility-based | 會對選擇打分數 | Ch2 p24–25 | 有:划算度 |
課本的四種 Agent 是一層一層往上加能力的,所以「是 utility-based」也同時代表它有記憶、有目標。
B4 把問題寫成「搜尋問題」
結論:課本 Ch3 說,任何「從起點走到目標」的問題,都可以用五個要素描述。把迷宮套進這五個要素,就可以用搜尋演算法(BFS、DFS、A*)來處理。
B4.1 五個要素
課本 Ch3 第 5 頁用「從 Arad 開車到 Bucharest」當例子。我們把迷宮套進去:
| 要素 | 白話 | 開車例子(課本) | 迷宮(這份作業) |
|---|---|---|---|
| Initial state 初始狀態 | 從哪裡開始 | 在 Arad | 站在起點,例如 (7, 0) |
| Actions 可以做的動作 | 在某個狀態下能做什麼 | 開往 Sibiu、Timisoara、Zerind | 往上、下、左、右 |
| Transition model 轉移模型 | 做了某個動作會變成什麼狀態 | 從 Arad 開往 Zerind → 到 Zerind | 從 (7, 0) 往右 → 到 (7, 1);如果是牆 → 留在原地 |
| Goal test 目標測試 | 怎麼判斷到了 | 是不是在 Bucharest | 是不是站在 GOAL 格 |
| Path cost 路徑成本 | 一條路花多少代價 | 開了幾公里 | 走了幾步(每步成本 1) |
課本還說:解(solution)就是一串從初始狀態走到目標的動作;解的好壞用路徑成本衡量。成本最低的解叫最佳解(optimal solution)。
這份作業的「最短路線步數」(評分公式裡的 optimal_steps)就是最佳解的成本。
B4.2 狀態空間:把迷宮看成一張「圖」
把每一個可以站的格子想成一個點,相鄰、可以走過去的兩格之間連一條線。這樣迷宮就變成一張由點和線組成的圖。所有狀態加起來叫狀態空間(state space)。
課本 Ch3 第 9 頁說:搜尋演算法從初始狀態出發,一步一步展開可能的動作,形成一棵搜尋樹(search tree)。樹根是初始狀態,樹枝是動作,每個節點是一個狀態。
B4.3 評估搜尋演算法的四個標準
課本 Ch3 第 11 頁:
| 標準 | 問的問題 |
|---|---|
| Completeness(完備性) | 只要有解,保證找得到嗎? |
| Optimality(最佳性) | 找到的是最好的解嗎? |
| Time complexity(時間複雜度) | 要花多久? |
| Space complexity(空間複雜度) | 要用多少記憶體? |
Ch3 的搜尋假設你事先知道整張地圖(轉移模型是已知的),所以可以先在腦子裡算好整條路,再出發。
這份作業看不到地圖:你不實際走到那格旁邊,就不知道往那邊走會不會撞牆。所以不能直接套 Ch3 的做法。這就是 B6 要講的「線上搜尋」。
B5 三種基本搜尋:BFS、DFS、A*
結論:三種方法的差別只在「下一個先展開誰」。BFS 先展開近的,保證找到最短路;DFS 先往深處鑽,省記憶體但不保證最短;A* 用「估計離終點多遠」來挑,又快又能找到最短,但前提是知道終點在哪。
B5.1 共同的骨架:frontier(待展開清單)
三種方法都用同一個骨架:
- 手上有一份「等著被展開的節點」的清單。課本叫它 frontier。一開始只有起點。
- 從清單裡挑一個節點拿出來。
- 是目標就結束;不是的話,把它的鄰居(還沒看過的)加進清單。
- 重複。
課本 Ch3 第 12 頁說:所有搜尋策略的差別,就在於節點被展開的順序。
Ch3 的 frontier=搜尋樹裡「等著被展開的節點清單」。
這份作業(和 C1 節)說的 frontier(邊境)=地圖上「已知是路、旁邊還有未知格」的格子。
兩個都是「下一個要去探索的地方」,概念相近,但不是同一個東西。考試要分清楚。
B5.2 BFS(廣度優先搜尋,Breadth-first search)
課本 Ch3 第 13~15 頁:先展開起點,再展開起點的所有鄰居,再展開鄰居的鄰居⋯⋯一層一層往外。
怎麼做到「一層一層」:清單用排隊的方式(先進先出,FIFO)。先加進去的先拿出來,所以近的一定先被處理。
小例子:3×3 的空地,從左上角 A 出發。
A B C D E F G H I
| 輪 | 拿出來 | 加進排隊 | 排隊裡現在有 |
|---|---|---|---|
| 1 | A(距離 0) | B、D(距離 1) | B D |
| 2 | B | C、E(距離 2) | D C E |
| 3 | D | G(距離 2;E 已經加過了) | C E G |
| 4 | C | F(距離 3) | E G F |
| ⋯ | ⋯ | ⋯ | ⋯ |
距離 1 的全部處理完,才輪到距離 2,再輪到距離 3。所以每一格第一次被加進排隊時的距離,就是最短距離。
| 性質 | BFS |
|---|---|
| 保證找得到嗎(完備) | 是(分支數有限時) |
| 保證最短嗎(最佳) | 是(每步成本一樣時) |
| 缺點 | 要記住很多節點,比較耗記憶體 |
所以對你的作業的影響是:我們的程式每一步都用 BFS,在「已經知道的地圖」上算出走到每一格的最短步數。
B5.3 DFS(深度優先搜尋,Depth-first search)
課本 Ch3 第 18~20 頁:永遠先展開最深的節點。一條路走到底,走不通才退回上一個岔路換一條。
怎麼做到「先往深處」:清單用疊盤子的方式(後進先出,LIFO)。最後加進去的先拿出來。
| 性質 | DFS |
|---|---|
| 保證找得到嗎(完備) | 看情況:狀態有限、而且會記住走過哪裡(graph search)時是完備的;不記走過哪裡(tree search)可能在迴圈裡繞不出來;狀態無限時不完備 |
| 保證最短嗎 | 不保證(可能繞一大圈才到) |
| 優點 | 記憶體用得少;而且除了退回的時候,下一個展開的都是現在這格的鄰居(這一點在 B6 很重要) |
B5.4 A*(A-star):有方向感的搜尋
BFS 和 DFS 都是「盲目」的:它們不知道目標大概在哪個方向(課本叫 uninformed search,無資訊搜尋)。
如果有一點線索,例如「目標大概在東北邊」,就可以優先往那邊找。這種線索叫 heuristic(啟發式函數),寫成 h(n):估計從節點 n 到目標還要花多少(Ch3 第 27 頁)。
A* 每次挑這個值最小的節點展開(Ch3 第 31 頁):
f(n) = g(n) + h(n)
g(n) = 從起點走到 n 已經花了多少
h(n) = 估計從 n 到目標還要多少
f(n) = 估計「經過 n 的整條路」總共多少
在格子地圖上,最常用的 h(n) 是曼哈頓距離:上下差幾格+左右差幾格。
A* 什麼時候保證找到最短路(Ch3 第 32~33 頁):
- admissible(可容許):
h(n)從不高估真正的剩餘成本。這樣 tree search 版的 A* 保證最短。 - consistent(一致):從 n 走一步到 n′,
h(n)不會大於「這一步的成本+h(n′)」。graph search 版的 A*(會記住走過哪裡、不重複展開)需要這個更強的條件才保證最短。
曼哈頓距離在格子地圖上兩個條件都滿足。
要算
h(n),必須知道目標在哪。這份作業的終點一開始未知,只有走到它旁邊才會發現。所以 A* 不能拿來「找終點」。不過我們借用了 A* 的精神:用一個估計值來決定優先順序。只是我們估計的不是「離終點多遠」,而是「終點可能在哪裡」(C3)。
B5.5 三種方法比較
| BFS | DFS | A* | |
|---|---|---|---|
| 下一個展開誰 | 最淺的(最近的) | 最深的 | f(n) 最小的 |
| 清單的資料結構 | 排隊(FIFO queue) | 疊盤子(LIFO stack) | 優先佇列(依 f 排序) |
| 保證最短 | 是 | 否 | 是(tree search 要 h 可容許;graph search 要 h 一致) |
| 需要知道目標在哪 | 否 | 否 | 是 |
| 在這份作業裡的角色 | 在已知地圖上算最短路線 | 對照組(C2 的走法 A) | 不直接用;借用它「用估計值排優先」的想法 |
B6 線上搜尋:看不到地圖時怎麼辦
結論:這份作業就是課本 Ch4 的「線上搜尋」(online search):走一步、看一下、想一下、再走一步。評分公式就是投影片講的 competitive ratio(競爭比)。我們的做法是「記住地圖+用 BFS 走到最值得探索的邊境」。
B6.1 離線 vs. 線上
課本 Ch4 第 29 頁(投影片角落編號 56):
| 離線搜尋(offline) | 線上搜尋(online) | |
|---|---|---|
| 做法 | 出發前先在腦子裡把整條路算好,再照著走 | 計算和行動交錯:先做一個動作,看看結果,再算下一步 |
| 需要 | 事先知道整個環境 | 不需要,邊走邊認識環境 |
| 例子 | Ch3 的開車找路(地圖都在手上) | 課本的例子:機器人被放進一棟陌生的建築,要邊探索邊畫地圖 |
| 這份作業 | 做不到(看不到地圖) | 就是這個 |
課本 Ch4 第 30~31 頁還說:線上搜尋的 Agent,不實際走到某個狀態、做某個動作,就不知道結果。這份作業也一樣:不走到那一格旁邊,就不知道那格是路還是牆。
B6.2 競爭比(competitive ratio):怎麼評價線上 Agent
課本 Ch4 第 33 頁(角落編號 60):
競爭比 = Agent 實際走的路徑成本 ÷ 如果事先知道地圖時的最短路徑成本
越小越好,最好是 1(等於直接走最短路)。
回頭看這份作業的評分公式(A2):
效率 = 最短路線步數 ÷ 實際移動步數
在 0 次撞牆時,剛好是競爭比倒過來。競爭比 2(走了最短的兩倍)→ 效率 0.5 → 效率分 10 分。
兩個細節:
- 老師的「實際移動步數」不算撞牆;課本的「實際路徑成本」每個動作都算。所以只有 0 撞牆時兩者才完全一樣。我們的程式永遠 0 撞牆,所以剛好一樣。
- 公式裡的
min(1, …)實際上不會起作用,因為成功時實際走的路一定不會比最短路還短。
所以老師評分裡的效率分那一項,就是課本評價線上搜尋 Agent 的方式。80 分基本分、失敗 0 分、撞牆扣分,則是老師另外加的規則。
B6.3 課本的提示:線上搜尋要「就近展開」
課本 Ch4 第 34 頁(角落編號 61):
- 每走一步,Agent 收到新的感知,就把它補進自己的地圖。
- 線上 Agent 要「實際走過去」才能展開一個節點。如果像 BFS 那樣在不同分支間跳來跳去,每跳一次都要真的走很遠的路,非常浪費。
- 所以線上搜尋最好就近展開。DFS 剛好有這個性質:下一個要展開的通常就在旁邊。
這就是為什麼課本的經典線上搜尋 Agent 是 DFS 型的。
B6.4 我們比課本的 DFS 更進一步
DFS 型的線上 Agent 有一個缺點:走到死路時,它要一格一格原路退回到上一個岔路。
我們的做法是:
- 記住整張已知地圖(不只記一條路)。
- 找出所有「站上去就能看到新東西」的格子,叫邊境(C1 會詳細講)。
- 用 BFS 在已知地圖上算最短路線,直接抄近路走到想去的邊境,不用原路退回。
- 有好幾個邊境時,用「划算度」挑最值得去的(C3)。
這種做法在機器人領域叫做以邊境為基礎的探索(frontier-based exploration)。課本投影片沒有用這個名字,是機器人探索的常見做法。
BFS 在這裡的角色變了:它不是用來「展開搜尋樹找終點」,而是用來「在已經知道的地圖上算走路路線」。這兩件事不一樣,考試時要講清楚。
B6.5 從框架到實作:剩下唯一要決定的事
到這裡,框架都有了:
| 需要的能力 | 從哪裡來 | 實作 |
|---|---|---|
| 記住看過的東西 | Model-based agent(B3) | self.known |
| 找出還能去探索的地方 | 線上搜尋(B6) | 邊境 |
| 算出怎麼走過去最快 | BFS(B5) | _bfs_from() |
| 永遠不撞牆 | 只走已知是路的格子 | BFS 只走 FREE |
剩下唯一要動腦的問題:有好幾個邊境時,先去哪一個?
這個選擇決定了冤枉路的多寡,也就決定了效率分。C 部分就是在回答這個問題:先比較四種選法(C2),再講最終版怎麼選(C3)。
C1 三個基本工具
結論:所有走法都靠三個工具:記憶地圖(記住摸過什麼)、邊境(哪裡還有東西沒看過)、BFS(在已知的路上找最短路線)。
工具 1:記憶地圖
Agent 每摸一次,就把結果寫進一本筆記本。程式裡這本筆記本叫 self.known,是一個 Python 的 dict(字典:用「座標」查「狀態」)。
self.known[(4, 7)] = "FREE" # 我站過的地方,一定是路
self.known[(5, 7)] = "WALL" # 摸到下面是牆
self.known[(4, 6)] = "FREE" # 摸到左邊是路
筆記本裡沒有寫到的格子 = 未知。未知的格子可能是路、牆、或終點。
README 特別提醒:「沒有看到牆壁,不代表已確認該格可通行」。所以我們只在已知是 FREE 的格子上規劃路線,不會賭未知的格子能走。
工具 2:邊境(frontier)
先講它在做什麼:找出「站上去才能看到新東西」的格子。
想一想:你要怎樣才能看到一個未知的格子?答案是:走到它旁邊,摸一下。
所以有用的目的地是:已知是路、而且旁邊還有未知格子的格子。這種格子就叫邊境(frontier),意思是「已知世界和未知世界的交界」。
精確地說,一格要同時符合 4 個條件才是邊境:
- 已經知道它是一般的路(不是牆、不是霧,也不是終點)。
- 不是機器人現在站的那一格。
- 從機器人現在的位置,只走「已知的路」走得到。
- 它的上下左右,至少有一格在地圖裡面、而且還是未知的。
下面是一個例子。@ 是你,. 是已知的路,# 是已知的牆,? 是未知,F 是邊境:
情境:你從左邊那格 . 出發,往右走到最右邊那格 .,再走回中間的 @。走過的三格,每一格都摸過它的上下左右。
? ? ? ? ? ? F F F ? # . @ . # ? F # F ? ? ? ? ? ?
每個 F 都是「已知是路」,而且旁邊都還有 ?,站上去就能看到新格子。
走過的三格(兩個 . 和 @)四周都已經摸過了,旁邊沒有 ?,所以它們都不是邊境。
終點只有在「旁邊」時才摸得到,而且一摸到,程式就直接走進去(C3 的步驟 2)。所以還在找的時候,終點一定還沒被摸到過,一定藏在某個 ? 裡。所以探索=不斷走到邊境,把 ? 變成已知,直到摸到 GOAL。
工具 3:BFS(廣度優先搜尋)
先講它在做什麼:從你現在的位置出發,算出走到每一格「最少要幾步」,以及「第一步該往哪走」。
做法像丟一顆石頭到水裡,漣漪一圈一圈往外擴:
- 第 0 圈:你自己,距離 0。
- 第 1 圈:你旁邊「已知是路」的格子(不含未知的格子),距離 1。
- 第 2 圈:第 1 圈旁邊、還沒算過的格子,距離 2。
- 一直擴下去,直到沒有新格子。
因為是一圈一圈擴,第一次碰到某格時的圈數,一定就是最短距離。這是 BFS 最重要的性質。Ch3 第 13~15 頁講 BFS 怎麼一層一層展開;「保證最短」是從這個展開順序推出來的,投影片文字沒有直接寫這句。
我們的 BFS 只在「已知是 FREE」的格子上擴散。它從機器人現在站的格子出發(不是地圖一開始的起點),順便記下「走到每一格,第一步要往哪個方向」。這樣選好目的地之後,馬上知道這一步要回傳什麼。
BFS 一圈一圈擴,保證找到最短路。DFS(深度優先)是一條路走到底再回頭,不保證最短。A* 是用 f(n) = g(n) + h(n) 排優先順序的搜尋(best-first search),h(n) 就是「方向感」:估計離終點還有多遠。所以它需要知道終點在哪。這份作業終點未知,所以 A* 不能直接拿來找終點;我們是用 BFS 在已知地圖上找路。
所以對你的影響是:這三個工具是每一種走法共用的骨架。不同的走法只差在「有好幾個邊境時,先去哪一個」。
C2 四種走法比一比
結論:我做了模擬考,比較四種走法。在公開地圖、以及「終點放遠處」的模擬地圖上,最終版最好,比最基本的 DFS 多 3.56 分(終點偏遠時);如果終點是隨便放的,最終版跟走法 C 打平(88.78 對 88.79),只比 DFS 多 0.64 分。
四種走法
| 走法 | 每一步怎麼選(精確規則) | 缺點 |
|---|---|---|
| A. DFS(深度優先) | 記得兩樣東西:自己站過哪些格子、一路走來的路線。每一步照「上、下、左、右」的順序,走進第一個「是路、而且自己還沒站過」的格子;四格裡沒有這種格子(死路),就退回「來這格之前站的那一格」(一次退一格)。課本 Ch4 線上搜尋講的經典做法。 | 退回時只能沿原路一格一格退,不會「抄近路」直接走到別的岔路。 |
| B. 最近邊境 | 每一步重新挑:用 BFS 算走到每個邊境要幾步(d),選 d 最小的。平手時先選上一步的舊目標,沒有的話選最先被 BFS 找到的。 | 只看近不近,不管方向。終點在遠處時,常常先繞完起點附近。例如公開地圖 01:B 一開始往下(遠離終點),32 步才到;D 20 步。 |
| C. 偏好遠處 | 跟 B 一樣每一步重新挑,但分數改成「d − 這個邊境離起點幾格」,選最小的。也就是邊境每離起點遠 1 格,就當作少走 1 步。平手規則同 B。 | 只看「邊境那一格」離起點多遠,看不到它後面還有多大一片霧。 |
| D. 最終版:寶藏划算度 | 把霧分給各邊境(分地盤),每格霧的分數=(離起點幾格 ÷(列數+欄數))⁶,加起來=寶藏分;選「寶藏分 ÷(d+2)」最大的,選定後走到它不再是邊境才重選;走超過步數上限的 5% 後改選 d 最小的(安全閥)。詳見 C3、C4。 | 計算比較多;優勢依賴「終點通常離起點很遠」這個從公開地圖看到的規律。 |
實際走一遍:四種走法在同一張地圖上的回放
結論:同一張地圖、同一個起點,四個機器人同時出發。按「播放」就能看到它們每一步怎麼走:誰一開始就走錯方向、誰一直走回頭路、誰直直走到終點。
這不是動畫示意,是四種走法的程式真的跑一遍記下來的每一步。
想像你在玩一個地圖被霧蓋住的遊戲。一開始整張地圖都是灰色的霧,只有你腳邊看得到。
你每走到一格,那一格的上下左右四格的霧就會散開。散開後才知道那裡是路還是牆。
邊境(黃色格子)=「霧的邊緣上、你走得到的路」。精確地說,一格要同時符合 4 個條件才是黃色格子:
1. 已經知道它是一般的路(不是牆、不是霧,也不是終點)。
2. 不是機器人現在站的那一格。
3. 從機器人現在的位置,只走「已知的路」走得到。
4. 它的上下左右,至少有一格在地圖裡面、而且還是霧。
只有走到黃色格子上,才能把更多霧吹散。
終點一定還藏在霧裡面。為什麼?因為四種走法都有同一條優先規則:只要旁邊摸到終點,就直接走進去。所以還在找的時候,終點一定還沒被摸到過,也就是還在霧裡。
所以要一直走到黃色格子去吹散霧,直到看見終點。
「走過去要幾步」一律用 BFS 算:只能走已知的路、要繞過牆,不是直線距離(BFS 怎麼算見 C1、C4)。
四種走法的差別,只有一件事:下一個要去哪一個黃色格子。下面每一種都講清楚四件事:看什麼、選哪個、平手怎麼辦、會不會中途改主意。
| 走法 | 每一步怎麼決定 | 平手怎麼辦 | 會不會中途改主意 |
|---|---|---|---|
| A. DFS (沒有虛線框) | DFS 記得兩樣東西:自己站過哪些格子,以及一路走來的路線(像一疊盤子,最新走到的格子放最上面)。它不記牆,也不找黃色格子。每一步: ① 旁邊有終點 → 走進去。 ② 照「上、下、左、右」的順序,找第一個「是路、而且自己還沒站過」的格子,走過去。 ③ 四格裡沒有任何一格「是路、而且還沒站過」,就叫死路。就算旁邊有路,只要都站過了,也算死路。這時退回「來這格之前站的那一格」(拿掉最上面的盤子),一次退一格,一直退到某一格還有沒站過的路。 | 不會平手:固定照上、下、左、右的順序 | 不適用(它沒有目標) |
| B. 最近邊境 | ① 用 BFS 算出走到每個黃色格子要幾步。 ② 選步數最少的那個,往它走一步。 | 先看:上一步選的目標如果也在平手名單裡,繼續選它。 否則:選最先被 BFS 找到的(BFS 先看「上一步走的方向」,再照上、下、左、右)。 | 會。每一步都重新算、重新挑 |
| C. 偏好遠處 | ① 用 BFS 算步數。 ② 每個黃色格子的分數=步數 − 它離起點幾格(離起點幾格=上下差幾格+左右差幾格)。 ③ 選分數最小的。意思是:黃色格子每離起點遠 1 格,就當作少走 1 步。 | 跟 B 一樣 | 會。每一步都重新挑 |
| D. 最終版 | ① 分地盤:每一格霧,歸「總距離最短」的那個黃色格子。總距離=機器人走到那個黃色格子的步數+從黃色格子穿過霧走到那格霧的格數(只能穿過霧)。一樣近時,歸 BFS 先找到的黃色格子。 ② 每一格霧的分數=(它離起點幾格 ÷(列數+欄數))的 6 次方;一個黃色格子地盤裡的分數加起來,叫寶藏分。 ③ 划算度=寶藏分 ÷(步數+2),選最大的。 ④ 安全閥:這張地圖已經走超過步數上限的 5%(10×10 是 20 步、15×15 是 45 步、20×20 是 80 步),需要重選目標時就改成選步數最少的。 | 安全閥啟動前:選最先被 BFS 找到的。這時比的是寶藏分,幾乎不會剛好平手。 安全閥啟動後:只比步數,平手很常見,一樣選最先被 BFS 找到的。 | 不會。選定後一直走向它,直到它不再是黃色格子(走到了,或它旁邊的霧在路上已經被看到)才重選。 安全閥不會取消已經選好的目標,只在要重選的那一刻起作用。 例外(四種走法都一樣):旁邊一摸到終點,就直接走進去。 |
建議看法:先選「公開地圖 01」,按「只看 D. 最終版」,然後一直按「下一步」。每按一次,地圖下面的解說框就會告訴你:這一步用了什麼資訊、怎麼算、算出來的數字、所以往哪邊走。解說裡的 ①②③ 對應地圖上標了 1、2、3 的黃色格子。看完 D,再換「只看 A」「只看 B」「只看 C」比較。
把滑鼠移到圖上,可以看到那一步四個機器人各自的數字;點一下就跳到那一步。圖上的虛線是現在播放到的步數。
公開地圖 01:空曠型 10×10:看哪裡
先停在第 0 步看。機器人站在起點 S,只看到身邊四格:上面是牆、左邊是地圖外面、下面和右邊是路。所以這時候黃色格子只有兩個:S 的下面一格、S 的右邊一格。
B、C 的虛線框在下面那格,D 的虛線框在右邊那格。這就是它們第一個不一樣的決定。
為什麼會這樣選:兩個黃色格子離機器人一樣近(都是 1 步)。C 的分數也一樣(兩格離起點都是 1 格,1 − 1 = 0)。
- B、C 平手。第 0 步沒有上一步、也沒有舊目標,所以選最先被 BFS 找到的。BFS 照上、下、左、右看:上面是牆,所以先找到下面那格。
- A(DFS)沒有平手的問題:它照上、下、左、右,走第一個「是路、而且沒站過」的格子。上面是牆,所以也往下。A 沒有虛線框,因為 DFS 不選目標。
- 終點在右上角,所以往下這一步是在遠離終點。
- D 算出來:右邊那格分到 77 格霧、寶藏分 1.9023、划算度 0.6341;下面那格分到 19 格霧、寶藏分 0.0895、划算度 0.0298。所以往右。(怎麼算的,見 C4。)
結果:D 一步回頭路都沒走,20 步到(最短 16 步)。B 32 步、C 24 步。A 走了 66 步,其中 12 步是回頭路。
「回頭路」的定義:走進一個之前已經站過的格子,那一步就算一次回頭路。
公開地圖 02:死路型 15×15:看哪裡
四個機器人走的路一模一樣,都是 45 步,剛好等於最短路線。
原因:這是死路型迷宮,只有一條正確的路。這張地圖上,四種走法在每個岔路口都剛好先選到正確的方向,所以沒有走進任何死路。這是運氣,換一張死路型迷宮就不一定了(C2 的模擬考裡,死路型 DFS 和最終版平均都約 91.9 分)。
這張地圖告訴你:不是每張地圖都分得出高下。
公開地圖 03:迴圈型 20×20:看哪裡
看 A(綠色)的路線。它一條路走到底,走不通就原路退回,路線上有很多來回重疊的線。238 步裡有 78 步是走回已經走過的格子。
C 和 D 前 17 步完全一樣,第 18 步才分開:C 走到 (9, 12),D 走到 (8, 11)。差別在打分數的方式:D 算的是「整片霧」的寶藏分,C 只看「邊境那一格離起點多遠」。最後 D 70 步到,C 90 步。
注意右邊的「離終點還有幾步」圖:A 的線上上下下好幾次,代表它常常走離終點越來越遠。
模擬地圖:空曠型 20×20(我產生的,不是老師的):看哪裡
這張是我產生的模擬地圖,不是老師的。我特地挑了一張「四種走法的排名跟平均一樣、但差距比較大」的地圖,讓差異看得清楚。平均情況的差距沒有這麼大(見上面的模擬考成績表)。
- 起點在最上面一排、偏左,終點在右下角。
- A、B、C 第一步都往左。原因:起點上面是地圖外面、下面 (1, 3) 是牆,只剩左右兩格,而且一樣近。照上、下、左、右的順序,先看到的是左邊。
- D 往右沿著最上面一排走,因為右邊和下面那一大片未知區域比較可能藏終點。
結果:D 37 步(最短 29 步),一步回頭路都沒走;A 353 步,其中 107 步是回頭路。
用表格看四張地圖的結果
| 地圖 | 走法 | 步數(最短) | 分數 | 回頭路步數 |
|---|---|---|---|---|
| 公開地圖 01:空曠型 10×10 | A. DFS | 66(16) | 84.85 | 12 |
| B. 最近邊境 | 32(16) | 90.0 | 3 | |
| C. 偏好遠處 | 24(16) | 93.33 | 2 | |
| D. 最終版 | 20(16) | 96.0 | 0 | |
| 公開地圖 02:死路型 15×15 | A. DFS | 45(45) | 100 | 0 |
| B. 最近邊境 | 45(45) | 100 | 0 | |
| C. 偏好遠處 | 45(45) | 100 | 0 | |
| D. 最終版 | 45(45) | 100 | 0 | |
| 公開地圖 03:迴圈型 20×20 | A. DFS | 238(26) | 82.18 | 78 |
| B. 最近邊境 | 116(26) | 84.48 | 11 | |
| C. 偏好遠處 | 90(26) | 85.78 | 15 | |
| D. 最終版 | 70(26) | 87.43 | 13 | |
| 模擬地圖:空曠型 20×20(我產生的,不是老師的) | A. DFS | 353(29) | 81.64 | 107 |
| B. 最近邊境 | 183(29) | 83.17 | 13 | |
| C. 偏好遠處 | 69(29) | 88.41 | 5 | |
| D. 最終版 | 37(29) | 95.68 | 0 |
所以對你的影響是:考試如果問「你的走法跟 DFS 差在哪」,可以用公開地圖 01 的第一步當例子:DFS 照固定順序往下走、離終點越來越遠;最終版會估計哪一邊的未知區域比較可能有終點,直接往那邊走。
為什麼要「偏好遠處」?——老師的地圖有規律
我分析了三張公開地圖:終點離起點的路程,比地圖上百分之幾的格子還遠?
| 公開地圖 | 類型 | 最短路線 | 終點比幾 % 的格子遠 |
|---|---|---|---|
| 「終點比幾 % 的格子遠」怎麼算:把從起點走得到的每一格,都用完整地圖算出「從起點走過去最少要幾步」。再數有幾 % 的格子步數比終點少。例如 99% 代表幾乎所有格子都比終點近,終點差不多是最遠的那格。實際數字是 98.8%、95.9%、83.5%,表裡四捨五入。 | |||
| public_map_01(10×10) | 空曠型 open | 16 步 | 99%(幾乎是最遠那格) |
| public_map_02(15×15) | 死路型 dead_ends | 45 步 | 96% |
| public_map_03(20×20) | 迴圈型 loops_dense | 26 步 | 84% |
三張都很遠。所以合理推測:老師出題時,故意把終點放在離起點遠的地方。這是推測,不是老師講的;所以我也測了「終點隨便放」的情況,確認最終版在那種情況下也不會變差。
公開地圖成績(老師給的 3 張)
| 走法 | 01:步數/分數 | 02:步數/分數 | 03:步數/分數 | 平均 |
|---|---|---|---|---|
| A. DFS | 66 / 84.85 | 45 / 100 | 238 / 82.18 | 89.01 |
| B. 最近邊境 | 32 / 90.00 | 45 / 100 | 116 / 84.48 | 91.49 |
| C. 偏好遠處 | 24 / 93.33 | 45 / 100 | 90 / 85.78 | 93.04 |
| D. 最終版 | 20 / 96.00 | 45 / 100 | 70 / 87.43 | 94.48 |
四種走法撞牆次數都是 0。02 號地圖四種都剛好走最短路(45 步),因為那是一個死路型迷宮,運氣好第一次就選對岔路。
模擬考成績(每種情況 2,700 張隨機地圖)
這張表用的是同一批地圖(亂數種子 777,三種大小 × 三種風格,每組 300 張)。C3 安全閥那張表、工作紀錄裡的數字,用的是別批地圖(種子 2026),所以數字會差一點點,不是算錯。
3 張公開地圖太少,運氣成分很大。所以我寫了一個地圖產生器,模仿老師的三種地圖風格,每種尺寸、每種風格各產 300 張,用老師的公式算分。
| 走法 | 終點偏遠(像老師的地圖) | 終點隨便放(保險測試) | 失敗張數 |
|---|---|---|---|
| A. DFS | 87.68 | 88.14 | 0 |
| B. 最近邊境 | 89.13 | 88.55 | 0 |
| C. 偏好遠處 | 90.45 | 88.79 | 0 |
| D. 最終版 | 91.24 | 88.78 | 0 |
怎麼讀這張表:
- 終點偏遠時(我們相信老師是這樣出題),最終版最好,比 DFS 多 3.5 分。
- 終點隨便放時,C 和 D 打平:88.79 對 88.78,只差 0.01 分。把同一批地圖逐張相減,差距的標準誤是 0.137,0.01 遠小於它,等於沒有差別。所以最終版「賭錯」也不會吃虧。
- 每一種都 0 失敗。
在「死路型」迷宮上,最終版(91.95)和 DFS(91.93)打平,沒有明顯優勢。原因是死路型迷宮只有一條正確路線,DFS「一條路走到底」的習慣剛好適合。最終版的優勢主要來自空曠型和迴圈型。(加安全閥之前,最終版在死路型是 91.24,略輸 DFS。)
C2b 四種走法,一步一步看
一句話結論
在同一個時刻,四種走法各自只看一樣東西:A 看「身邊哪一格還沒去過」、B 看「走過去要幾步」、C 看「黃色格子離起點多遠」、D 看「黃色格子後面那片霧值多少分」。所以 A、B、C 都往下,只有 D 往右。
這一頁只用一個時刻:公開地圖 01 的第 0 步。每種走法拆成小步驟,一個小步驟只講一件事,盡量配一張圖。
0 場景:機器人剛出發
| @ | ① | ||||||||
| ② | |||||||||
紅底 @=機器人 黑色=已知的牆 黃底 ①=右邊那個黃色格子 黃底 ②=下面那個黃色格子 灰色=霧(還沒看過)
- 機器人站在起點,只摸了身邊四格:上面是牆,左邊是地圖外面,右邊和下面是路。
- 右邊和下面那兩格,旁邊都還有霧,所以都是黃色格子(邊境)。叫它們 ①(右邊)和 ②(下面)。
- 其他 96 格全是霧。
四種走法要回答的問題都一樣:下一步往右(去 ①),還是往下(去 ②)?
A DFS:照順序,走第一個沒去過的格子
A-1 它只看什麼
只看身邊四格,以及「自己站過哪些格子」。它不看黃色格子,也不看霧。
A-2 照「上、下、左、右」一格一格問
| ✗ | |||||||||
| @ | ① | ||||||||
| ✓ | |||||||||
| 順序 | 那一格是什麼 | 能去嗎 |
|---|---|---|
| 1. 上 | 牆(圖上 ✗) | 不能 |
| 2. 下 | 路,而且還沒站過(圖上 ✓) | 可以 → 就是它,不用再往下問 |
| 3. 左、4. 右 | 沒有問到 | — |
A-3 結果:往下
DFS 不比較哪邊比較好,第一個能去的就去。右邊排在最後,所以根本沒被問到。
B 最近邊境:哪個黃色格子走過去最少步
B-1 算走到每個黃色格子要幾步
| @ | ①1 | ||||||||
| ②1 | |||||||||
用 BFS(從機器人出發、一圈一圈往外數,只走已知的路)算。① 和 ② 都在隔壁,都是 1 步(圖上黃色格子旁的小數字)。
B-2 比大小
① 1 步、② 1 步,一樣多,平手。
B-3 平手怎麼辦
選 BFS 先找到的那個。BFS 照「上、下、左、右」看四周,「下」比「右」先看到,所以先找到 ②。
B-4 結果:往下
C 偏好遠處:黃色格子離起點越遠越好
C-1 算每個黃色格子離起點幾格
| @ | ①1 | ||||||||
| ②1 | |||||||||
這裡的「離起點幾格」是直接數格子(上下差幾格+左右差幾格),不管中間有沒有牆。① 和 ② 都緊貼起點,都是 1 格。
C-2 算分數:走過去幾步 − 離起點幾格(越小越好)
| 走過去幾步 | 離起點幾格 | 分數 | |
|---|---|---|---|
| ① | 1 | 1 | 1 − 1 = 0 |
| ② | 1 | 1 | 1 − 1 = 0 |
一樣是 0,平手。
C-3 平手怎麼辦:跟 B 一樣,選 BFS 先找到的 ②
C-4 結果:往下
C 只看黃色格子「那一格本身」離起點多遠。① 和 ② 都貼著起點,所以一樣。
它看不到 ① 後面有一大片霧、② 後面只有一小片。D 就是為了補這個缺點。
D 最終版:黃色格子後面那片霧,值多少分
D 有四個小步驟。D-1 跟 B 一樣;D-2 是新的,也最容易卡住,所以拆得最細。
D-1 算走到每個黃色格子要幾步
跟 B-1 一樣:① 1 步、② 1 步。
D-2 分地盤:每一格霧歸哪個黃色格子
做法只有一句話:對每一格霧問同一個問題——「從 ① 走過去比較少步,還是從 ② 走過去比較少步?」哪邊少步,這格就歸哪邊。
數步數之前,先記住兩條規則:
- 霧全部當成路。機器人不知道霧裡哪裡是牆,只能先假設都走得過去。所以這是「估計」,不是真的走過。
- 只能穿過霧,不能穿過已知的格子。機器人站的格子、牆、黃色格子都不能穿。原因是:從機器人走到黃色格子那一段,已經用 BFS 算好了(就是「走過去幾步」);這裡只數「從黃色格子出發,往霧裡走」的那一段。
機器人知道地圖是 10×10(老師的程式一開始就告訴它),所以雖然看不到霧裡面,還是知道霧在哪幾格,可以在腦中數格子。
第 1 小步:挑一格霧,標成 ★
| ★ | |||||||||
| @ | ① | ||||||||
| ② | |||||||||
現在要問:★ 這一格,歸 ① 還是歸 ②?
第 2 小步:從 ① 走到 ★ 要幾步
| ● | ● | ★ | |||||||
| ● | |||||||||
| ● | |||||||||
| @ | ① | ||||||||
| ② | |||||||||
紅點是路線:從 ① 往上 3 格,再往右 2 格。紅點 4 個,加上 ★ 本身,從 ① 出發 5 步。
機器人走到 ① 本來就要 1 步,所以合計 1 + 5 = 6 步。
第 3 小步:從 ② 走到 ★ 要幾步
| ★ | |||||||||
| ● | |||||||||
| ● | |||||||||
| @ | ① | ● | |||||||
| ② | ● | ● | ● | ||||||
② 不能直接往上:上面是機器人站的格子,再上去是牆。所以只能先往右繞 3 格,再往上 4 格。藍點 6 個,加上 ★ 本身,從 ② 出發 7 步。
機器人走到 ② 也要 1 步,合計 1 + 7 = 8 步。
第 4 小步:比大小
| 機器人走到黃色格子 | 黃色格子走到 ★ | 合計 | |
|---|---|---|---|
| ① | 1 | 5 | 6 |
| ② | 1 | 7 | 8 |
6 比 8 少,所以 ★ 歸 ①,塗成 ① 的顏色(淡紅):
| ★ | |||||||||
| @ | ① | ||||||||
| ② | |||||||||
第 5 小步:如果一樣多步(平手)怎麼辦
換一格霧,標成 ☆。左圖是從 ① 走過去,右圖是從 ② 走過去:
| @ | ① | ● | ● | ● | |||||
| ② | ☆ | ||||||||
| @ | ① | ||||||||
| ② | ● | ● | ● | ☆ | |||||
兩邊都是 5 步,平手。規則是:平手歸「先處理」的那個黃色格子。程式先處理 ②,因為 BFS 照「上、下、左、右」的順序看四周,「下」排在「右」前面,所以 ② 比 ① 先被找到。
所以 ☆ 歸 ②。這條規則沒有特別的道理,只是平手時總要有人拿走這格。
第 6 小步:96 格霧全部照這樣問完
每一格霧都做一次第 2~5 小步,結果是這樣(淡紅=歸 ①,淡藍=歸 ②):
| @ | ① | ||||||||
| ② | |||||||||
| 哪一片 | 歸誰 | 為什麼 |
|---|---|---|
| 第 7 列(機器人那一列)和更上面 | ①,共 77 格 | ① 就在這一列,往上、往右都比較近。② 要先繞過機器人和牆,總是多 2 步以上。 |
| 最下面兩列 | ②,共 19 格 | ② 一樣近或更近。一樣近的格子(像 ☆)平手,歸先處理的 ②。 |
77 + 19 = 96,剛好是全部的霧。這就是「① 分到 77 格霧、② 分到 19 格霧」的意思。
這是程式真的算出來的結果(用 agent.py 的 _share_unknown_cells 重跑確認過),不是手畫的。
D-3 寶藏分:每一格霧值多少分
先講它在做什麼:猜「終點藏在這一格」的機會有多大。老師的公開地圖,終點都放得離起點很遠,所以離起點越遠的霧,分數越高。
每格霧的分數 = (這格離起點幾格 ÷ 20) 的 6 次方
(20 = 10 列 + 10 欄)
挑三格來算:
| 遠 | |||||||||
| ★ | |||||||||
| @ | ① | ||||||||
| ② | |||||||||
| 近 |
| 圖上的字 | 離起點幾格 | ÷ 20 | 6 次方(分數) |
|---|---|---|---|
| 遠(右上角) | 16 | 0.80 | 0.2621 |
| ★(剛剛那格) | 6 | 0.30 | 0.0007 |
| 近(左下角) | 2 | 0.10 | 0.000001(幾乎是 0) |
6 次方會把遠近的差距放大:右上角一格,抵得過幾百格靠近起點的霧。
每個黃色格子的寶藏分=它地盤裡每格霧的分數加起來:
| 地盤 | 寶藏分 | 為什麼差這麼多 | |
|---|---|---|---|
| ① | 77 格 | 1.9023 | 地盤包含右上那一大片遠方的霧 |
| ② | 19 格 | 0.0895 | 地盤只有最下面兩列,都在起點附近,每格幾乎 0 分 |
逐組相加的完整算式在 C4.1 步驟三。
D-4 划算度:寶藏分 ÷(走過去幾步 + 2),挑最大的
| 寶藏分 | 走過去幾步 + 2 | 划算度 | |
|---|---|---|---|
| ① | 1.9023 | 1 + 2 = 3 | 0.6341 ← 最大 |
| ② | 0.0895 | 1 + 2 = 3 | 0.0298 |
「+2」的用意:讓「很近的小角落」不要佔太多便宜(詳見 C3 步驟 5)。這一步兩邊都是 3,不影響結果。
D 的結果:往右
D 的兩條附加規則
附加規則一:承諾
選定 ① 之後,就一直走向 ①,直到它不再是黃色格子,才重新做 D-1~D-4。這次 ① 就在隔壁,走一步就到,下一步就會重新挑。
為什麼要這條:不加的話,機器人可能這一步想去 ①、下一步又想去 ②,來回踱步把步數用光(詳見 C3 步驟 6)。
附加規則二:安全閥
走超過 20 步(10×10 地圖步數上限 400 的 5%)之後,需要重新挑時,D-2~D-4 都不做了,改成跟 B 一樣「選最近的」。第 0 步還沒到 20 步,所以這次不啟動(詳見 C3)。
四種走法放在一起
| 走法 | 只看什麼 | ① 右邊 | ② 下面 | 結果 |
|---|---|---|---|---|
| A. DFS | 上、下、左、右的順序,哪格沒去過 | 排第 4,沒問到 | 排第 2,可以去 | 往下 |
| B. 最近邊境 | 走過去幾步(越小越好) | 1 | 1 | 平手 → 往下 |
| C. 偏好遠處 | 步數 − 離起點格數(越小越好) | 0 | 0 | 平手 → 往下 |
| D. 最終版 | 寶藏分 ÷(步數+2)(越大越好) | 0.6341 | 0.0298 | 往右 |
所以對你的影響是:考試如果問「C 跟 D 差在哪」,就用這個例子回答:C 只看黃色格子那一格離起點多遠,兩格都是 1 格,分不出來;D 看黃色格子後面整片霧,77 格對 19 格,分得出來。
想看黃色格子變多時(第 12 步,有 14 個黃色格子)四種走法怎麼選,見 C4.2。想看四個機器人實際怎麼走,見 C2 的回放。
C3 最終版的想法
結論:把每個邊境想成一扇門,門後面有一片未知的「地盤」。地盤裡離起點越遠的格子越可能藏著終點。算出每扇門的「寶藏分 ÷ 走過去的步數」,挑最划算的門走過去,而且選定了就一直走向它,直到它不再是邊境才重新挑。
七個步驟
每一步都從步驟 1 開始。步驟 2 如果發現終點,就直接走進去,後面都不做。否則做 3、6、7;步驟 4、5 只在「需要重新挑目標」時才做。「需要重新挑」的意思是:還沒有目標(剛出發),或舊目標已經不是邊境了。程式實際執行的順序是 1 → 2 → 3 → 6 →(需要時才做 4 → 5)→ 7。
| # | 步驟 | 白話 |
|---|---|---|
| 1 | 記憶 | 把這次摸到的四格寫進筆記本。 |
| 2 | 看終點 | 旁邊有 GOAL?直接走進去,結束。 |
| 3 | 找邊境 | 用 BFS 從機器人現在的位置出發,算出走到每一格「已知的路」要幾步(只算走得到的),再從裡面挑出符合 4 個條件的邊境(見 C1)。 |
| 4 | 分地盤 | 把未知格分給各個邊境:誰最快到得了,就歸誰。(被已知的牆完全圍住、從任何邊境都碰不到的未知格,不會被分到。) |
| 5 | 算划算度 | 每個邊境的「寶藏分」÷(步數 + 2),挑最高的。安全閥:如果已經走超過步數上限的 5%,就不算划算度,直接挑最近的邊境。 |
| 6 | 承諾 | 如果上一步選的目標還是邊境,就繼續走向它,不重新挑。目標不再是邊境(走到了,或者它旁邊的未知格在路上已經被摸到了),才回到步驟 4、5 重新挑。 |
| 7 | 走一步 | 沿著 BFS 的最短路線走第一步。最短路線不只一條時,BFS 先看「上一步走的方向」,所以會優先繼續直走。 |
步驟 4:分地盤
先講它在做什麼:決定每一個未知格子「屬於哪一扇門」。
想像每個邊境同時派出一隊人,往未知區域擴散。可是每隊的出發時間不一樣:機器人走到那個邊境要 d 步,那隊就晚 d 步出發。
每個未知格子,被哪一隊先碰到,就歸那一隊。兩隊在同一圈碰到同一格時,歸 BFS 順序在前的那個邊境。隊伍只能穿過未知格子,不能穿過已知的路或牆。
這樣一來,每扇門後面就有一塊自己的地盤。這種做法叫「多起點 BFS」(multi-source BFS),同時從好幾個起點一起擴散。
看不懂的話,先看 C2b 的 D-2:用一格霧當例子,從 ①、② 各畫一條路線數步數,一張圖一個小步驟。
步驟 5:寶藏分怎麼算
每個未知格子都有一個「藏著終點的可能性分數」:
注意:這裡的「起點」是這張地圖一開始的出發格(reset() 給的 start_position),不是機器人現在站的位置。
遠近 = 這格離起點的曼哈頓距離 ÷ (地圖列數 + 地圖欄數)
分數 = 遠近 ** 6 (遠近的 6 次方)
曼哈頓距離(Manhattan distance)就是「只能橫著走、直著走」時的格數:上下差幾格+左右差幾格。像在棋盤街道的城市裡走路。
為什麼要 6 次方?因為次方會把「遠」和「近」的差距放大。在 10×10 地圖上:
| 離起點 | 遠近 | 6 次方(寶藏分) |
|---|---|---|
| 18 格(很遠) | 0.9 | 0.53 |
| 14 格 | 0.7 | 0.12 |
| 10 格(中間) | 0.5 | 0.016 |
| 4 格(很近) | 0.2 | 0.00006 |
所以一個很遠的格子,價值是中間格子的 30 倍以上。意思是:「近的地方幾乎不可能藏終點,遠的地方才值得去看」。
一扇門的寶藏分 = 它地盤裡所有未知格的分數加起來。
6 這個數字是我用模擬考試出來的。在 900 張「終點偏遠」的地圖上(有承諾、分母加 2),次方 2、4、6、8、10 的平均分分別是 90.52、91.01、91.31、91.21、91.15,6 最高。次方 0(等於不看遠近)只在還沒加承諾的版本試過,是 89.02。
步驟 5(續):划算度
划算度 = 寶藏分 ÷ (走過去的步數 + 2)
舉例(這兩行是假設的數字,用來說明公式;真實數字見 C4):
| 邊境 | 寶藏分 | 步數 | 划算度 |
|---|---|---|---|
| A(很近,但後面地盤小、又靠近起點) | 0.9 | 3 | 0.9 ÷ 5 = 0.18 |
| B(比較遠,但後面一大片遠方未知) | 4.0 | 10 | 4.0 ÷ 12 = 0.33 ← 選這個 |
為什麼要「+2」?它的作用是縮小「近處邊境」的優勢。不加的話,步數 1 和步數 2 的邊境,划算度差 2 倍,Agent 會很容易被旁邊的小角落吸走;加 2 之後變成「3 比 4」,只差約 1.33 倍,遠方的大片未知區比較有機會勝出。(步數最少是 1,不會除以 0。)2 這個數字是模擬考試出來的:加 1 和加 2 幾乎一樣(終點偏遠 91.33 對 91.31、終點隨機 88.74 對 88.76),加 3 是 91.16,所以選 2。(加 6 只在還沒加承諾的版本試過,比較差。)
步驟 5 的安全閥:走太久就改成「先去最近的」
先講它在做什麼:程式自己數已經走了幾步。超過步數上限的 5% 之後,需要重選目標時,就不再「偏好遠處」,改成「先去離自己最近的邊境」。
精確地說:程式數的是「這張地圖 act() 被呼叫了幾次(包含這一次)」。這個數字大於 0.05 × 4 × 列數 × 欄數,安全閥才啟動。所以 10×10 地圖是從第 21 次呼叫開始。它只在需要重選目標的那一刻起作用,不會取消已經選好的目標。
在 3 張公開地圖和回放的 4 張地圖上,最終版都在安全閥啟動前就走到終點了(公開地圖:20/45/70 步,門檻 20/45/80 步)。
| 地圖大小 | 步數上限 | 安全閥在第幾步後啟動 |
|---|---|---|
| 10×10 | 400 | 20 步 |
| 15×15 | 900 | 45 步 |
| 20×20 | 1,600 | 80 步 |
為什麼要加:獨立審查員專門造了一張刁鑽地圖,讓舊版程式走了 812 步(上限 900),只差 88 步就 0 分。原因是探索快結束時,只剩一些零散的小角落沒看過,舊版照「離起點多遠」決定先去哪個,結果在地圖兩端來回跑。
為什麼不會少分:「偏好遠處」真正有用的時候,是一開始決定往哪個大方向走。走了一陣子之後:
- 如果終點真的在遠處,前面那段「往遠處走」已經幫上忙了。
- 如果還沒找到,代表已經走了很多冤枉路,效率分本來就剩不多了。這時候最重要的是「不要步數用完」,所以改走保守的「最近優先」。
實際數字:同一批地圖上,沒有安全閥 91.10/88.86,安全閥 5% 是 91.18/88.96,沒有變差。
實驗結果(試過 5%、7.5%、10%、15%、20%、25%、30%、40%,用三批不同的地圖確認):
| 版本 | 最刁鑽地圖最多用掉步數 | 模擬地圖平均(終點遠/隨機) |
|---|---|---|
| 沒有安全閥(舊版) | 90.2% | 91.10/88.86 |
| 安全閥 25% | 50.3% | 91.08/88.99 |
| 安全閥 10% | 35.5% | 91.16/88.90 |
| 安全閥 5%(採用) | 29.7% | 91.18/88.96 |
平均分那一欄是同一批 2,700+2,700 張地圖(跟調參數用的同一個亂數種子 2026,但每組 300 張,比調參數時的 100 張多)。另外兩批地圖(種子 777、4242)的結論相同:安全閥越早啟動越安全,平均分不會變差。
這個做法完全合規:程式只用到 reset() 給的地圖大小,加上自己數的步數,沒有偷看任何地圖資訊。
步驟 6:承諾——這一步是「不會失敗」的關鍵
一開始我沒有這一步。結果在 900+900 張模擬地圖上,依參數不同,有 0~11 張失敗了(0 分)。
原因是「三心二意」:Agent 每走一步,地盤就重新分一次,划算度也跟著變。它可能這一步覺得 A 門好、往 A 走一步;下一步又覺得 B 門好、往回走一步⋯⋯就這樣在兩扇門之間來回踱步,把步數用光。
解法:一旦選定目標,就一直走向它,直到它不再是邊境(走到了,或它旁邊的未知格已經被摸到)才重新挑。加上之後,5,400 張壓力測試 0 失敗。平均分只差一點:以次方 4 那組為例,終點偏遠 91.08 → 91.01(少 0.07),終點隨機 88.76 → 88.80(多 0.04)。用一點點分數,換到「不會失敗」,很划算。
這是 utility-based agent(效用導向的 Agent):它不只問「能不能到終點」,而是對每個選擇算一個分數(效用),挑最高的。效用=「估計的收穫(寶藏分:越遠的未知格分數越高,這是啟發式估計,不是真的機率)」÷「代價(步數 + 2)」。
C4 用真實數字算一遍:BFS、寶藏分、四種走法怎麼選
結論:四種走法每一步都在做同一件事:從黃色格子(邊境)裡挑一個去。差別只在「打分數的公式」。這一節拿公開地圖 01 的兩個真實時刻,把每一個數字算給你看。這些數字是程式實際算出來的,不是我編的。
下面的數字都是「最終版(D)真的走到那一步時」的狀態。為了比較,我問的是:如果 A、B、C 站在同一個位置、知道一樣多的東西,它們會選哪一個?
(在回放裡,A、B、C 自己走的路不一樣,所以實際上不會站在同一個位置。這裡是為了讓你在同一個畫面上比較四種公式。)
C4.1 第一個例子:第 0 步,剛出發
這就是回放裡「大家分道揚鑣」的那一刻。機器人站在起點(7, 0),只摸了一次四周:
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | ||||||||||
| 1 | ||||||||||
| 2 | ||||||||||
| 3 | ||||||||||
| 4 | ||||||||||
| 5 | ||||||||||
| 6 | ||||||||||
| 7 | @ | B | ||||||||
| 8 | A | |||||||||
| 9 |
@ 機器人 黑色=已知的牆 黃色字母=邊境 灰色=霧(還不知道)
- 上面 (6, 0) 是牆;左邊是地圖外面(不在圖上)。
- 下面 (8, 0)、右邊 (7, 1) 是路,而且它們旁邊都還有霧,所以它們是邊境,叫 A、B。
- 其他 96 格全部是霧。
步驟一:BFS 算「走過去要幾步」
這一步很簡單:A、B 都在隔壁,都是 1 步。BFS 詳細怎麼跑,第二個例子(C4.2)再講,因為那時候格子比較多。
步驟二:分地盤——「分到 77 格霧」是什麼意思
回放的解說框用 ① ② 表示(照划算度排名):① = 右邊那格 (7, 1),② = 下面那格 (8, 0)。
這一節的表格用字母表示(照 BFS 找到的順序):A = 下面那格 (8, 0),B = 右邊那格 (7, 1)。
所以:① = B,② = A。下面的分輪圖用 ① ② 標,跟回放一致。
做法只有一句話:對每一格霧問同一個問題——「從 ① 走過去比較少步,還是從 ② 走過去比較少步?」哪邊少步,這格就歸哪邊。
數步數之前,先記住兩條規則:
- 霧全部當成路。機器人不知道霧裡哪裡是牆,只能先假設都走得過去。所以這是「估計」,不是真的走過。
- 只能穿過霧,不能穿過已知的格子。機器人站的格子、牆、黃色格子都不能穿。原因是:從機器人走到黃色格子那一段,已經用 BFS 算好了(就是「走過去幾步」);這裡只數「從黃色格子出發,往霧裡走」的那一段。
機器人知道地圖是 10×10(老師的程式一開始就告訴它),所以雖然看不到霧裡面,還是知道霧在哪幾格,可以在腦中數格子。
第 1 小步:挑一格霧,標成 ★
| ★ | |||||||||
| @ | ① | ||||||||
| ② | |||||||||
現在要問:★ 這一格,歸 ① 還是歸 ②?
第 2 小步:從 ① 走到 ★ 要幾步
| ● | ● | ★ | |||||||
| ● | |||||||||
| ● | |||||||||
| @ | ① | ||||||||
| ② | |||||||||
紅點是路線:從 ① 往上 3 格,再往右 2 格。紅點 4 個,加上 ★ 本身,從 ① 出發 5 步。
機器人走到 ① 本來就要 1 步,所以合計 1 + 5 = 6 步。
第 3 小步:從 ② 走到 ★ 要幾步
| ★ | |||||||||
| ● | |||||||||
| ● | |||||||||
| @ | ① | ● | |||||||
| ② | ● | ● | ● | ||||||
② 不能直接往上:上面是機器人站的格子,再上去是牆。所以只能先往右繞 3 格,再往上 4 格。藍點 6 個,加上 ★ 本身,從 ② 出發 7 步。
機器人走到 ② 也要 1 步,合計 1 + 7 = 8 步。
第 4 小步:比大小
| 機器人走到黃色格子 | 黃色格子走到 ★ | 合計 | |
|---|---|---|---|
| ① | 1 | 5 | 6 |
| ② | 1 | 7 | 8 |
6 比 8 少,所以 ★ 歸 ①,塗成 ① 的顏色(淡紅):
| ★ | |||||||||
| @ | ① | ||||||||
| ② | |||||||||
第 5 小步:如果一樣多步(平手)怎麼辦
換一格霧,標成 ☆。左圖是從 ① 走過去,右圖是從 ② 走過去:
| @ | ① | ● | ● | ● | |||||
| ② | ☆ | ||||||||
| @ | ① | ||||||||
| ② | ● | ● | ● | ☆ | |||||
兩邊都是 5 步,平手。規則是:平手歸「先處理」的那個黃色格子。程式先處理 ②,因為 BFS 照「上、下、左、右」的順序看四周,「下」排在「右」前面,所以 ② 比 ① 先被找到。
所以 ☆ 歸 ②。這條規則沒有特別的道理,只是平手時總要有人拿走這格。
第 6 小步:96 格霧全部照這樣問完
每一格霧都做一次第 2~5 小步,結果是這樣(淡紅=歸 ①,淡藍=歸 ②):
| @ | ① | ||||||||
| ② | |||||||||
| 哪一片 | 歸誰 | 為什麼 |
|---|---|---|
| 第 7 列(機器人那一列)和更上面 | ①,共 77 格 | ① 就在這一列,往上、往右都比較近。② 要先繞過機器人和牆,總是多 2 步以上。 |
| 最下面兩列 | ②,共 19 格 | ② 一樣近或更近。一樣近的格子(像 ☆)平手,歸先處理的 ②。 |
77 + 19 = 96,剛好是全部的霧。這就是「① 分到 77 格霧、② 分到 19 格霧」的意思。
這是程式真的算出來的結果(用 agent.py 的 _share_unknown_cells 重跑確認過),不是手畫的。
進階:程式實際上不是一格一格問,而是「兩隊同時擴散」(結果一樣)
一格一格問太慢,程式用的是「多起點 BFS」:兩個黃色格子同時派一隊人往霧裡擴散,一格霧被誰先踩到就歸誰。踩到的先後,剛好就是上面算的步數多少,所以結果跟一格一格問完全一樣。
規則(照程式 _share_unknown_cells 的做法):
- 每個黃色格子派一隊人。出發時間=機器人走到那個黃色格子要幾步。① 和 ② 都是 1 步,所以同時出發。
- 每一輪,每一隊從「自己已經佔領的格子」往上下左右擴一格,佔領還沒被佔、而且是霧的格子。
- 只能走進霧,不能走進已知的牆或已知的路。
- 同一輪兩隊搶同一格:先處理的那隊先搶到。先處理的是 BFS 先找到的黃色格子,也就是 ②(下面那格;因為 BFS 照上、下、左、右看,「下」比「右」先)。
- 一直擴散,直到所有霧都被佔領。
實際過程(紅底的 ①=被 ① 佔領的霧,藍底的 ②=被 ② 佔領的霧,灰色=還沒被佔的霧,黃底=黃色格子本身,紅色 @=機器人,黑色=已知的牆):
| ① | |||||||||
| @ | ① | ① | |||||||
| ② | ② | ||||||||
| ② |
| ① | |||||||||
| ① | ① | ||||||||
| @ | ① | ① | ① | ||||||
| ② | ② | ② | |||||||
| ② | ② |
| ① | |||||||||
| ① | ① | ① | |||||||
| ① | ① | ① | |||||||
| @ | ① | ① | ① | ① | |||||
| ② | ② | ② | ② | ||||||
| ② | ② | ② |
| ① | ① | ① | ① | ① | ① | ① | ① | ① | ① |
| ① | ① | ① | ① | ① | ① | ① | ① | ① | ① |
| ① | ① | ① | ① | ① | ① | ① | ① | ① | ① |
| ① | ① | ① | ① | ① | ① | ① | ① | ① | ① |
| ① | ① | ① | ① | ① | ① | ① | ① | ① | ① |
| ① | ① | ① | ① | ① | ① | ① | ① | ① | ① |
| ① | ① | ① | ① | ① | ① | ① | ① | ① | |
| @ | ① | ① | ① | ① | ① | ① | ① | ① | ① |
| ② | ② | ② | ② | ② | ② | ② | ② | ② | ② |
| ② | ② | ② | ② | ② | ② | ② | ② | ② | ② |
看第 1 輪:① 在右邊那格,往上佔了 (6, 1)、往右佔了 (7, 2)。② 在下面那格,往下佔了 (9, 0)、往右佔了 (8, 1)。
(8, 1) 其實 ① 往下也碰得到,兩隊同一輪搶同一格。但 ② 先處理,所以 (8, 1) 歸 ②。
為什麼最後 ② 只拿到最下面兩列:
- 第 8 列的每一格,① 和 ② 都在同一輪碰到,② 先處理,所以全部歸 ②。
- 第 9 列在第 8 列下面,從 ② 那邊過去比較近,也歸 ②。
- 第 7 列以上,從 ① 出發都比較近,全部歸 ①。
所以 ② 拿到第 8 列 9 格+第 9 列 10 格=19 格,① 拿到其餘 77 格。合計 96 格,就是全部的霧。
機器人不知道霧裡哪裡是牆,所以分地盤時把霧都當成走得過去。實際上,這 96 格霧裡有 14 格其實是牆(① 的地盤裡有 9 格、② 的有 5 格)。
所以「分到 77 格」不是說 77 格都能走,而是「以目前知道的資訊,這 77 格從 ① 過去比較快」。等機器人走過去、看到更多,下一次重選時就會用新的地圖重新分。
步驟三:算寶藏分——每一格霧值多少分
先講它在做什麼:猜「終點在這一格」的可能性有多高。我們觀察到老師的終點都放得離起點很遠,所以:離起點越遠的霧,分數越高。
每一格霧的分數這樣算:
遠近 = 這格離起點的格數(曼哈頓距離) ÷ (列數 + 欄數)
分數 = 遠近 ** 6 (遠近乘自己 6 次)
這張地圖 10 列 10 欄,所以除以 20。舉三格當例子:
| 格子 | 離起點 (7, 0) 幾格 | 遠近 | 分數 = 遠近⁶ |
|---|---|---|---|
| 右上角 (0, 9) 剛好就是終點,但機器人還不知道 | 7 + 9 = 16 | 16 ÷ 20 = 0.80 | 0.80 × 0.80 × 0.80 × 0.80 × 0.80 × 0.80 = 0.2621 |
| 中間 (3, 5) | 4 + 5 = 9 | 9 ÷ 20 = 0.45 | 0.0083 |
| 左下角 (9, 0) | 2 + 0 = 2 | 2 ÷ 20 = 0.10 | 0.000001(幾乎是 0) |
右上角一格的分數,是中間那格的 30 倍以上。這就是「6 次方」的效果:把遠和近的差距放大。
一個邊境的寶藏分=它地盤裡所有霧的分數加起來。下面這張表把霧按「離起點幾格」分組,一組一組加:
| 離起點幾格 | 遠近 | 每格分數 | A 有幾格 | A 小計 | B 有幾格 | B 小計 |
|---|---|---|---|---|---|---|
| 2 | 2/20 = 0.10 | 0.0000 | 2 | 0.0000 | 3 | 0.0000 |
| 3 | 3/20 = 0.15 | 0.0000 | 2 | 0.0000 | 4 | 0.0000 |
| 4 | 4/20 = 0.20 | 0.0001 | 2 | 0.0001 | 5 | 0.0003 |
| 5 | 5/20 = 0.25 | 0.0002 | 2 | 0.0005 | 6 | 0.0015 |
| 6 | 6/20 = 0.30 | 0.0007 | 2 | 0.0015 | 7 | 0.0051 |
| 7 | 7/20 = 0.35 | 0.0018 | 2 | 0.0037 | 8 | 0.0147 |
| 8 | 8/20 = 0.40 | 0.0041 | 2 | 0.0082 | 8 | 0.0328 |
| 9 | 9/20 = 0.45 | 0.0083 | 2 | 0.0166 | 8 | 0.0664 |
| 10 | 10/20 = 0.50 | 0.0156 | 2 | 0.0312 | 7 | 0.1094 |
| 11 | 11/20 = 0.55 | 0.0277 | 1 | 0.0277 | 6 | 0.1661 |
| 12 | 12/20 = 0.60 | 0.0467 | 0 | 0.0000 | 5 | 0.2333 |
| 13 | 13/20 = 0.65 | 0.0754 | 0 | 0.0000 | 4 | 0.3017 |
| 14 | 14/20 = 0.70 | 0.1176 | 0 | 0.0000 | 3 | 0.3529 |
| 15 | 15/20 = 0.75 | 0.1780 | 0 | 0.0000 | 2 | 0.3560 |
| 16 | 16/20 = 0.80 | 0.2621 | 0 | 0.0000 | 1 | 0.2621 |
| 加總(寶藏分) | 19 | 0.0895 | 77 | 1.9023 | ||
A 的地盤都在起點附近(離起點最多 11 格),每格分數都很小,加起來只有 0.0895。B 的地盤包含很多遠方的霧,加起來 1.9023,是 A 的 21 倍。
步驟四:算划算度,挑最高的
划算度 = 寶藏分 ÷ (走過去幾步 + 2)
A(下面):0.0895 ÷ (1 + 2) = 0.0298
B(右邊):1.9023 ÷ (1 + 2) = 0.6341 ← 最高,D 選它
同一個時刻,四種走法各選哪個
| 邊境 | 位置 | 走過去幾步 d (BFS 算的) | B 的分數 = d 越小越好 | 離起點幾格 (曼哈頓) | C 的分數 = d − 離起點格數 越小越好 | D:分到幾格未知 | D:寶藏分 | D 的划算度 = 寶藏分 ÷ (d + 2) 越大越好 |
|---|---|---|---|---|---|---|---|---|
| A B 選C 選 | (8, 0) | 1 | 1 | 1 | 0 | 19 | 0.0895 | 0.0298 |
| B D 選 | (7, 1) | 1 | 1 | 1 | 0 | 77 | 1.9023 | 0.6341 |
| 走法 | 怎麼選 | 這一步的計算 | 結果 |
|---|---|---|---|
| A. DFS | 不看邊境。照「上、下、左、右」的順序,挑身邊第一個沒去過的路 | 上是牆 → 下是路、沒去過 → 就是它 | 往下 |
| B. 最近邊境 | 挑「走過去幾步 d」最小的邊境 | A、B 都是 1 步,平手 → 選先被 BFS 找到的 A | 往下 |
| C. 偏好遠處 | 挑「d − 離起點格數」最小的邊境(每離起點遠 1 格,就當作少走 1 步) | A:1 − 1 = 0;B:1 − 1 = 0,平手 → 選 A | 往下 |
| D. 最終版 | 挑划算度最高的邊境 | A:0.0298;B:0.6341 | 往右 |
規則有兩層:
① B、C:如果上一步選的目標也在平手名單裡,就繼續選它(避免左右搖擺)。
② 不然的話,誰先被 BFS 找到,就選誰。BFS 看四周有固定順序:先看「上一步走的方向」(等於繼續直走),再照「上、下、左、右」看其他方向。
第 0 步還沒走過,沒有「上一步」,也沒有舊目標,所以只用第 ② 層,順序就是上、下、左、右:上面是牆,接著看「下」,下面那格先被找到。所以 B、C 選下面。
補充三點:
・離機器人 1 步的邊境,最多 4 個(剛出發、四周都是路的時候)。走了之後一定最多 3 個:走來的那一格機器人站過,站過的格子四周都摸過了,不可能是邊境。
・「隔壁是路」不等於「隔壁是邊境」:那格旁邊還有霧,才算邊境。
・D 也可能平手,規則是「選最先被 BFS 找到的」。安全閥啟動前,它比的是「整片霧的分數」,幾乎不會剛好一樣;安全閥啟動後只比步數,就常常平手。
重點:C 也想「往遠處走」,但它只看邊境那一格本身離起點多遠。下面那格和右邊那格離起點都是 1 格,所以 C 分不出差別。D 看的是邊境後面整片霧,所以看得出右邊那一大片比較值得去。
C4.2 第二個例子:第 12 步,邊境變多了
最終版已經走了 12 步:沿著第 7 列往右走到底,再往上一格、往左兩格,現在站在 (6, 7)。地圖上有 14 個邊境。
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | ||||||||||
| 1 | ||||||||||
| 2 | ||||||||||
| 3 | ||||||||||
| 4 | ||||||||||
| 5 | B | |||||||||
| 6 | K | I | G | E | C | A | @ | |||
| 7 | ||||||||||
| 8 | N | M | L | J | F | D | H | |||
| 9 |
BFS 是怎麼跑的
先講它在做什麼:從機器人現在的位置出發,算出走到每一格「已知的路」最少要幾步,順便記下「第一步要往哪走」。它只走已知是路的格子,不走霧、不走牆。
做法像排隊領號碼:
- 機器人自己先排進隊伍,距離 0。
- 從隊伍最前面叫一個人出來,看他上下左右四格。
- 是已知的路、而且還沒算過的,就給它「出來的人的距離 + 1」,排到隊伍最後面。
- 重複,直到隊伍空了。
因為「先排的先處理」,距離 1 的全部處理完,才會輪到距離 2 的。所以每一格第一次拿到的號碼,就是最短距離。
看四周的順序:上一步是往左走的,所以先看「左」,再看上、下、右。這樣兩條路一樣短的時候,會優先繼續直走。
實際的前 8 輪:
| 輪 | 叫出來的格子 | 新加進隊伍的格子 | 隊伍現在長這樣 |
|---|---|---|---|
| 1 | (6, 7)(0 步) | (6, 6)(1 步,第一步往左)、(5, 7)(1 步,第一步往上)、(7, 7)(1 步,第一步往下)、(6, 8)(1 步,第一步往右) | (6, 6)、(5, 7)、(7, 7)、(6, 8) |
| 2 | (6, 6)(1 步) | (6, 5)(2 步,第一步往左)、(7, 6)(2 步,第一步往左) | (5, 7)、(7, 7)、(6, 8)、(6, 5)、(7, 6) |
| 3 | (5, 7)(1 步) | 沒有新格子(旁邊都是牆、霧或算過的) | (7, 7)、(6, 8)、(6, 5)、(7, 6) |
| 4 | (7, 7)(1 步) | (8, 7)(2 步,第一步往下)、(7, 8)(2 步,第一步往下) | (6, 8)、(6, 5)、(7, 6)、(8, 7)、(7, 8) |
| 5 | (6, 8)(1 步) | (6, 9)(2 步,第一步往右) | (6, 5)、(7, 6)、(8, 7)、(7, 8)、(6, 9) |
| 6 | (6, 5)(2 步) | (6, 4)(3 步,第一步往左)、(7, 5)(3 步,第一步往左) | (7, 6)、(8, 7)、(7, 8)、(6, 9)、(6, 4)、(7, 5) |
| 7 | (7, 6)(2 步) | (8, 6)(3 步,第一步往左) | (8, 7)、(7, 8)、(6, 9)、(6, 4)、(7, 5)、(8, 6) |
| 8 | (8, 7)(2 步) | 沒有新格子(旁邊都是牆、霧或算過的) | (7, 8)、(6, 9)、(6, 4)、(7, 5)、(8, 6) |
一直做到隊伍空了,全部的結果(數字=走過去要幾步;字母旁的小數字=那個邊境要幾步):
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | ||||||||||
| 1 | ||||||||||
| 2 | ||||||||||
| 3 | ||||||||||
| 4 | ||||||||||
| 5 | B1 | |||||||||
| 6 | K6 | I5 | G4 | E3 | C2 | A1 | @ | 1 | 2 | |
| 7 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 2 | 3 |
| 8 | N9 | M7 | L6 | J5 | F3 | D2 | H4 | |||
| 9 |
四種走法各選哪個
| 邊境 | 位置 | 走過去幾步 d (BFS 算的) | B 的分數 = d 越小越好 | 離起點幾格 (曼哈頓) | C 的分數 = d − 離起點格數 越小越好 | D:分到幾格未知 | D:寶藏分 | D 的划算度 = 寶藏分 ÷ (d + 2) 越大越好 |
|---|---|---|---|---|---|---|---|---|
| A B 選 | (6, 6) | 1 | 1 | 7 | -6 | 6 | 0.1778 | 0.0593 |
| B C 選D 選 | (5, 7) | 1 | 1 | 9 | -8 | 15 | 1.4083 | 0.4694 |
| C | (6, 5) | 2 | 2 | 6 | -4 | 6 | 0.1042 | 0.0260 |
| D | (8, 7) | 2 | 2 | 8 | -6 | 2 | 0.0239 | 0.0060 |
| E | (6, 4) | 3 | 3 | 5 | -2 | 6 | 0.0583 | 0.0117 |
| F | (8, 6) | 3 | 3 | 7 | -4 | 2 | 0.0059 | 0.0012 |
| G | (6, 3) | 4 | 4 | 4 | 0 | 6 | 0.0308 | 0.0051 |
| H | (8, 9) | 4 | 4 | 10 | -6 | 1 | 0.0277 | 0.0046 |
| I | (6, 2) | 5 | 5 | 3 | 2 | 6 | 0.0153 | 0.0022 |
| J | (8, 4) | 5 | 5 | 5 | 0 | 1 | 0.0007 | 0.0001 |
| K | (6, 1) | 6 | 6 | 2 | 4 | 12 | 0.0099 | 0.0012 |
| L | (8, 3) | 6 | 6 | 4 | 2 | 1 | 0.0002 | 0.0000 |
| M | (8, 2) | 7 | 7 | 3 | 4 | 2 | 0.0001 | 0.0000 |
| N | (8, 0) | 9 | 9 | 1 | 8 | 1 | 0.0000 | 0.0000 |
| 走法 | 這一步的計算 | 結果 |
|---|---|---|
| A. DFS | 上面 (5, 7) 是路、沒去過 → 就是它 | 往上 |
| B. 最近邊境 | 最小的 d 是 1,有兩個:A、B。平手 → 這裡假設沒有舊目標,所以選先被 BFS 找到的 A (6, 6)(上一步往左,BFS 先看左邊) | 往左(往回走,朝起點的方向) |
| C. 偏好遠處 | 「d − 離起點格數」最小的是 B:1 − 9 = -8 | 往上 |
| D. 最終版 | 划算度最高的是 B:分到 15 格霧,寶藏分 1.4083,÷ (1 + 2) = 0.4694 | 往上 |
重點:
- B 只看「近不近」,A 和 B 一樣近,它照順序挑了 A,往回走:離終點的真實步數從 8 步變成 9 步(機器人自己不知道這件事)。
- C 和 D 這次選一樣,但理由不同:C 是因為「B 這一格離起點比較遠」;D 是因為「B 後面那片霧最大、最遠」(它的地盤是 15 格,旁邊 A 只有 6 格)。
- 這時候已經走了 12 步,還沒超過 20 步(10×10 地圖步數上限 400 的 5%),所以安全閥還沒啟動,D 還在用划算度。
C4.3 四個公式放在一起
| 走法 | 看什麼 | 公式 | 選 | 缺點 |
|---|---|---|---|---|
| A. DFS | 身邊四格+自己站過哪些格子+一路走來的路線 | 照上、下、左、右順序,第一個「是路、而且沒站過」的格子;沒有就退回上一格 | 第一個 | 不看大局;死路要原路一格一格退回 |
| B. 最近邊境 | 所有邊境的步數 | d | 最小 | 不管方向,常常往回走 |
| C. 偏好遠處 | 邊境那一格離起點多遠 | d − 離起點格數 | 最小 | 只看一格,看不到後面有多大片霧 |
| D. 最終版 | 邊境後面整片霧 | Σ(遠近⁶) ÷ (d + 2) | 最大 | 算比較多;靠「終點通常很遠」這個規律 |
所以對你的影響是:考試問「你的 Agent 怎麼決定下一步」,可以直接用第 0 步這個例子:兩個邊境一樣近,但右邊那個後面有 77 格霧、寶藏分 1.90,下面那個只有 19 格、0.09,所以往右。
C5 逐段讀程式
結論:agent.py 分成「老師規定的三個方法」和「八個小幫手」。每個小幫手只做一件事:其中五個對應 C3 節的步驟,另外三個(_step、_inside、_safe_move)是基本工具和備案。
整份程式的地圖
| 名稱 | 對應步驟 | 做什麼 |
|---|---|---|
POWER = 6.0 | 5 | 寶藏分的次方 |
COST_OFFSET = 2.0 | 5 | 划算度分母的「+2」 |
SAFE_FRACTION = 0.05 | 5 | 安全閥:走超過步數上限的 5% 後,改去最近的邊境 |
reset() | — | 新地圖:清空筆記本、目標、上一步方向 |
act() | — | 呼叫 _decide();萬一出錯就用 _safe_move() 保命 |
_decide() | 1→7 | 主流程,照七個步驟走 |
_step(cell, action) | — | 算「從某格往某方向走一格」是哪一格 |
_inside(cell) | — | 這格在地圖範圍內嗎 |
_unknown_count(cell) | 3 | 這格旁邊有幾個未知格(大於 0 就是邊境) |
_bfs_from(position) | 3、7 | BFS:走到每格的步數、第一步方向 |
_choose_target() | 5 | 算每個邊境的划算度,挑最高的 |
_share_unknown_cells() | 4 | 分地盤(多起點 BFS) |
_safe_move() | — | 備案:照上、下、左、右的順序,回傳第一個是路或終點的方向;四個都不是就回傳 UP。在「程式出錯」或「找不到邊境」時才用(後者實際上不會發生,見 C6) |
reset():每張新地圖都從零開始
def reset(self, rows, cols, start_position):
self.rows = rows
self.cols = cols
self.start_position = tuple(start_position)
self.known = {} # 筆記本:座標 → "FREE" / "WALL" / "GOAL"
self.target = None # 目前要去的邊境
self.last_action = None # 上一步的方向(平手時繼續直走)
self.steps_taken = 0 # 這張地圖已經走了幾步(安全閥用)
這裡每一個記憶都重新建立,所以換地圖時不會帶著舊記憶。
act():外面包一層保險
def act(self, percept):
self.steps_taken += 1 # 每被問一次,就是走一步
try:
return self._decide(percept)
except Exception:
return self._safe_move(percept)
try / except 的意思是:「試著做;萬一中途出錯,不要整個當掉,改做備案」。
為什麼要這樣?因為如果程式出錯,老師的評分程式會判那張地圖 0 分。有了備案,最慘也只是走一步不太聰明的路。正常情況下備案永遠用不到(壓力測試時我把保險拿掉跑,0 次出錯)。
_decide():主流程
position = tuple(percept["position"])
neighbors = percept["neighbors"]
# 1. 記憶:自己站的格子一定是路;四個鄰居照摸到的寫
self.known[position] = "FREE"
for action, status in neighbors.items():
self.known[self._step(position, action)] = status
# 2. 旁邊有終點就走進去
for action in MOVES:
if neighbors[action] == "GOAL":
return action
# 3. BFS,挑出所有邊境(不包含自己站的這格)
distance, first_move = self._bfs_from(position)
frontiers = [cell for cell in distance
if cell != position and self._unknown_count(cell) > 0]
if not frontiers: # 找不到邊境(實際上不會發生,見 C6)
return self._safe_move(percept)
# 6. 承諾:舊目標還是邊境,就不換
if self.target not in frontiers:
self.target = self._choose_target(frontiers, distance) # 4 + 5
# 7. 走最短路線的第一步
self.last_action = first_move[self.target]
return self.last_action
注意第 3 步:distance 只包含「已知是 FREE、而且走得到」的格子。所以挑出來的邊境一定走得到,走過去的路上也一定沒有牆。
_bfs_from():一圈一圈擴散
distance = {position: 0} # 走到每格要幾步
first_move = {position: None} # 走到每格的第一步方向
queue = deque([position]) # 排隊等著往外擴的格子
while queue:
cell = queue.popleft() # 先進先出:先處理比較近的
for action in order:
n = self._step(cell, action)
if n not in distance and self.known.get(n) == "FREE":
distance[n] = distance[cell] + 1
first_move[n] = first_move[cell] or action
queue.append(n)
deque 是 Python 內建的「排隊隊伍」,從尾巴加入、從頭拿出(先進先出)。這就是 BFS 能「一圈一圈」的原因:近的先排、先處理。
first_move[cell] or action 的意思:cell 是現在正在往外擴的那一格。如果 cell 已經有「第一步」,新格子就沿用它;如果 cell 就是機器人自己(它的第一步是 None,還沒有),那這一步的方向 action 就是第一步。
order 會把「上一步的方向」排第一。這樣兩條路一樣短的時候,Agent 會繼續直走,不會左右扭來扭去。
_share_unknown_cells():分地盤
buckets = {}
for f in frontiers: # 每個邊境在「自己的距離」那一層出發
buckets.setdefault(distance[f], []).append((f, f))
owner = {}
level = min(buckets)
while buckets:
for cell, frontier in buckets.pop(level, []):
for action in MOVES:
n = self._step(cell, action)
if self._inside(n) and n not in self.known and n not in owner:
owner[n] = frontier # 這個未知格歸這個邊境
buckets.setdefault(level + 1, []).append((n, frontier))
level += 1
buckets 是「一層一層的籃子」:第 L 層的籃子裝「總距離= L」的(格子, 歸屬的邊境)。總距離=機器人走到那個邊境的 d+從邊境穿過霧走到那格的格數。每個邊境一開始放在第 d 層的籃子,所以遠的邊境等於「晚出發」。
只擴散到未知格(n not in self.known),因為我們要分的是未知的地盤。
_choose_target():算划算度
if self.steps_taken > SAFE_FRACTION * 4 * self.rows * self.cols:
return min(frontiers, key=lambda f: distance[f]) # 安全閥:最近的邊境
owner = self._share_unknown_cells(frontiers, distance) # 分地盤
span = self.rows + self.cols # 列數 + 欄數
sr, sc = self.start_position # 地圖一開始的起點
value = {f: 0.0 for f in frontiers} # 每個邊境的寶藏分,從 0 開始
for cell, frontier in owner.items():
far = (abs(cell[0] - sr) + abs(cell[1] - sc)) / span # 曼哈頓距離 ÷ (列+欄)
value[frontier] += far ** POWER # 加進那扇門的寶藏分
best, best_ratio = None, -1.0
for f in frontiers: # 照 BFS 找到的順序
ratio = value[f] / (distance[f] + COST_OFFSET) # 划算度
if ratio > best_ratio: # 只有「嚴格比較大」才換人
best, best_ratio = f, ratio
return best
因為只有「嚴格比較大」才換人,兩個邊境划算度一樣時,會留下比較早出現的那個,也就是 BFS 先找到的那個。
所以對你的影響是:考試如果問「你的 Agent 怎麼決定下一步」,你可以照 _decide() 的七個註解順序講一遍,每一步都對得上程式。
C6 為什麼不會撞牆、不會卡死
結論:「不撞牆」和「一定會結束、一定會找到終點」這兩件事,都有道理可以證明。但「一定在步數上限內找到」證明不了,那一點是靠大量實測支撐的。
C6.1 為什麼永遠不撞牆(可以證明)
- 旁邊有 GOAL 時,我們走進 GOAL,不是牆。
- 其他時候,我們走的是 BFS 路線的第一步。
- BFS 只會走「已知是 FREE」的格子(
self.known.get(n) == "FREE")。 - 「已知是 FREE」一定是真的:感知不會出錯,地圖也不會改變。
- 所以第一步一定是走進一條真的路,不會是牆。
_safe_move()(備案)也只會選 FREE 或 GOAL 的方向。它會在兩種情況被叫到:
_decide()執行時出錯(被try/except接住)。- 找不到任何邊境(
if not frontiers)。下面 C6.2 會證明這種情況其實不會發生。
它只有在「四面都是牆」時才會回傳 "UP" 去撞牆。但這不會發生:起點一定有一個鄰居是路(因為起點到終點有路,而終點不在起點旁邊);之後走到的每一格,至少「走來的那一格」是路。
C6.2 為什麼一定會找到終點(可以證明,但沒有步數保證)
分三段想。
第一段:還沒找到終點時,一定還有邊境可以去
- 起點到終點一定有一條路。沿著這條路從起點往終點走。
- 終點現在還是未知的(一摸到終點,程式就直接走進去了,所以還在找就代表還沒摸到),所以這條路上一定有「第一個未知格」。
- 它的前一格已經知道是路,而且從起點沿著已知的路走得到。機器人是從起點一路走過來的,所以「現在的位置」和起點也用已知的路連著。因此從現在的位置,也走得到這一格。
- 這一格旁邊有未知格,所以它是邊境。它也不會是我們自己站的格子,因為自己站的格子四周都已經摸過了。
- 所以只要還沒找到終點,就一定至少有一個邊境。這也表示程式裡
if not frontiers那個分支實際上跑不到。
第二段:每換一次目標,至少有一個未知格變成已知
目標只會在兩種情況下被換掉:
- 走到目標了。站上去之後會摸到它的四個鄰居,它旁邊的未知格就變成已知。
- 還沒走到,目標就不再是邊境了。這代表它旁邊的未知格,在路上已經被我們從別的格子摸到了。
兩種情況都至少有一格從「未知」變成「已知」。
在換目標之前,我們一直沿著最短路線走向同一個目標。每走一步,離目標就近一步:新摸到的資訊只會「多發現路」,不會讓已知的路消失,所以距離只會變短、不會變長。因此每一段都在有限步內結束。
第三段:未知格是有限的
地圖最多 400 格,所以未知格最多 400 個。每換一次目標至少少一個,所以換目標的次數有限。
終點就藏在某個未知格裡。第一段說「還沒找到就一定有邊境可以去」,第二段說「每一趟都會減少未知格」,所以在未知格用完之前,一定會摸到終點。
上面只證明了「有限步內一定會結束」,沒有證明「一定在 4 × 列數 × 欄數 步以內」。理論上最壞的情況,可能要換目標幾百次、每次走很遠,總步數可能超過上限。
所以「80 分基本分很穩」這句話,依據是實測,不是證明:加了「安全閥」(C3)之後,所有測試裡最多只用掉上限的約 30%,包含審查員專門造來刁難程式的地圖。
如果
_decide() 一直出錯,每一步都會落到 _safe_move()。它每次都照上、下、左、右,選第一個是路或終點的方向,所以可能在兩格之間來回踱步,直到步數用完。上面的證明不涵蓋這種情況。這一點靠的也是實測:獨立審查員和我的壓力測試都把保險拿掉跑過,_decide() 一次都沒出錯。
所以對你的影響是:考試問「你的 Agent 為什麼不會撞牆」可以講證明;問「為什麼一定找得到終點」可以講三段論證;但要誠實補一句「步數上限內完成是實驗驗證的,不是證明的」。
D1 怎麼驗證的、還有什麼風險
結論:做了三種驗證,全部通過。最大的不確定是「隱藏地圖跟我模擬的不一樣」,所以 91 分是預估,不是保證。
三種驗證
| 驗證 | 怎麼做 | 結果 |
|---|---|---|
| 1. 老師的公開測試 | 在 HW2\HW2\ 資料夾執行 py -3.11 public_grader.py | 3/3 成功,0 撞牆 |
| 2. 抄寫檢查 | 正式版跟實驗版在同一批地圖上逐張比對步數(加安全閥前比 1,800 張、加安全閥後比 720 張) | 每一張都一模一樣 |
| 3. 壓力測試 | 全新一批 5,400 張地圖(調參數、選安全閥時都沒用過),把 try/except 保險拿掉,有錯就會爆出來 | 0 失敗、0 撞牆、0 出錯;最低分 80.15;最多用掉步數上限 27% |
| 4. 刁鑽地圖 | 審查員 2 專門造來刁難程式的 60 張地圖,每張試遍所有終點位置 | 全部成功;最多用掉步數上限 29.7%(加安全閥前是 90.2%) |
| 5. 獨立審查 | 三個互相獨立的審查員 | 見 D2 |
預估分數
正式考試是 9 張(README 說三種尺寸各三張;我假設三種風格各一張)。我用「終點放遠處」的模擬地圖,模擬了兩萬次「抽 9 張考一次」:
| 情況 | 9 張平均 |
|---|---|
| 運氣差(最差的 5%) | 88.4 |
| 一般(中間值) | 91.2 |
| 運氣好(最好的 5%) | 94.2 |
單張地圖來看:一半的地圖在 90.9 分以上,有 11% 的地圖拿到滿分 100。
還有什麼風險
| 風險 | 機率 | 後果 | 我們的處理 |
|---|---|---|---|
| 隱藏地圖的終點不是放在遠處 | 不確定 | 平均少約 2.5 分(模擬:91.24 → 88.78) | 已測「終點隨便放」:走法 C 最高(88.79),最終版 88.78,只低 0.01,等於打平 |
| 隱藏地圖的形狀跟我模擬的不一樣 | 不確定 | 分數會有出入 | 我的產生器是照三張公開地圖的風格做的;走法本身不依賴特定形狀 |
| Linux 和 Windows 差異 | 很低 | — | 程式只用 collections.deque,沒有讀檔、沒有路徑、沒有作業系統相關的東西 |
| Python 版本問題 | 很低 | — | 已經用你電腦上的 Python 3.11 測過,跟正式環境同版本 |
| 跑太慢超時 | 很低 | — | 公開地圖每張約 0.05~0.1 秒;最難的對抗地圖最慢 3.3 秒,9 張加起來估計不到 30 秒 |
「老師故意把終點放遠」是我從三張公開地圖推測的,老師沒有明講。隱藏地圖的實際分數要等老師公布才知道。
D2 獨立審查結果
結論:三個互相獨立的審查員都沒有找到會讓作業被判 0 分的問題。但它們找到了一個程式的安全風險(最慘情況用掉 90% 步數)和文件裡 7 個會影響考試答案的錯誤,全部已經修好並重新驗證。
D2.1 為什麼要找「別人」來審查
自己寫的東西自己檢查,很容易「看到自己想看的」。所以我派了三個全新的 AI 審查員:
- 它們看不到我的想法和對話,只拿到老師的規定、程式和文件。
- 每個只負責一個方向,而且被要求「假設有錯,去找出來」。
- 它們不准改任何檔案,只能回報。修改由我來做,修完再重新驗證。
D2.2 三個審查員各做了什麼
| 審查員 | 查什麼 | 做了多少 | 結論 |
|---|---|---|---|
| 1. 規章合規 | 程式和 zip 有沒有違反 README 和 COOL 公告的任何一條 | 逐條核對 README 第 6、7、10、11 節;同一台機器人連跑 300 張地圖檢查記憶有沒有清乾淨;餵 6 種壞掉的資料給 act();比對 zip 和原檔的指紋 | 可以交,沒有會 0 分的問題。唯一要你做的:改 zip 檔名 |
| 2. 程式抗壓 | 想盡辦法讓程式失敗、撞牆、出錯、跑太慢 | 約 5.7 萬局真實測試;12 類手工刁鑽地圖;用「爬山法」專門造出 38 萬張讓程式最慘的地圖;7 種介面濫用 | 0 失敗、0 撞牆、0 出錯。但找到一張地圖用掉 90% 步數(已修,見 D2.3) |
| 3. 內容正確性 | 文件的每一句話對不對:規則、程式說明、投影片頁數、觀念 | 逐條核對投影片原文 28 處引用;重跑我的實驗確認數字;檢查證明有沒有漏洞 | 數字和程式說明大致正確;7 個錯誤、14 個容易誤會的地方(已全部修正,見 D2.4) |
D2.3 程式的修改:加了「安全閥」
審查員 2 找到的問題:它造出一張 15×15 地圖,讓程式走了 812 步,上限 900 步,只差 88 步就會 0 分。
原因:「偏好遠處」太強。探索快結束時,地圖上只剩零星的小死角,程式照「離起點多遠」決定先去哪個,而不是照「離自己多近」,結果在地圖兩端來回跑。那張地圖上它換了 118 次目標,其中 19 趟長途移動就花了 684 步。
修法:程式自己數已經走了幾步。超過步數上限的 5% 之後,就改成「先去離自己最近的邊境」。詳見 C3 的「安全閥」。
修完的結果:
| 項目 | 修之前 | 修之後 |
|---|---|---|
| 審查員的 60 張最刁鑽地圖,最多用掉步數上限 | 90.2% | 29.7% |
| 那張 812 步的地圖 | 812/900 | 242/900 |
| 終點離起點 2 步、卻先跑完整張圖的 20×20 地圖 | 1,090/1,600 | 444/1,600 |
| 模擬地圖平均分(終點遠/終點隨機) | 91.14/88.72 | 91.24/88.78(沒有變差) |
| 全新一批 5,400 張壓力測試 | — | 0 失敗、0 撞牆、0 出錯,最多用掉 27% |
舊版程式備份在 C:\D槽\TAICA課程\人工智慧導論\HW2\agent_v1_before_safety.py,舊的 zip 在 zip_v1_before_safety.zip,想退回隨時可以。
同時也修了 agent.py 裡一行說明文字:原本說「+2 是為了避免除以很小的數」,審查員指出不對(步數最少是 1,不會除以 0)。改成正確的理由:縮小近處邊境的優勢。
D2.4 文件的修改:7 個會影響考試答案的錯誤
| # | 原本寫錯的 | 改成 | 在哪 |
|---|---|---|---|
| 1 | 邊境示意圖標錯兩格(有兩格明明是邊境卻沒標) | 用程式模擬出一個真的走得出來的情況,重畫 | C1 |
| 2 | 「known/unknown 正確答案是 known」 | 這題有爭議。老師的 Ch4 投影片把這種迷宮叫 unknown environment,考試建議答 unknown,再補一句「移動規則本身已知」 | B2、E1、E2 Q3 |
| 3 | 「A* 只要 h 不高估就保證最短」 | 補上:記住走過哪裡的版本(graph search)還需要 h「一致」(Ch3 第 33 頁) | B5 |
| 4 | 「知道終點、只是不知道牆,就能用 A*」 | 錯。A* 要出發前先算好整條路,需要知道地圖。看不到地圖,就算知道終點也不能直接用 | E2 Q6 |
| 5 | 「選定目標就走到底,中途不換」「七個步驟每步都做」 | 程式實際上是「目標不再是邊境就換」;步驟 4、5 只在要重選時才做 | C3、C6、E2 Q7 |
| 6 | 「備案只在程式出錯時用」,而且程式摘錄漏了兩行 | 補上「找不到邊境時也會用」(並證明這不會發生),摘錄補回漏掉的兩行 | C5、C6 |
| 7 | 把 sequential 翻成「連續性」 | 「連續」是 continuous 的譯名,考試會被誤會。改成「序列式」 | B2、E1 |
D2.5 其他修正(容易誤會、小問題)
- C6 證明重寫:補上三個缺口。最重要的是誠實寫出:只能證明「一定會找到終點」,不能證明「一定在步數上限內」,那一點靠實測。
- 評分和競爭比的關係:原本說「評分就是競爭比」,改成「效率分那一項,在 0 撞牆時剛好是競爭比的倒數」。
- A2 的計分例子:改成跟 README 一樣的「總共 43 步、撞牆 3 次(實際移動 40 步)」。
- A1.14 標題:從「違反就 0 分」改成「有些違反會 0 分」,並逐條寫出 README 的根據。
- 「三種風格各一張」:README 沒這樣說,改標成推測。
- 「划算度=找到終點的機會」:寶藏分不是真的機率,改成「估計的寶藏價值」。
- A* 的描述:不是「BFS 加方向感」,而是「用 f = g + h 排優先順序的搜尋」。
- DFS 的完備性:補上「什麼情況下保證找得到」,考試常問。
- 預估分數的前提:補上「前提是終點放得遠;隨便放約 88.8」。
- 步數使用率、執行時間:原本寫的 44%、0.05 秒都太樂觀,改成實測值。
- 投影片引用:改正 Ch4 標題原文、理性 Agent 的頁數、utility 那句漏掉的字。
- 調參過程的紀錄:實驗程式被反覆改寫,舊的數字查不到了。補了一份紀錄檔
HW2\lab\EXPERIMENT_LOG.md。
D2.6 審查後還剩下的風險
| 風險 | 會怎樣 | 說明 |
|---|---|---|
| 隱藏地圖的終點不是放在遠處 | 平均少 2~3 分,不會 0 分 | 審查員 2 實測:終點全在近處時,平均約 86~87.5 分 |
| 終點就在起點附近 | 那一張約 80 分 | 「偏好遠處」的代價:近處的角落會晚一點才去看。因為不撞牆,最多少 20 分,不會 0 分 |
| 步數上限 | 理論上無法證明不超過 | 但目前所有測試(包含專門造的刁鑽地圖)最多只用到約 30% |
| 老師對 AI 協助的規定 | 不確定 | 課程沒寫作業能不能用 AI,只寫了期末考不能用 |
D3 怎麼交件
結論:zip 已經打包好了,你只要把檔名改成自己的學號姓名,上傳到 NTU COOL。
步驟
- 打開
C:\D槽\TAICA課程\人工智慧導論\HW2\。 - 找到
學號_姓名.zip。 - 改名成你的學號和姓名,例如
P76123456_王小明.zip。底線是半形的_,不要留大括號。 - 到 NTU COOL 的 Homework 2 上傳這個 zip。截止:10/15(四)23:59。
zip 裡面是「打包當下」的
agent.py,不會自動更新。改完要重新打包:zip 打開後最外層只能有一個 agent.py,不能有資料夾。
老師 README 的繳交前檢查(逐條對過)
| 檢查項 | 狀態 |
|---|---|
| 已在 Python 3.11 測試程式 | 已做(py -3.11) |
public_grader.py 正常執行,三張公開地圖均在步數限制內完成 | 3/3 |
reset() 能處理不同尺寸與起點,並清除上一張地圖的狀態 | 已做(壓力測試用同一個 Agent 連續跑上百張不同尺寸地圖) |
act() 每次只回傳一個合法方向字串 | 已做(連出錯時的備案也只回傳合法方向) |
| 不需要使用者輸入、不讀檔 | 程式裡沒有 input()、沒有 open() |
| 只使用標準函式庫 | 只匯入 collections.deque |
ZIP 命名正確,內部只有一份 agent.py | 內容已確認只有 agent.py;檔名要你改 |
D4 結論:一條龍總回顧
結論:這份作業從頭到尾是一條因果鏈:看不到地圖 → 要有記憶 → 要找邊境 → 要用 BFS 走過去 → 要決定先去哪個邊境 → 用划算度+承諾來決定 → 驗證不會失敗、不會撞牆 → 預估約 91 分。
D4.1 一條龍:每一環為什麼接到下一環
| # | 這一環 | 為什麼會走到下一環 | 在哪一節 |
|---|---|---|---|
| 1 | 題目:蒙眼走迷宮,只摸得到四格,走越少分數越高 | 只摸得到四格=看不到全部 | A1 |
| 2 | 分數:80 基本分+最多 20 效率分-撞牆扣分 | 要拿高分:不能失敗、不能撞牆、少走冤枉路 | A2 |
| 3 | 這是一個 Agent:感知 → 決定 → 行動;用 PEAS 描述任務 | 先搞清楚環境是什麼樣子,才知道 Agent 需要什麼能力 | B1 |
| 4 | 環境是部分可觀察的 | 看不到全部,就必須自己記住看過的東西 | B2 |
| 5 | 需要有記憶、有目標、會打分數的 Agent | 有了記憶的地圖,就能把問題寫成搜尋問題 | B3 |
| 6 | 搜尋問題:起點、動作、轉移、目標、成本 | 搜尋問題可以用 BFS、DFS、A* 解,但它們假設地圖已知 | B4、B5 |
| 7 | 線上搜尋:邊走邊看;評分=競爭比 | 要決定「下一個去探索哪裡」 | B6 |
| 8 | 三個工具:記憶地圖、邊境、BFS | 有好幾個邊境時,要選一個 | C1 |
| 9 | 比較四種選法(DFS、最近、偏好遠處、划算度) | 「終點通常很遠」這個線索讓划算度勝出 | C2 |
| 10 | 最終版:分地盤 → 算划算度 → 選最高 → 承諾走向它;走太久就改去最近的(安全閥) | 要確認它真的不會出錯 | C3、C5 |
| 11 | 保證:只走已知的路所以不撞牆;承諾+格子有限所以一定走到 | 理論說得通,還要實際測 | C6 |
| 12 | 驗證:公開 3/3、5,400 張模擬 0 失敗 0 撞牆、獨立審查 | 可以交了 | D1、D2 |
| 13 | 交件:改 zip 檔名、上傳 COOL | — | D3 |
D4.2 數字總表
| 項目 | 數字 |
|---|---|
| 公開地圖 | 3/3 成功、0 撞牆;步數 20/45/70(最短 16/45/26);平均 94.48 分 |
| 模擬地圖(終點偏遠,2,700 張) | 平均 91.24 分,0 失敗 |
| 模擬地圖(終點隨便放,2,700 張) | 平均 88.78 分,0 失敗 |
| 預估正式 9 張平均 | 約 91.2(運氣差 88.4、運氣好 94.2;前提是終點放得遠) |
| 比最基本的 DFS 多 | 約 3.5 分(終點偏遠時) |
| 步數上限最多用掉 | 約 30%(審查員專門造的最刁鑽地圖是 29.7%;一般模擬地圖最多 27%) |
| 程式長度 | 230 行,只用 Python 內建的 deque |
D4.3 如果只能記五句話
- 這份作業是 線上搜尋(online search):看不到地圖,只能邊走邊看。評分裡的效率分,就是 competitive ratio 的倒數(0 撞牆時剛好相等)。
- 環境是 部分可觀察的,所以 Agent 必須有 記憶(model-based),只看眼前的反射型會繞圈。
- 每一步找出所有 邊境(已知是路、旁邊還有未知格),用 BFS 在已知地圖上走最短路線過去;只走已知的路,所以 永遠不撞牆。
- 有好幾個邊境時,用 划算度=估計的寶藏分 ÷(步數 + 2) 挑一個(utility-based);因為終點通常離起點遠,遠處的未知格權重比較高。
- 選定之後 承諾走向它(它不再是邊境才換),避免來回踱步;再加上格子數有限,所以 一定找得到終點。走超過步數上限的 5% 就改去 最近的邊境(安全閥),讓最慘情況也離步數上限很遠。
E1 跟課本的對照表
結論:這份作業就是 Ch4 投影片「Online Searching Agents with Unknown Environments」(線上搜尋)的實作,評分裡的效率分就是投影片講的 competitive ratio(競爭比)倒過來。另外也用到 Ch2 的環境分類、Agent 種類,和 Ch3 的 BFS。
這一節是 B 部分的濃縮對照表,考前快速複習用。每個觀念的詳細說明在 B1~B6。
Ch4:線上搜尋(最直接相關)
出處:Chapter 4 Search in Complex Environments.pdf PDF 第 29~35 頁(投影片角落編號 56~63)
| 投影片說的 | 白話 | 在作業裡是什麼 |
|---|---|---|
| offline search: compute a complete solution before setting foot in the real world | 離線搜尋:先把整條路想好,再出發 | 不可能,因為看不到地圖 |
| an online search agent interleaves computation and action | 線上搜尋:走一步、看一下、想一下、再走一步 | 就是 act() 被一直呼叫的流程 |
| a robot that is placed in a new building and must explore it | 機器人被放進一棟陌生建築,要邊走邊探索 | 就是這份作業 |
| The agent cannot determine RESULT(s,a) except by actually being in s and doing a | 不實際去做,就不知道結果 | 不走到那一格旁邊,就不知道那格是什麼 |
| compare this cost with the path cost ... if it knew the search space in advance. This is called the competitive ratio | 競爭比=「實際走的路」÷「如果事先知道地圖,最短要走的路」,越小越好 | 作業的「效率」=最短 ÷ 實際移動步數,在 0 撞牆時剛好是競爭比倒過來 |
| an online agent receives a percept ... it can augment its map | 每次收到感知,就補充自己的地圖 | 就是 self.known 筆記本 |
| an online algorithm better expands nodes in a local order. DFS has exactly this property | 線上搜尋要「就近」展開,DFS 剛好有這個性質 | C2 節的走法 A(DFS)。我們的最終版用 BFS 找路,可以直接「抄近路」走到遠處的邊境,所以比 DFS 好 |
Ch2:這是什麼樣的環境?
出處:Chapter 2 Intelligent Agents.pdf 第 11~14 頁
| 環境性質 | 這份作業是 | 為什麼 |
|---|---|---|
| Fully vs. partially observable | Partially observable(部分可觀察) | 只看得到上下左右四格 |
| Single vs. multiagent | Single agent | 只有你一個在走 |
| Deterministic vs. stochastic | Deterministic(確定性) | 往右走就一定往右一格(或撞牆不動),沒有隨機 |
| Episodic vs. sequential | Sequential(序列式) | 現在走哪,會影響之後的位置和選擇 |
| Static vs. dynamic | Static(靜態) | 你在想的時候,地圖不會變 |
| Discrete vs. continuous | Discrete(離散) | 一格一格、四個方向 |
| Known vs. unknown | 建議答 Unknown(有爭議) | Ch4 把地圖事先未知的迷宮叫 unknown environment;但移動規則已知,所以也可以主張 known。詳見 B2 的提醒 |
Known vs. unknown 那一列是我依投影片定義("state of knowledge about the laws of physics of the environment")判斷的,老師沒有針對這份作業講過。
Ch2:這是哪一種 Agent?
出處:Chapter 2 Intelligent Agents.pdf 第 20~25 頁
| Agent 種類 | 投影片的定義 | 我們的 Agent 有沒有 |
|---|---|---|
| Model-based | keeps track of the part of the world it can't see now; maintain some sort of internal state | 有:self.known 就是 internal state(內部狀態) |
| Goal-based | needs some sort of goal information | 有:目標是走進 GOAL |
| Utility-based | chooses the action that maximizes the expected utility; handle the uncertainty inherent in stochastic or partially observable environments | 有:划算度就是效用,挑最高的 |
Ch3:搜尋演算法
出處:Chapter 3 Solving Problems by Searching.pdf BFS 第 13~15 頁、DFS 第 18~20 頁、A* 第 31~35 頁
- BFS:我們每一步都用它在已知地圖上找最短路線。
- frontier:Ch3 的 frontier 是「搜尋樹裡等著被展開的節點」。作業裡的 frontier 是「已知和未知的交界格」。名字一樣,概念相近(都是「下一個要去探索的地方」),但不是同一個東西,考試要分清楚。
- A*:需要知道終點在哪才能算 heuristic(估計離終點多遠)。這份作業終點未知,所以不適用。
E2 考試可能怎麼問
結論:下面是我根據投影片內容推測的題目(老師沒說會考這些)。題目用英文出(期末考是英文出題),可以用中文作答。答案收在摺疊裡,先自己想再打開。
Q1. What is the difference between offline search and online search?
看答案
離線搜尋:出發前先算出完整的解(整條路),再照著執行。需要事先知道整個環境。
線上搜尋:計算和行動交錯進行。先做一個動作、觀察結果、再算下一步。用在環境未知、必須邊走邊探索的情況(例如機器人在陌生建築裡)。
Q2. Define the competitive ratio.
看答案
Agent 實際走的路徑成本 ÷ 如果事先知道整個環境時的最短路徑成本。越小越好,最好是 1。
(作業的 efficiency = optimal / movement_steps。在 0 次撞牆時,它剛好是競爭比的倒數;老師的 movement_steps 不算撞牆,課本的路徑成本則每個動作都算。)
Q3. Describe the task environment of an agent exploring an unknown grid maze where it can only sense its four neighbors.
看答案
Partially observable(只看到四鄰)、single agent、deterministic、sequential、static、discrete;known/unknown 建議答 unknown(Ch4 把地圖未知的迷宮叫 unknown environment),並補一句「移動規則本身是已知的」。
Q4. Why does an online search agent prefer DFS-like local expansion over BFS-like expansion?
看答案
因為線上 Agent 要「實際走過去」才能展開一個節點。BFS 會在不同分支之間跳來跳去,每次跳都要實際走很遠的路。DFS 是就近展開,下一個節點通常就在旁邊。
補充(作業的心得):我們的 Agent 先記住地圖,再用 BFS 在已知地圖上找最短路線走到邊境。這不是「用 BFS 展開搜尋樹」,而是「用 BFS 算走路路線」,兩件事不一樣。
Q5. Why is the agent in HW2 a model-based agent?
看答案
因為環境是部分可觀察的,Agent 每次只看到四格。它必須維護一個內部狀態(記憶地圖 self.known),記住過去看過的東西,才能做出好的決定。只看當下感知的 simple reflex agent 會在迷宮裡打轉。
Q6. Can A* be used directly to find the goal in HW2? Why or why not?
看答案
不能直接用,有兩個原因:
1. A* 是離線演算法:它要在出發前,先在腦子裡展開節點、算出整條路。展開節點需要知道「做某個動作會到哪個狀態」(轉移模型),也就是要知道地圖。HW2 看不到地圖,Ch4 第 30 頁說線上 Agent "cannot determine RESULT(s,a) except by actually being in s and doing a"。
2. A* 的 h(n) 需要知道終點在哪。HW2 的終點一開始未知,只有走到它旁邊才會發現。
補充:Ch4 第 32 頁說,線上 Agent 如果知道終點位置,可以用曼哈頓距離當 heuristic 來引導探索。但這仍然是線上搜尋,不是直接跑 A*。
Q7. 你的 Agent 怎麼保證不會撞牆?怎麼保證不會無限繞圈?
看答案
不撞牆:只沿著「已知是 FREE」的格子規劃路線,走之前已經知道下一格不是牆。
一定找得到終點:(1) 還沒找到終點時,一定還有邊境可以去;(2) 選定目標後一直走向它,直到它不再是邊境才換,每走一步離目標近一步;(3) 每換一次目標,至少有一個未知格變成已知,格子數有限,所以終點一定會被發現。
要誠實補充:這證明了「有限步內會找到」,但沒有證明「一定在步數上限內」,那一點是靠大量實測驗證的。
Q8. 你的探索策略是什麼?為什麼比 DFS 好?
看答案
邊境探索+效用選擇:每一步找出所有「已知可走、旁邊還有未知」的格子(邊境),估計每個邊境後面未知區域藏有終點的可能性(離起點越遠越可能),除以走過去的步數,選最高的。
比 DFS 好的原因:DFS 走到死路要一格一格原路退回;我們可以用 BFS 直接走最短路線到任何一個邊境,而且優先去比較可能有終點的方向。模擬考中,終點偏遠時多 3.56 分;終點隨機時只多 0.64 分。
E3 名詞小字典
結論:這份作業會遇到的詞,一行一個白話。
| 名詞 | 白話 |
|---|---|
| Agent | 任何「透過感應器感知環境、透過執行器做出動作」的東西(Ch2 第 2 頁),不一定是程式。這份作業裡,它是一個程式,就是那個蒙眼走迷宮的人。 |
| Percept(感知) | Agent 每一步收到的資訊:我在哪、四個鄰居是什麼。 |
| Grid map(格子地圖) | 像圍棋盤一樣,由一格一格組成的地圖。 |
| Frontier(邊境) | 已知是路、旁邊還有未知格的格子。站上去才看得到新東西。 |
| BFS(廣度優先搜尋) | 像漣漪一圈一圈往外擴,能找到最短路線。 |
| DFS(深度優先搜尋) | 一條路走到底,走不通再退回來換一條。 |
| A* | 用 f(n) = g(n) + h(n)(已走的成本+估計還要走的成本)排優先順序的搜尋,需要知道終點在哪。 |
| Heuristic(啟發式) | 一個「大概估計」,例如「離終點大約還有幾步」。 |
| Manhattan distance(曼哈頓距離) | 只能橫著、直著走時的格數:上下差幾格+左右差幾格。 |
| Multi-source BFS(多起點 BFS) | 同時從好幾個起點一起擴散的 BFS。我們用它來分地盤。 |
| Utility(效用) | 對一個選擇打的分數,越高越想選。這裡是划算度。 |
| Competitive ratio(競爭比) | 實際走的路 ÷ 事先知道地圖時的最短路。越接近 1 越好。 |
| Online search(線上搜尋) | 邊走邊想,走一步看一步。 |
| Partially observable(部分可觀察) | Agent 一次只看得到環境的一部分。 |
| Internal state(內部狀態) | Agent 自己的記憶,例如記憶地圖。 |
deque | Python 內建的排隊隊伍,頭尾都能快速放進拿出。BFS 用它來排隊。 |
try / except | Python 的「試著做,出錯就改做備案」。 |
| 安全閥 | 我們程式裡的保險:走超過步數上限的 5% 後,不再偏好遠處,改去最近的邊境,避免步數用完。 |
本報告的每一項判斷都來自實際查證(讀老師的 README 與程式、實際執行公開測試與模擬考、對照課程投影片原文)。標明「推測」的部分(終點放遠是老師的出題習慣、考試可能怎麼問、Known vs. unknown 的歸類)不是老師講的。