回溯法解決排列組合問題

回溯法簡介

回溯法(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
    }
}

如果我的文章可以幫助到大家,請不吝賜贊。另外,如果想及時收到更多關於算法和前端方面的訊息,可以關注我的博客。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章