回溯算法——全排列问题

全排列问题(Permutation Problem)

一、问题特点

给出一个数组,返回所有可能的全排列。其中“全排列”的定义如下:

将n个元素按照一定的顺序排列起来,所有的排列情况的集合叫全排列

全排列问题的整体思路和其他回溯问题相仿,但去重操作和其他问题有所不同,这是由其自身性质决定的:

  • 排列问题中每一条路径都必须遍历原集合中所有的元素(终止条件恒为 path.size() == nums.size() )
  • 排列问题中每一种状态中元素的排列顺序必须各不相同
  • 任何一条路径都不能重复遍历同一个元素

二、遍历方法

因为不能使用startIndex修剪搜索空间,每层遍历都要从nums[0]遍历到nums[nums.size()-1]。
路径中的状态不需要记录到结果数组中,终止条件恒为 path.size() == nums.size()

三、去重方法

1.树枝去重

原理

由于同一条路径不能重复取同一个元素,所以必须进行树枝去重。在以往的组合、切割和子集问题中,我们通过使用startIndex限制剩余树枝的搜索空间来完成对同一树枝上元素的去重,但这种去重方法将压减右侧树枝的深度,导致除最左侧树枝以外的所有树枝无法满足路径长度等于原集合大小的条件。此外,这种去重方法也忽视了排列问题解的有序性(例:若原集合为[7, 1],则解集中[7, 1] 和[1, 7]将无法同时出现,因为当右边的树枝取到1时,它以下的路径的取值范围已经变成了[],元素7已被剪除)
为了进行树枝去重并保证每一条路径都能取到所有元素,需要建立一个数组visited[]存储每一路径中各个元素被使用的情况。

  • visited[i] == false :说明当前路径上还没有使用过nums[i],此时左侧路径有可能使用过nums[i]
  • visited[i] == true :说明当前路径上已经使用过nums[i],不可重复使用
例题 46.全排列(LeetCode)

题目说明:

给定一个不含重复数字的数组 nums ,返回其所有可能的全排列 。你可以按任意顺序返回答案。

示例:

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

提示:

1 <= nums.length <= 6
-10 <= nums[i] <= 10
nums 中的所有整数互不相同

代码示例:

class Solution {
public:
    void backtracking(vector<int> nums, vector<bool>& visited) {
        if(path.size() == nums.size()) {
            ans.push_back(path);
            return;
        }
        for(int i = 0; i < nums.size(); i++) {
            if(visited[i]) {
                continue;
            }
            path.push_back(nums[i]);
            visited[i] = true;
            backtracking(nums, visited);
            path.pop_back();
            visited[i] = false;
        }
        return;
    } 
    vector<vector<int>> permute(vector<int>& nums) {
        vector<bool> visited(nums.size(), false);
        backtracking(nums, visited);
        return ans;
    }
private:
    vector<vector<int>> ans;
    vector<int> path;
};

2.树层去重

原理

使用条件: 原集合中有重复元素
全排列问题对树层的去重操作与组合问题的思路相同(详情可以参考《回溯算法及其应用》中的40.组合总和II(LeetCode)
仍然使用visited[]数组去重,但在此之前要先将原集合进行排序,保证所有相同值的元素相邻。然后通过以下语句完成树层去重。

if(i > 0 && nums[i] == nums[i-1] && visited[i-1] == false) {
    // 如果有相同值的上一位元素不是在本树枝上被遍历的,说明在同一层上已被取过,不可重复取值
    continue;
}
例题 47.全排列II(LeetCode)

题目说明:

给定一个可包含重复数字的数组 nums ,返回其所有可能的全排列 。你可以按任意顺序返回答案。

示例:

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

提示:

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

代码示例:

class Solution {
public:
    void backtracking(vector<int> nums, vector<bool>& visited) {
        if(path.size() == nums.size()) {
            ans.push_back(path);
            return;
        }
        for(int i = 0; i < nums.size(); i++) {
            if(i > 0 && nums[i-1] == nums[i] && visited[i-1] == false) {
                // 树层去重
                continue;
            }
            if(visited[i] == false) {
                // 树枝去重
                visited[i] = true;
                path.push_back(nums[i]);
                backtracking(nums, visited);
                path.pop_back();
                visited[i] = false;
            }
        }
    }
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        vector<bool> visited(nums.size(), false);
        sort(nums.begin(), nums.end());
        backtracking(nums, visited);
        return ans;
    }
private:
    vector<int> path;
    vector<vector<int>> ans;
};
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值