【LeetCode回溯演演算法#07】子集問題I+II,鞏固解題模板並詳解回溯演演算法中的去重問題

2023-03-11 12:01:11

子集

力扣題目連結

給你一個整數陣列 nums ,陣列中的元素 互不相同 。返回該陣列所有可能的子集(冪集)。

解集 不能 包含重複的子集。你可以按 任意順序 返回解集。

範例 1:

輸入:nums = [1,2,3]
輸出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
範例 2:

輸入:nums = [0]
輸出:[[],[0]]

思路

如果把 子集問題、組合問題、分割問題都抽象為一棵樹的話,那麼組合問題和分割問題都是收集樹的葉子節點,而子集問題是找樹的所有節點

其實這題就是模板題,與 組合總和分割回文串 不同的是,這題需要全部收集遍歷到的值,包括最開始path為空也要算進結果陣列中

那就套用回溯問題的解題模板即可,真的就是直接套

程式碼

class Solution {
private:
    //定義結果陣列
    vector<int> path;
    vector<vector<int>> res;
    //確定回溯函數的引數和返回值
    void backtracking(vector<int>& nums, int beginIndex){
        res.push_back(path);//放在這裡,不會把path為空的情況漏掉
        //確定停止條件
        if(beginIndex >= nums.size()){//寫不寫都行,因為當滿足條件時for迴圈也到頭了,也會自己停止
            return;
        }
        //確定單層處理邏輯
        //直接遍歷獲取樹結構中的所有值
        for(int i = beginIndex; i < nums.size(); ++i){
            path.push_back(nums[i]);
            backtracking(nums, i + 1);//因為子集不能有重複的,因此要跳過本次的值
            path.pop_back();
        }
    }

public:
    vector<vector<int>> subsets(vector<int>& nums) {
        backtracking(nums, 0);
        return res;
    }
};

關於backtracking(nums, i + 1)為什麼要跳,對比看->組合總和思路部分單層處理邏輯來理解

子集II

力扣題目連結(opens new window)

給定一個可能包含重複元素的整數陣列 nums,返回該陣列所有可能的子集(冪集)。

說明:解集不能包含重複的子集。

範例:

  • 輸入: [1,2,2]
  • 輸出: [ [2], [1], [1,2,2], [2,2], [1,2], [] ]

思路

相較 子集I ,這題題目加了「可能包含重複元素,但不能包含重複子集

這意味著什麼?又得去重

經過 組合總合II 的訓練,現在你知道了我們可以使用一個布林陣列used來標記用過的元素,並通過該陣列的狀態以及相鄰相同元素來進行去重操作

本題的其他部分程式碼與 子集I 區別不大,在其基礎之上新增去重邏輯就行了

正好這裡可以很明顯的體現出去重邏輯應該寫在哪些地方

程式碼

class Solution {
private:
    //定義結果陣列
    vector<int> path;
    vector<vector<int>> res;
    //確定回溯函數的引數和返回值
    void backtracking(vector<int>& nums, int beginIndex, vector<bool>& used){
        res.push_back(path);//放在這裡,不會把path為空的情況漏掉
        //確定停止條件
        if(beginIndex >= nums.size()){//寫不寫都行,因為當滿足條件時for迴圈也到頭了,也會自己停止
            return;
        }
        //確定單層處理邏輯
        //直接遍歷獲取樹結構中的所有值
        for(int i = beginIndex; i < nums.size(); ++i){
            if(i > 0 && nums[i] == nums[i - 1] && used[i - 1] == 0){//去重邏輯詳見 組合總和II
                continue;//在單層遍歷時有重複值就跳過
            }
            path.push_back(nums[i]);
            used[i] = true;//記錄使用的元素
            backtracking(nums, i + 1, used);//因為子集不能有重複的,因此要跳過本次的值
            used[i] = false;//回溯
            path.pop_back();
        }
    }

public:
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        vector<bool> used(nums.size(), false);
        //排序
        sort(nums.begin(), nums.end());
        backtracking(nums, 0, used);
        return res;
    }
};

與上一題的程式碼對比來看不難發現

新增的去重邏輯大部分都在單層處理部分

這也和之前我們在組合總和II中分析的一致,即:在被視為樹結構的回溯問題中,「樹層」中出現重複是不行的,但是縱向的「樹枝」中可以出現重複

下面總結一下目前使用的回溯演演算法模板中的去重問題

去重操作總結

本題與組合總和II都應用了「去重」這一操作

這兩個問題都在題幹中給出了類似的要求

(本題)「給定一個可能包含重複元素的整數陣列 nums...解集不能包含重複的組合」

(組合)「candidates 中的每個數位在每個組合中只能使用一次...解集不能包含重複的組合」

發現什麼了嗎?

這類題目都會給一個有重複值(或者暗示有重複值)的資料,然後讓你找出所有組合,並且組合不能有重複

以後看見這些字眼要想到去重操作

再回顧一下出現重複值的本質

重複值的本質

當我們對一個已經排序了的資料進行回溯遍歷時,當相鄰的兩個重複值中的前一個已經通過遞迴遍歷完樹結構的一個分支後,此時因為回溯我們會回到最開始觸發遞迴的位置,然後繼續從相鄰重複值的後一個值再次開啟遞迴遍歷

那之後開啟的遞迴遍歷中的所有組合(或者是需要獲取的某種結果)一定都在前一次遞迴遍歷中出現過了

這就是重複值的本質

處理方法
邏輯

為了處理上述問題,我們引入一個布林陣列used來記錄當前使用過的元素

used陣列的大小會和題目給的陣列大小一樣(為了一一對應陣列中的元素)

當我們使用遞迴到單層處理部分時,此時我們會記錄組合結果,同時我們也在used中記錄當前組合使用到的元素

等到下一次再記錄組合結果之前,就判斷一下used的狀態

關鍵點來了,也就是重複的條件

重複的條件

重複的條件是,判斷當前遍歷到的元素(也就是要用到組合中的元素)的前一個元素與其本身是否構成相鄰重複值

也就是當前這個元素在上次遍歷時是否也出現過

如果確實出現過,那麼再看當前used的前一個元素位置是否有記錄

如果有記錄(used[i - 1] != 0),那麼意味著本次遞迴是處於樹結構的一條枝幹中,可以使用重複值

如果沒有記錄(used[i - 1] == 0),說明本次遞迴的上一次遞迴已經使用過相鄰重複值了,那麼本次遞迴再進行下去的結果將全部被包含於上次的結果中,因此要跳過本次for迴圈,使用下一個值進行遞迴遍歷尋找新的組合

可見,used陣列是負責判斷當前處於"樹枝"還是"樹層"的一個輔助工具

而關鍵點還是得有相鄰重複值,這是造成重複值出現的本質核心

步驟總結

1、在回溯函數中引入used布林陣列

2、在單層處理邏輯中加入對於相鄰重複值和used狀態的判斷

3、在主函數中對用來尋找組合的陣列進行提前排序(使用sort)

以後有新理解再補充