减治法(二) 生成排列的减治算法及其他算法



从这篇文章始,将花连续2篇文章来介绍2个很重要的问题:生成排列和生成子集

它们都有减一治策略的算法,但这2篇文章将不仅仅局限于减一治策略来实现,而将介绍更多的实现算法。这篇介绍排列,下篇介绍子集。


--------------------------------------------------------------------------------------------------------------------------------------------------

介绍3种构造排列的方法:


算法1:减一治从底向上构造排列

假设n-1个元素的排列已经生成好,那么将第n个元素插入到 每一个排列项不同位置,就得到一个新的排列项。举个例子:

2011061722450231.jpg


注意这可以 递归的定义,用二维数组的第一维表示一个排列项,第二维表示排列项的每一位。

 
  
public static int [][] getArrange( int a) {
// 输出1到a的排列
int [][] result = new int [Fact(a)][a]; // 共Fact(a)个结果,结果的每个数存在第二维中

if (a == 1 )
result[
0 ][ 0 ] = 1 ;
else // 递归的从下往上构造
{
int [][] temp = getArrange(a - 1 ); // 得到a-1时的结果
int [] aaa = new int [a - 1 ];

int position = 0 ; // result当前可用位置

for ( int i = 0 ; i < temp.length; i ++ ) {
aaa
= temp[i]; // 依次取出每个结果

// 对这些结果做插入操作就得到了新的一个排列
for ( int j = 0 ; j < a; j ++ ) // 插入的不同位置决定了一个结果
{
result[position][j]
= a;
// 将aaa的位放入剩下的几个位置
for ( int k = 0 , s = 0 ; k < a && s < a - 1 ; k ++ )
if (result[position][k] != a) {
result[position][k]
= aaa[s];
s
++ ;
}
position
++ ;
}

}

}

return result;
}




算法2:Johnson-Trotter算法通过移动来生成排列

给每个元素赋一个方向,这可以用一个数组来表示,0指向左,1指向右。

2011061722541130.jpg

有了可移动元素的概念,Johnson-Trotter算法的描述如下:

2011061722550816.jpg

算法证明应该很难,我就照着流程写的:

 
  
public static int [][] JohnsonTrotter( int a) {
// Johnson-Trotter算法实现排列
int [][] result = new int [Fact(a)][a];

// 得到第一个排列及方向的初始化
int [] direct = new int [a]; // 方向,0指向左,1指向右边
for ( int i = 0 ; i < a; i ++ )
result[
0 ][i] = i + 1 ;

int position = 1 ; // result下一个可用位置
// int[] aaa = result[0]; // 非常典型的错误!!!aaa此时和result[0]指向同一个引用,最周会导致result[0]是最后一个aaa的值
int [] aaa = new int [a];
for ( int i = 0 ; i < a; i ++ )
aaa[i]
= result[ 0 ][i];

while (hasMove(aaa, direct)) {
int maxMove = maxMove(aaa, direct); // 求当前最大移动元素
int indexMax = 0 ; // 最大元素的下标
for ( int i = 0 ; i < aaa.length; i ++ )
if (aaa[i] == maxMove)
indexMax
= i;

// 把k和它箭头指向的相邻元素互换
if (direct[indexMax] == 0 ) {
// 元素互换
int temp = aaa[indexMax];
aaa[indexMax]
= aaa[indexMax - 1 ];
aaa[indexMax
- 1 ] = temp;

// 相应的direct数组也要改变!!!
direct[indexMax] = direct[indexMax - 1 ];
direct[indexMax
- 1 ] = 0 ;
}
else if (direct[indexMax] == 1 ) {
int temp = aaa[indexMax];
aaa[indexMax]
= aaa[indexMax + 1 ];
aaa[indexMax
+ 1 ] = temp;

// 相应的direct数组也要改变!!!
direct[indexMax] = direct[indexMax + 1 ];
direct[indexMax
+ 1 ] = 1 ;
}

// 调转所有大于k的元素的方向
for ( int i = 0 ; i < aaa.length; i ++ )
if (aaa[i] > maxMove) {
if (direct[i] == 0 )
direct[i]
= 1 ;
else
direct[i]
= 0 ;
}

// 将新排列加入到结果列表
for ( int i = 0 ; i < aaa.length; i ++ )
result[position][i]
= aaa[i];
position
++ ;

// 错误同上,引用问题!!!这样会导致每个position的值实际上是一直在跟踪aaa
// result[position] = aaa;
// position++;
}

return result;
}


