CCh99

分治法之排列的字典问题

东 华 大 学

《算法分析设计与综合实践》实验报告

 

学生姓名:曹晨学号:171310402 指导教师:章昭辉

实验时间:2019-3-13 实验地点:图文信息楼三号机房

请勿转载或抄袭

  1. 实验名称

    排列的字典序列问题

  2. 实验目的

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

  3. 实验内容

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

字典序值

0

1

2

3

4

5

排列

123

132

213

231

312

321

数据输入:

输入的第一行是元素的个数n。接下来的1行是n个元素{1,2,‧‧‧,n}的一个排列

结果输出:

输出的第一行是字典序值,第二行是按字典序排列的下一个排列

 

  1. 实验过程

    自己的思路:

    经过分析,可以得出26458173的字典序为1*7!+4*6!+2*5!+2*4!+3*3!+0*2!+1*1!。

    解释如下:

    第一位为2,排在首位为2前面的有首位为1的排列,所以是1*7!。(在第一位为2的前提下)

    第二位为6,排在第二位为6 前面的有第二位为1,3,4,5的排列,所以是4*6!。(在第一位为2,第二位为6的前提下)

    第三位为4,排在第三位为4前面的有第三位为1,3的排列,所以是2*5!。(在第一位为2,第二位为6,第三位为4 的前提下)

    ‧‧‧‧‧‧‧‧‧

    依次类推可以得到式子1*7!+4*6!+2*5!+2*4!+3*3!+0*2!+1*1!=8227

    在STL库中有函数next_permutation和prev_permutation,前面的是求当前排列的下一个排列,后面的是求当前排列的上一个排列

    参考答案上的思路:

    1. 由排列计算字典序值

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

    以π[1]开头的第一个排列(π[1]-1)(n-1)!<=rank(π,n)<= π[1](n-1)!-1。以π[1]开头的最后一排列

    这是什么意思呢?明白意思就要先搞明白π[1]代表什么,前面条件告诉我们,π是一个排列,那么π[1]就是这个排列第一个元素的值,π[i]就是第i个元素的值(这个我想了一晚上才反应过来),假如π="26458173"的话,那么π[1]=2,π[2]=6, π[3]=4等。

    最主要的思想来了,也是我没有想到的:设r是π在以π[1]开头的所有排列中的序号,则r也是{π[2],π[3],‧‧‧π[n]}作为集合{1,2,‧‧‧,n}-{π[1]}(减去π[1])中排列的的字典序值。如果将{π[2],π[3],‧‧‧,π[n]}中每个大于π[1]的元素都减去1,则得到集合{1,2,‧‧‧,n-1}的一个排列π'',其字典序值也是r。由此得到计算rank(π,n)的递归式如下:

    rank(π,n)= (π[1]-1)(n-1)!+ rank(π'',n-1)

    初始条件为rank(π[1],1)=0

26458173

(2-1)*7!

6458173->5347162

5347162

(5-1)*6!

347162->346152

346152

(3-1)*5!

46152->35142

35142

(3-1)*4!

5142->4132

4132

(4-1)*3!

132->132

132

(1-1)*2!

32->21

21

(2-1)*1!

1->1

1

(1-1)*0!

 

(2-1)*7!+(5-1)*6!+(3-1)*5!+(3-1)*4!+(4-1)*3!+(1-1)*2!+(2-1)1*1!+(1-1)*0!=8227

  1. 由字典序值计算排列

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

设r=rank(π,n),则π[1]=dn-1+1进而由r''=r- dn-1(n-1)!= rank(π'',n-1)可递归找到排列π''。

r= dn-1* (n-1)!+ ‧‧‧+d2*2!+d1*1!+d0*0!

先得到尾项再得到前项,因为(r%1!)=0,任意的d0=(r%1)/0!=0,所以初始条件为1

r=8228

d=(r%2!)/1!=0

b[6]=1

 

 

大于b[i]的已经出现的都要加1,和上面的减1对称

12

r=8288

d=(r%3!)/2!=1

b[5]=2

213

r=8226

d=(r%4!)/3!=3

b[4]=4

4213

r=8202

d=(r%5!)/4!=2

b[3]=3

35214

r=8160

d=(r%6!)/5!=2

b[2]=3

346215

r=7290

d=(r%7!)/6!=4

b[1]=5

5347216

r=5040

d=(r%8!)/7!=1

b[0]=2

