National Yunlin University of Science and Technology
Uedu Main Site
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try a Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

1

Local AI

--

Uedu Code

--

雲林科技大學 AQI 62 26°C PM2.5 12
AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

Unit 23

回溯法

排列、子集合、N 皇后,以及剪枝
Interaction實作 30 minute(s)

Learning Objectives

  • 說出回溯法「做選擇、遞迴、撤銷選擇」的三個步驟,並在程式裡找出它們
  • 把排列與子集合的產生過程畫成決策樹,追蹤路徑在每一步怎麼變化
  • 用回溯法解 N 皇后問題,說明行與斜線的衝突怎麼檢查
  • 用剪枝提早放棄不可能成功的分支,說明剪枝成立的前提
  • 估計回溯法的時間複雜度,知道它在最壞情況下仍是指數時間

把所有可能一個一個試出來

排座位、排值日生、從一堆零錢裡湊出剛好 100 元、在棋盤上擺棋子不互相攻擊:這些問題都沒有一條公式能直接算出答案, 但它們有一個共同點:答案是一連串的選擇。第一個位置坐誰、第二個位置坐誰……每一步都有幾個選項,選完所有步驟就得到一個候選答案。

回溯法(backtracking)就是有系統地試遍這些選擇:

  1. 做選擇:從目前可以選的選項裡挑一個,記到「路徑」上
  2. 遞迴:帶著這個選擇,去決定下一步
  3. 撤銷選擇:從下一步回來以後,把剛才的選擇拿掉,換下一個選項再試

第 3 步就是「回溯」這個名字的由來:走進死路,就退回上一個岔路口換一條路。 把所有選擇畫成一棵樹,回溯法其實就是在這棵決策樹上做深度優先搜尋(DFS,第 17 課),一路走到底再退回來。

def backtrack(路徑, 選項):
    if 路徑已經完整:
        記錄答案
        return
    for 選項 in 目前可以選的:
        做選擇
        backtrack(新的路徑, 新的選項)
        撤銷選擇

這一課用這個模板解四個問題:排列、子集合、N 皇后,以及用剪枝加速的子集和。

排列:每一層決定一個位置

把 1、2、3 排成所有可能的順序。遞迴樹的每一層決定一個位置:第一層選第一個數,第二層選第二個數。注意 path 在整個過程中只有一個:往下走時 append,回來時 pop,所以它永遠是「從根走到目前節點」的那條路。6 片葉子對應 6 個排列,也就是 3! = 6。

撤銷選擇為什麼不能省

path 和 used 是所有遞迴呼叫共用的同一份資料(範例把它們放在函式外面,每一層讀寫的都是它們)。 如果從下一層回來時不把選擇撤銷,上一層的迴圈換下一個數字時,路徑上還留著剛才那個數字,後面產生的結果就全錯了。

撤銷的順序要和做選擇的順序相反:先 used[i] = True 再 append,回來時就先 pop 再 used[i] = False。這樣每一層回到迴圈開頭時,狀態都和進入這一層時一模一樣。

有多少個節點? n 個不同的數字有 n! 個排列,決策樹的葉子就有 n! 片,每片葉子印出一個長度 n 的結果,所以時間是 O(n × n!)。

n n!
3 6
5 120
8 40,320
10 3,628,800
12 479,001,600

回溯法能保證不漏掉任何答案,代價是答案本身就有這麼多個。第 25 課會看到這種成長速度為什麼讓某些問題「難解」。

子集合:每個節點都是一個答案

子集合和排列不同:{1, 2} 和 {2, 1} 是同一個子集合,所以選了 nums[i] 以後,下一層只能從 i + 1 開始選,不能回頭選前面的數。另一個差別是每個節點都是答案(不必等到路徑滿了),所以進入函式就先印出目前的路徑。3 個數字有 2³ = 8 個子集合,遞迴樹剛好有 8 個節點。

想一想:排列與子集合

1.

用上面的子集合程式處理 nums = [1, 2, 3],印出的第 5 行是什麼?

2.

排列程式少寫了 path.pop()(used[i] = False 還在),用 nums = [1, 2] 執行,會印出什麼?

3.

5 個不同的數字,排列程式的決策樹有幾片葉子(印出幾行)?子集合程式印出幾行?

第 1 次作答

N 皇后:在棋盤上放互不攻擊的皇后

西洋棋的皇后可以沿著同一列、同一行、兩條斜線走任意格。N 皇后問題:在 N × N 的棋盤上放 N 個皇后,讓任兩個都不在同一列、同一行或同一條斜線上。

