递增全排列算法

问题

给定一个递增序列<a_1,a_2,\cdots,a_n>
其中,\forall i,j \in \{1,2,\cdots,n\},若i<j,则a_i<a_j。求它的递增全排列集合\mathbb P

所谓递增全排列\mathbb P是这样一个有序集合:
{\mathbb P}=<p_1,p_2,\cdots,p_{n!}>其中p_i(i=1,2,\cdots,n!)是序列的一个全排列,并且\forall i,j \in \{1,2,\cdots,n!\},都有p_i<p_j,表示p_j的自然顺序大于p_i。例如,对于递增序列<1,2,3>123,231都是它的一个全排列,且231的自然顺序大于123
显然,对于\mathbb P,一定有p_1=a_1a_2\cdots a_np_{n!}=a_n a_{n-1} \cdots a_1

分析

{\mathbb P}=P(<a_1,a_2,\cdots,a_n>)
我们定义加法运算a+b表示两个序列a,b的拼接。例如,若a=a_1a_2\cdots a_mb=b_1b_2\cdots b_n,则
a+b=a_1a_2\cdots a_m b_1b_2\cdots b_n
那么,自然可以定义一个序列a与一个有序集合的加法运算
a+{\mathbb P}=<a+p_1,a+p_2,\cdots,a+p_{n!}>
这个式子进一步可以用求和符号简化为
a+{\mathbb P}=<\sum_{i=1}^{n!}a+p_i>

现在考虑函数P,由于其产生一个递增全排列集合,显然前(n-1)!项全排列的第一个元素是a_1,第(n-1)!+1到第2\cdot (n-1)!项全排列的第一个元素都是a_2,……,第(k-1)\cdot(n-1)!+1到第k\cdot (n-1)!项的第一个元素都是a_k。这就意味着
P(<a_1,a_2,\cdots,a_n>)=<\sum_{i=1}^n \left( a_i+P(<a_1,a_2,\cdots,a_{i-1},a_{i+1},\cdots,a_n>)\right)>
由于剩余的序列<a_1,a_2,\cdots,a_{i-1},a_{i+1},\cdots,a_n>仍然是有序的,因此对剩余的(n-1)个序列产生递增的全排列集合就成为该问题的子问题,我们使用递归定义了它们。

现在考虑最基本的情况:

  1. 序列没有任何元素,显然P(<>)=\varnothing
  2. 序列只有一个元素,显然P(<a>)=<a>,即一个元素的全排列只有一种,就是它本身。

由此我们可以得到产生递增的全排列集合的算法。

算法

  • 输入:一个有限的递增有序序列A=<a_1,a_2,\cdots,a_n>
  • 输出:序列A递增全排列集合R
  1. 初始化结果集R=\varnothing
  2. 若序列A中仅有一个元素a,或为空(此时a=\varnothing),则R=R\cup \{<a>\},算法结束,返回R
  3. 否则,分别令i=1,2,\cdots,n,对于a_i,递归地使用该算法计算剩余的有序序列<a_1,a_2,\cdots,a_{i-1},a_{i+1},\cdots,a_n>的递增全排列集合R'_i,令R_i=a_i+R'_i,由此我们得到了以a_i为固定首位的全排列集合。从而归并到结果集合中R=R\cup R_i
  4. 由于A中元素是有限的,所以算法的递归过程一定会到达2中的状态并终止,从而算法在有限步骤后一定会终止。

代码

使用JavaScript实现的代码如下

function generatePermutation(n){
    let seq=(length=>Array.from({length},($,i)=>i+1))(n),result=[];
    //Require substring of seq starting from start is a rigidly-ascending ordered sequence,
    //namely, for any i,j with start<=i<j<length, seq[i]<seq[j] holds.
    (function permutation(seq,result,start=0){
        //recursive function for all permutations of seq with a specified start index(counting from 0).
        //The permutation of single element is this element itself, so stop here.
        start===seq.length-1 && result.push(Array.from(seq));

        //Now,we can get all permutations by the following: 
        //  Permutation(<a1,a2,...,an>)
        //  =SUM_[from i=1 to n] ( ai + Permutation(<a1,a2,...,a_{i-1},a_{i+1},...,an>) )
        //  where a1<a2<...<an, and notation <...> stands for an ordered sequence.
        for(let i=start;i<n;i++){
            // to achieve this, we can move ai to the head, leaving the remnants,say,
            // <a1,a2,...,a_{i-1},a_{i+1},...,an> ,still ordered. Thus, we apply permutation
            // on these remnants, which turns to be a subproblem. 
            // NOTICE: if ascending ordered permutations are required, we use function move.
            // Otherwise, a simple swap can be used here.
            // 
            move(seq,i,start);      //swap(seq,i,start);
            permutation(seq,result,start+1);
            move(seq,start,i+1);    //swap(seq,i,start);
        }
    })(seq,result);
    return result;
}

//move seq[from] to before seq[to]
function move(seq,from,to){
    [[to,0,seq[from]],[from+(from>to),1]].forEach(arr=>seq.splice(...arr))
}

// swap seq[i1] with seq[i2]
function swap(seq,i1,i2){
    [seq[i1],seq[i2]]=[seq[i2],seq[i1]];
}

generatePermutation(4)

为了保证剩余序列的有序,使用move函数将a_i元素移动到数列的首位,此时对剩余序列(start=start+1开始的下标)进行递归排序。

注意,代码中还提供了一个交换函数swap,如果将generatePermutation中的move替换为swap,那么产生的结果集合仍然是全排列集合,但是它不是有序排列的集合。