回溯算法练习题(2024/6/12)

1组合总和 II

给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用 一次

**注意:**解集不能包含重复的组合。

示例 1:

输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]

示例 2:

复制代码
输入: candidates = [2,5,2,1,2], target = 5,
输出:
[
[1,2,2],
[5]
]

提示:

  • 1 <= candidates.length <= 100
  • 1 <= candidates[i] <= 50
  • 1 <= target <= 30

思路:

  1. 递归函数的返回值以及参数:

    • 返回值:递归函数 void backtracking(...) 没有返回值,通过引用参数 vector<vector<int>>& result 来存储最终的结果。
    • 参数:递归函数的参数包括候选数组 vector<int>& candidates、目标值 int target、当前路径和 int sum、起始索引 int startindex、以及标记数组 vector<bool>& used。其中 sum 表示当前路径的和,startindex 表示当前选择的起始位置,used 用于标记每个元素是否被使用过。
  2. 回溯函数终止条件:

    • sum 等于 target 时,表示当前路径的和等于目标值,将当前路径加入结果集中,并返回。
  3. 单层搜索的过程:

    • 在单层搜索的过程中,通过遍历候选数组 candidates,从 startindex 开始,选取一个元素加入当前路径,更新 sum
    • 为了避免重复解,在选择下一个元素时,需要判断当前元素是否与上一个相同,并且上一个元素未被使用,若满足条件则跳过当前循环。
    • 将选取的元素加入当前路径,并标记为已使用,然后递归调用 backtracking 函数,继续搜索下一个元素。
    • 递归返回后,撤销当前元素的选择,即将当前元素标记为未使用,恢复 sum,并从当前路径中移除当前元素,继续下一轮循环。

重点过程:

去重的关键在于确保同一树层上相同的元素只被使用一次。在回溯过程中,如果发现当前元素与前一个元素相同,并且前一个元素未被使用过,那么就需要跳过当前循环,避免重复选择相同元素。

具体实现逻辑如下:

  1. 排序: 首先对候选数组进行排序,这样相同的元素就会相邻排列。

  2. 判断重复: 在遍历候选数组时,对于当前元素 candidates[i],如果它与前一个元素 candidates[i - 1] 相同,那么需要判断前一个元素是否已经被使用过,即 used[i - 1] 是否为 true。

  3. 跳过重复: 如果 candidates[i] == candidates[i - 1] 并且 used[i - 1] 为 false,说明前一个树枝上已经使用过相同的元素 candidates[i - 1],因此当前树枝上就不应该再使用 candidates[i],直接跳过当前循环。

代码:

cpp 复制代码
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;
        }
        // 从startindex开始遍历候选数组
        for (int i = startindex; i < candidates.size() && sum + candidates[i] <= target; i++) {
            // 避免重复解,当当前元素等于前一个元素并且前一个元素未被使用时,跳过当前循环
            if (i > 0 && candidates[i] == candidates[i - 1] && !used[i - 1]) {
                continue;
            }
            sum += candidates[i]; // 将当前元素加入路径和中
            path.push_back(candidates[i]); // 将当前元素加入路径中
            used[i] = true; // 标记当前元素已被使用
            backtracking(candidates, target, sum, i + 1, used); // 递归搜索下一个元素
            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); // 记录每个元素是否被使用过
        sort(candidates.begin(), candidates.end()); // 排序输入数组,方便后续判断重复解
        backtracking(candidates, target, 0, 0, used); // 调用回溯函数开始搜索
        return result; // 返回结果集
    }
};

2分割回文串

给你一个字符串 s,请你将s分割成一些子串,使每个子串都是

回文串

。返回 s 所有可能的分割方案。

示例 1:

复制代码
输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]

示例 2:

复制代码
输入:s = "a"
输出:[["a"]]

提示:

  • 1 <= s.length <= 16
  • s 仅由小写英文字母组成

思路:

  1. 递归函数的返回值以及参数: 在本题中,递归函数 backtracking 的返回值是 void,因为它主要用于收集满足条件的结果而不是返回单个结果。它的参数包括 startindex(当前索引)、s(待分割的字符串)、path(当前路径,存储当前分割的回文串)、result(存储所有满足条件的分割方案)。

  2. 回溯函数终止条件: 切割线切到了字符串最后面,说明找到了一种切割方法,此时就是本层递归的终止条件。回溯函数的终止条件是当 startindex 大于等于字符串 s 的长度时,说明已经完成了一次分割,需要将当前路径 path 加入结果集 result 中,并返回上一层继续搜索其他分割方案。

  3. 单层搜索的过程解题思路: 单层搜索过程是指在每一层递归中,我们进行路径选择和路径限制。路径选择是从当前索引 startindex 开始遍历字符串 s,选择以当前索引开始的所有可能子串作为候选解,并将其添加到当前路径 path 中;路径限制是通过判断选取的子串是否为回文串来进行筛选。如果是回文串,则继续向下遍历;如果不是,则跳过当前选择,进行回溯。

重点过程:

在处理组合问题时,切割线通常表示了当前轮递归遍历的起始位置。在回溯过程中,我们需要确定每一次递归搜索的起点,这个起点就是切割线,决定了我们在当前轮次搜索的范围。

