全排列有两种枚举顺序:
(1)按顺序枚举每个位置填哪个数
(2)按顺序枚举每个数填哪个位置

两种顺序都能解,但如果要保证字典序,则需要使用(1)按顺序枚举每个位置填哪个数。因为优先考虑将小的数放到前面位置。

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

示例 1:
输入: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 中的所有整数 互不相同

方案:采用(1)按顺序枚举每个位置填哪个数

class Solution {
    List<Integer> path = new ArrayList<>();
    List<List<Integer>> ans = new ArrayList<>();;
    public List<List<Integer>> permute(int[] nums) {
        dfs(nums, 0, 0);
        return ans;
    }

    // u:坑位,按顺序枚举每个坑位填哪个数
    public void dfs(int[] nums, int u, int state) {
        if (u == nums.length) {
            ans.add(new ArrayList<>(path));
            return;
        }
        for (int i = 0; i < nums.length; i++) { //枚举每个数
            if ((state >> i & 1) == 0) { 
                path.add(nums[i]);
                dfs(nums, u + 1, state + (1 << i));
                path.remove(path.size() - 1);
            }
        }
    }
}

leetcode 47. 全排列 II

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

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

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

方法1:[依次枚举每个数,看能填到哪些坑位上]

class Solution {
public:
    vector<vector<int>> ans;
    vector<int> path;
    vector<vector<int>> permutation(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        path.resize(nums.size());
        dfs(nums, 0, 0, 0);
        return ans;
    }

 // u:枚举到第几个数 start:上一个数的坑位在哪 state: 标记坑位是否被占
    void dfs(vector<int> &nums, int u, int start, int state) {
        if (u == nums.size()) {
            ans.push_back(path);
            return;
        }
        if (!u || nums[u] != nums[u - 1]) start = 0; //下一个重复元素只能放在后面,有序性。其余都从第0个坑位开始看能否填

        for (int i = start; i < nums.size(); i ++) //枚举坑位,看能否填上nums[u]
            if (!(state >> i & 1)) {
                path[i] = nums[u];
                dfs(nums, u + 1, i + 1, state + (1 << i));
            }
    }
};

此处i:坑位,u:第几个数。此处和上一题不一样!上一题是依次枚举每个坑位看能填哪些数,而此题是依次枚举每个数,看能填到哪些坑位上。(因为此题依次枚举每个数可以判断重复数字,重复的只能放到上一个重复填的坑位之后)
链接:https://www.acwing.com/activi...

解法2:【第u坑位枚举每个数能否填,即按顺序枚举每个位置填哪个数】

vector<vector<int>> ans;
vector<int> path;
vector<bool> st;

void dfs(vector<int>& nums, int u) {
    if (u == nums.size()) {
        ans.push_back(path);
        return;
    }

    for (int i = 0; i < nums.size(); i++) // 依次枚举每个数,所以可以规定相同数的顺序
        if (!st[i]) {
            if (i && (nums[i] == nums[i - 1]) && !st[i - 1]) // 用第一次没有用过的相同数,不能跳。则顺序就定下来了
                continue;
            st[i] = true;
            path[u] = nums[i]; // 因为依次枚举每个位置填哪个数,故u递增也可以使用path.push_back(nums[i]);
            dfs(nums, u + 1);
            st[i] = false;     // 此时就需要path.pop_back();
        }
}

vector<vector<int>> permuteUnique(vector<int>& nums) {
    sort(nums.begin(), nums.end());
    st = vector<bool>(nums.size(), false);
    path = vector<int>(nums.size());
    dfs(nums, 0);
    return ans;
}

又回来看一遍视频,讲的实在太好了,有两句最核心的内容,我加上自己的理解整理写了一下。
A 1分24秒到2分00秒:讲了为什么要先排序 因为结果是要去重的,而结果都是列表形式的,那列表怎么判重呢?就是排序之后一个一个的比较,所以那还不如先排序之后再计算,再计算的过程中判断是否有重复,免得对每个结果再做一次排序去重操作。
B 2分25秒到4分10秒:讲了怎么判断当前分支是否是重复的分支。 因为之前已经排好序了,所以当前元素nums[i] 如果和前一个元素相同,即nums[i] == nums[i-1]就说明该分支有可能是重复的。但是这个相等条件有两种可能一种是,1 1‘ 2,也就是选择完1之后再选择第二个1,两个元素虽然重复,但是第二个元素是前一个元素的下一层,这时是没有问题的。另一种是之前的同层分支已经有 1 1‘ 2了,这次的选择是 1‘ 1 2 。两个元素重复,且重的是同层路径。那就说明是重复分支。
具体区分的办法是 nums[i-1] 的used状态是被选择的,那么说明当前的nums[i] 是 nums[i-1]的下一层路径。否则如果 nums[i-1] 的状态是没被选择的,那么说明当前的nums[i] 是nums[i-1] 同层路径。

有内鬼终止AC
2021-10-01
@GB2312 说的太好了,补充一下
为什么" nums[i-1] 的used状态是被选择的,那么说明当前的nums[i] 是 nums[i-1]的下一层路径。"
原因:因为往下层递归的话,一直再往下层走,dfs还未return,也就是说used还没有被回溯为未选择状态,所以同一条分支上,nums[i-1] 的used状态一定是被选择的。
为什么" 如果 nums[i-1] 的状态是没被选择的,那么说明当前的nums[i] 是nums[i-1] 同层路径。"
原因:递归到叶节点,开始往上回溯了,回溯到某一层时把used[i-1]回溯为未选择状态,然后for循环i++横向移动,自然这时再判断used[i-1]就一定是未选择状态。

因为我们是枚举每个坑位填哪个数,所以相同数的枚举到的顺序肯定是依次的,不能也不会跳过!

PS:两种解法中排序的目的不是让数组有序,而只是让相同的数放在一起。


zahlenw2
1 声望0 粉丝

引用和评论

0 条评论