Offer必备算法31_DFS回溯剪枝_九道力扣题详解(由易到难)

目录

回溯算法简介

①力扣46. 全排列

解析代码

②力扣78. 子集

解析代码1

解析代码2

③力扣1863. 找出所有子集的异或总和再求和

解析代码

④力扣47. 全排列 II

解析代码1_直接去重

解析代码2_剪枝_关心不合法分支

解析代码3_剪枝_关心合法分支

⑤力扣17. 电话号码的字母组合

解析代码

⑥力扣22. 括号生成

解析代码

⑦力扣77. 组合

解析代码

⑧力扣494. 目标和

解析代码(path设置成全局)

解析代码(path设置全局)

⑨力扣39. 组合总和

解析代码1

解析代码2

本篇完。


回溯算法简介

回溯算法是一种经典的递归算法,通常⽤于解决组合问题、排列问题和搜索问题等。

        回溯算法的基本思想:从一个初始状态开始,按照⼀定的规则向前搜索,当搜索到某个状态无法前进时,回退到前一个状态,再按照其他的规则搜索。回溯算法在搜索过程中维护一个状态树,通过遍历状态树来实现对所有可能解的搜索。

        回溯算法的核心思想:“试错”,即在搜索过程中不断地做出选择,如果选择正确,则继续向前搜索,否则,回退到上一个状态,重新做出选择。回溯算法通常用于解决具有多个解,且每个解都需要搜索才能找到的问题。

        回溯算法的模板:

void dfs(vector<int>& path, vector<int>& choice, ...)
{
	// 满⾜结束条件
	if (/* 满⾜结束条件 */)
	{
		// 将路径添加到结果集中
		res.push_back(path);
		return;
	}
	// 遍历所有选择
	for (int i = 0; i < choices.size(); i++)
	{
		// 做出选择
		path.push_back(choices[i]);
		// 做出当前选择后继续搜索
		dfs(path, choices);
		// 撤销选择
		path.pop_back();
	}
}

        其中, path表示当前已经做出的选择, choices表示当前可以做的选择。在回溯算法中,我们需要做出选择,然后递归地调用回溯函数。如果满足结束条件,则将当前路径添加到结果集中。

        否则,我们需要撤销选择,回到上一个状态,然后继续搜索其他的选择。回溯算法的时间复杂度通常较高,因为它需要遍历所有可能的解。但是,回溯算法的空间复杂度较低,因为它只需要维护一个状态树。在实际应用中,回溯算法通常需要通过剪枝等方法进行优化,以减少搜索的次数,从而提高算法的效率。

        回溯算法是一种非常重要的算法,可以解决许多组合问题、排列问题和搜索问题等。回溯算法的核心思想是搜索状态树,通过遍历状态树来实现对所有可能解的搜索。回溯算法的模板非常简单,但是实现起来需要注意一些细节,比如何做出选择、如何撤销选择等。


①力扣46. 全排列

46. 全排列

难度 中等

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

示例 1:

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

示例 2:

输入:nums = [0,1]
输出:[[0,1],[1,0]]

示例 3:

输入:nums = [1]
输出:[[1]]

提示:

  • 1 <= nums.length <= 6
  • -10 <= nums[i] <= 10
  • nums 中的所有整数 互不相同
class Solution {
public:
    vector<vector<int>> permute(vector<int>& nums) {

};

解析代码

之前练习next_permutation时写过,代码在下面注释起来了。

深搜的思路是循环模仿遍历树的结点,到叶子结点就返回,不是就进入循环。

        在下面for循环里要考虑这个位置要填哪个数。根据题目要求我们肯定不能填已经填过的数,因此很容易想到的一个处理手段就是定义一个标记数组来标记已经填过的数,那么在填这个数的时候我们遍历题目给定的所有数,如果这个数没有被标记过,我们就尝试填入,并将其标记,继续尝试填下一个位置,

        回溯的时候要撤销这一个位置填的数以及标记,并继续尝试其他没被标记过的数。

class Solution {
    vector<vector<int>> ret;
    vector<int> path;
    bool cheak[6] = {0}; // 映射下标->数字是否用过->剪枝
public:
    vector<vector<int>> permute(vector<int>& nums) {
        dfs(nums);
        return ret;
    }

