全排列问题及其引申问题

全排列问题是经典的算法题目。实现可以使用库函数(如STL next_permutation), 也可以递归回溯法。

同时其follow up又包括:

当排列中有重复元素时如何处理;next permutaion的实现原理;查找第K个排列等。

本文就这些问题进行讨论。

注:上述问题对应LeetCode46, 47, 31, 60。 

1. 用next_permutaion实现

首先不考虑算法题目本身,单纯从应用的角度来讲,用库函数自然是最便捷的方法。

STL next_permutaion的接口如下:

template <class BidirectionalIterator>
  bool next_permutation (BidirectionalIterator first,
                         BidirectionalIterator last);

template <class BidirectionalIterator, class Compare>
  bool next_permutation (BidirectionalIterator first,
                         BidirectionalIterator last, Compare comp);

当需要自己制定排序规则时,使用第三个参数。

一般使用do-while结构解决全排列问题。 看如下例子即可理解使用:

 1 class Solution {
 2 public:
 3     vector<vector<int>> permute(vector<int>& nums) {
 4         vector<vector<int>> result;
 5         int len = nums.size();
 6         sort(nums.begin(), nums.end());
 7         do {
 8             result.push_back(nums);
 9         } while (next_permutation(nums.begin(),nums.end()));
10     
11         return result;
12     }
13 };

注: 1. 如果需要用全排列解决其他问题,do 中即为对每个排列的操作。

   2. 使用next_permutaion可以天然解决重复元素问题(原因见后述next_permutation实现原理)。

 

2. 递归解决全排列(回溯法)

回溯法的思路也不难理解。

考察其如何递归,以1234为例。首先需要考虑第一位,可以以此与后续元素交换得到2-134, 3-214, 4-231.

然后对后三位递归地调用(与后续元素交换)即可。当递归到无法交换(就剩一位),则输出。

代码如下:

class Solution {
private:
    vector<vector<int>> result;
    void helper(vector<int>& nums, int start, int end) {
        if (start == end) {
            result.push_back(nums);
        }
        for (int i = start; i <= end; ++i) {
            swap(nums[start], nums[i]);
            helper(nums, start + 1, end);
            swap(nums[start], nums[i]);
        }
    }
public:
    vector<vector<int>> permute(vector<int>& nums) {
        helper(nums, 0, nums.size() - 1);
        return result;
    }
};

 

3. 有重复元素怎么办?

考察1223, 则一步递归可以处理为1-223, 2-123, 3-221.

也就是说,第 i 个字符与 第 j 个字符交换后,要求[i, j)中没有与 第 j 个字符相等的数。

所以加上一个判断函数isDuplicate判断即可。

代码:

 1 class Solution {
 2 private:
 3     vector<vector<int>> result;
 4     bool isDuplicate(vector<int>& nums, int start, int end) {
 5         for (int i = start; i < end; ++i) {
 6             if (nums[i] == nums[end]) {
 7                 return false;
 8             }
 9         }
10         return true;
11     }
12     void helper(vector<int>& nums, int start, int end) {
13         if (start == end) {
14             result.push_back(nums);
15         }
16         for (int i = start; i <= end; ++i) {
17             if (!isDuplicate(nums, start, i)) {
18                 continue;
19             }
20             swap(nums[start], nums[i]);
21             helper(nums, start + 1, end);
22             swap(nums[start], nums[i]);               
23         }
24     }
25 public:
26     vector<vector<int>> permuteUnique(vector<int>& nums) {
27         sort(nums.begin(), nums.end());
28         helper(nums, 0, nums.size() - 1);
29         return result;
30     }
31 };

 

4. next_permutation到底怎么实现的?

上述递归地思路理解以后,要考察的就是next_permutation这个算法的原理到底是什么。

这个还是一个比较经典但是不太好想的算法。

算法可以总结为如下几个步骤:

