子集、全排列、組合 問題模式

78. 子集

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

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

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

子集: 對每個元素都有 取與不取 兩個狀態,所以共有2^n種子集,正好可以使用對應位的二進制掩碼來判斷每個元素的狀態,1取0不取,共有1<<n種狀態

拿題目來舉例子:
對應二進制數字爲:000 001 010 011 100 101 110 111

class Solution {
public:
    vector<vector<int>> subsets(vector<int>& nums) {
        int len = nums.size();
        if(len<1) return vector<vector<int>> {{}};

        int bit_map = 0;
        int end = 1<<len; // 2^n class 
        vector<vector<int>> res;
        vector<int> tmp;
        // 2^n個掩碼(狀態),然後取當前掩碼對應的子集,需要遍歷數組 n*2^n
        while(bit_map<end){
            for(int i=0;i<len;i++)
                if((1<<i)&bit_map) tmp.push_back(nums[i]);
            res.push_back(tmp);
            tmp.clear();
            bit_map++;
        }
        return res;
    }
};

複雜度分析

時間複雜度:O(N×2^N),生成所有的子集,並複製到輸出列表中。
空間複雜度:O(2^N),存儲所有子集,共N個元素,每個元素都有可能存在或者不存在。

遞歸回溯 求解

其實存在兩種遞歸求解的方法,一種是把解空間當做二叉樹進行深度優先或者廣度優先的遍歷,一種則是把解空間當做圖(多叉樹)進行深度優先的遍歷。

  1. 當成一個滿二叉樹 求解, 對於每一個 元素 都有 選與不選 兩個結果,對應左右節點,這樣每個葉子節點都是一個結果,但是這樣處理會 遍歷所有節點,速度會稍慢。
    在這裏插入圖片描述
class Solution {
public:
    vector<vector<int>> res;
    void dfs(const vector<int>& nums,int index,vector<int>& ans){
        if(index == nums.size()) res.push_back(ans);
        if(index < nums.size()){
            dfs(nums,index+1,ans);  // 不選
            ans.push_back(nums[index]);
            dfs(nums,index+1,ans); // 選
            ans.pop_back();  // 刪除 回溯到上一個狀態
        }
    }
    vector<vector<int>> subsets(vector<int>& nums) {
        if(nums.size()<1) return vector<vector<int>> {{}};
        vector<int> ans;
        dfs(nums,0,ans);
        return res;
    }
};
// [[],[3],[2],[2,3],[1],[1,3],[1,2],[1,2,3]]

深度搜索,一直不選擇到底,然後回溯慢慢添加元素,

  1. 回溯

它的每一個結點是滿足題目要求的一個解,結點之間的相鄰關係取決於該結點是否還可以塞入其他元素。
在這裏插入圖片描述

class Solution {
public:
    vector<vector<int>> res;
    void backtrack(const vector<int> nums,int index,vector<int>& tmp){
        res.push_back(tmp);
        for(int i=index;i<nums.size();i++){
            tmp.push_back(nums[i]);
            backtrack(nums,i+1,tmp);
            tmp.pop_back();
        }
    }
    vector<vector<int>> subsets(vector<int>& nums) {
        if(!nums.size()) return res;
        vector<int> tmp;
        backtrack(nums,0,tmp);
        return res;
    }
};

遞歸回溯可能模板


void dfs(待搜索集合,遞歸層數,狀態變量1,2,結果集){
	// 終止條件  return 
	// 或者 達到搜索結果 return
	for(可執行分支){
		// 剪枝
		if 遞歸到第幾層, 狀態變量 1, 狀態變量 2, 符合一定的剪枝條件:
            continue
        // do something  對狀態變量的操作
        dfs(待搜索集合,遞歸層數+1,狀態變量1,2,結果集); // 繼續遞歸
        // do something  回溯  對狀態變量的操作
        // 如  push_back(i)  pop_back() 刪除這個元素,回溯到添加這個元素上一步
	}
}

90 子集II

回溯

去重,首先對數組進行排序,使重複元素挨在一起
然後回溯時,如果同一層分支中存在重複元素,則跳過

在這裏插入圖片描述

class Solution {
public:
    vector<vector<int>> res;

    void backtrack(const vector<int> nums,int index,vector<int>& tmp){
        res.push_back(tmp);
        for(int i=index;i<nums.size();i++){
        	//同層遞歸中,第二次遇到重複元素跳出
            if(i != index && nums[i-1]==nums[i])
                continue;
            tmp.push_back(nums[i]);
            backtrack(nums,i+1,tmp);
            tmp.pop_back();
        }
    }
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        if(!nums.size()) return res;
        vector<int> tmp;

        sort(nums.begin(),nums.end());
        backtrack(nums,0,tmp);

        return res;

    }   
};

77 組合

回溯

  1. 使用多叉圖的方法,對每個

在這裏插入圖片描述

class Solution {
public:
    vector<vector<int>> res;
    void backtrack(int n,int k,vector<int>& tmp,int index){
        if(tmp.size() == k) res.push_back(tmp);
        for(int i=index;i<=n;i++){
            tmp.push_back(i);
            backtrack(n,k,tmp,i+1);
            tmp.pop_back();
        }
    }

