减治法(二) 生成排列的减治算法及其他算法
从这篇文章始,将花连续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
--------------------------------------------------------------------------------------------------------------------------------------------------
总结:
算法的思路上面的描述都很清晰了,但实现起来其实还不是很容易。
这些代码是十几天前写的了,还要好好看看具体的实现。