全排列的几种实现(含字典序排列算法分析)

  始于一个很简单的问题:生成{0,1,2,3,...,n-1}的n!种排列,即全排列问题。下面介绍几种全排列的实现,以及探讨一下其解题思路。

基于枚举/递归的方法

思路:

  基于枚举的方法,也可以说是基于递归的方法,此方法的思路是先将全排列问题的约束进行放松,形成一个较容易解决的新问题,解新问题,再对新问题进行约束,解出当前问题。以上全排列问题是生成{0,1,2,...,n-1}的n!个排列,隐含的一个约束是这个n个位置上的数必须是给出的集合中的数,不能重复使用。当我们将此约束放松的时候,问题就变成了n个位置每个位置上有0~n-1种可能出现的数字,列出所有nn种数列,即在每一位上枚举所有的可能。新问题的算法非常简单:

private Integer[] perm;

private void permut(int pos, int n) {
        if (pos == n) {
            for (int i = 0; i < perm.length; i++) {
                System.out.print(perm[i]);
            }
            System.out.println();
            return;
        }
        for (int i = 0; i < n; i++) {
            perm[pos] = i;
            permut(pos+1, n);
        }
    }

  而我们实际的问题只要保证每一位上的数字在其他位置上没有使用过就行了。

private boolean[] used;
private Integer[] perm;

private void permut(int pos, int n) {
        if (pos == n) {
            for (int i = 0; i < perm.length; i++) {
                System.out.print(perm[i]);
            }
            System.out.println();
            return;
        }     //针对perm的第pos个位置,究竟使用0~n-1中的哪一个进行循环
        for (int i = 0; i < n; i++) {
            if (used[i] == false) {
                perm[pos] = i;
                used[i] = true;  //i已经被使用了,所以把标志位设置为True
                permut(pos+1, n);
                used[i] = false;  //使用完之后要把标志复位
            }
        }
    }

  或者完全按递归是思想,对{0,1,2,...,n-1}进行排列,分别将每个位置交换到最前面位,之后全排列剩下的位:

 private static void PermutationList(int fromIndex, int endIndex)
        {
            if (fromIndex == endIndex)
                Output();
            else
            {
                for (int index = fromIndex; index <= endIndex; ++index)
                {
                    // 此处排序主要是为了生成字典序全排列,否则递归会打乱字典序
                    Sort(fromIndex, endIndex);
                    Swap(fromIndex, index);
                    PermutationList(fromIndex + 1, endIndex);
                    Swap(fromIndex, index);
                }
            }
        }

基于字典序的方法

  基于字典序的方法,生成给定全排列的下一个排列,所谓一个的下一个就是这一个与下一个之间没有其他的。这就要求这一个与下一个有尽可能长的共同前缀,也即变化限制在尽可能短的后缀上。计算下一个排列的算法内容如下:

   一般而言,设P是[1,n]的一个全排列。
      P = P1P2…Pn = P1P2 … Pj-1PjPj+1 … Pk-1PkPk+1 … Pn
   find:  j = max{i|Pi<Pi+1}        k = max{i|Pi>Pj}
      1,  对换Pj,Pk      2,  将Pj+1 … Pk-1PjPk+1 … Pn 翻转       P’= P1P2 … Pj-1PkPn … Pk+1PjPk-1 … Pj+1 即P的下一个

按照算法可以实现:

public class Permutation2 {

    public static String nextPerm(String aStr) {
        int index_j = -1;
        int index_k = -1;
        int length = aStr.length();
        StringBuffer buffer = new StringBuffer(aStr);

        for (int i = length-1; i > 0; i--) {
            if (aStr.charAt(i) > aStr.charAt(i-1)) {
                index_j = i-1;
                break;
            }
        }
        if (index_j != -1) {
            for (int i = length-1; i > index_j; i--) {
                if (aStr.charAt(i) > aStr.charAt(index_j)) {
                    index_k = i;
                    break;
                }
            }
        }else {
            return null;
        }

        char tmp = buffer.charAt(index_j);
        buffer.setCharAt(index_j, buffer.charAt(index_k));
        buffer.setCharAt(index_k, tmp);

        StringBuffer subBuffer1 = new StringBuffer(buffer.subSequence(index_j+1, length));
        String subBuffer2 = buffer.substring(0, index_j+1);
        subBuffer1.reverse();

        return subBuffer2 + subBuffer1;
    }

