LeetCode40.組合總和II
力扣題目連結(opens new window)
給定一個陣列 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]
]
思路
這道題目和[LeetCode39. 組合總和 - Tomorrowland_D - 部落格園 (cnblogs.com)]()如下區別:
- 本題candidates 中的每個數字在每個組合中只能使用一次。
- 本題陣列candidates的元素是有重複的,而[LeetCode39. 組合總和 - Tomorrowland_D - 部落格園 (cnblogs.com)]()是無重複元素的陣列candidates
最後本題和[LeetCode39. 組合總和 - Tomorrowland_D - 部落格園 (cnblogs.com)]()要求一樣,但是解集不能包含重複的組合。
本題的難點在於區別2中:集合(陣列candidates)有重複元素,但還不能有重複的組合。
-
我們直觀的可以想到以下辦法:我把所有組合求出來,再用set或者map去重,這麼做很容易超時!
-
所以要在搜尋的過程中就去掉重複組合。
-
這個去重為什麼很難理解呢,所謂去重,其實就是使用過的元素不能重複選取。 這麼一說好像很簡單!
-
都知道組合問題可以抽象為樹形結構,那麼“使用過”在這個樹形結構上是有兩個維度的,一個維度是同一樹枝上使用過,一個維度是同一樹層上使用過。沒有理解這兩個層面上的“使用過” 是造成大家沒有徹底理解去重的根本原因。
-
那麼問題來了,我們是要同一樹層上使用過,還是同一樹枝上使用過呢?
-
回看一下題目,元素在同一個組合內是可以重複的,怎麼重複都沒事,但兩個組合不能相同。
-
所以我們要去重的是同一樹層上的“使用過”,同一樹枝上的都是一個組合裡的元素,不用去重。
-
為了理解去重我們來舉一個例子,candidates = [1, 1, 2], target = 3,(方便起見candidates已經排序了)
強調一下,樹層去重的話,需要對陣列排序!
1.遞迴的引數
- 和Leetcode39組合一樣,需要result存放結果,path存放單條路徑
- sum來存放當前的所有和
- startindex來標誌當前遍歷的位置
- 還需要一個used陣列來用於去重,在下面會重點介紹去重!!!
2.遞迴的結束條件
- 與上題一樣,當sum>=targetSum就返回,如果等於,我們就收集結果
3.單層搜尋的邏輯
這裡與LeetCode39.組合總和最大的不同就是要去重了。
前面我們提到:要去重的是“同一樹層上的使用過”,如何判斷同一樹層上元素(相同的元素)是否使用過了呢。
如果candidates[i] == candidates[i - 1]
並且 used[i - 1] == false
,就說明:前一個樹枝,使用了candidates[i - 1],也就是說同一樹層使用過candidates[i - 1]。
此時for迴圈裡就應該做continue的操作。
這塊比較抽象,如圖:
我在圖中將used的變化用橘黃色標註上,可以看出在candidates[i] == candidates[i - 1]相同的情況下:
-
used[i - 1] == true,說明同一樹枝candidates[i - 1]使用過
-
used[i - 1] == false,說明同一樹層candidates[i - 1]使用過
-
為什麼 used[i - 1] == false 就是同一樹層呢,因為同一樹層,used[i - 1] == false 才能表示,當前取的 candidates[i] 是從 candidates[i - 1] 回溯而來的。
-
而 used[i - 1] == true,說明是進入下一層遞迴,去下一個數,所以是樹枝上,如圖所示
單層遞迴的程式碼如下:
for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {
// used[i - 1] == true,說明同一樹枝candidates[i - 1]使用過
// used[i - 1] == false,說明同一樹層candidates[i - 1]使用過
// 要對同一樹層使用過的元素進行跳過
if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) {
continue;
}
sum += candidates[i];
path.push_back(candidates[i]);
used[i] = true;
backtracking(candidates, target, sum, i + 1, used); // 和39.組合總和的區別1:這裡是i+1,每個數字在每個組合中只能使用一次
used[i] = false;
sum -= candidates[i];
path.pop_back();
}
程式碼:
class Solution {
public:
vector<int> path;
vector<vector<int> > result;
void backtracking(vector<int> candidates, int targetSum, int sum, int startindex, vector<bool>& used) {
if (sum >= targetSum) {
if (sum == targetSum) result.push_back(path);
return;
}
//這裡的剪枝過程在組合總和中有講到過!
for (int i = startindex; i < candidates.size() && sum + candidates[i]<=targetSum; i++) {
//如果是同一層的相同元素,就去重!也就是跳過本輪迴圈
// used[i - 1] == true,說明同一樹枝candidates[i - 1]使用過
// used[i - 1] == false,說明同一樹層candidates[i - 1]使用過
// 要對同一樹層使用過的元素進行跳過
if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == 0) continue;
sum += candidates[i];
path.push_back(candidates[i]);
used[i] = 1;
//要注意這裡是i+1,與之前講解的組合總和不同,這裡不能夠選取重複的元素
backtracking(candidates, targetSum, sum, i + 1, used);
sum -= candidates[i];
path.pop_back();
//回溯的時候將之前使用過的元素置為0,標誌著這是同一層的元素(樹層),而不是樹枝上的元素
used[i] = 0;
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
path.clear(); result.clear();
if (candidates.size() == 0) return result;
vector<bool> used(candidates.size(), 0);
//注意這裡一定要排序
sort(candidates.begin(), candidates.end());
backtracking(candidates, target, 0, 0, used);
return result;
}
};
注意:
- 本文中還多次引用到了作者程式碼隨想錄 的原圖,想要深入瞭解的可以關注原作者,閱讀原作者的文章![程式碼隨想錄 (programmercarl.com)]()