回溯算法练习题(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],直接跳过当前循环。

代码:

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` 可以被理解为切割线,决定了每一轮递归搜索的起点,帮助我们避免重复搜索和有效地控制搜索范围。

代码:

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 中移除,以便搜索其他分支。

代码:


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; // 返回结果集
    }
};


  • 27
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值