    void dfs(vector<int> nums)
    {
        if(nums.size() == path.size())
        {
            ret.push_back(path);
            return;
        }
        for(int i = 0; i < nums.size(); ++i)
        {
            if(cheak[i] == false) // 如果没有用过
            {
                path.push_back(nums[i]);
                cheak[i] = true;
                dfs(nums); // 此时路径已经加上一个了,在让其进入递归

                path.pop_back(); // 回溯,恢复现场,(递归往回走了)
                cheak[i] = false;
            }
        }
    }
};

// class Solution {
// public:
//     vector<vector<int>> permute(vector<int>& nums) {
//         vector<vector<int>> vv;
//         sort(nums.begin(), nums.end());
//         do
//         {
//             vv.push_back(nums);
//         }while(next_permutation(nums.begin(), nums.end()));
//         return vv;
//     }
// };


②力扣78. 子集

78. 子集

难度 中等

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

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

示例 1:

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

示例 2:

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

提示:

  • 1 <= nums.length <= 10
  • -10 <= nums[i] <= 10
  • nums 中的所有元素 互不相同
class Solution {
public:
    vector<vector<int>> subsets(vector<int>& nums) {

    }
};

解析代码1

        为了获得 nums 数组的所有子集,我们需要对数组中的每个元素进行选择或不选择的操作,即 nums数组⼀定存在 2^(数组长度) 个子集。对于查找子集,具体可以定义一个数组,来记录当前的状态,并对其进行递归。

        对于每个元素有两种选择:1. 不进行任何操作。2. 将其添加至当前状态的集合。在递归时我们需要保证递归结束时当前的状态与进行递归操作前的状态不变,而当我们在选择进行步骤 2 进行递归时,当前状态会发生变化,因此我们需要在递归结束时撤回添加操作,即进行回溯。

        还是定义两个全局变量,一个是返回的二维数组,一个是路径数组。解法一是单独看一个数,分为选和不选的情况,到叶子结点时后就得到所有子集。

× 代表不选,√ 代表选:

class Solution {
    vector<vector<int>> ret;
    vector<int> path;
public:
    vector<vector<int>> subsets(vector<int>& nums) {
        // 时间O(N * 2^N) 空间O(N)
        dfs(nums, 0);
        return ret;
    }

    void dfs(vector<int> nums, int pos)
    {
        if(pos == nums.size())
        {
            ret.push_back(path);
            return;
        }
        path.push_back(nums[pos]); // 选
        dfs(nums, pos + 1);
        path.pop_back(); // 恢复现场

        dfs(nums, pos + 1); // 不选
    }
};

解析代码2

解法二也是定义两个全局变量,从pos位置循环数组,这样就可以实现剪枝。

class Solution {
    vector<vector<int>> ret;
    vector<int> path;
public:
    vector<vector<int>> subsets(vector<int>& nums) {
        // 时间O(N * 2^N) 空间O(N)
        dfs(nums, 0);
        return ret;
    }

    void dfs(vector<int> nums, int pos)
    {
        ret.push_back(path); // 每个结点都是子集
        for(int i = pos; i < nums.size(); ++i)
        {
            path.push_back(nums[i]);
            dfs(nums, i + 1);
            path.pop_back(); // 回溯,恢复现场
        }
    }
};


③力扣1863. 找出所有子集的异或总和再求和

1863. 找出所有子集的异或总和再求和

难度 简单

一个数组的 异或总和 定义为数组中所有元素按位 XOR 的结果;如果数组为  ,则异或总和为 0 。

