先說一個消息,爲了方便互相交流學習,青銅三人行建了個微信羣,感興趣的夥伴可以掃碼加下面的小助手抱你入羣哦!
每週一題,代碼無敵~這次讓我們回到算法本身,來探討一下回溯算法
青銅三人行——每週一題@組合總和
力扣題目
給定一個無重複元素的數組 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,這樣看下來,其實算法離我們也沒有那麼遠。事實上如此,算法本身也是爲了解決具體的問題而誕生的。而我們在練習的過程中,要理解到算法具體解決了什麼問題,就可以在遇到類似的問題的時候迎刃而解啦~
下週見~
三人行