leetcode 回溯法 全排列的四道题

 

1.leetcode 31 题 next permuation

题目描述:给定任一非空正整数序列,生成这些数所能排列出的下一个较大序列。若给出的序列为最大序列,则生成最小序列。

输入 → 输出
1,2,3 → 1,3,2
3,2,1 → 1,2,3
1,1,5 → 1,5,1

一开始没弄明白序列的大小是怎么定义的,看了几篇解析才弄明白,一个序列如果是非减的序列,则是最小的序列;如果一个序列是非增的序列,则是最大的序列。 

设序列p(n)=3,6,4,2,根据定义可算得下一个序列p(n+1)=4,2,3,6
  1. 观察p(n)可以发现,其子序列6,4,2已经为减序,那么这个子序列不可能通过交换元素位置得出更大的序列了。因此必须移动最高位3的位置,且要在子序列6,4,2中找一个数来取代3的位置
  2. 子序列6,4,2中6和4都比3大,但6大于4。如果用6去替换3得到的序列一定会大于4替换3得到的序列,因此只能选4。将4和3的位置对调后形成排列4,6,3,2。对调后得到的子序列6,3,2仍保持减序,即这3个数能够生成的最大的一种序列
  3. 而4是第1次作为首位的,需要右边的子序列最小,因此4右边的子序列应为2,3,6,这样就得到了正确的一个序列p(n+1)=4,2,3,6

      由此,我们可以知道,本题的关键即是求出数组末尾的最长的非递增子序列。
      不妨假设在数组nums中,nums[k+1]...nums[n]均满足前一个元素大于等于后一个元素,即这一子序列非递增。
      那么,我们要做的,就是把nums[k]与其后序列中稍大于nums[k]的数交换,接着再逆序nums[k+1]...nums[n]即可。

class Solution {
    public void nextPermutation(int[] nums) {
        for(int i = nums.length - 2; i >= 0; i--){
            if(nums[i + 1] > nums[i]){
                for(int j = nums.length - 1; j > i; j--){
                    if(nums[j] > nums[i]){
                        swap(nums, i, j);
                        //翻转一下
                        reverse(nums, i + 1, nums.length - 1);
                        return;
                    }
                }
            }
        }
        
        Arrays.sort(nums);
    }
    
    private void swap(int[] nums, int left, int right){
        int temp = nums[left];
        nums[left] = nums[right];
        nums[right] = temp;
    }
    
    private void reverse(int[] nums, int start, int end){
        while(start <= end){
            swap(nums, start, end);
            start++;
            end--;
        }
    }
}

2.leetcode 46 permutations

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

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

分析:

   方法一:题目给的数组是没有重复元素的,直接进行递归回溯。用一个循环遍历数组中的每一个元素,并将该元素至于排列的序列首部,通过方法swap交换是该元素到达首部,继续递归,遍历剩余的元素,并将剩余的元素置于排列的序列首部,继续递归......直到得到一个排列,结束;方法返回后,重置当前递归层次的子序列首部元素,并将下一个元素置于子序列首部,即回溯到分支处,并选择另外的分支继续递归。

   方法二:利用Boolean数组used去标记已经用过的元素,在递归时每次都从从开始递归遍历,但是可以通过used数组跳过已经使用过的元素。

解法一:遍历元素并利用交换置于序列首部,仅用于数组无重复元素
class Solution {
    private List<List<Integer>> result = new ArrayList<>();
    
    public List<List<Integer>> permute(int[] nums) {
        Integer[] myNums = new Integer[nums.length];
        for(int i = 0; i < nums.length; i++)
            myNums[i] = nums[i];
        
        backTrack(myNums, 0);
        
        return result;
    }
    
    private void backTrack(Integer[] nums, int index){
        if(index == nums.length){
            result.add(new ArrayList<>(Arrays.asList(nums)));
            return;
        }
        
        for(int i = index; i < nums.length; i++){
            swap(nums, index, i);
            backTrack(nums, index + 1);
            swap(nums, index, i);
        }
    }
    
