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



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

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


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

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


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

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




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

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指向右。



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



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

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;
}



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

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



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

总结:

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

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



posted @ 2011-06-17 23:07  jinmengzhe  阅读(3567)  评论(0编辑  收藏  举报