排列的字典序问题

Problem Description

n个元素{1,2,...,n}有n!个不同的排列。将这n!个排列按字典序排列并编号为0,1,...,n!-1。每个排列的编号为其字典序值。例如,当n=3时,6个不同排列的字典序值如下:

字典序值012345
排列123132213231312321

给定n,以及n个元素{1,2,...,n}的一个排列,计算出这个排列的字典序值,以及按字典序排列的下一个排列。

 Input

输入包括多组数据。
每组数据的第一行是元素个数n(1<=n<=13),接下来1行是n个元素{1,2,...,n}的一个排列。

 Output

对于每组数据,输出两行,第一行是字典序值,第2行是字典序排列的下一个排列。

 Sample Input

8 2 6 4 5 8 1 7 3

 Sample Output

8227
2 6 4 5 8 3 1 7

 

-----------------------------------------------------------------此处为比较,可以掠过不看,直接下拉----------------------------------------------------------------------------------------------------------------------------

排列的字典序,此题似乎与之前说过的全排列问题类似,但是输入顺序有区别的。

输入
1 2 3
全排列输出:
1 2 3
1 3 2
2 1 3
2 3 1
3 2 1
3 1 2

字典序输出:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1

区别在于最后的两行,为什么会出现这样的情况呢?

是因为在全排列问题时,利用的是对原数组进行替换。

初始:1 2 3  

交换[1] [3] ,则变成 3 2 1, 

再接下来的替换中,就按照 2 1进行的。

 

若想按照字典序输入,需要利用头尾数组算法

建立两个数组 head,tail

初始化head为空,tail 为n个顺序元素。

 {} {1,2,3}

伪代码:

permDict(head,tail,n)
IF    size(head)==n
        print head
ELSE
         FOR  i  IN  tail
                headtemp = head    tailtemp = tail  //初始化两个临时变量 
                headtemp.insert(i)   //向headtemp中添加 i
                tailtemp.erase(i)    //删除teailtemp中的i
                permDict(headtemp,tailtemp,n) 
    

 

c++代码:

void PermDict(list<int> head,list<int> tail,int n)
{
    if(head.size()==n)
    {
        for(list<int>::iterator iter=head.begin();iter!=head.end();++iter)
            cout<<*iter<<" ";
        cout<<endl;
    }
    else{
        for(list<int>::iterator iter=tail.begin();iter!=tail.end();++iter)
        {
            list<int> headtemp=head,tailtemp=tail;          
            headtemp.push_back(*iter);                  
            tailtemp.erase(find(tailtemp.begin(),tailtemp.end(),*iter));
            PermDict(headtemp,tailtemp,n);
        }
    }
}

 

 ------------------------------------------------------------------------华丽的分割线 ------------------------------------------------------------------------------------------------------------------------------------------

言归正传;

(由排列计算字典序)

设给定的{1,2,.....n}的排列为∏,其字典序值为 rank(∏,n).按字典序的定义显然有

          (∏[1]-1*(n-1)! ≤ rank(∏,n) ≤ ∏[1]*(n-1)!-1

此公示理解:分析(∏[1]-1)(n-1)!         (n-1)!为n-1位元素的全排列的个数。∏[1]为排列的第一个位元素,(∏[1]-1)(n-1)!为{∏[1],∏[2],...,∏[8]}的字典序值,其中∏[i]<∏[i+1], 1<i<n.即以∏[1]开头的第一个字典序列。

      同理 ∏[1](n-1)!-1 为以∏[1]开头的最后一个字典序列。

设 r 是∏[1]开头的所有排列中的相对序号,则 r 也是{∏[2],...,∏[8]}作为集合{1,2,...,n}/{∏[1]}(去掉∏[1])中排列的字典序值。如果将{∏[2],...,∏[8]}中每个大于∏[1] 的元素都减 1 ,则得到集合{1,2,...,n-1}的一个排列∏`,其字典序值也是r,由此得到计算rank(∏,n)的递归式如下:

          rank(∏,n) = (∏[1]-1)*(n-1)! + rank(∏`,n-1)

初始条件为:

      rank([1],1) = 0

 

(注意:后面的元素大于∏[1]时,要把此元素减 1 )

 

