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

1子集 II

给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的 

子集

(幂集)。

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

示例 1:

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

示例 2:

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

提示:

  • 1 <= nums.length <= 10
  • -10 <= nums[i] <= 10

回溯思路:

回溯三部曲:

  • 回溯函数模板返回值以及参数
  • 回溯函数终止条件
  • 回溯搜索的遍历过程

回溯算法模板框架如下:

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}
  1. 递归函数的返回值以及参数:
    • backtracking 函数的返回类型是 void,因为它的主要作用是通过修改全局变量 result 来存储所有符合条件的子集。
    • 参数包括 nums(原始数组)、startindex(当前遍历的起始位置)、used(记录元素是否被使用过的数组)。
  2. 回溯函数终止条件:这里没有明确的终止条件判断 startindex >= nums.size(),因为要考虑处理重复元素的情况。实际上,整个数组都要遍历到,因此终止条件是在每次递归时自然完成的。
  3. 单层搜索的过程解题思路:
    • 在每一层递归中,首先将当前的 path 路径加入到 result 结果集中,即使它可能是重复的子集。
    • 接下来遍历从 startindex 开始的 nums 数组元素,确保在同一层级不重复选择相同的元素。这里通过排序数组和判断相邻元素是否相同来避免重复选择问题。
    • 如果发现当前元素和上一个相同,并且上一个元素未被使用过(即 !used[i-1]),则跳过当前元素,以确保不重复选择同一层级的相同元素。
    • 否则,将当前元素加入 path 中,标记为已使用,然后递归进入下一层级。
    • 在递归返回后,撤销选择,即将当前元素的标记置为未使用,同时从 path 中移除当前元素,以进行回溯到上一层决策树。
  4. 去重部分的实现:

    • 在 backtracking 函数中,通过排序原始数组 nums 来确保重复元素相邻。
    • 在每次递归选择元素时,判断当前元素是否与上一个相同且上一个元素未被使用过,若是,则跳过当前元素的选择,以避免重复生成相同的子集。

代码:

class Solution {
private:
    vector<vector<int>> result; // 最终结果存储的容器
    vector<int> path; // 当前路径的容器

    void backtracking(vector<int>& nums, int startindex, vector<bool>& used) {
        result.push_back(path); // 将当前路径加入最终结果

        // 遍历选择列表
        for (int i = startindex; i < nums.size(); ++i) {
            // 剪枝条件:确保不重复选择同一层级的相同元素
             // 如果同一树层的相邻元素相同且前一个元素未被使用过,则跳过
            if (i > startindex && nums[i] == nums[i - 1] && !used[i - 1]) {
                continue;
            }

            path.push_back(nums[i]); // 做出选择
            used[i] = true; // 记录选择

            backtracking(nums, i + 1, used); // 进入下一层决策树

            used[i] = false; // 撤销选择
            path.pop_back(); // 回溯,撤销选择
        }
    }
    
public:
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        vector<bool> used(nums.size(), false); // 记录元素是否被使用过
        sort(nums.begin(), nums.end()); // 排序,为了去重
        backtracking(nums, 0, used); // 调用回溯算法
        return result; // 返回最终结果
    }
};

2非递减子序列

给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。

数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。

示例 1:

输入:nums = [4,6,7,7]
输出:[[4,6],[4,6,7],[4,6,7,7],[4,7],[4,7,7],[6,7],[6,7,7],[7,7]]

示例 2:

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

提示:

  • 1 <= nums.length <= 15
  • -100 <= nums[i] <= 100

思路:

  1. 递归函数的返回值以及参数
    • backtracking 函数用于在 nums 数组中查找所有递增子序列,并将结果存储在 result 中。
    • 参数包括 nums 数组和 startindex,表示当前递归的起始位置。
  2. 回溯函数终止条件
    • 当 path 中的元素个数大于1时,将 path 加入到 result 中,因为题目要求子序列长度至少为2。
  3. 单层搜索的过程
    • 使用 usetunordered_set<int>)来进行元素的去重操作,确保同一层级(即同一递归深度)中不重复选择相同的元素。
    • 在 for 循环中,从 startindex 开始遍历 nums 数组。
    • 对于每一个元素 nums[i],检查是否满足以下两个条件之一:
      • 如果 path 不为空且 nums[i] < path.back(),则说明当前元素不符合递增要求,跳过。
      • 如果 nums[i] 已经在 uset 中存在,也跳过,避免重复选择。
    • 若满足条件,则将 nums[i] 加入到 path 和 uset 中,并递归调用 backtracking(nums, i + 1) 继续寻找下一个元素。
    • 递归完成后,需要进行回溯操作,即从 path 中移除当前添加的元素,以便尝试其他可能的组合。

  1. 去重部分的实现

    • 使用 uset 来存储已经选择过的元素,保证在同一层级中不会重复选择相同的元素,从而避免生成重复的子序列。

