青銅三人行之組合總和

先說一個消息,爲了方便互相交流學習,青銅三人行建了個微信羣,感興趣的夥伴可以掃碼加下面的小助手抱你入羣哦!
青銅三人行小助手

每週一題,代碼無敵~這次讓我們回到算法本身,來探討一下回溯算法

青銅三人行——每週一題@組合總和

力扣題目

給定一個無重複元素的數組 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]
]

思路

對於這道題來說,最困難的點就在於「candidates 中的數字可以無限制重複被選取」, 這個條件導致了最後結果的集合裏面可以選的元素的數量不一定,直接導致了滿足條件的可能性組合的數量暴增,給程序的複雜性帶來了一定的挑戰。

面對這種情況,我們就不得不嘗試組合出各種能容納最多元素的組合。在學習算法的過程中,可以理解到,類似面臨這種「查找最遠路徑」的問題,最適合的算法場景就是「深度優先」搜索算法。

回到這個題目當中,我們想要找出所有滿足條件的組合,就是要「從長到短」、「從小到大」嘗試所有相加不超過 target 的組合。而在如果遇到組合超過 target 的情況,則回到更「短」一點的組合嘗試其他可能性:

以這道題目的 示例2 爲例:
在這裏插入圖片描述

如圖所示,我們從左往右,每次嘗試去取到最多元素的可能性,當組合的和大於或等於 target 的時候(等於的時候要記錄結果),就返回上一層,嘗試新的組合(新的組合的數要比之前的大)。相當於在這裏「剪掉」了後面的可能性,並「返回」了上一層去嘗試。因此這種算法也被稱爲了「回溯剪枝算法」。提一下,「回溯剪枝算法」其實就是一種「深度優先查找」(DFS) 算法。

對於這個題來說,這個算法必須在有序數組中才可以纔行,因爲數值越大,深度就越有限。

解法

瞭解了思路,我們先來看看 Helen 的解法

/**
 * @param {number[]} candidates
 * @param {number} target
 * @return {number[][]}
 */
var combinationSum = function(candidates, target) {
    const result = [];
    let tmpPath = [];
    let start = 0;

    candidates = candidates.sort((a, b) => a - b);

    function backtrack(tmpPath, target, start) {
        if (target === 0) {
            result.push(tmpPath);
            return; 
        }

        for (let index = start; index < candidates.length; index++) {
            if (target < 0) break;
            tmpPath.push(candidates[index]);
            backtrack(tmpPath.slice(), target - candidates[index], index);
            tmpPath.pop();     //回溯
        }
    }

    backtrack(tmpPath, target, start);

    return result;
};

在這裏,helen 定義了一個 backtrack 的回溯函數,在其中遍歷了 candidates 數組,並在其中遞歸地又去回溯,從而找出所有的可能性。

注意其中 target < 0 這個條件,其實就是一個“剪枝”,把超出的可能性剪掉。只不過用了減法的形式,有點反直覺,可以多琢磨下。

而書香稍微改了下結構,把代碼縮短了點:

var combinationSum = function (candidates, target) {
    const sliceArr = candidates.filter(item => item <= target).sort((a, b) => a - b);
    const finalArr = [];
    function findCompose(target, offset, last) {
        for (let i = offset; i < sliceArr.length; i++) {
            const subTarget = target - sliceArr[i];
            if (subTarget == 0 ) finalArr.push([...last, sliceArr[i]]);
            if (subTarget > 0) findCompose(subTarget, i, [...last, sliceArr[i]]);
        }
    }
    findCompose(target, 0, []);
    return finalArr;
};

其實差不太多,不過是因爲用了 es6 數組的解構賦值方法,沒有把每個分支都 push 進去,所以回溯的時候就可以少寫一個 pop 啦~

extra

曾大師 go 語言時間~
他在註釋裏順便給我們解釋了 「示例1」,並且直接將函數命名成了 DFS (深度優先搜索) 。果然很有算法大師的風範呀!

// 深度搜索加減枝,具體過程如下
// 2 -> 22 -> 222 -> 2222 -> 223(合適) -> 23 -> 233 -> 26 -> 3 -> 33 -> 333 -> 36 -> 6 -> 66 ->7(合適)

var result [][]int
var currCandidate []int
func combinationSum(candidates []int, target int) [][]int {
    sort.Ints(candidates)
    result=make([][]int,0)
    currCandidate=make([]int,0)
    DFS(target,candidates)
    return result
}

func DFS(target int,candidates []int) int {

    if getSum(currCandidate)==target{
        temCandidate:=make([]int,len(currCandidate))
        copy(temCandidate,currCandidate)
        result=append(result,temCandidate)
        return 0
    } else if getSum(currCandidate)>target{
        return -1
    }  else{ //主要看這裏用0代表相同,-1代表已經超過了當前target,1則表示還能繼續加
        for i:=0;i<len(candidates);i++{
            currCandidate=append(currCandidate,candidates[i])
            temp:= DFS(target,candidates[i:])
            currCandidate=currCandidate[:len(currCandidate)-1]
            if temp<=0{
                break
            }
        }
    }
    return 1
}

func getSum(nums []int) int {
    sum:=0
    for i:=0;i<len(nums);i++{
        sum+=nums[i]
    }
    return sum
}

結語
OK,這樣看下來,其實算法離我們也沒有那麼遠。事實上如此,算法本身也是爲了解決具體的問題而誕生的。而我們在練習的過程中,要理解到算法具體解決了什麼問題,就可以在遇到類似的問題的時候迎刃而解啦~

下週見~


三人行

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