1)找字符串中最后一个升序位置(从后往前找第一个满足s[i] < s[i + 1]);

2)  在s[i+1, ... N-1]中找比S[i]大的最小值S[j];

3)  交换s[i], s[j];

4)  翻转s[i + 1, ... N - 1]。

 

这一串跟咒语一样的算法道理何在呢?可以考察如下的例子分析其原理:

1)  首先要注意的是,我们找的是下一个排列,所以应该想要改变之后的增值尽可能小。如 124653 -> 125346;

     也就是说,高位应该尽量一致,能动低位的时候就不要动高位。如上例子,当4改成5就能增大的时候,不要动1,2(653不能改变顺序使其增大)。

2)  那4是如何找到的,也就是说最后一个能动的地位是谁呢? 这就应该从后往前看,显然53没得动,653也没得动,但4653可以动了。(这就是上述算法步骤1

     原因在于,如果从后往前看的时候,得到的后方元素都是递减的,也就是在这一局部(比如653)他已经没有next_permutation了,所以要再向前找。

     只到发现一个位置i, nums[i] < nums[i+1]这意味着 nums[i....size-1] (如4653)这一局部是还有next_permutation。所以位置 i 就是需要被交换。

3)  但他应该交换谁呢?还是考虑上面说的想要改变之后的增值尽可能小,所以应该交换大于nums[i]的最小值

     也就是后面位置中从后往前数(从小往大数)第一个大于nums[i]的元素。(对应算法步骤2,3

4)  当交换完之后,即例子中变为125643,可以发现。nums[i+1,...size-1](即643)一定是完全降序的。

     所以为了能组成元素的最小值(这样增值才最小),应该reverse这一部分,变为346,(对于算法步骤4)。

     得到最终结果125346。

所以上述算法四个步骤也可以简写为:后找,小大,交换,翻转(这个简写原作者为邹博

将上述算法步骤写为代码就很简单了,如下所示:

 1 class Solution {
 2 public:
 3     void nextPermutation(vector<int>& nums) {
 4         if (nums.size() < 2) {
 5             return;
 6         }
 7         int i = nums.size() - 2;
 8         for (i; i >= 0; i--) {
 9             if (nums[i] < nums[i+1]) {
10                 break;
11             }
12         }
13         if (i == -1) {
14             sort(nums.begin(), nums.end());
15             return;
16         }
17         int j = nums.size() - 1;
18         for (j; j > i; j--) {
19             if (nums[j] > nums[i]) {
20                 break;
21             }
22         }
23         swap(nums[i], nums[j]);
24         reverse(nums.begin() + i + 1, nums.end());
25         
26     }
27 };

 

5. 给定一个排列,他的字典序第K个是多少呢?

当然这个问题最直观的思路还是用next_permutation一个一个找,边找边记录个数。但是显然这里的效率太低。

考虑直接构造出第K个。

k / (n-1)! 表示了当前这一位应该取剩下的备选元素中的第几个元素;

然后k 减去因为布置好这一位占据的序列数,并且n--, 以此递推下去,可以得到结果。

代码:

 1 class Solution {
 2 private:
 3     int fact(int n) {
 4         int result = 1;
 5         for (int i = 1; i <= n; ++i) {
 6             result *= i;
 7         }
 8         return result;
 9     }
10 public:
11     string getPermutation(int n, int k) {
12         string result;
13         vector<int> init(n);
14         for (int i = 0; i < n; ++i) {
15             init[i] = i + 1;
16         }
17         int i = k - 1, count = n;
18                    
19         while (i >= 0 && count > 0) {
20             int temp = i / fact(count - 1);
21             result += ('0' + init[temp]);
22             i -= ( temp * fact(count - 1));
23             init.erase(init.begin() + temp);
24             count--;
25         }
26         return result;
27     }
28 };

 

posted @ 2016-10-10 22:59  wangxiaobao1114  阅读(1007)  评论(0编辑  收藏  举报