• 全排列问题及其引申问题


    全排列问题是经典的算法题目。实现可以使用库函数(如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 };
  • 相关阅读:
    在oracle配置mysql数据库的dblink
    项目中非常有用并且常见的ES6语法
    原生js的容易忽略的相似点(二)
    原生js的容易忽略的相似点(一)
    json常用方法和本地存储方法
    vue-cli下面的config/index.js注解 webpack.base.conf.js注解
    vue跨域解决及打包
    js里面Object的一些方法
    vw+vh+rem响应式布局
    toast插件的简单封装(样式适用pc后台管理系统的场景)
  • 原文地址:https://www.cnblogs.com/wangxiaobao/p/5947633.html
Copyright © 2020-2023  润新知