    private void swap(Integer[] nums, int left, int right){
        Integer temp = nums[left];
        nums[left] = nums[right];
        nums[right] = temp;
    }
}
解法二:比解法一更加一般的解法,这种方法更好。利用used数组来标记nums数组中哪些元素已经用过了
class Solution {
    private List<List<Integer>> result = new ArrayList<>();
    
    public List<List<Integer>> permute(int[] nums) {
        backTrack(nums, new ArrayList<>(), new boolean[nums.length], nums.length);
        return result;
    }
    
    private void backTrack(int[] nums, List<Integer> out, boolean[] used, int length){
        if(out.size() == length){
            result.add(new ArrayList<>(out));
            return;
        }
        for(int start = 0; start < length; start++){
            if(used[start])
                continue;
            
            used[start] = true;
            out.add(nums[start]);
            backTrack(nums, out, used, length);
            out.remove(out.size() - 1);
            used[start] = false;
        }
    }
}

3.leetcode 47 permutationsII

题目描述:给定一个含有重复数字的序列,返回这些数所能排列出的所有不同的序列。 

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

分析:这道题和上一题的区别在于给的数组包含重复元素,而且题目要求找出所有不同的全排列。而如果仍按照上题的解法去做,则会因为重复元素的存在而出现重复的排列。因此需要在上一题的基础之上进行剪枝,将重复的排列剪去。

   方法一:如果仍采用交换的方法,则有简单的去重方法,可以用空间来换取简便,用set去重。

   方法二:为了能够剪枝(即判断当前元素是否和前一个元素相同)我们先对题目给的数组进行排序。然后仍和上一题一样创建used数组,并进行dfs + 回溯,只不过我们需要将重复的元素剪去,如果出现重复元素,则不要重复将该元素置于序列首部(注意我们剪枝剪去的是重复元素置于首部),只需置一次即可,由于数组是有序的,如果前一个元素a和当前元素b相同,由于遍历有先后顺序,a在b之前已经当做序列的首部用过了,如果此时元素a在数组中的位置为i,且used[i]为false,即a没用过,则不能将b放在首部(会出现重复,和b相同的a之前已经当过首部了;而此时的a却没用过,即当前递归中要将b置于首部,所以会和之前a当首部重复)

解法二:不利用交换,利用used数组标记已经使用过的元素,这种方法更一般,更好用
class Solution {
    private List<List<Integer>> result = new ArrayList<>();
    
    public List<List<Integer>> permuteUnique(int[] nums) {
        Arrays.sort(nums);
        backTrack(nums, new ArrayList<Integer>(), new boolean[nums.length], nums.length);
        return result;
    }
    
    private void backTrack(int[] nums, List<Integer> out, boolean[] used, int length){
        if(out.size() == length){
            result.add(new ArrayList<>(out));
            return;
        }
        for(int start = 0; start < length; start++){
            if(used[start] || start != 0 && nums[start - 1] == nums[start] && !used[start - 1])
                continue;
            
            used[start] = true;
            out.add(nums[start]);
            backTrack(nums, out, used, length);
            out.remove(out.size() - 1);
            used[start] = false;
        }
    }
}

4.leetcode 60 permutation sequence

题目描述:给定正整数n和k,要求返回在[1,2,...,n]所有的全排列中,第k大的字符串序列。

输入:[3 , 4]

输出:“231”

分析:首先关于“大小”的定义,仍和第一题类似。具体请看第一题。这道题其实和前面一样的解法,只不过不需要保留所有的解法。

方法一:和上一题的解法二类似,使用used数组标记已经用过的数组元素,并进行递归回溯。将第k个排列保存下来。

想不通,写出来的代码为什么总是输出空。

问题在于之间使用stringbuilder的substring方法在递归回溯后去掉尾部字符,substring方法返回的是新的string而不是stringbuilder,应该用deleteCharAt方法。

解法一:
class Solution {
    private String result = new String();
    private int count = 1;
    
    public String getPermutation(int n, int k) {
        int[] nums = new int[n];
        for(int i = 1; i <= n; i++){
            nums[i - 1] = i;
        }
        backTrack(nums, new StringBuilder(), new boolean[n], n, k);
    
        return result;
    }
    
