全排列问题及其引申问题
全排列问题是经典的算法题目。实现可以使用库函数(如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 };