    public static void main(String[] args) {
        String aNum = "123";
        while ((aNum = Permutation2.nextPerm(aNum)) != null) {
            System.out.println(aNum);
        }
    }
}

原理:

  根据如上算法为什么能得到已知排列的下一个排列?我们来分析一下。

  假设我们对已知排列 P1P2…P求其下一个排列,默认为按字典序递增,P1P2…P可能是一串数字,为了便于计算,统统将其看作一个字符串。首先我们需要清楚的一点是下一个刚好比 P1P2…P大的排列应当和原排列有尽可能长的相同前缀(高位保持一致,尽可能在低位上发生变化),剩下变化的部分称为后缀,假设为  PjPj+1 ... Pn  ,我们的所有变化都在这个子串上进行。

对于上述子串 PjPj+1 ... Pn ,隐含如下信息:

  1. 对于下一个排列的生成过程,实际上就是对子串 PjPj+1 ... P某些位置上的数值进行交换。
  2. P位置上的数值必然会与之后某个位置上的数值进行交换,因为 Pj 是后缀上的第一个位置,故必须发生变化,而且必须与后缀其他位置上的数字交换。
  3. 交换之后Pj位置上的数值必然比原数值大,因为我们按默认字典序递增计算下一个排列,也就是说,在 P之后的某个位置上必须存在比 Pj 大的数值。

  根据以上三点,我们就能确定 P的位置了。为了保证尽可能长的前缀,我们需要从尾部向前检查,检查的条件是满足 Pi<Pi+1 。一旦满足这个条件,就保证了在后缀上至少有一个数值大于 P,即 Pi+1 ,如果不满足这个条件,从后向前是一个递增的序列,在后缀上不会存在大于Pi的数值,即不满足以上第三点,继续向前检查,第一个满足 Pi<Pi+1 的 P就是我们要寻找的 P(理由是尽可能高位保持一致)。这时候在后缀上至少存在 Pi+1 是大于 P(即 P)的,但同时也可能后缀存在多个大于 Pj 的数值,我们应该选取哪一个与Pj交换呢?当然是刚好比 Pj 大的那个,即比Pj大的数值中最小的那个(设为 P),原因很简单,如果选择了一个不是最小的数值 P与 P交换,那生成的排列与原排列之间必然还有其他排列(这个排列就是后缀中任何一个比 P小且比 P大的数值与 P交换产生的排列),那就不是我们需要的下一个排列了。因此:

          PjPj+1 ... PkPk+1 ... Pn(Pk为后缀中刚好比Pj大的数值)

交换之后:

          PkPj+1 ... PjPk+1 ... Pn

  此时 P(原 P)处已经确定下来了,那后面的排列怎么排呢?我们既然是要产生刚好比原排列大的下一个排列,当然是在满足情况的前提下使新排列尽可能的小,而此时 P(原 P)位置比原此位置上的数值大,因此后面无论怎么排,新生成的排列都比原排列大,因此在只要 P之后的排列找到一个最小的就行了。而在 Pj 与 Pk 交换之前这段序列是从后向前递增有序的,那交换以后呢?

           因为 Pj < Pk , Pj > Pk+1, Pk < Pk-1

           所以 Pk-1 > Pj > Pk+1

  所以交换之后仍然是从后向前递增有序,因此只需要把后面的序列逆置一下就行了,最后生成的新排列就是我们所有的下一个排列。

时间: 2024-10-27 08:08:16

全排列的几种实现(含字典序排列算法分析)的相关文章

全排列与字典序排列

首先,全排列是一个比较简单的问题,但我却没有真正的去实现过全排列. 让我独自思考全排列的话,如将 " abcd" 进行全排列,这种简单的全排列也能将我难住,因为真的没有考虑过这种问题.思考了一会,我只能给出以下比较麻烦的算法: //字符串全排列 void printRE(char* str,int index,char s[],int length){ if(index == length) printf("%s \n",str); else{ bool exsis

hdu1027(n个数的按字典序排列的第m个序列)