  • 例如,数组 [2,5,6] 的 异或总和 为 2 XOR 5 XOR 6 = 1 。

给你一个数组 nums ,请你求出 nums 中每个 子集 的 异或总和 ,计算并返回这些值相加之  。

注意:在本题中,元素 相同 的不同子集应 多次 计数。

数组 a 是数组 b 的一个 子集 的前提条件是:从 b 删除几个(也可能不删除)元素能够得到 a 。

示例 1:

输入:nums = [1,3]
输出:6
解释:[1,3] 共有 4 个子集:
- 空子集的异或总和是 0 。
- [1] 的异或总和为 1 。
- [3] 的异或总和为 3 。
- [1,3] 的异或总和为 1 XOR 3 = 2 。
0 + 1 + 3 + 2 = 6

示例 2:

输入:nums = [5,1,6]
输出:28
解释:[5,1,6] 共有 8 个子集:
- 空子集的异或总和是 0 。
- [5] 的异或总和为 5 。
- [1] 的异或总和为 1 。
- [6] 的异或总和为 6 。
- [5,1] 的异或总和为 5 XOR 1 = 4 。
- [5,6] 的异或总和为 5 XOR 6 = 3 。
- [1,6] 的异或总和为 1 XOR 6 = 7 。
- [5,1,6] 的异或总和为 5 XOR 1 XOR 6 = 2 。
0 + 5 + 1 + 6 + 4 + 3 + 7 + 2 = 28

示例 3:

输入:nums = [3,4,5,6,7,8]
输出:480
解释:每个子集的全部异或总和值之和为 480 。

提示:

  • 1 <= nums.length <= 12
  • 1 <= nums[i] <= 20
class Solution {
public:
    int subsetXORSum(vector<int>& nums) {

};

解析代码

和《力扣78. 子集》类似,路径和回溯变了一下:

        所有子集可以解释为:每个元素选择在或不在一个集合中(因此,子集有2^N个)。本题需要求出所有子集,将它们的异或和相加。因为异或操作满足交换律,所以可以定义一个变量,直接记 录当前状态的异或和。使用递归保存当前集合的状态(异或和),选择将当前元素添加至当前状态与否,并依次递归数组中下一个元素。当递归到空元素时,表示所有元素都被考虑到,记录当前状态 (将当前状态的异或和添加至答案中)。

class Solution {
    int path; // 其子集异或的结果
    int ret;
public:
    int subsetXORSum(vector<int>& nums) {
        dfs(nums, 0);
        return ret;
    }

    void dfs(vector<int> nums, int pos)
    {
        ret += path;
        for(int i = pos; i < nums.size(); ++i)
        {
            path ^= nums[i];
            dfs(nums, i + 1);
            path ^= nums[i]; // 回溯,恢复现场
            // 异或两个一样的数相当没异或
        }
    }
};


④力扣47. 全排列 II

47. 全排列 II

难度 中等

给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。

示例 1:

输入:nums = [1,1,2]
输出:
[[1,1,2],
 [1,2,1],
 [2,1,1]]

示例 2:

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

提示:

  • 1 <= nums.length <= 8
  • -10 <= nums[i] <= 10
class Solution {
public:
    vector<vector<int>> permuteUnique(vector<int>& nums) {

    }
};

解析代码1_直接去重

在力扣46. 全排列的基础上用set对结果去重:

class Solution {
    set<vector<int>> ret;
    vector<vector<int>> tmp();
    vector<int> path;
    bool cheak[8] = {false};
public:
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        dfs(nums);
        vector<vector<int>> tmp(ret.begin(), ret.end());
        return tmp;
    }
    void dfs(vector<int> nums)
    {
        if(nums.size() == path.size())
        {
            // if(find(ret.begin(), ret.end(), path) == ret.end())
            //     ret.push_back(path);
            ret.insert(path);
            return;
        }
        for(int i = 0; i < nums.size(); ++i)
        {
            if(cheak[i] == false) // 如果没有用过
            {
                path.push_back(nums[i]);
                cheak[i] = true;
                dfs(nums); // 此时路径已经加上一个了,在让其进入递归
                path.pop_back(); // 回溯,恢复现场,(递归往回走了)
                cheak[i] = false;
            }
        }
    }
};

// class Solution {
// public:
//     vector<vector<int>> permuteUnique(vector<int>& nums) {
//         vector<vector<int>> vv;
//         sort(nums.begin(), nums.end());
//         do
//         {
//             vv.push_back(nums);
//         }while(next_permutation(nums.begin(), nums.end()));
//         return vv;
//     }
// };

解析代码2_剪枝_关心不合法分支