26458317

  1. 由排列计算下一个排列

    对于给定的排列π

    首先找到下标i,使得π[i]< π[i+1],且π[i+1]> π[i+2]> ‧‧‧>π[n];

    其次找到下标j,使得π[i]< π[j]且对所有j<k<=n有π[k]< π[i]

    然后交换π[i]和π[j]

    最后将子排列{π[i+1],π[i+2],‧‧‧,π[n]}反转

    举个例子如下:对于 8,1,6,4,7,5,3,2

    1.首先从后向前遍历,后段7,5,3,2已经是一个逆序排列了,它是以8,1,6,4开头的最后一个排列,下一个排列不能以8,1,6,4开始,此时的4就是上面所说的i。

    2.不能从8,1,6,4开始,那么只能从8,1,6,4的下一个开始,因为是字典序,所以要从后面找比4大的数,则是5(不能为7,8167超过了8165),此时的5就是上面所说的j。

    3.交换4和5,变成了8,1,6,5,7,4,3,2

    4.得到以8165开头的第一个排列,需要反转后面的排列7,4,3,2

    5.最后得到排列8,1,6,5,2,3,4,7

  1. 结果分析

    这个结果是我用自己的思想编写代码输出的,经验证答案正确

    这个结果是我用参考答案和csdn上的思想编写代码输出的,经验证答案正确

  2. 实验总结

    这次的实验又让我看到了分治法的广泛应用,虽然我自己写的代码不太好,但是我学习了很多令人拍手叫绝的算法,不禁感慨自己的水平还是很低。

    另外我对c++中的库函数有了很大的兴趣,良好的运用它们可以省很多的力气。

附录:(要求代码里各行要有注释

用自己的思想编写的代码(利用next_permutation库函数)

int factorial(int n)

{

if(n==1)

return 1;

else return n*factorial(n-1);

}//用于计算n的阶乘

void findSum(int *a,int n)

{

int count=0;

int sum=0;//该排列的字典序值

for(int i=0;i<n-1;i++)//从第一位遍历到第n-1位

{

for(int j=i+1;j<n;j++)

{

if(a[i]>a[j])

{

count++;//用来表示第i位的后面比它小的数的个数

}

}

sum+=count*factorial(n-i-1);//加上这一位前面的排序数

count=0;

}

cout<<sum<<endl;//输出字典序值

}

int main()

{

int n;//元素的个数

cin>>n;

int a[n];

for(int i=0;i<n;i++)

{

cin>>a[i];

}//数组用来存放元素

findSum(a,n);//调用计算字典序值的函数

if(next_permutation(a,a+n))//调用计算下一个排列的库函数

{

for(int i=0;i<n;i++)

{

cout<<a[i]<<" ";

}//输出下一个排列

}

return 0;

}

用(参考答案和csdn上的思想)自己编写的代码:

https://blog.csdn.net/anderyu/article/details/21481183

//计算n的阶乘

int factorial(int n)

{

if(n==1)

return 1;

else if(n==0)

return 0;

else return n*factorial(n-1);

}

//由排序计算字典序值

int permRank(int n,int *a) {

int r=0;//字典序值初始化

int i;

int b[n];

for(i=0;i<n;i++)

{

b[i]=a[i];

}//把元素放进数组b中

for(i=0;i<n;i++)//从第一个遍历数组

{

r+=(b[i]-1)*factorial(n-i-1);//加上当前阶层的字典序值

for(int j=i+1;j<n;j++)

{

if(b[i]<b[j])

{

b[j]--;

}//改变后段的值,把比当前的值大的值减1

}

}

return r;//返回字典序值

}

//由字典序值计算排列

void permUnrank(int n,int r,int *b)

{

if(n>=factorial(n))

return ;//如果n的数值大于排列的最大序值,则无输出

b[n-1]=1;//初始化最后一位数为1

for(int j=1;j<n;j++)

{

int d=(r%factorial(j+1))/factorial(j);//求当前阶层的d

r-=d*factorial(j);//总值减去这一阶层的值

b[n-1-j]=d+1;//当前位的值等于d+1

for(int i=n-1-j+1;i<n;i++)

{

if(b[i]>d)//可以改为if(b[i]>=b[n-1-j])

{

b[i]++;

}

}//这个for循环是用来把大于等于当前位且已存在的值加1

}

for(int i=0;i<n;i++)

{

cout<<b[i]<<" ";

}//输出排序

return ;

}

//由排列计算下一个排列

void permSucc(int n,int *a,int &flag)

{

int i=n-2;//从最后一个开始往前遍历

while(a[i+1]<a[i]&&i!=-1)

{

i--;

}//直到遇到前面一个的值小于后面一个的值时停止

if(i==-1)

flag=0;//flag等于0意味着a的排列时所有排列中的最后一个,即逆序,没有下一个排列了

else

{

flag=1;//反之意味着有下一个排列

int j=n-1;//从最后一个开始寻找j

while(a[j]<a[i])

j--;//直到j的值大于i的值时停止

swap(a[i],a[j]);

for(int m=i+1;m<=(n-1+i)/2;m++)

{

swap(a[m],a[n+i-m]);

}//交换i和j的值

}

for(int m=0;m<n;m++)

{

cout<<a[m]<<" ";

}//将后段的排序反转

return ;

}

//主函数

int main()

{

int n;

cin>>n;//排列的元素个数

int a[n];

for(int i=0;i<n;i++)

{

cin>>a[i];

}//存放元素

int r=permRank(n,a);//调用求排列字典序值的函数

int b[n];

cout<<r<<endl;

permUnrank(n,r+1,b);//调用由字典序值求出排列的函数

cout<<endl;

int flag;

permSucc(n,a,flag);//调用由排列计算下一个排列的函数

return 0;

}

posted on 2019-03-26 18:33  CCh99  阅读(1164)  评论(0编辑  收藏  举报

导航