全排列问题算法及实现(Permutation)

前言

做项目遇到数据采集系统中ADC拼合问题,如果顺序不对,波形就是错误的(题外话),为了找到正确的顺序,涉及到排列问题。

什么是排列组合

定义

一般地,从n个不同元素中取出m(m≤n)个元素,按照一定的顺序排成一列,叫做从n个元素中取出m个元素的一个排列(Arrangement)。特别地,当m=n时,这个排列被称作全排列(Permutation)。 
Ex:考虑三个数字1,2,3,这个序列有6个可能的排列组合 
123 
132 
213 
231 
312 
321 
这些排列组合根据less-than操作符做字典顺序的排序。 
字典顺序顾名思义是就是将1-n的一个排列看成一个数,然后按照字典的顺序从小到达的输出

全排列及序号

所谓的全排列,就是说将数字进行不重复的排列,所有得到的序列,就是全排列 
给定数字1 , 2 , 3 , 4,其全排列是: 
{1,2,3,4}, {1,2,4,3}, {1,3,2,4}, {1,3,4,2}, {1,4,2,3}, {1,4,3,2}

{2,1,3,4}, {2,1,4,3}, {2,3,1,4}, {2,3,4,1}, {2,4,1,3}, {2,4,3,1}

{3,1,2,4}, {3,1,4,2}, {3,2,1,4}, {3,2,4,1}, {3,4,1,2}, {3,4,2,1}

{4,1,2,3}, {4,1,3,2}, {4,2,1,3}, {4,2,3,1}, {4,3,1,2}, {4,3,2,1} 
全排列如上所示,那么什么是全排列的序号?这里我们通常将全排列按照字典序进行编排,就如上面从左到右看,就是按照字典序排列的。 
我们说,对于1,2,3,4的全排列,第20号序列是{4,1,3,2},因为其在这个按照字典序排列的全排列中处在第20的位置。

排列组合涉及的问题

  • 下一个全排列
  • 上一个全排列
  • 给定一个排列的序号以及排列中数字的个数,那么这个排列是什么
  • 给定一个排列,求这个排列的序号是多少

下一个全排列(next_permutation)

在STL中,有next_permutation的算法实现。 
next_permutation()会取得[first,last) 所标之序列的下一个排列组合。如果没有下一个排列组合,便返回false,否则返回true。 
算法: 
首先,从最尾端开始往前寻找两个相邻元素,令第一个元素为*i,第二个元素为*ii,且满足*i < *ii。找到这样一组相邻元素后,再从最尾端开始往前检验,找出第一个大于*i 的元素,令为*j ,将i,j元素对调,再将ii之后的所有元素颠倒排序。 
如下图:方框为i和ii 
全排列问题算法及实现(Permutation)_第1张图片 
Code:

class Solution {
public:
    void nextPermutation(vector<int>& nums) {
        vector<int>::iterator first=nums.begin();
        vector<int>::iterator last=nums.end();
        if(first==last)      //empty
            return;
        vector<int>::iterator i=first;
        i++;
        if(i==last)    //only one element
            return;
        i=last;        //i指向尾端
        i--;           
        for(;;)
        {
            vector<int>::iterator ii=i;
            --i;   //锁定一组(两个)相邻元素
            if(*i<*ii)   //如果前一个元素小于后一个元素
            {
                vector<int>::iterator j=last;    //j指向尾端
                while(!(*i < *--j));             //从尾端往前找,直到比*i大的元素
                iter_swap(i,j);
                reverse(ii,last);
                return;
            }
            if(i==first)
            {
                reverse(first,last);
                return;
            }
        }
    }
};

上一个全排列(prev_permutation)

首先,从最尾端开始往前寻找两个相邻元素,令第一个元素为*i,第二个元素为*ii,且满足*i > *ii。找到这样一组相邻元素后,再从最尾端开始往前检验,找出第一个小于*i 的元素,令为*j ,将i,j元素对调,再将ii之后的所有元素颠倒排序。 
代码和next_permutation类似

已知排列求序号

康托展开式实现了由1到n组成的全排列序列到其编号之间的一种映射 
公式: 
X=an*(n-1)!+an-1*(n-2)!+…+ai*(i-1)!+…+a2*1!+a1*0!

由1到n这n个数组成的全排列,共n!个,按每个全排列组成的数从小到大进行排列,并对每个序列进行编号(从0开始),并记为X;比如说1到4组成的全排列中,1234对应编号0,1243对应编号1。

对于ai(系数)的解释需要用例子来说明:

对1到4的全排列中,我们来考察3214,则

  1. a4={3在集合(3,2,1,4)中是第几大的元素,有多少个逆序对}=2

  2. a3={2在集合(2,1,4)中是第几大的元素,有多少个逆序对}=1

  3. a2={1在集合(1,4)中是第几大的元素,有多少个逆序对}=0

  4. a1=0(最后只剩下一项)

也就是说康托公式中的每一项依次对应全排列序列中的每个元素,并按上述规则映射;

则X=2*3!+1*2!+0*1!+0*0!=14,即3214对应的编号为14。

Code:

//已知排列求序号
long long getIndex(short dim, short *rankBuf)
{
    int i, j;
    long long index = 0;
    long long k = 1;
    for (i = dim - 2; i >= 0; k *= dim - (i--))//每一轮后k=(n-i)!,注意下标从0开始
        for (j = i + 1; jif (rankBuf[j]index += k;//是否有逆序,如有,统计,即计算系数
    return index;
}

已知序号求排列

康托公式可以根据排列的序号来求出该排列,即通过X的值求出ai(i大于等于1小于等于n)的值,运用辗转相除的方法即可实现,现在已知一个编号14(注意该编号是从0开始的,如果是从1开始,此处要减1),求其对应的全排列序列: 
14 / (3!) = 2 余 2 
2 / (2!) = 1 余 0 
0 / (1!) = 0 余 0 
0 / (0!) = 0 余 0 
故得到:a4=2,a3=1,a2=0,a1=0,由ai的定义即可确定14对应的全排列为2103.
Code:

//已知序号求排列
void getPermutation(int dim, short *rankBuf, long long index){
    short i, j; 
    //求逆序数数组
    for (i = dim - 1; i >= 0; i--)
    {
        rankBuf[i] = index % (dim - i);
        index /= dim - i;
    }
    for (i = dim - 1; i; i--)
        for (j = i - 1; j >= 0; j--)
            if (rankBuf[j] <= rankBuf[i])
                rankBuf[i]++;       //根据逆序数数组进行调整   
}

你可能感兴趣的:(全排列问题算法及实现(Permutation))