        因为题目不要求返回的排列顺序,因此我们可以对初始状态排序,将所有相同的元素放在各自相邻的位置,方便之后操作。

class Solution {
    vector<int> path;
    vector<vector<int>> ret;
    bool check[9] = {false};

public:
    vector<vector<int>> permuteUnique(vector<int>& nums)
    {
        sort(nums.begin(), nums.end());
        dfs(nums, 0);
        return ret;
    }
    void dfs(vector<int>& nums, int pos)
    {
        if (pos == nums.size()) // 选到数组的下一个位置
        {
            ret.push_back(path);
            return;
        }
        for (int i = 0; i < nums.size(); i++)
        {
            if (check[i] == true || (i != 0 && nums[i] == nums[i - 1] && check[i - 1] == false))
                continue; // 剪枝_关心不合法分支_左边是上一层用过的,右边是这一层重复的
            path.push_back(nums[i]);
            check[i] = true;
            dfs(nums, pos + 1);
            path.pop_back(); // 恢复现场
            check[i] = false;
        }
    }
};

解析代码3_剪枝_关心合法分支

class Solution {
    vector<int> path;
    vector<vector<int>> ret;
    bool check[9] = {false};

public:
    vector<vector<int>> permuteUnique(vector<int>& nums)
    {
        sort(nums.begin(), nums.end());
        dfs(nums, 0);
        return ret;
    }
    void dfs(vector<int>& nums, int pos)
    {
        if (pos == nums.size()) // 选到数组的下一个位置
        {
            ret.push_back(path);
            return;
        }
        for (int i = 0; i < nums.size(); i++)
        {
            if (check[i] == false && (i == 0 || nums[i] != nums[i - 1] || check[i - 1] != false))
            {   // 剪枝_关心合法分支_左边是上一层没用过的,右边是这一层没用过的
                path.push_back(nums[i]);
                check[i] = true;
                dfs(nums, pos + 1);
                path.pop_back(); // 恢复现场
                check[i] = false;
            }
        }
    }
};


⑤力扣17. 电话号码的字母组合

17. 电话号码的字母组合

难度 中等

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

示例 1:

输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]

示例 2:

输入:digits = ""
输出:[]

示例 3:

输入:digits = "2"
输出:["a","b","c"]

提示:

  • 0 <= digits.length <= 4
  • digits[i] 是范围 ['2', '9'] 的一个数字。
class Solution {
public:
    vector<string> letterCombinations(string digits) {

    }
};

解析代码

        每个位置可选择的字符与其他位置并不冲突,因此不需要标记已经出现的字符,只需要将每个数字对应的字符依次填入字符串中进行递归,在回溯时撤销填入操作即可。

在递归之前我们需要定义一个哈希数组 hash,记录 2~9 各字对应的字符。

全局变量:string path(字符串当前状态),vector<string> ret(所有成立的字符串);

递归函数设计:void dfs(string& digits, int pos)

参数:pos(已经处理的元素个数)

函数作用:查找所有合理的字母组合并存储在答案列表中。

递归函数流程如下:

  • 1. 递归结束条件:当 pos 等于 digits 的长度时,将 path 加入到 ret 中并返回。
  • 2. 取出当前处理的数字 digit,根据 hash取出对应的字母列表 letters。
  • 3. 遍历字母列表 letters,将当前字母加入到组合字符串 path 的末尾,然后递归处理下一个数字(传入 pos+ 1,表示处理下一个数字)。
  • 4. 递归处理结束后,将加入的字母从 path 的末尾删除,表示回溯。
  • 5. 最终返回 ret 即可。
class Solution {
    string hash[10]={"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
    string path;
    vector<string> ret;
    
public:
    vector<string> letterCombinations(string digits) {
        if(digits.size() == 0)
            return ret;

        dfs(digits, 0);
        return ret;
    }

    void dfs(const string& digits, int pos)
    {
        if(pos == digits.size())
        {
            ret.push_back(path);
            return;
        }

        for(auto ch : hash[digits[pos] - '0']) // 遍历数组里的字符串
        {
            path.push_back(ch);
            dfs(digits, pos + 1);
            path.pop_back(); // 恢复现场
        }
    }
};


⑥力扣22. 括号生成

22. 括号生成

难度 中等

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

示例 1:

输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]

示例 2:

输入:n = 1
输出:["()"]

提示:

  • 1 <= n <= 8
class Solution {
public:
    vector<string> generateParenthesis(int n) {

    }
};