    vector<vector<int>> combine(int n, int k) {
        if(k<1) return res;
        vector<int> tmp;
        backtrack(n,k,tmp,1);
        return res;
    }
};

在這裏插入圖片描述

二叉樹 深搜

  1. 對每個節點,都有選擇與不選擇 兩個狀態,分別對應兩個分支, 但是因爲取到k個元素就滿足,所以遞歸到k深度 即可返回
class Solution {
public:
    vector<vector<int>> res;

    void dfs(int n,int k,int index,vector<int>& tmp){
        if(tmp.size() == k) {
            res.push_back(tmp);
            return ;
        }
        if(index<=n && tmp.size() < k){
            dfs(n,k,index+1,tmp);
            tmp.push_back(index);
            dfs(n,k,index+1,tmp);
            tmp.pop_back();
        }
    }

    vector<vector<int>> combine(int n, int k) {
        if(k<1) return vector<vector<int>> {{}};
        vector<int> tmp;
        dfs(n,k,1,tmp);
        return res;
    }
};

46 全排列

class Solution {
public:

    vector<vector<int>> res;
    void dfs(const vector<int> nums,vector<bool>& dp,vector<int>& tmp,int len){
        if(tmp.size() == len) res.push_back(tmp);

        for(int i=0;i<len;i++){
            if(!dp[i]){
                tmp.push_back(nums[i]);
                dp[i] = true;
                dfs(nums,dp,tmp,len);
                tmp.pop_back();
                dp[i] = false;
            }
        }
    }

    vector<vector<int>> permute(vector<int>& nums) {
        int len = nums.size();
        if(!len) return res;
        
        vector<bool> dp(len,false);
        vector<int> tmp;

        dfs(nums,dp,tmp,len);

        return res;
    }
};

47 全排列II


class Solution {
public:

    vector<vector<int>> res;
    void dfs(const vector<int> nums,vector<bool>& dp,vector<int>& tmp,int len){
        if(tmp.size() == len) res.push_back(tmp);

        for(int i=0;i<len;i++){
            if(i!=0 && nums[i-1]==nums[i] && !dp[i-1]) continue;
            if(!dp[i]){
                tmp.push_back(nums[i]);
                dp[i] = true;
                dfs(nums,dp,tmp,len);
                tmp.pop_back();
                dp[i] = false;
            }
        }
    }

    vector<vector<int>> permuteUnique(vector<int>& nums) {
        int len = nums.size();
        if(!len) return res;
        
        sort(nums.begin(),nums.end());

        vector<bool> dp(len,false);
        vector<int> tmp;
        dfs(nums,dp,tmp,len);
        
        return res;
    }   
};


39 組合總和

在這裏插入圖片描述

  1. 回溯算法

這一題的難點在於數字可以無限制重複,但是又要避免解集中出現重複,由此想到累加的下一次要從自身的index開始。

遞歸條件 加上當前元素 不超過目標值 則可以進一步遞歸 超過則退出
可以先排序,然後超過元素之後的所有元素都可以跳過 不排序的話,就對所有元素進行遍歷,判斷是否進入下一層遞歸

去重

如果不去重,會產生 [223] [232] [322] 這些類似結果


class Solution {
public:
    vector<vector<int>> res;

    void dfs(const vector<int> nums,int sum,int target,int len,vector<int>& tmp,int index){
        if(sum == target) {
            res.push_back(tmp);
            return ;
        }
        for(int i=index;i<len;i++){
            if(sum+nums[i]>target) continue;
            tmp.push_back(nums[i]);
            dfs(nums,sum+nums[i],target,len,tmp,i);
            tmp.pop_back();
        }

    }

    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        int len = candidates.size();
        if(!len) return res;

        vector<int> tmp;
        dfs(candidates,0,target,len,tmp,0);

        return res;
    }
};


// sort(nums)
// if(sum+nums[i]>target) break; 
// 排序後 如果當前元素大於目標值  則後面所有元素都大於  直接跳出循環

40 組合總和II

在這裏插入圖片描述

回溯

  1. 每個字符只能使用一次 所以將index當做狀態變量遞歸
  2. 去重,首先對數組進行排序,重複的元素在一起,遞歸時 判斷上次元素和這次元素相等則跳出,
  3. 剪枝 如果當前元素已經大於目標值,則跳出循環,此後所有元素不滿足 目標狀態
class Solution {
public:
    vector<vector<int>> res;

    void dfs(const vector<int>& nums,int target,int sum,int index,vector<int>& tmp){
        if(sum == target) res.push_back(tmp);
        
        for(int i=index;i<nums.size() && sum+nums[i]<=target;i++){
            if(i!=index && nums[i-1]==nums[i]) continue;
            tmp.push_back(nums[i]);
            dfs(nums,target,sum+nums[i],i+1,tmp);
            tmp.pop_back();
        }

    }

    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        int len = candidates.size();
        if(!len) return res;

        sort(candidates.begin(),candidates.end());
        vector<int> tmp;
        dfs(candidates,target,0,0,tmp);

        return res;
    }
};

關於去重一個比較清晰的解釋

其實就是如何 去除同層的重複 並且保留不同層的相同值
if(i!=index && nums[i-1]==nums[i]) continue;主要實現就是 i!=index,重複元素在同層第二次取到時 一定滿足 i!=index 所以將它去除
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章