「leetcode」78. 子集【回溯演算法】詳解!

程式碼隨想錄發表於2020-11-23

本文 https://github.com/youngyangyang04/leetcode-master 已經收錄,裡面還有leetcode刷題攻略、各個型別經典題目刷題順序、思維導圖,可以fork到自己倉庫,有空看一看一定會有所收穫,如果對你有幫助也給一個star支援一下吧!

第78題. 子集

題目地址:https://leetcode-cn.com/problems/subsets/

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

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

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

思路

求子集問題和回溯演算法:求組合問題!回溯演算法:分割問題!又不一樣了。

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

其實子集也是一種組合問題,因為它的集合是無序的,子集{1,2} 和 子集{2,1}是一樣的。

那麼既然是無序,取過的元素不會重複取,寫回溯演算法的時候,for就要從startIndex開始,而不是從0開始!

有同學問了,什麼時候for可以從0開始呢?

求排列問題的時候,就要從0開始,因為集合是有序的,{1, 2} 和{2, 1}是兩個集合,排列問題我們後續的文章就會講到的。

以示例中nums = [1,2,3]為例把求子集抽象為樹型結構,如下:

78.子集

從圖中紅線部分,可以看出遍歷這個樹的時候,把所有節點都記錄下來,就是要求的子集集合

回溯三部曲

  • 遞迴函式引數

全域性變數陣列path為子集收集元素,二維陣列result存放子集組合。(也可以放到遞迴函式引數裡)

遞迴函式引數在上面講到了,需要startIndex。

程式碼如下:

vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& nums, int startIndex) {
  • 遞迴終止條件

從圖中可以看出:

78.子集

剩餘集合為空的時候,就是葉子節點。

那麼什麼時候剩餘集合為空呢?

就是startIndex已經大於陣列的長度了,就終止了,因為沒有元素可取了,程式碼如下:

if (startIndex >= nums.size()) {
    return;
}

其實可以不需要加終止條件,因為startIndex >= nums.size(),本層for迴圈本來也結束了

  • 單層搜尋邏輯

求取子集問題,不需要任何剪枝!因為子集就是要遍歷整棵樹

那麼單層遞迴邏輯程式碼如下:

for (int i = startIndex; i < nums.size(); i++) {
    path.push_back(nums[i]);    // 子集收集元素
    backtracking(nums, i + 1);  // 注意從i+1開始,元素不重複取
    path.pop_back();            // 回溯
}

C++程式碼

根據關於回溯演算法,你該瞭解這些!給出的回溯演算法模板:

void backtracking(引數) {
    if (終止條件) {
        存放結果;
        return;
    }

    for (選擇:本層集合中元素(樹中節點孩子的數量就是集合的大小)) {
        處理節點;
        backtracking(路徑,選擇列表); // 遞迴
        回溯,撤銷處理結果
    }
}

可以寫出如下回溯演算法C++程式碼:

class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(vector<int>& nums, int startIndex) {
        result.push_back(path); // 收集子集,要放在終止新增的上面,否則會漏掉自己
        if (startIndex >= nums.size()) { // 終止條件可以不加
            return;
        }
        for (int i = startIndex; i < nums.size(); i++) {
            path.push_back(nums[i]);
            backtracking(nums, i + 1);
            path.pop_back();
        }
    }
public:
    vector<vector<int>> subsets(vector<int>& nums) {
        result.clear();
        path.clear();
        backtracking(nums, 0);
        return result;
    }
};

在註釋中,可以發現可以不寫終止條件,因為本來我們就要遍歷整顆樹。

有的同學可能擔心不寫終止條件會不會無限遞迴?

並不會,因為每次遞迴的下一層就是從i+1開始的。

總結

相信大家經過了

洗禮之後,發現子集問題還真的有點簡單了,其實這就是一道標準的模板題。

但是要清楚子集問題和組合問題、分割問題的的區別,子集是收集樹形結構中樹的所有節點的結果

而組合問題、分割問題是收集樹形結構中葉子節點的結果

就醬,如果感覺收穫滿滿,就幫Carl宣傳一波「程式碼隨想錄」吧!

我是程式設計師Carl,可以找我組隊刷題,也可以在B站上找到我,本文leetcode刷題攻略已收錄,更多精彩演算法文章盡在公眾號:程式碼隨想錄,關注後就會發現和「程式碼隨想錄」相見恨晚!

如果感覺對你有幫助,不要吝嗇給一個?吧!

相關文章