解析代码

        从左往右进行递归,在每个位置判断放置左右括号的可能性,若此时放置左括号合理,则放置左括号继续进行递归,右括号同理。

        一种判断括号是否合法的方法:从左往右遍历,左括号的数量始终大于等于右括号的数量,并且左括号的总数量与右括号的总数量相等。因此我们在递归时需要进行以下判断:

  1. 放入左括号时需判断此时左括号数量是否小于字符串总长度的一半(若左括号的数量大于等于字符串长度的一半时继续放置左括号,则左括号的总数量一定大于右括号的总数量)
  2. 放入右括号时需判断此时右括号数量是否小于左括号数量

定义两个全局变量 left 和 right 分别记录左括号和右括号的数,还有常用的string path;和vector<string> ret;,递归流程如下:

  • 递归结束条件:当前状态字符串长度与 2*n 相等,记录当前状态并返回
  • 若此时左括号数量小于字符串总长度的一半,则在当前状态的字符串末尾添加左括号并继续递归, 递归结束撤销添加操作
  • 若此时右括号数量小于左括号数量,则在当前状态的字符串末尾添加右括号并递归,递归结束撤销添加操作
class Solution {
    int left, right, _n;
    string path;
    vector<string> ret;

public:
    vector<string> generateParenthesis(int n) {
        _n = n;
        dfs();
        return ret;
    }

    void dfs()
    {
        if(right == _n) // 或者path.size() == 2 * n
        {
            ret.push_back(path);
            return;
        }

        if(left < _n) // 添加左括号
        {
            path += '(';
            ++left;
            dfs();
            path.pop_back(); // 恢复现场
            --left;
        }

        if(right < left) // 添加右括号
        {
            path += ')';
            ++right;
            dfs();
            path.pop_back(); // 恢复现场
            --right;
        }
    }
};


⑦力扣77. 组合

77. 组合

难度 中等

给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。

你可以按 任何顺序 返回答案。

示例 1:

输入:n = 4, k = 2
输出:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]

示例 2:

输入:n = 1, k = 1
输出:[[1]]

提示:

  • 1 <= n <= 20
  • 1 <= k <= n
class Solution {
public:
    vector<vector<int>> combine(int n, int k) {

    }
};

解析代码

        题目要求我们从 1 到 n 中选择 k 个数的所有组合,其中不考虑顺序。也就是说,[1,2] 和 [2,1] 等价。我们需要找出所有的组合,但不能重复计算相同元素的不同顺序的组合。对于选择组合,我们需要进行如下流程:

  1. 所有元素分别作为首位元素进行处理。
  2. 在之后的位置上同理,选择所有元素分别作为当前位置元素进行处理。
  3. 为避免计算重复组合,规定选择之后位置的元素时必须比前一个元素大,这样就不会有重复的组合 ([1,2] 和 [2,1] 中 [2,1] 不会出现)。
class Solution {
    int _n, _k;
    vector<int> path;
    vector<vector<int>> ret;
public:
    vector<vector<int>> combine(int n, int k) {
        _n = n;
        _k = k;
        dfs(1);
        return ret;
    }

    void dfs(int pos)
    {
        if(path.size() == _k)
        {
            ret.push_back(path);
            return;
        }

        for(int i = pos; i <= _n; ++i) // 剪枝
        {
            path.push_back(i);
            dfs(i + 1);
            path.pop_back(); // 恢复现场
        }
    }
};


⑧力扣494. 目标和

494. 目标和

难度 中等

给你一个非负整数数组 nums 和一个整数 target 。

向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :

  • 例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1" 。

返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

示例 1:

输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3

示例 2:

输入:nums = [1], target = 1
输出:1

提示:

  • 1 <= nums.length <= 20
  • 0 <= nums[i] <= 1000
  • 0 <= sum(nums[i]) <= 1000
  • -1000 <= target <= 1000
class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {

    }
};

解析代码(path设置成全局)

        在动态规划部分写过这题,现在用DFS写一写。对于每个数,可以选择加上或减去它,依次枚举每一个数字,在每个数都被选择时检查得到的和是否等于目标值。如果等于,则记录结果。 

在这一题中可以窥见path是设置成全局的好还是设置成递归参数好:

如果path是一个类型,如 int 设置成递归参数好,参数可以帮我们完成回溯,代码会简洁。

