回溯法簡介
回溯法(Backtrack)其實是基於遞歸來實現的。
但是它的思考邏輯很有意思,和走迷宮一樣。比如我們走到一個分叉口,我們不知道哪一個路口是正確的,但是我們可以先隨便選擇一個路口。如果最後走不通,我們可以原地返回,在之前的分叉口重新抉擇。
正是因爲這種回溯的思考方式,所以這種算法稱之爲回溯法。
籠統地講,回溯算法很多時候都應用在“搜索”這類問題上。不過這裏說的搜索,並不是狹義的指我們前面講過的圖的搜索算法,而是在一組可能的解中,搜索滿足期望的解。
理論的東西還是很抽象,下面講一講用回溯法解決的實際問題。
本文題目來自力扣,代碼均爲 JavaScript,全部答案通過了測試,可放心食用。
排列組合問題
組合總和
題目:
給定一個無重複元素的數組 candidates 和一個目標數 target ,找出 candidates 中所有可以使數字和爲 target 的組合。
candidates 中的數字可以無限制重複被選取。
說明:
- 所有數字(包括 target)都是正整數。
- 解集不能包含重複的組合。
示例 1:
輸入: candidates = [2,3,6,7], target = 7,
所求解集爲:
[
[7],
[2,2,3]
]
示例 2:
輸入: candidates = [2,3,5], target = 8,
所求解集爲:
[
[2,2,2,2],
[2,3,3],
[3,5]
]
分析:
這一題的解題思路很簡單,我們直接暴力枚舉即可。其實我們開頭說到了,枚舉是回溯法的體現,回溯法的關鍵之處在於“回”,就是什麼時候條件終止,回頭在找。話不多說,直接上代碼。
/**
* @param {number[]} candidates
* @param {number} target
* @return {number[][]}
*/
var combinationSum = function(candidates, target) {
const ans = [], list = []
backtrack(candidates, target, 0, list, ans)
return ans
};
function backtrack(candidates, target, start, list, ans) {
const total = list.reduce((total, a) => {
total += a
return total
}, 0)
if (total >= target) {
if (total === target) {
// 因爲是引用類型,做一次淺拷貝操作
ans.push(list.slice(0))
}
// 這裏開始回溯
return
}
const n = candidates.length
for (let i = start; i < n; i++) {
list.push(candidates[i])
backtrack(candidates, target, i, list, ans)
list.pop()
}
}
組合總和 II
上面一題雖然簡單,但是涵蓋了回溯法的精髓,可以說是一種模板,遇到類似的題可以根據這個模板來套。我們再看一道題,與上面類似,但是修改了一些條件。
題目:
給定一個數組 candidates 和一個目標數 target ,找出 candidates 中所有可以使數字和爲 target 的組合。
candidates 中的每個數字在每個組合中只能使用一次。
說明:
- 所有數字(包括目標數)都是正整數。
- 解集不能包含重複的組合。
示例 1:
輸入: candidates = [10,1,2,7,6,1,5], target = 8,
所求解集爲:
[
[1, 7],
[1, 2, 5],
[2, 6],
[1, 1, 6]
]
示例 2:
輸入: candidates = [2,5,2,1,2], target = 5,
所求解集爲:
[
[1,2,2],
[5]
]
分析:
這一題給定的數組中可能包含重複元素,其次數組中的元素每一個只能使用一次。
/**
* @param {number[]} candidates
* @param {number} target
* @return {number[][]}
*/
var combinationSum2 = function(candidates, target) {
const ans = [], list = []
let total = 0
// 這裏排序主要是爲了和後面去重和搜索剪枝配合
candidates.sort((a, b) => a - b)
backtrack(candidates, target, 0, list, ans, total)
return ans
};
function backtrack(candidates, target, start, list, ans, total) {
if (total >= target) {
if (total === target) {
// 因爲是引用類型,做一次淺拷貝操作
ans.push(list.slice(0))
}
return
}
// 這裏可以使用參數進行傳遞,讀者可以自行優化
const n = candidates.length
for (let i = start; i < n; i++) {
// 模板式去重,這裏注意要大於 start,而不是 0
if (i > start && candidates[i] === candidates[i - 1]) continue
// 搜索剪枝,這一步優化可以大幅提高效率,leetcode 上擊敗 97.79%
if (total + candidates[i] > target) break
list.push(candidates[i])
total += candidates[i]
backtrack(candidates, target, i + 1, list, ans, total)
list.pop()
total -= candidates[i]
}
}
還有一個 組合總和 III 很有意思,限於篇幅,這裏不在介紹,方法都是回溯的思想。
全排列
上面兩道題是關於組合的,組合和排列的區別是前者無關順序,而後者需要注意順序。
題目:
給定一個 沒有重複 數字的序列,返回其所有可能的全排列。
示例:
輸入: [1,2,3]
輸出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
分析:
其實排列問題最大的不同是順序,同樣兩個數,不同的順序其表示的答案也是不同的。
/**
* @param {number[]} nums
* @return {number[][]}
*/
var permute = function(nums) {
const list = [], ans = []
const n = nums.length
const mark = new Array(n)
// 這一步可以不做,默認爲 undefined,和 false 作用類似
mark.fill(false)
backtrack(nums, n, mark, list, ans)
return ans
};
function backtrack(nums, n, mark, list, ans) {
if (list.length === n) {
// ES6 淺拷貝
ans.push([...list])
return
}
for (let i = 0; i < n; i++) {
// 標記哪一個數被使用過。如果第一次不會,記住就好
if (mark[i]) continue
list.push(nums[i])
mark[i] = true
backtrack(nums, n, mark, list, ans)
list.pop()
mark[i] = false
}
}
全排列 II
題目:
給定一個 可包含重複數字 的序列,返回所有不重複的全排列。
示例:
輸入: [1,1,2]
輸出:
[
[1,1,2],
[1,2,1],
[2,1,1]
]
分析:
這題有兩個比較大的變化,第一數組中包含重複數組,第二求全排列,意思每組結果包含所有元素。
/**
* @param {number[]} nums
* @return {number[][]}
*/
var permuteUnique = function(nums) {
const list = [], ans = []
const n = nums.length
const mark = new Array(n)
mark.fill(false)
// 注意,這裏需要排序,和後面模板式去重配合
nums.sort((a, b) => a - b)
backtrack(nums, n, mark, list, ans)
return ans
};
function backtrack(nums, n, mark, list, ans) {
if (list.length === n) {
ans.push(list.slice(0))
return
}
for (let i = 0; i < n; i++) {
// 模板式去重
if (i > 0 && !mark[i - 1] && nums[i] === nums[i - 1]) continue
if (mark[i]) continue
list.push(nums[i])
mark[i] = true
backtrack(nums, n, mark, list, ans)
list.pop()
mark[i] = false
}
}
如果我的文章可以幫助到大家,請不吝賜贊。另外,如果想及時收到更多關於算法和前端方面的訊息,可以關注我的博客。