直接列舉「N² 格裡選 N 格」太多了(8 × 8 棋盤選 8 格超過 44 億種)。回溯法的做法是利用規則縮小選項:

  • 每一列剛好放一個皇后,所以一列一列放:第 row 層只決定第 row 列的皇后放在哪一行
  • 放之前先檢查這一格有沒有被前面的皇后攻擊到,有的話直接跳過,不往下走

檢查衝突用三個布林陣列,每次查詢都是 O(1):

陣列 意思 索引
cols 這一行有沒有皇后 col
d1 左上到右下的斜線:同一條線上 row + col 都相同 row + col(0~2N-2)
d2 右上到左下的斜線:同一條線上 row - col 都相同 row - col + N - 1(加 N-1 讓索引不是負數)

下面的範例找出 4 皇后的第一組解就停下來:solve(row) 回傳「從第 row 列往下能不能全部放好」,下一層成功就一路回傳成功,不再試別的位置。

4 皇后:放不下就退回上一列

第 0 列先把皇后放在第 0 行,結果第 1 列試了兩個位置、往下都走不通,最後整條路失敗,退回第 0 列改放第 1 行。看遞迴樹:回傳 F 的節點就是死路,回傳 T 的是成功的那一條。4 皇后共有 2 組解(左右鏡像),這個範例找到第一組就停。

更大的棋盤有幾組解

把範例改成「找到一組解不停下來,而是數一數」,就能算出每個 N 的解數。只看結果(不逐步播放):

N 4 5 6 7 8
解數 2 10 4 40 92

N = 2、3 沒有解;N = 6 的解反而比 N = 5 少。8 皇后的 92 組解裡,扣掉旋轉與鏡像後是 12 組本質不同的解。

這一課最後的實作就是把這個數解的程式寫出來。

剪枝:提早放棄不可能成功的分支

N 皇后裡「這一格被攻擊就跳過」已經是一種剪枝(pruning):不用把皇后放下去、往下走到最後一列,才發現失敗。 剪掉一個節點,它底下的整棵子樹都不用走了,這是回溯法比單純列舉快的關鍵。

子集和問題:從 [2, 3, 5, 6, 8] 裡挑一些數,讓總和剛好是 10。不剪枝的話,要走過全部 2⁵ = 32 個子集合。 如果數字已經由小到大排好,可以加一條規則:

目前的總和加上 nums[i] 已經超過 10,就不用試 nums[i];而且後面的數更大,也都不用試,直接 break。

這條規則成立的前提是「排好序」而且「數字都是正的」。如果有負數,現在超過了,之後加上負數還可能拉回來,就不能這樣剪。 剪枝一定要確定被剪掉的分支不可能有答案,否則會漏掉解。

子集和:超過目標就不再往下

看遞迴樹節點上的總和:沒有一個超過 10。例如總和是 5(選了 2、3)時,下一個數 6 會讓總和變成 11,程式在這裡 break,8 也不試了。最後只呼叫了 13 次,不剪枝要 32 次。

想一想:N 皇后與剪枝

1.

4 皇后的範例裡,row = 1, col = 0 這一格為什麼一檢查就跳過?(第 0 列的皇后在第 0 行)

2.

子集和的範例裡,如果 nums 沒有排序(例如 [2, 9, 3, 5]),total + nums[i] > target 時還能直接 break 嗎?

3.

下面哪一個敘述正確?

第 1 次作答

實作:數出 N 皇后的解數

完成 count:它回傳「從第 row 列放到最後一列,共有幾種放法」。和範例不同的是,找到一組解以後不要停,要把每一格往下數到的解數加起來。程式會印出 N = 4~8 的解數。

你的輸出
預期輸出

重點整理

  • 回溯法=做選擇、遞迴、撤銷選擇;它是在決策樹上做深度優先搜尋
  • 共用的狀態(路徑、已使用標記)從下一層回來時一定要撤銷,順序和做選擇時相反
  • 排列:每一層決定一個位置,用 used 避免重複使用,共 n! 個
  • 子集合:下一層從 i + 1 開始選,每個節點都是答案,共 2ⁿ 個
  • N 皇后:一列放一個,用 cols、d1(row + col)、d2(row - col + n - 1)在 O(1) 內檢查衝突
  • 剪枝:確定不可能有答案的分支就不走;剪枝的條件必須真的成立(例如排序後才能 break)
  • 剪枝讓實際執行快很多,但最壞情況通常仍是指數時間

下一課:字串比對。在一長段文字裡找出某個字串出現的位置,看 KMP 怎麼利用已經比對過的資訊,避免從頭再比。

這一課完成了!
Next lesson:字串比對
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。