如果path是一个数组之类的,设置成全局好,如力扣257. 二叉树的所有路径,否则效率低。

class Solution {
    int path, ret, _target;
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        _target = target;
        dfs(nums, 0);
        return ret;
    }

    void dfs(vector<int>& nums, int pos)
    {
        if(pos ==  nums.size())
        {
            if(path == _target)
                ++ret;
            return;
        }

        path += nums[pos]; // 加法
        dfs(nums, pos + 1);
        path -= nums[pos]; // 回溯

        path -= nums[pos]; // 减法
        dfs(nums, pos + 1);
        path += nums[pos]; // 回溯
    }
};

解析代码(path设置全局)

此题path设置全局效率并没有提高很多,但是代码会简洁:

class Solution {
    int ret, _target;
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        _target = target;
        dfs(nums, 0, 0);
        return ret;
    }

    void dfs(vector<int>& nums, int pos, int path)
    {
        if(pos ==  nums.size())
        {
            if(path == _target)
                ++ret;
            return;
        }
        dfs(nums, pos + 1, path + nums[pos]);  // 加法
        dfs(nums, pos + 1, path - nums[pos]); // 减法
    }
};


⑨力扣39. 组合总和

39. 组合总和

LCR 081. 组合总和

难度 中等

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。

candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。 

对于给定的输入,保证和为 target 的不同组合数少于 150 个。

示例 1:

输入:candidates = [2,3,6,7], target = 7

输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。

示例 2:

输入: candidates = [2,3,5], target = 8
输出: [[2,2,2,2],[2,3,3],[3,5]]

示例 3:

输入: candidates = [2], target = 1
输出: []

提示:

  • 1 <= candidates.length <= 30
  • 2 <= candidates[i] <= 40
  • candidates 的所有元素 互不相同
  • 1 <= target <= 40
class Solution {
public:
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        
    }
};

解析代码1

此题算是组合总和1,后面还有组合总和2、3、4,可以自己完成。

        candidates 的所有元素互不相同,因此我们在递归状态时只需要对每个元素进行如下判断:

  1. 跳过,对下一个元素进行判断;
  2. 将其添加至当前状态中,我们在选择添加当前元素时,之后仍可以继续选择当前元素(可以重复选择同一元素)。

因此在选择当前元素并向下传递下标时,应该直接传递当前元素下标。

class Solution {
    int _target;
    vector<int> path;
    vector<vector<int>> ret;

public:
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        _target = target;
        dfs(candidates, 0, 0);
        return ret;
    }

    void dfs(vector<int>& candidates, int pos, int sum)
    {
        if(sum > _target)
            return;
        if(sum == _target)
        {
            ret.push_back(path);
            return;
        }

        for(int i = pos; i < candidates.size(); ++i)
        {
            path.push_back(candidates[i]);
            dfs(candidates, i, sum + candidates[i]);
            path.pop_back();
        }
    }
};


解析代码2

还可以换一个思路:如

示例 1:

输入: candidates = [2,3,6,7], target = 7
输出: [[7],[2,2,3]]

从选0个2开始,选1、2、3、4...个2,直到sum >= target。

然后在上面选2的个数的基础上开始选3...,直到选完数组的数。所有情况枚举完再恢复现场。

class Solution {
    int _target;
    vector<int> path;
    vector<vector<int>> ret;

public:
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        _target = target;
        dfs(candidates, 0, 0);
        return ret;
    }

    void dfs(vector<int>& candidates, int pos, int sum)
    {
        if(sum == _target)
        {
            ret.push_back(path);
            return;
        }
        if(sum > _target || pos == candidates.size()) // 下面没判断pos这就要判断
            return;

        for (int k = 0; k * candidates[pos] + sum <= _target; ++k) // 枚举个数
        {
            if (k != 0)
                path.push_back(candidates[pos]);
            dfs(candidates, pos + 1, sum + k * candidates[pos]);
        }
        for (int k = 1; k * candidates[pos] + sum <= _target; ++k) // 恢复现场
        {
            path.pop_back();
        }
    }
};


本篇完。

下一部分开始学一学贪心算法然后刷一部分题,再回来刷DFS的题。

  • 46
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GR鲸鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值