跳轉到內容

演算法/回溯

來自華夏公益教科書,開放的書籍,開放的世界

頂部,章節:123456789A

回溯是一種通用的演算法技術,它考慮搜尋所有可能的組合以解決最佳化問題。回溯也稱為深度優先搜尋分支限界。透過插入更多關於問題的知識,可以修剪搜尋樹以避免考慮看起來沒有希望的情況。雖然回溯對我們不知道更有效解決方案的難題很有用,但對於其他技術更擅長的解決日常問題來說,它是一個糟糕的解決方案。

然而,動態規劃和貪婪演算法可以被認為是對回溯的最佳化,因此回溯背後的通用技術對於理解這些更高階的概念很有用。首先學習和理解回溯技術為這些更高階的技術提供了良好的墊腳石,因為您不必一次學習多個新概念。

回溯方法
  1. 將選擇一個解決方案視為一系列選擇
  2. 對於每個選擇,遞迴地考慮所有選項
  3. 返回找到的最佳解決方案

這種方法足夠通用,可以應用於大多數問題。但是,即使小心改進回溯演算法,它也可能仍然需要指數時間而不是多項式時間。此外,回溯演算法的精確時間分析可能極其困難:相反,給出了可能不緊密的更簡單的上限。

最長公共子序列 (窮舉版本)

[編輯 | 編輯原始碼]

LCS 問題類似於 Unix "diff" 程式所做的。Unix 中的 diff 命令將兩個文字檔案AB 作為輸入,並輸出AB 之間的逐行差異。例如,diff 可以顯示A 中缺少的行已新增到B 中,以及A 中存在但在B 中刪除的行。目標是獲得一個可以用來將A 轉換為B 的新增和刪除列表。對這個問題過於保守的解決方案會說A 中的所有行都被刪除了,而B 中的所有行都被添加了。雖然這會在粗略的意義上解決這個問題,但我們關心的是實現正確轉換所需的最小新增和刪除次數。考慮如何自己實現這個問題的解決方案。

LCS 問題,而不是處理文字檔案中的行,而是關心在兩個不同的陣列之間找到共同的項。例如,

let a := array {"The", "great", "square", "has", "no", "corners"}
let b := array {"The", "great", "image", "has", "no", "form"}

我們要找到在ab 中以相同順序找到的項的最長子序列。ab 的 LCS 是

"The", "great", "has", "no"

現在考慮另外兩個序列

let c := array {1, 2, 4, 8, 16, 32}
let d := array {1, 2, 3, 32, 8}

這裡,cd 有兩個最長的公共子序列

1, 2, 32;和
1, 2, 8

注意

1, 2, 32, 8

不是一個公共子序列,因為它只是一個d 的有效子序列,而不是c(因為c 在 32 之前有 8)。因此,我們可以得出結論,對於某些情況,LCS 問題的解決方案並不唯一。如果我們有更多關於序列的資訊,我們可能更喜歡一個子序列而不是另一個:例如,如果序列是計算機程式中的文字行,我們可能會選擇保持函式定義或配對註釋分隔符完整的子序列(而不是選擇在語法中未配對的分隔符)。

在頂層,我們的問題是實現以下函式

// lcs -- returns the longest common subsequence of a and b
function lcs(array a, array b): array

它以兩個陣列作為輸入,並輸出子序列陣列。

如何解決這個問題?您可以從注意到如果兩個序列以相同的詞開頭,那麼最長的公共子序列總是包含那個詞。您可以自動將該詞新增到您的列表中,並且您只需要將問題簡化為找到其餘兩個列表的最長公共子集。因此,問題變小了,這很好,因為它表明取得了進展。

但是,如果兩個列表沒有以相同的詞開頭,那麼a 中的第一個元素或b 中的第一個元素,或兩者都不屬於最長的公共子序列。但是,其中一個可能在裡面。如何確定哪一個(如果有)要新增?

該解決方案可以用回溯方法來思考:嘗試兩種方式看看!無論哪種方式,兩個子問題都在操作更小的列表,因此您知道遞迴最終會終止。哪個試驗導致更長的公共子序列就是贏家。

我們不透過從陣列中刪除該項來“丟棄”它,而是使用陣列切片。例如,切片

a[1,..,5]

表示元素

{a[1],a[2],a[3],a[4],a[5]}

陣列本身的陣列。如果您的語言不支援切片,您將不得不將開始和/或結束索引與整個陣列一起傳遞。這裡,切片只有以下形式

a[1,..]

它在使用 0 作為陣列中第一個元素的索引時,會產生一個不包含第 0 個元素的陣列切片。(因此,此演算法的非切片版本只需要傳遞開始有效索引,並且該值必須從完整陣列的長度中減去才能獲得偽切片的長度。)

// lcs -- returns the longest common subsequence of a and b
function lcs(array a, array b): array
  if a.length == 0 OR b.length == 0:
    // if we're at the end of either list, then the lcs is empty
    
    return new array {}
  else-if a[0] == b[0]:
    // if the start element is the same in both, then it is on the lcs,
    // so we just recurse on the remainder of both lists.
    
    return append(new array {a[0]}, lcs(a[1,..], b[1,..]))
  else
    // we don't know which list we should discard from. Try both ways,
    // pick whichever is better.
    
    let discard_a := lcs(a[1,..], b)
    let discard_b := lcs(a, b[1,..])
    
    if discard_a.length > discard_b.length:
      let result := discard_a
    else
      let result := discard_b
    fi
    return result
  fi
end

最短路徑問題 (窮舉版本)

[編輯 | 編輯原始碼]

在後面的部分中,將改進為 Dijkstra 演算法。

最大獨立集

[編輯 | 編輯原始碼]

邊界搜尋

[編輯 | 編輯原始碼]

如果您已經找到了“更好”的東西,並且您正在一個永遠不會像您已經看到的那樣好的分支上,您可以提前終止該分支。(要使用的示例:以 1 2 開頭的數字之和,然後每個後續數字是任何數字加上最後一個數字的總和。顯示效能改進。)

受約束的 3 色著色

[編輯 | 編輯原始碼]

這個問題沒有直接的自相似性,因此問題首先需要進行概括。方法:如果沒有自相似性,請嘗試概括問題,直到它具有自相似性。

旅行商問題

[編輯 | 編輯原始碼]

在這裡,回溯是已知的最佳解決方案之一。



頂部,章節:123456789A

華夏公益教科書