代码:

class Solution {
private:
    vector<vector<int>> result;  // 存储最终结果的二维数组
    vector<int> path;            // 回溯过程中当前的子序列
    void backtracking(vector<int>& nums, int startindex) {
        // 当前子序列长度大于1时,将其加入结果集
        if (path.size() > 1) {
            result.push_back(path);
        }
        
        unordered_set<int> uset;  // 用于记录当前已经选择过的元素
        
        // 从startindex开始遍历nums数组
        for (int i = startindex; i < nums.size(); i++) {
            // 如果当前path不为空且当前元素小于path中最后一个元素,跳过该元素
            // 或者当前元素已经在uset中出现过,也跳过该元素
            if (!path.empty() && nums[i] < path.back() || uset.find(nums[i]) != uset.end()) {
                continue;
            }
            
            // 将当前元素加入path和uset
            uset.insert(nums[i]);
            path.push_back(nums[i]);
            
            // 递归调用backtracking,从i+1位置开始继续寻找
            backtracking(nums, i + 1);
            
            // 回溯,将当前元素从path中移除
            path.pop_back();
        }
    }

public:
    // 主函数,寻找nums数组中的所有递增子序列
    vector<vector<int>> findSubsequences(vector<int>& nums) {
        backtracking(nums, 0);  // 调用回溯函数
        return result;          // 返回最终结果
    }
};

3全排列

给定一个不含重复数字的数组 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 中的所有整数 互不相同

思路:

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

返回值和参数:

  • void backtracking(vector<int>& nums, vector<bool>& used)
    • 返回值为 void,因为我们不需要从每次调用中返回特定的值,而是通过引用参数来改变全局的 result 和 path
    • 参数 nums 是输入的原始数组,used 是一个标记数组,用于记录每个位置的元素是否已经被使用过。

2. 回溯函数的终止条件

终止条件:

  • if (path.size() == nums.size())
    • 当 path 的长度等于 nums 的长度时,表示已经形成了一个完整的排列,此时将 path 加入到 result 中,然后返回。

3. 单层搜索的过程

解题思路:

  • 选择路径: 在每一层的递归中,遍历 nums 数组的所有元素。
  • 判断条件: 使用 used 数组来判断当前元素是否已经被使用过,如果已经被使用过,则跳过。
  • 标记和递归:
    • 将当前未使用的元素加入 path 中,并标记为已使用 (used[i] = true)。
    • 递归调用 backtracking,继续向下一层搜索。
    • 在递归返回后,执行回溯操作:将 path 的最后一个元素移除 (path.pop_back()),并将当前位置的标记恢复为未使用 (used[i] = false),以便进行下一次选择。

去重部分:

  • 在排列问题中,为了避免重复,需要确保每次选择的元素都是从未被使用过的元素中选择。
  • 去重部分: 在本题中,通过 used 数组来标记每个元素是否已经使用过,从而避免重复选择相同的元素。

  • for 循环遍历时 i=0 与之前的 i=startindex 的区别
    • for 循环遍历: 在回溯的过程中,每次递归调用时,通过 for 循环遍历整个 nums 数组。与上面的回溯算法相比,我们并没有显式地使用 startindex,而是每次从 i=0 开始遍历整个数组。这是因为在每一层递归中,我们需要考虑所有未被使用过的元素,而不是仅从某个特定位置开始。

代码:


class Solution {
private:
    vector<vector<int>> result;  // 存储最终结果的二维向量
    vector<int> path;  // 存储当前路径的一维向量

    // 回溯函数,参数为原始数组nums和标记数组used
    void backtracking(vector<int>& nums, vector<bool>& used) {
        // 当路径长度等于数组长度时,将当前路径加入结果集合
        if (path.size() == nums.size()) {
            result.push_back(path);
            return;
        }
        // 遍历数组nums
        for (int i = 0; i < nums.size(); i++) {
            // 如果元素已经被使用过,则跳过
            if (used[i]) continue;
            // 标记当前元素为已使用
            used[i] = true;
            // 将当前元素加入路径中
            path.push_back(nums[i]);
            // 递归进入下一层决策树
            backtracking(nums, used);
            // 回溯操作,撤销选择
            path.pop_back();
            // 恢复当前元素为未使用
            used[i] = false;
        }
    }

public:
    // 主函数,生成所有排列的入口函数
    vector<vector<int>> permute(vector<int>& nums) {
        // 标记数组,记录每个位置的元素是否被使用过
        vector<bool> used(nums.size(), false);
        // 调用回溯函数,从第一个位置开始生成排列
        backtracking(nums, used);
        // 返回最终的结果集合
        return result;
    }
};

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值