相应的支持方法见后面完整的代码。




算法3:字典序来生成排列,这样生成的排列是按顺序的。

其实其关键在于 怎样按顺序找出一个排列项的下一个排列项(这应该属于高中数学排列组合里面一个还比较难的问题)

设P是1~n的一个全排列:P = P1P2……Pn = P1P2……Pj-1PjPj+1……Pk-1PkPk+1……Pn

(1)从排列的最右端开始, 找出第一个比右边数字小的数字的序号j(j从左端开始计算),即 j=max{i|pi

(2) 在Pj的右边的数字中,找出所有比Pj大的数中最小的数字Pk,即 k=max{i|Pi>Pj}(右边的数从右至左是递增的,因此k是所有大于Pj的数字中序号最大者)

(3)对换Pj,Pk

(4)再将Pj+1……Pk-1PkPk+1Pn反转,即得到排列P的下一个排列。

举个例子:

例如839647521是数字1~9的一个排列。从它生成下一个排列的步骤如下:
  自右至左找出排列中第一个比右边数字小的数字4 839647521
  在该数字后的数字中找出比4大的数中最小的一个5 839647521
  将5与4交换 839657421
  将7421倒转 839651247
  所以839647521的下一个排列是839651247。


 
  
public static int [][] dicArrange( int a) {
// 字典序生成排列
int [][] result = new int [Fact(a)][a]; // 结果数组
for ( int i = 0 ; i < a; i ++ )
result[
0 ][i] = i + 1 ; // 初始化得到第一个排列,升序

int [] aaa = new int [a];
for ( int i = 0 ; i < result[ 0 ].length; i ++ )
aaa[i]
= result[ 0 ][i]; // 将aaa作为一个中介,来变化得到不同的排列方式

int position = 1 ;

while ( ! isDesending(aaa)) {
int m = 0 , n = 0 , mindex = 0 , nindex = 0 ;
for ( int i = aaa.length - 2 ; i >= 0 ; i -- )
if (aaa[i] < aaa[i + 1 ]) {
m
= aaa[i]; // 自右至左第一个比右边小的数
mindex = i;
break ; // 铸成大错
// System.out.print(m + " ");
}
for ( int i = aaa.length - 1 ; i > mindex; i -- )
if (m < aaa[i]) {
n
= aaa[i]; // m的右边比m大的最小的数
nindex = i;
break ; // 铸成大错
// System.out.print(n + " ");
}
// 将m,n交换
aaa[mindex] = n;
aaa[nindex]
= m;

// 将mindex以后的位置逆序(从mindex+1开始)
int pairs = (aaa.length - mindex - 1 ) / 2 ; // 交换这么多对即可
for ( int i = 0 ; i < pairs; i ++ ) {
// int s1Index = i + mindex,s1 = aaa[s1Index]; // 错了一个下标
int s1Index = i + mindex + 1 , s1 = aaa[s1Index];
int s2Index = aaa.length - 1 - i, s2 = aaa[s2Index];
aaa[s1Index]
= s2;
aaa[s2Index]
= s1;
}

for ( int i = 0 ; i < aaa.length; i ++ )
result[position][i]
= aaa[i];
position
++ ;
}

return result;
}



一些支持方法,见三种算法的完整代码:

ContractedBlock.gif ExpandedBlockStart.gif Arrangement
 
   
package Section5;

import java.util.Scanner;

/* 第5章 减治法 生成排列(3种算法) */

public class Arrangement {

/**
*
@param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub

Scanner scan
= new Scanner(System.in);
int a = scan.nextInt();
// 输出从1到a的排列

int [][] result1 = getArrange(a);
int [][] result2 = JohnsonTrotter(a);
int [][] result3 = dicArrange(a);

System.out.println(
" \n减1治自底向上生成排列的算法: " );
for ( int i = 0 ; i < result1.length; i ++ ) {
for ( int j = 0 ; j < a; j ++ )
System.out.print(result1[i][j]);
System.out.print(
" " );
}

System.out.println(
" \n\nJohnson-Trotter算法通过移动来生成排列: " );
for ( int i = 0 ; i < result2.length; i ++ ) {
for ( int j = 0 ; j < a; j ++ )
System.out.print(result2[i][j]);
System.out.print(
" " );
}

System.out.println(
" \n\n字典序来构造排列的算法: " );
for ( int i = 0 ; i < result3.length; i ++ ) {
for ( int j = 0 ; j < a; j ++ )
System.out.print(result3[i][j]);
System.out.print(
" " );
}
}

public static int [][] dicArrange( int a) {
// 字典序生成排列
int [][] result = new int [Fact(a)][a]; // 结果数组
for ( int i = 0 ; i < a; i ++ )
result[
0 ][i] = i + 1 ; // 初始化得到第一个排列,升序

int [] aaa = new int [a];
for ( int i = 0 ; i < result[ 0 ].length; i ++ )
aaa[i]
= result[ 0 ][i]; // 将aaa作为一个中介,来变化得到不同的排列方式

int position = 1 ;

while ( ! isDesending(aaa)) {
int m = 0 , n = 0 , mindex = 0 , nindex = 0 ;
for ( int i = aaa.length - 2 ; i >= 0 ; i -- )
if (aaa[i] < aaa[i + 1 ]) {
m
= aaa[i]; // 自右至左第一个比右边小的数
mindex = i;
break ; // 铸成大错
// System.out.print(m + " ");
}
for ( int i = aaa.length - 1 ; i > mindex; i -- )
if (m < aaa[i]) {
n
= aaa[i]; // m的右边比m大的最小的数
nindex = i;
break ; // 铸成大错
// System.out.print(n + " ");
}
// 将m,n交换
aaa[mindex] = n;
aaa[nindex]
= m;

// 将mindex以后的位置逆序(从mindex+1开始)
int pairs = (aaa.length - mindex - 1 ) / 2 ; // 交换这么多对即可
for ( int i = 0 ; i < pairs; i ++ ) {
// int s1Index = i + mindex,s1 = aaa[s1Index]; // 错了一个下标
int s1Index = i + mindex + 1 , s1 = aaa[s1Index];
int s2Index = aaa.length - 1 - i, s2 = aaa[s2Index];
aaa[s1Index]
= s2;
aaa[s2Index]
= s1;
}

for ( int i = 0 ; i < aaa.length; i ++ )
result[position][i]
= aaa[i];
position
++ ;
}

return result;
}

private static boolean isDesending( int [] aaa) {
// 判断序列aaa是否为降序
for ( int i = 0 ; i < aaa.length - 1 ; i ++ )
if (aaa[i] < aaa[i + 1 ])
return false ;
return true ;
}

public static int [][] getArrange( int a) {
// 输出1到a的排列
int [][] result = new int [Fact(a)][a]; // 共Fact(a)个结果,结果的每个数存在第二维中

if (a == 1 )
result[
0 ][ 0 ] = 1 ;
else // 递归的从下往上构造
{
int [][] temp = getArrange(a - 1 ); // 得到a-1时的结果
int [] aaa = new int [a - 1 ];

int position = 0 ; // result当前可用位置

for ( int i = 0 ; i < temp.length; i ++ ) {
aaa
= temp[i]; // 依次取出每个结果

// 对这些结果做插入操作就得到了新的一个排列
for ( int j = 0 ; j < a; j ++ ) // 插入的不同位置决定了一个结果
{
result[position][j]
= a;
// 将aaa的位放入剩下的几个位置
for ( int k = 0 , s = 0 ; k < a && s < a - 1 ; k ++ )
if (result[position][k] != a) {
result[position][k]
= aaa[s];
s
++ ;
}
position
++ ;
}

}

}

return result;
}


public static int [][] JohnsonTrotter( int a) {
// Johnson-Trotter算法实现排列
int [][] result = new int [Fact(a)][a];

// 得到第一个排列及方向的初始化
int [] direct = new int [a]; // 方向,0指向左,1指向右边
for ( int i = 0 ; i < a; i ++ )
result[
0 ][i] = i + 1 ;

int position = 1 ; // result下一个可用位置
// int[] aaa = result[0]; // 非常典型的错误!!!aaa此时和result[0]指向同一个引用,最周会导致result[0]是最后一个aaa的值
int [] aaa = new int [a];
for ( int i = 0 ; i < a; i ++ )
aaa[i]
= result[ 0 ][i];

while (hasMove(aaa, direct)) {
int maxMove = maxMove(aaa, direct); // 求当前最大移动元素
int indexMax = 0 ; // 最大元素的下标
for ( int i = 0 ; i < aaa.length; i ++ )
if (aaa[i] == maxMove)
indexMax
= i;

// 把k和它箭头指向的相邻元素互换
if (direct[indexMax] == 0 ) {
// 元素互换
int temp = aaa[indexMax];
aaa[indexMax]
= aaa[indexMax - 1 ];
aaa[indexMax
- 1 ] = temp;

// 相应的direct数组也要改变!!!
direct[indexMax] = direct[indexMax - 1 ];
direct[indexMax
- 1 ] = 0 ;
}
else if (direct[indexMax] == 1 ) {
int temp = aaa[indexMax];
aaa[indexMax]
= aaa[indexMax + 1 ];
aaa[indexMax
+ 1 ] = temp;

// 相应的direct数组也要改变!!!
direct[indexMax] = direct[indexMax + 1 ];
direct[indexMax
+ 1 ] = 1 ;
}

// 调转所有大于k的元素的方向
for ( int i = 0 ; i < aaa.length; i ++ )
if (aaa[i] > maxMove) {
if (direct[i] == 0 )
direct[i]
= 1 ;
else
direct[i]
= 0 ;
}

// 将新排列加入到结果列表
for ( int i = 0 ; i < aaa.length; i ++ )
result[position][i]
= aaa[i];
position
++ ;

// 错误同上,引用问题!!!这样会导致每个position的值实际上是一直在跟踪aaa
// result[position] = aaa;
// position++;
}

return result;
}

private static int maxMove( int [] a, int [] direct) {
// 求最大移动元素
int max = 0 ;
for ( int i = a.length - 1 ; i >= 0 ; i -- ) {
if (direct[i] == 0 && i != 0 )
if (a[i] > a[i - 1 ] && a[i] > max)
max
= a[i];
if (direct[i] == 1 && i != a.length - 1 )
if (a[i] > a[i + 1 ] && a[i] > max)
max
= a[i];
}

return max;
}

private static boolean hasMove( int [] a, int [] direct) {
// 判断序列a是否存在可移动元素
for ( int i = a.length - 1 ; i >= 0 ; i -- ) {
if (direct[i] == 0 && i != 0 )
if (a[i] > a[i - 1 ])
return true ;
if (direct[i] == 1 && i != a.length - 1 )
if (a[i] > a[i + 1 ])
return true ;
}
return false ;
}

private static int Fact( int a) {
// 求a的阶乘
int fact = 1 ;
while (a != 0 ) {
fact
= fact * a;
a
-- ;
}
return fact;
}
}


运行结果(生成1到4的排列):

4

减1治自底向上生成排列的算法:
4321 3421 3241 3214 4231 2431 2341 2314 4213 2413 2143 2134 4312 3412 3142 3124 4132 1432 1342 1324 4123 1423 1243 1234

Johnson-Trotter算法通过移动来生成排列:
1234 1243 1423 4123 4132 1432 1342 1324 3124 3142 3412 4312 4321 3421 3241 3214 2314 2341 2431 4231 4213 2413 2143 2134

字典序来构造排列的算法:
1234 1243 1324 1342 1423 1432 2134 2143 2314 2341 2413 2431 3124 3142 3214 3241 3412 3421 4123 4132 4213 4231 4312 4321



--------------------------------------------------------------------------------------------------------------------------------------------------

总结:

算法的思路上面的描述都很清晰了,但实现起来其实还不是很容易。

这些代码是十几天前写的了,还要好好看看具体的实现。



转载于:https://www.cnblogs.com/jmzz/archive/2011/06/17/2084007.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值