網際網路公司高頻面試題目「回溯算法」:求組合總和II

2020-11-04 代碼隨想錄

這篇可以說是全網把組合問題如何去重,講得最清晰的了!

通知:我將公眾號文章和學習相關的資料整理到了Github :https://github.com/youngyangyang04/leetcode-master,方便大家在電腦上學習,可以fork到自己倉庫,順便也給個star支持一波吧!

40.組合總和II

題目連結:https://leetcode-cn.com/problems/combination-sum-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]
]

思路

這道題目和如下區別:

  1. 本題candidates 中的每個數字在每個組合中只能使用一次。
  2. 本題數組candidates的元素是有重複的,而是無重複元素的數組candidates

最後本題和要求一樣,解集不能包含重複的組合。

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

一些同學可能想了:我把所有組合求出來,再用set或者map去重,這麼做很容易超時!

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

很多同學在去重的問題上想不明白,其實很多題解也沒有講清楚,反正代碼是能過的,感覺是那麼回事,稀裡糊塗的先把題目過了。

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

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

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

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

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

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

選擇過程樹形結構如圖所示:

可以看到圖中,每個節點相對於我多加了used數組,這個used數組下面會重點介紹。

回溯三部曲

  • 「遞歸函數參數」

與套路相同,此題還需要加一個bool型數組used,用來記錄同一樹枝上的元素是否使用過。

這個集合去重的重任就是used來完成的。

代碼如下:

vector<vector<int>> result; // 存放組合集合vector<int> path;           // 符合條件的組合void backtracking(vector<int>& candidates, int target, int sum, int startIndex, vector<bool>& used) {

  • 「遞歸終止條件」

與相同,終止條件為 sum > targetsum == target

代碼如下:

if (sum > target) { // 這個條件其實可以省略     return;}if (sum == target) {    result.push_back(path);    return;}

sum > target 這個條件其實可以省略,因為和在遞歸單層遍歷的時候,會有剪枝的操作,下面會介紹到。

  • 「單層搜索的邏輯」

這裡與最大的不同就是要去重了。

前面我們提到:要去重的是「同一樹層上的使用過」,如果判斷同一樹層上元素(相同的元素)是否使用過了呢。

「如果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]使用過

「這塊去重的邏輯很抽象,網上搜的題解基本沒有能講清楚的,如果大家之前思考過這個問題或者刷過這道題目,看到這裡一定會感覺通透了很多!」

那麼單層搜索的邏輯代碼如下:

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();}

「注意sum + candidates[i] <= target為剪枝操作,在有講解過!」

C++代碼

回溯三部曲分析完了,整體C++代碼如下:

class Solution {private:    vector<vector<int>> result;    vector<int> path;    void backtracking(vector<int>& candidates, int target, int sum, int startIndex, vector<bool>& used) {        if (sum == target) {            result.push_back(path);            return;        }        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();        }    }public:    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {        vector<bool> used(candidates.size(), false);        path.clear();        result.clear();        // 首先把給candidates排序,讓其相同的元素都挨在一起。        sort(candidates.begin(), candidates.end());        backtracking(candidates, target, 0, 0, used);        return result;    }};

總結

本題同樣是求組合總和,但就是因為其數組candidates有重複元素,而要求不能有重複的組合,所以相對於難度提升了不少。

「關鍵是去重的邏輯,代碼很簡單,網上一搜一大把,但幾乎沒有能把這塊代碼含義講明白的,基本都是給出代碼,然後說這就是去重了,究竟怎麼個去重法也是模稜兩可」

所以Carl有必要把去重的這塊徹徹底底地給大家講清楚,「就連「樹層去重「和「樹枝去重」都是我自創的詞彙,希望對大家理解有幫助!」

就醬,如果感覺「代碼隨想錄」誠意滿滿,就幫Carl宣傳一波吧,感謝啦!

我是程式設計師Carl,個人主頁:https://github.com/youngyangyang04

這裡每天8:35準時推送一道經典算法題目,我選擇的每道題目都不是孤立的,而是由淺入深,環環相扣,幫你梳理算法知識脈絡,輕鬆學算法!

期待你的關注

相關焦點