    private void backTrack(int[] nums, StringBuilder out, boolean[] used, int length, int k){
        if(out.length() == length && count == k){
            result = out.toString();
            count++;
            return;
        }
        if(out.length() == length && count != k){
            count++;
            return;
        }
        for(int start = 0; start < length; start++){
            if(used[start])
                continue;
            
            used[start] = true;
            out.append(nums[start]);
            backTrack(nums, out, used, length, k);
            out.deleteCharAt(out.length() - 1);
            used[start] = false;
        }
    }
}

方法二:方法一存在冗余,如果k位于中间位置,则即便是找到了第k个,递归却仍在进行,浪费时间复杂度。

这道题是让求出n个数字的第k个排列组合,由于其特殊性,我们不用将所有的排列组合的情况都求出来,然后返回其第k个,我们可以只求出第k个排列组合即可,那么难点就在于如何知道数字的排列顺序,可参见网友喜刷刷的博客,首先我们要知道当n = 3时,其排列组合共有3! = 6种,当n = 4时,其排列组合共有4! = 24种,我们就以n = 4, k = 17的情况来分析,所有排列组合情况如下:

1234
1243
1324
1342
1423
1432
2134
2143
2314 
2341
2413
2431
3124
3142
3214
3241
3412 <--- k = 17
3421
4123
4132
4213
4231
4312
4321

我们可以发现,每一位上1,2,3,4分别都出现了6次,当最高位上的数字确定了,第二高位每个数字都出现了2次,当第二高位也确定了,第三高位上的数字都只出现了1次,当第三高位确定了,那么第四高位上的数字也只能出现一次,下面我们来看k = 17这种情况的每位数字如何确定,由于k = 17是转化为数组下标为16:

最高位可取1,2,3,4中的一个,每个数字出现3!= 6次(因为当最高位确定了,后面三位可以任意排列,所以是3!,那么最高位的数字就会重复3!次),所以k = 16的第一位数字的下标为16 / 6 = 2,在 "1234" 中即3被取出。这里我们的k是要求的坐标为k的全排列序列,我们定义 k' 为当最高位确定后,要求的全排序列在新范围中的位置,同理,k'' 为当第二高为确定后,所要求的全排列序列在新范围中的位置,以此类推,下面来具体看看:

第二位此时从1,2,4中取一个,k = 16,则此时的 k' = 16 % (3!) = 4。如下所示,每个数字出现2!= 2次,所以第二数字的下标为4 / 2 = 2,在 "124" 中即4被取出。

3124
3142
3214
3241
3412 <--- k' = 4=2!
3421

第三位此时从1,2中去一个,k' = 4=2!,则此时的k'' = 4 % (2!) = 0,如下所示,而剩下的每个数字出现1!= 1次,所以第三个数字的下标为 0 / 1 = 0,在 "12" 中即1被取出。

3412 <--- k'' = 0
3421

第四位是从2中取一个,k'' = 0,则此时的k''' = 0 % (1!) = 0,如下所示,而剩下的每个数字出现0!= 1次,所以第四个数字的下标为0 / 1= 0,在 "2" 中即2被取出。

3412 <--- k''' = 0

那么我们就可以找出规律了 an为结果序列中第n个字符在nums中的索引,nums需要不断更新,每次拿到一个字符,都需要将其从nums中删除,因为每次都是从剩下的字符中取一个。
a1 = k / (n - 1)!
k1 = k %(n-1)!

a2 = k1 / (n - 2)!
k2 = k1 % (n - 2)!
...

an-1 = kn-2 / 1!
kn-1 = kn-2 % 1!

an = kn-1 / 0!
kn = kn-1 % 0!

class Solution {
    public String getPermutation(int n, int k) {
        k--;
        List<Integer> nums = new ArrayList<>();
        for(int i = 1; i <= n; i++)
            nums.add(i);
        
        int[] f = new int[n];
        f[0] = 1;
        for(int i = 1; i < n; i++)
            f[i] = f[i - 1] * i;
        
        StringBuilder result = new StringBuilder();
        int temp = 0;
        
        for(int i = n - 1; i >= 0; i--){
            temp = k / f[i];
            k = k % f[i];
            result.append(nums.get(temp));
            nums.remove(temp);
        }
        
        return result.toString();
    }
}

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值