LeetCode40.組合總和II

Tomorrowland_D發表於2024-08-14

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)]()如下區別:

  1. 本題candidates 中的每個數字在每個組合中只能使用一次。
  2. 本題陣列candidates的元素是有重複的,而[LeetCode39. 組合總和 - Tomorrowland_D - 部落格園 (cnblogs.com)]()是無重複元素的陣列candidates

最後本題和[LeetCode39. 組合總和 - Tomorrowland_D - 部落格園 (cnblogs.com)]()要求一樣,但是解集不能包含重複的組合。

本題的難點在於區別2中:集合(陣列candidates)有重複元素,但還不能有重複的組合

  • 我們直觀的可以想到以下辦法:我把所有組合求出來,再用set或者map去重,這麼做很容易超時!

  • 所以要在搜尋的過程中就去掉重複組合。

  • 這個去重為什麼很難理解呢,所謂去重,其實就是使用過的元素不能重複選取。 這麼一說好像很簡單!

  • 都知道組合問題可以抽象為樹形結構,那麼“使用過”在這個樹形結構上是有兩個維度的,一個維度是同一樹枝上使用過,一個維度是同一樹層上使用過。沒有理解這兩個層面上的“使用過” 是造成大家沒有徹底理解去重的根本原因。

  • 那麼問題來了,我們是要同一樹層上使用過,還是同一樹枝上使用過呢?

  • 回看一下題目,元素在同一個組合內是可以重複的,怎麼重複都沒事,但兩個組合不能相同。

  • 所以我們要去重的是同一樹層上的“使用過”,同一樹枝上的都是一個組合裡的元素,不用去重

  • 為了理解去重我們來舉一個例子,candidates = [1, 1, 2], target = 3,(方便起見candidates已經排序了)

強調一下,樹層去重的話,需要對陣列排序!

40.組合總和II

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的操作。

這塊比較抽象,如圖:

40.組合總和II1

我在圖中將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)]()

相關文章