题目信息:给出n,m,求n个数的按字典序排列的第m个序列 http://acm.hdu.edu.cn/showproblem.php?pid=1027 AC代码: /** *全排列的个数(次序) */ #include<iostream> #include<cstdio> #include<algorithm> int a[1001],x; using namespace std; void print(int n){ for(int i=1;i<n;i++){

[51nod-1364]最大字典序排列

[51nod-1364]最大字典序排列 Online Judge:51nod-1364 Label:线段树,树状数组,二分 题目描述 题解: 根据题意很容易想到60%数据的\(O(N^2logN)\)暴力做法,即每次从大数往小数找,如果它能在m步内换到当前位置就把它换到前面去,然后再把选中的位置设为0,可以用树状数组在\(O(logN)\)完成. 0.O(N^2logN)暴力 cin>>n; for(int i=1;i<=n;i++){ int x; cin>>x,pos[x

51nod1364 最大字典序排列

不断的在cur的后面找最大的符合条件的数扔到cur的前面. 用线段树维护操作就可以了. #include<cstdio> #include<cstring> #include<cctype> #include<algorithm> using namespace std; #define rep(i,s,t) for(int i=s;i<=t;i++) #define dwn(i,s,t) for(int i=s;i>=t;i--) #defin

51Node 1364--- 最大字典序排列(树状数组)

51Node  1364--- 最大字典序排列(树状数组) 1364 最大字典序排列 基准时间限制:1 秒 空间限制:131072 KB 分值: 80 难度:5级算法题  收藏  关注 给出一个1至N的排列,允许你做不超过K次操作,每次操作可以将相邻的两个数交换,问能够得到的字典序最大的排列是什么? 例如:N = 5, {1 2 3 4 5},k = 6,在6次交换后,能够得到的字典序最大的排列为{5 3 1 2 4}. Input 第1行:2个数N, K中间用空格分隔(1 <= N <= 1

51nod 1364 最大字典序排列(线段树)

1364 最大字典序排列基准时间限制:1 秒 空间限制:131072 KB 分值: 80 难度:5级算法题 给出一个1至N的排列,允许你做不超过K次操作,每次操作可以将相邻的两个数交换,问能够得到的字典序最大的排列是什么? 例如:N = 5, {1 2 3 4 5},k = 6,在6次交换后,能够得到的字典序最大的排列为{5 3 1 2 4}. Input 第1行:2个数N, K中间用空格分隔(1 <= N <= 100000, 0 <= K <= 10^9). 第2至N + 1行

全排列的几种算法

全排列,我们高中时就学过,数学上很简单,可是用计算机的算法实现还是有点味道的, 今天我将我碰到的几种算法如数奉上,欢迎交流! 第一种:递归 最常见的也是最好理解的方法: 简单点:比如"a" ,"b","c"全排列,可以看做事"a" +"b","c"的全排列 及"b"+ "a","c"的全排列 及"c" +  

[Swift-2019力扣杯春季决赛]2. 按字典序排列最小的等效字符串

给出长度相同的两个字符串:A 和 B,其中 A[i] 和 B[i] 是一组等价字符.举个例子,如果 A = "abc" 且 B = "cde",那么就有 'a' == 'c', 'b' == 'd', 'c' == 'e'. 等价字符遵循任何等价关系的一般规则: 自反性:'a' == 'a' 对称性:'a' == 'b' 则必定有 'b' == 'a' 传递性:'a' == 'b' 且 'b' == 'c' 就表明 'a' == 'c' 例如,A 和 B 的等价信息

几种线程池的实现算法分析

1. 前言 在阅读研究线程池的源码之前,一直感觉线程池是一个框架中最高深的技术.研究后才发现,线程池的实现是如此精巧.本文从技术角度分析了线程池的本质原理和组成,同时分析了JDK.Jetty6.Jetty8.Tomcat的源码实现,对于想了解线程池本质.更好的使用线程池或者定制实现自己的线程池的业务场景具有一定指导意义. 2. 使用线程池的意义 复用:类似WEB服务器等系统,长期来看内部需要使用大量的线程处理请求,而单次请求响应时间通常比较短,此时Java基于操作系统的本地调用方式大量的创建和销