全排列生成算法(二)

Steinhaus-Johnson-Trotter算法是一种基于最小变换的全排列生成算法,对于排列a[1...n],该算法通过将a[i],与a[i-1](或a[i+1])进行交换,生成下一个排列,直到所有排列生成完毕为止,这样,当前排列与其后继排列只是两个相邻位置的元素发生了调换。当然,为了防止重复生成某一个排列,算法并非随意调换某两个元素之间的位置,其生成全排列的具体规则如下。

  • 首先,以字典序最小的排列起始,并且为该排列的每个元素赋予一个移动方向,初始所有元素的移动方向都向左。
  • 在排列中查找这样的元素,该元素按照其对应的移动方向移动,可以移动到一个合法位置,且移动方向的元素小于该元素,在所有满足条件的元素中,找到其中的最大者。
  • 将该元素与其移动方向所对应的元素交换位置。
  • 对于排列中,所有元素值大于该元素的元素,反转其移动方向。

这里有几个概念需要说明一下,所谓合法位置,是指该元素按照其移动方向移动,不会移动到排列数组之外,例如对于<4,<1,<2,<3,此时对于元素4,如果继续向左移动,就会超过数组范围,所以4的下一个移动位置是非法位置。而且,所有元素,都只能向比自己小的元素的方向移动,如上面例子中的元素2,3,而元素1是不能够移动到元素4的位置的。每次移动,都要对可以移动的所有元素中的最大者进行操作,上例中元素1,4不能移动,2,3都存在合法的移动方案,此时需要移动3,而不能移动2。合法移动之后,需要将所有大于移动元素的元素的移动方向反转,上例中的元素3移动后的结果是4>,1<,<3,<2,可以看到,元素4的移动方向改变了。再如此例子<2,<1,3>,4>,对于其中的元素2,4,其对应的下一个移动位置都是非法位置,而对于元素1,3,其下一个移动位置的元素,都比他们要大,对于该排列就找不到一个可以的移动方案,这说明该算法已经达到终态,全排列生成结束。下面是该算法的代码

 1 inline int SJTNext(unsigned int* index, size_t array_size, int* move)
 2 {
 3     unsigned int i, j, t;
 4 
 5     //找到最大合法移动的元素索引
 6     for(i = array_size - 1, j = array_size; i != UINT_MAX; --i)
 7     {
 8         if(i + move[i] < array_size && index[i] > index[i + move[i]])
 9         {
10             if(j == array_size)
11             {
12                 j = i;
13                 continue;
14             }
15 
16             if(index[i] > index[j])
17             {
18                 j = i;
19             }
20         }
21     }
22 
23     //未发现合法的移动策略
24     if(j == array_size)
25     {
26         return 1;
27     }
28 
29     t = index[j];//要交换位置的元素
30     i = j + move[j];//发生交换的位置
31     swap(index, i, j);
32     swap(move, i, j);
33 
34     //将所有比t大的元素的移动方向反转
35     for(i = 0; i < array_size; ++i)
36     {
37         if(index[i] > t)
38         {
39             move[i] = -move[i];
40         }
41     }
42 
43     return 0;
44 }
45 
46 /*
47  * 基于最小变换的Steinhaus–Johnson–Trotter算法
48  */
49 void FullArray(char* array, size_t array_size)
50 {
51     unsigned int index[array_size];
52     int move[array_size];
53 
54     for(unsigned int i = 0; i < array_size; ++i)
55     {
56         index[i] = i;
57         move[i] = -1;
58     }
59 
60     ArrayPrint(array, array_size, index);
61 
62     while(!SJTNext(index, array_size, move))
63     {
64         ArrayPrint(array, array_size, index);
65     }
66 }

代码使用了一个伴随数组move标记对应位置元素的移动方向,在元素移动时,move数组中的对应元素也要相应移动。该算法从初始排列<1,<2,<3,<4开始,可以生成4元素的所有排列,直至最终排列<2,<1,3>,4>为止,其状态转移如下图所示,该图片来自于Wiki百科。


实际上该算法是Shimon Even对于Steinhaus-Johnson-Trotter三人提出的全排列生成算法的改进算法,在算法中实际上还有一个问题需要解决,就是对于给定的排列,如何判断其所有元素的移动方向,如果上面所谓终态的移动方向是<2,<1,3>,<4,那么这个状态就还存在可行的移动方案。Johnson(1963)给出了判断当前排列各元素移动方向的方法,对于排列中的每个元素,判断所有比该元素小的元素所生成序列的逆序数,如果逆序数为偶,则该元素的移动方向为向左,否则移动方向向右,我们用这条原则来看一下上面的终态2,1,3,4。对于元素1,没有比1小的元素,此时我们认为,空序列的逆序数为偶,所以元素1的移动方向向左;对于元素2,比2小的元素形成的序列为1,单元素序列的逆序数为偶,所以2的移动方向向左;对于元素3,小于3的元素组成的序列为21,逆序数为1,奇数,所以3的移动方向向右;对于元素4,对应序列为213,逆序数为奇数,所以4的移动方向向右。根据该规则就可以知道,给定某一排列,其对应元素的移动方向是确定的。

posted @ 2014-03-18 11:24  玩笑528  阅读(829)  评论(0编辑  收藏  举报