int f(int n)  //  n的阶乘 
{
    int r=1;
    if(n>0)
    {
        for(int i=1;i<=n;i++)
        r*=i;
    }
    return r;
}
int permRank(int n,int p[])    
{
    int r=0;
    int rho[n];
    for(int j=0;j<n;j++) rho[j]=p[j];
    for(int j=0;j<n;j++)
    {
        r+=(rho[j]-1)*f(n-j-1);
        for(int i=j;i<n;i++)        //对后面的元素遍历,若 < 则减 1 
            if(rho[i]>rho[j]) rho[i]--;
    }
    return r;
}

 

 

由字典序值计算排列

对于每个整数 r ,0≤r≤n-1,都有唯一的阶乘分解:r=Σ di * ! ,  (0≤ d≤ i  ,  1≤ i ≤n-1)

分析:此式相当于   rank(∏,n) = (∏[1]-1)*(n-1)! + rank(∏`,n-1)   的展开。

设r = rank(∏,n) , 则显然有∏[1] = dn-1 +1.

进一步,rank(∏',n-1) = r' = r - dn-1*(n-1)!  可递归地找到排列∏‘ 。

r = dn-1 * (n-1)+dn-1 *(n-2)! +....+d0*(0)!

先得到尾项再得到前项.   m=r%(1)!    此时会将{(n-1)!,n(n-2)!,(n-3)!,...,(1)!}带有这些的前项全部除掉,只剩下d0*(0)!,且 d0=r%(1)!/(0)!  递归向前得到d1,d2,...,dn-1.

void permUnrank(int n,int r,int *p)
{
    p[n-1]=1;
    for(int j=0;j<n;j++)
    {
        int d =(r%f(j+1))/f(j);     //将 (n+1)!>(n)! 的项全部去掉,只留下留下 dn*(n)!  
        p[n-j-1]=d+1;
        r-=d*f(j);
        if(j==0)
            continue;
        for(int i=n-j;i<n;i++)      //在之前减过的元素,重新+1
            if(p[i]>=p[n-j-1])p[i]++;
    }
}

 

由排列计算下一个排列

按字典序的定义可设计从一个排列计算下一个排列的算法。对于给定的排列∏,

(1)首先找到下标 i ,使得∏[ i ] < ∏[ i+1 ],且∏[ i +1] > ∏[ i+2 ] > ...> ∏[n];

(2)其次找到下标 j,使得∏[ i ]<∏[ j ]且对所有j<k≤n 有∏[k]<∏[ i ];

(3)然后交换∏[ i ] 和 ∏[ j ];

(4)最后将排列[∏[i+1],∏[i+2],...,∏[n] ]反转。

举例 :8 4 6 5 3 2 1

(1)首先从后向前遍历,若后段都是逆序,则此后段已经是最后的一个排列,必须从前面更换。

即:8 4 6 5 3 2 1,此排列是以84开始的排列的最后一个排列,下一个排列的不能再是84开始的.

此时的4 就是上边所记 i.

(2)此时不能从84开始,则必须从84的下一个开始,因为是字典序,所以要从后边找到比4大的数,则是5(不能是6,86就超越了85),而此时的5 就是上边所记j

(3)交换 4 5

(4)此时为 8 5 6 4 3 2 1 ,按(1)理解,此排列应该是以85开始的最后一个排列,要想得到第一个排列的话,就应该反转后段6 4 3 2 1。

最后得到排列 8 5 1 2 3 4 6。

 

void permSucc(int n,int *p,int &flag)
{
    p[0]=0;
    int i=n-1;
    while(p[i+1]<p[i])i--;      // 得到  i
    if(i==0) flag=0;
    else{
        flag=1;
        int j=n;
        while(p[j]<p[i])j--;       //  得到 j
        swap(p[j],p[i]);            // 交换
        int rho[n-i];
        for(int k=i+1;k<=n;k++) rho[k]=p[k];            //反转
        for(int k=i+1;k<=n;k++) p[k]=rho[n-(k-i-1)];
    }
}

 

 

转载于:https://www.cnblogs.com/dztgc/archive/2013/04/21/3033220.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值