具体来说,在回溯函数中,切割线(也就是 `startIndex`)是一个很重要的参数,因为它告诉我们从哪里开始搜索下一个可能的解。每次递归调用时,我们都会更新切割线,使得下一次搜索的范围不会重复之前已经搜索过的部分。

对于回文串分割问题,切割线 `startIndex` 就是确定了下一次递归搜索时的起始位置。如果当前已经搜索到索引 `i` 的位置,下一次搜索就从 `i+1` 的位置开始,这样确保了不会重复搜索已经处理过的部分,也符合回溯算法的思路。

所以,在处理组合问题时,递归参数中的 `startIndex` 可以被理解为切割线,决定了每一轮递归搜索的起点,帮助我们避免重复搜索和有效地控制搜索范围。

代码:

cpp 复制代码
class Solution {
private:
    vector<vector<string>> result;  // 存储最终结果
    vector<string> path;            // 存储当前路径
    // 回溯函数,寻找回文串的分割
    void backtracking(const string& s, int startindex) {
        // 当起始索引超过字符串长度时,将当前路径加入结果集
        if (startindex >= s.size()) {
            result.push_back(path);
            return;
        }
        // 遍历字符串
        for (int i = startindex; i < s.size(); i++) {
            // 如果从当前索引到 i 构成的子串是回文串
            if (isPalindrome(s, startindex, i)) {
                // 将该子串加入当前路径
                string str = s.substr(startindex, i - startindex + 1);
                path.push_back(str);
            } else {
                continue;  // 如果不是回文串,则跳过当前循环
            }
            // 递归调用,继续寻找回文串
            backtracking(s, i + 1);
            path.pop_back();  // 回溯,撤销选择
        }
    }
    // 判断子串是否为回文串
    bool isPalindrome(const string& s, int start, int end) {
        // 使用双指针判断是否为回文串
        for (int i = start, j = end; i < j; i++, j--) {
            if (s[i] != s[j]) {
                return false;
            }
        }
        return true;
    }
public:
    // 分割回文串
    vector<vector<string>> partition(string s) {
        result.clear();  // 清空结果集
        path.clear();    // 清空当前路径
        backtracking(s, 0);  // 开始回溯
        return result;  // 返回结果集
    }
};

3子集

给你一个整数数组 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 中的所有元素 互不相同

思路:

  1. 递归函数的返回值以及参数: 在这个例子中,递归函数 backtracking 的返回值是 void,因为它主要用于收集满足条件的结果而不是返回单个结果。它的参数包括 nums(原始数组)、startindex(当前遍历的起始位置)。

  2. 回溯函数终止条件: 回溯函数的终止条件是当 startindex 大于等于 nums 数组的长度时,说明已经完成了一轮搜索,需要返回上一层。剩余集合为空的时候,就是叶子节点。那么什么时候剩余集合为空呢?就是startIndex已经大于数组的长度了,就终止了,因为没有元素可取了。

  3. 单层搜索的过程解题思路: 单层搜索过程是指在每一层递归中,我们进行路径选择和路径限制。路径选择是将当前元素加入到路径 path 中,然后递归搜索下一层;路径限制是确保在当前层递归中,不会重复选择已经选择过的元素。具体来说,我们从 startindex 开始遍历原始数组 nums,将当前元素加入到路径 path 中,然后递归调用下一层,并在递归完成后进行回溯,将当前元素从路径 path 中移除,以便搜索其他分支。

代码:

cpp 复制代码
class Solution {
public:
    vector<int> path; // 存储当前路径的数组
    vector<vector<int>> result; // 存储所有子集的数组

    // 回溯函数,startindex表示当前遍历的起始位置
    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(); // 回溯,将当前元素从路径中移除
        }
    }

    // 主函数,返回所有子集
    vector<vector<int>> subsets(vector<int>& nums) {
        backtracking(nums, 0); // 调用回溯函数
        return result; // 返回结果集
    }
};
相关推荐
XiaoLeisj15 分钟前
【递归,搜索与回溯算法 & 综合练习】深入理解暴搜决策树:递归,搜索与回溯算法综合小专题(二)
数据结构·算法·leetcode·决策树·深度优先·剪枝
Jasmine_llq34 分钟前
《 火星人 》
算法·青少年编程·c#
闻缺陷则喜何志丹1 小时前
【C++动态规划 图论】3243. 新增道路查询后的最短距离 I|1567
c++·算法·动态规划·力扣·图论·最短路·路径
Lenyiin1 小时前
01.02、判定是否互为字符重排
算法·leetcode
鸽鸽程序猿1 小时前
【算法】【优选算法】宽搜(BFS)中队列的使用
算法·宽度优先·队列
Jackey_Song_Odd1 小时前
C语言 单向链表反转问题
c语言·数据结构·算法·链表
Watermelo6171 小时前
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
开发语言·前端·javascript·算法·数据挖掘·数据分析·ecmascript
乐之者v1 小时前
leetCode43.字符串相乘
java·数据结构·算法
A懿轩A2 小时前
C/C++ 数据结构与算法【数组】 数组详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·数组