  • 網際網路高頻面試題目:「回溯算法」求組合總和
    = targetSum 直接返回}「單層搜索過程」本題和回溯算法:求組合問題!     path.pop_back(); // 回溯 }「別忘了處理過程 和 回溯過程是一一對應的,處理有加,回溯就要有減!」
  • 網際網路公司高頻面試題目:「回溯算法」電話號碼的字母組合
    大家應該感覺出和回溯算法:求組合問題!遇到的一樣的問題,就是這for循環的層數如何寫出來,此時又是回溯法登場的時候了。n個for循環的問題對於回溯法還不了解的同學看這篇:關於回溯算法,你該了解這些!「因為本題每一個數字代表的是不同集合,也就是求不同集合之間的組合,而77. 組合和216.組合總和III都是是求同一個集合中的組合!」
  • 經典面試題目「回溯算法」求組合總和(二)
    組合總和題目連結:https://leetcode-cn.com/problems/combination-sum/給定一個無重複元素的數組 candidates 和一個目標數 target ,找出 candidates 中所有可以使數字和為 target 的組合。candidates 中的數字可以無限制重複被選取。
  • 網際網路公司高頻面試題目「回溯算法」求子集問題(二)
    第90題.子集II題目連結:https://leetcode-cn.com/problems/subsets-ii/給定一個可能包含重複元素的整數數組這道題目和區別就是集合裡有重複元素了,而且求取的子集要去重。
  • 網際網路公司經典面試題目:「回溯算法」求組合問題
    組合題目連結:https://leetcode-cn.com/problems/combinations/給定兩個整數 n 和 k,返回 1 ... n 中所有可能的 k 個數的組合。那麼回溯法怎麼暴力搜呢?上面我們說了「要解決 n為100,k為50的情況,暴力寫法需要嵌套50層for循環,那麼回溯法就用遞歸來解決嵌套層數的問題」。
  • 網際網路公司高頻面試題目「回溯算法」:分割回文串
    這種題目,想用for循環暴力解法,可能都不那麼容易寫出來,所以要換一種暴力的方式,就是回溯。一些同學可能想不清楚 回溯究竟是如果切割字符串呢?我們來分析一下切割,「其實切割問題類似組合問題」。此時可以發現,切割問題的回溯搜索的過程和組合問題的回溯搜索的過程是差不多的。
  • 網際網路公司高頻面試題目「回溯算法」求子集問題
    如果把 子集問題、組合問題、分割問題都抽象為一棵樹的話,「那麼組合問題和分割問題都是收集樹的葉子節點,而子集問題是找樹的所有節點!」「那麼既然是無序,取過的元素不會重複取,寫回溯算法的時候,for就要從startIndex開始,而不是從0開始!」有同學問了,什麼時候for可以從0開始呢?
  • 經典面試題目本周小結!(回溯算法系列一)
    「回溯是遞歸的副產品,只要有遞歸就會有回溯」。回溯法就是暴力搜索,並不是什麼高效的算法,最多在剪枝一下。組合總和問題還有一些花樣,下周還會介紹到。周五在 中,開始用多個集合來求組合,還是熟悉的模板題目,但是有一些細節。
  • 網際網路公司高頻面試題目「回溯算法」遞增子序列
    「其實用數組來做哈希,效率就高了很多」。注意題目中說了,數值範圍[-100,100],所以完全可以用數組來做哈希。「對於養成思維定式或者套模板套嗨了的同學,這道題起到了很好的警醒作用。更重要的是拓展了大家的思路!」
  • 網際網路公司高頻面試題目:一網打盡二叉樹!(三十三道題目精講)
    「三十三篇二叉樹的文章」,詳細講解了「30+二叉樹經典題目」,一直堅持下來的錄友們一定會二叉樹有深刻理解了。在每一道二叉樹的題目中,我都使用了「遞歸三部曲」來分析題目,相信大家以後看到二叉樹,看到遞歸,都會想:返回值、參數是什麼?終止條件是什麼?單層邏輯是什麼?
  • 騰訊面試題目「回溯算法」排列問題
    所以正如我們在關於 所講的為什麼回溯法是暴力搜索,效率這麼低,還要用它?「因為一些問題能暴力搜出來就已經很不錯了!」] 和[2,1] 是兩個集合,這和之前分析的子集以及組合所不同的地方」。每層都是從0開始搜索而不是startIndex需要used數組記錄path裡都放了哪些元素了排列問題是回溯算法解決的經典題目
  • 經典面試題目「回溯算法」解數獨
    「二維遞歸」。大家已經跟著「代碼隨想錄」刷過了如下回溯法題目,例如:77.組合(組合問題),131.分割回文串(分割問題),78.子集(子集問題),46.全排列(排列問題),以及51.N皇后(N皇后問題),其實這些題目都是一維遞歸。
  • 騰訊面試題目「回溯算法」排列問題(二)
    47.全排列 II題目連結:https://leetcode-cn.com/problems/permutations-ii/給定一個可包含重複數字的序列 nums ,按任意順序「給定一個可包含重複數字的序列」,要返回「所有不重複的全排列」。
  • 回溯算法:組合問題如何剪枝?
    「那麼重點來了,來都來了,順便給一個star吧,哈哈」在中,我們通過回溯搜索法,解決了n個數中求k個數的組合問題。文中的回溯法是可以剪枝優化的,本篇我們繼續來看一下題目。連結:https://leetcode-cn.com/problems/combinations/「看本篇之前,需要先看回溯算法:求組合問題!」。大家先回憶一下[77.
  • 經典面試題目「回溯算法」N皇后問題
    (引用自 百度百科 - 皇后 )思路都知道n皇后問題是回溯算法解決的經典問題,但是用回溯解決多了組合、切割、子集、排列問題之後,遇到這種二位矩陣還會有點不知所措那麼我們用皇后們的約束條件,來回溯搜索這顆樹,「只要搜索到了樹的葉子節點,說明就找到了皇后們的合理位置了」。
  • 關於回溯算法,你該了解這些
    「雖然回溯法很難,很不好理解,但是回溯法並不是什麼高效的算法」。另外,會有一些同學可能分不清什麼是組合,什麼是排列?「組合是不強調元素順序的,排列是強調元素順序」。「回溯法解決的問題都可以抽象為樹形結構」,是的,我指的是所有回溯法的問題都可以抽象為樹形結構!
  • (回溯算法系列三)
    「這道題目神奇的地方就是這塊網上的資料魚龍混雜,一些所謂的經典面試書籍根本不講回溯算法,算法書籍對這塊也避而不談,感覺就像是算法裡模糊的邊界。「所以這塊就說一說我個人理解,對內容持開放態度,集思廣益,歡迎大家來討論!」
  • 網際網路公司高頻面試題目:如何把二叉搜索樹轉成累加樹
    那麼有序的元素如果求累加呢?「其實這就是一棵樹,大家可能看起來有點彆扭,換一個角度來看,這就是一個有序數組[2, 5, 13],求從後到前的累加數組,也就是[20, 18, 13],是不是感覺這就簡單了。」
  • 網際網路公司面試題目之哈希表總結篇!(每逢總結必經典)
    對於哈希表,要知道「哈希函數」和「哈希碰撞」在哈希表中的作用這道題目包含小寫字母,那麼使用數組來做哈希最合適不過。在中同樣要求只有小寫字母,那麼就給我們濃濃的暗示,用數組!本題和很像,是求 字符串a 和 字符串b 是否可以相互組成,在中是求字符串a能否組成字符串b,而不用管字符串b 能不能組成字符串a。一些同學可能想,用數組幹啥,都用map不就完事了。
  • 網際網路面試題目「貪心算法」K次取反後最大化的數組和
    很多錄友都反饋昨天的題目雖然這道題目大家做的時候,可能都不會去想什麼貪心算法,一鼓作氣,就AC了。「我這裡其實是為了給大家展現出來 經常被大家忽略的貪心思路,這麼一道簡單題,就用了兩次貪心!」這也算是算法?我認為這不是貪心?本題其實很簡單,不會貪心算法的同學都可以做出來,但是我還是全程用貪心的思路來講解。因為貪心的思考方式一定要有!