回溯算法练习题(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; // 返回结果集
    }
};
相关推荐
Java与Android技术栈7 分钟前
图像编辑器 Monica 之 CV 常见算法的快速调参
算法
别NULL19 分钟前
机试题——最小矩阵宽度
c++·算法·矩阵
珊瑚里的鱼20 分钟前
【单链表算法实战】解锁数据结构核心谜题——环形链表
数据结构·学习·程序人生·算法·leetcode·链表·visual studio
无限码力24 分钟前
[矩阵扩散]
数据结构·算法·华为od·笔试真题·华为od e卷真题
gentle_ice24 分钟前
leetcode——矩阵置零(java)
java·算法·leetcode·矩阵
查理零世26 分钟前
保姆级讲解 python之zip()方法实现矩阵行列转置
python·算法·矩阵
zhbi981 小时前
测量校准原理
算法
时间很奇妙!1 小时前
decison tree 决策树
算法·决策树·机器学习
sysu632 小时前
95.不同的二叉搜索树Ⅱ python
开发语言·数据结构·python·算法·leetcode·面试·深度优先
红鲤鱼遇绿鲤鱼2 小时前
uva 1354 Mobile Computing
算法