回溯:排列问题

LeetCode46. 全排列

https://leetcode-cn.com/problems/permutations/

给定一个 没有重复 数字的序列,返回其所有可能的全排列。

输入: [1,2,3]
输出:
[
  [1,2,3],
  [1,3,2],
  [2,1,3],
  [2,3,1],
  [3,1,2],
  [3,2,1]
]
思路

回溯模板

void backtracking(选择列表,路径,结果) 
{
    if (终止条件) 
    {
        存放结果;
        return;
    }

    for (在选择列表中选择) 
    {
        处理节点;
        backtracking(选择列表,路径,结果); 
        回溯,撤销处理结果
    }
}

所有用回溯解决的问题都可以抽象为树形结构

图片

回溯三部曲

1.递归函数的返回值和参数

首先排列是有序的,也就是说[1,2] 和[2,1] 是两个不同的排序,这和组合有所不同

可以看出元素1在[1,2]中已经使用过了,但是在[2,1]中还要在使用一次1,所以处理排列问题就不用使用startIndex了。但排列问题需要一个used数组,记录此时path里有哪些元素已经使用了,一个排列里一个元素只能使用一次,如上图橘黄色部分所示。

    vector<vector<int>> result;  //存放所有符合条件的路径的结果集合  
    vector<int> path;  //存放符合条件的路径

    void backtrack(vector<int>& nums, vector<bool>& used)

2.递归函数的终止条件

可以看出到达叶子节点,就是递归函数的终止条件。

当收集元素的数组path的大小达到和数组nums一样大的时候,说明找到了一个全排列,也表示到达了叶子节点。

        if (path.size() == sz)
        {
            result.push_back(path);
            return;
        }

3.搜索过程
我们定义的backtrack函数其实就像一个指针,在树中游走同时正确维护每个节点的属性。每当走到叶子节点(即碰到递归函数的终止条件),就保存符合条件的路径,返回并回溯。

        //控制树的横向遍历
        for (int i = 0; i < sz; ++i)
        {
            //path里已经使用的元素,直接跳过
            if (used[i]) continue;

            used[i] = true;
            path.push_back(nums[i]);
                
            backtrack(nums, used);  //递归:控制树的纵向遍历

            used[i] = false;
            path.pop_back();
            
        }
代码
class Solution 
{
private:
    vector<vector<int>> result;  //存放所有符合条件的路径的结果集合  
    vector<int> path;  //存放符合条件的路径

    void backtrack(vector<int>& nums, vector<bool>& used)
    {
        int sz = nums.size();

        if (path.size() == sz)
        {
            result.push_back(path);
            return;
        }
        
        //控制树的横向遍历
        for (int i = 0; i < sz; ++i)
        {
            //path里已经使用的元素,直接跳过
            if (used[i]) continue;

            used[i] = true;
            path.push_back(nums[i]);
                
            backtrack(nums, used);  //递归:控制树的纵向遍历

            used[i] = false;
            path.pop_back();
            
        }
    }

public:
    vector<vector<int>> permute(vector<int>& nums) 
    {
        //used数组存放bool类型的元素,初始值都为false表示数组nums的元素都没被访问过
        vector<bool> used(nums.size(), false); 
        backtrack(nums, used);

        return result;
    }
};

LeetCode47. 全排列II

https://leetcode-cn.com/problems/permutations-ii/

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

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

本题与LeetCode46. 全排列的区别是:本题数组nums的元素是有重复的,但不能有重复的组合。

所以本题的大体思路与LeetCode46. 全排列是相同的,重点是要去重。所谓去重,其实就是使用过的元素不能重复选取。

用回溯解决的问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上使用过,一个维度是同一树层上使用过。

根据题意:元素在同一个组合内是可以重复的,但两个组合不能相同。同一树层上的是两个组合里的元素,所以我们要去重的是同一树层上的“使用过”;而同一树枝上的都是一个组合里的元素,所以不用去重

要在代码层面实现以上的去重逻辑,就需要先对数组进行排序,然后定义一个bool类型数组used。这个数组的元素初始值都为false表示对应的candidate的元素都没被访问过。如果nums[i] == nums[i - 1] 并且 used[i - 1] == false,就说明:前一个树枝,使用了nums[i - 1],也就是说同一树层使用过nums[i - 1],此时for循环里应该做continue的操作。

所有用回溯解决的问题都可以抽象为树形结构

图片

代码
class Solution 
{
private:
    vector<vector<int>> result;  //存放所有符合条件的路径的结果集合  
    vector<int> path;  //存放符合条件的路径

    void backtrack(vector<int>& nums, vector<bool>& used)
    {
        int sz = nums.size();

        if (path.size() == sz)
        {
            result.push_back(path);
            return;
        }
        
        //控制树的横向遍历
        for (int i = 0; i < sz; ++i)
        {
            if (i >= 1 && nums[i] == nums[i-1] && used[i-1] == false) continue;

            //path里已经使用的元素,直接跳过
            if (used[i]) continue;

            used[i] = true;
            path.push_back(nums[i]);
                
            backtrack(nums, used);  //递归:控制树的纵向遍历

            used[i] = false;
            path.pop_back();
            
        }
    }

public:
    vector<vector<int>> permuteUnique(vector<int>& nums) 
    {
        sort(nums.begin(), nums.end());

        //used数组存放bool类型的元素,初始值都为false表示数组nums的元素都没被访问过
        vector<bool> used(nums.size(), false); 
        backtrack(nums, used);

        return result;
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值