快速排列 -> 数字 -> 排列映射算法

127
我有n个元素。以7个元素为例,它们是1234567。我知道这些7个元素有7! = 5040 种可能的排列方式。
我需要一个快速算法,包含两个函数:
f(number):将0到5039之间的数字映射到唯一的排列方式;
f'(permutation) :将排列方式映射回生成它的数字。
我不关心数字和排列方式之间的对应关系,只要每个排列都有自己独特的数字即可。
例如,我的函数可能是:
f(0) = '1234567'
f'('1234567') = 0

我能想到的最快的算法是枚举所有排列并在两个方向上创建查找表,这样一旦表被创建,f(0)将是 O(1),而 f('1234567') 将是对字符串的查找。但是,这种方法需要大量的内存,特别是当 n 变大时。

有人能提出另一个可以快速工作且没有内存缺点的算法吗?


尽管下面的算法非常全面,但你正确地指出最快的算法是查找表。你真的没有谈论到“那么多”的内存,当然这取决于你的系统和平台。但如果一个查找表就足够了,并且这是一个真实世界的应用程序,请使用它。快速而简单! - Kirk Broadhurst
18
你说得没错,但n并不需要很大才会变得荒谬。对于12个元素来说,12!有479,001,600种排列组合,这是一个非常庞大的查找表! - ijw
不要被不同的帖子中使用n表示不同含义所困惑。有些n代表字符串长度,有些n代表可能排列的数量。不要盲目比较大O符号。--晚来者自警-- - 把友情留在无盐
13个回答

167

描述一个包含n个元素的排列时,可以看到第一个元素所在的位置有n个可能性,因此可以用0到n-1之间的一个数字来描述它。对于下一个元素结束的位置,您还有剩余的n-1个可能性,所以可以用0到n-2之间的一个数字来描述它。
类推直到你得到n个数字。

以n = 5为例,考虑将abcde带到caebd的排列。

  • a,即第一个元素,最终处于第二个位置,因此我们将其分配给索引1
  • b最终位于第四个位置,原本应该是第3个,但是现在只剩下2个,因此我们将其分配给2
  • c最终位于仅剩的第一个位置,始终为0
  • d最终位于最后一个剩余的位置,也就是仅剩的两个位置中的一个,即1
  • e最终位于唯一剩余的位置,索引为0

所以我们有了索引序列{1, 2, 0, 1, 0}

现在您知道对于例如二进制数字,'xyz'表示z + 2y + 4x。对于十进制数,
它是z + 10y + 100x。每个数字都乘以某个权重,然后将结果相加。显然,在权重中的模式是权重为w = b^k,其中b是数字的基数,k是数字的索引。(我总是从右侧开始计算数字,并从最右侧的数字的索引0开始计算。同样,当我谈论“第一个”数字时,我指的是最右侧的数字。)

权重为什么遵循这种模式的原因是能够用从0到k的数字表示的最大数字必须比只使用数字k+1表示的最小数字低1。在二进制中,0111必须比1000低1。在十进制中,099999必须比100000低1。

变基编码
连续数字之间的间距恰好为1是重要的规则。有了这个认识,我们可以通过变基数来表示我们的索引序列。每个数字的基数是该数字可能出现的不同数量。对于十进制,每个数字有10个可能性,对于我们的系统,最右边的数字将只有1个可能性,而最左边的数字将有n个可能性。但是由于最右边的数字(我们序列中的最后一个数字)总是0,因此我们将其省略。这意味着我们剩下的基数是2到n。通常,第k位数字的基数为b[k] = k + 2。数字k允许的最大值为h[k] = b[k] - 1 = k + 1。

我们关于数字权重w[k]的规则要求,在i从i = 0到i = k的情况下,h[i] * w[i]的总和应等于1 * w[k+1]。递归地表述,w[k+1] = w[k] + h[k] * w[k] = w[k]*(h[k] + 1)。第一个权重w[0]应始终为1。从那里开始,我们有以下价值:

k    h[k] w[k]    

0    1    1  
1    2    2    
2    3    6    
3    4    24   
...  ...  ...
n-1  n    n!  

(容易通过归纳证明 w[k-1] = k!。)

转换我们序列得到的数字是 s[k] * w[k] 的和,其中 k 从0到 n-1。这里的 s[k] 是序列的第 k 个(最右边的从0开始)。例如,取我们的 {1, 2, 0, 1, 0},去掉前面提到的最右边的元素:{1, 2, 0, 1}。我们的总和为 1 * 1 + 0 * 2 + 2 * 6 + 1 * 24 = 37

请注意,如果我们对每个索引都取最大位置,我们会得到 {4,3,2,1,0},它转换为 119。由于我们的数字编码中选择的权重是不跳过任何数字的,所有数字从0到119都是有效的。在我们的例子中,n=5时有正好120个这些数字,这恰好是不同排列的数量。因此,您可以看到我们编码的数字完全指定了所有可能的排列。

从可变基数解码
解码类似于转换为二进制或十进制。常用的算法如下:

int number = 42;
int base = 2;
int[] bits = new int[n];

for (int k = 0; k < bits.Length; k++)
{
    bits[k] = number % base;
    number = number / base;
}

对于我们的变基数字:

int n = 5;
int number = 37;

int[] sequence = new int[n - 1];
int base = 2;

for (int k = 0; k < sequence.Length; k++)
{
    sequence[k] = number % base;
    number = number / base;

    base++; // b[k+1] = b[k] + 1
}

这样就能正确将37解码成{1, 2, 0, 1}(在本代码示例中,sequence{1, 0, 2, 1},但只要您适当地索引即可)。我们只需要在右端添加0(请记住,最后一个元素总是只有一种可能的新位置),即可得到我们原始序列{1, 2, 0, 1, 0}。

使用索引序列对列表进行排列
您可以使用下面的算法根据特定的索引序列对列表进行排列。不幸的是,这是一个O(n²)的算法。

int n = 5;
int[] sequence = new int[] { 1, 2, 0, 1, 0 };
char[] list = new char[] { 'a', 'b', 'c', 'd', 'e' };
char[] permuted = new char[n];
bool[] set = new bool[n];

for (int i = 0; i < n; i++)
{
    int s = sequence[i];
    int remainingPosition = 0;
    int index;

    // Find the s'th position in the permuted list that has not been set yet.
    for (index = 0; index < n; index++)
    {
        if (!set[index])
        {
            if (remainingPosition == s)
                break;

            remainingPosition++;
        }
    }

    permuted[index] = list[i];
    set[index] = true;
}

排列的常见表示方式
通常情况下,你不会像我们做的那样使用难以理解的方式来表示一个排列,而是根据每个元素在应用排列后的绝对位置来表示。例如,对于abcdecaebd的排列{1,2,0,1,0}通常表示为{1,3,0,4,2}。在这种表示中,从0到4(或一般地,从0到n-1)的每个索引恰好出现一次。

以这种形式应用排列很容易:

int[] permutation = new int[] { 1, 3, 0, 4, 2 };

char[] list = new char[] { 'a', 'b', 'c', 'd', 'e' };
char[] permuted = new char[n];

for (int i = 0; i < n; i++)
{
    permuted[permutation[i]] = list[i];
}

反转它非常相似:

for (int i = 0; i < n; i++)
{
    list[i] = permuted[permutation[i]];
}

从我们的表示转换为通用表示
请注意,如果我们使用我们的索引序列对列表进行排列,然后将其应用于恒等置换{0, 1, 2,...,n-1},我们会得到表示为通用形式的逆序置换。(在我们的示例中为{2, 0, 4, 1, 3})。

要获取非反转排列,我们应用刚刚展示的排列算法:

int[] identity = new int[] { 0, 1, 2, 3, 4 };
int[] inverted = { 2, 0, 4, 1, 3 };
int[] normal = new int[n];

for (int i = 0; i < n; i++)
{
    normal[identity[i]] = list[i];
}

或者您可以直接应用排列,使用逆排列算法:

char[] list = new char[] { 'a', 'b', 'c', 'd', 'e' };
char[] permuted = new char[n];

int[] inverted = { 2, 0, 4, 1, 3 };

for (int i = 0; i < n; i++)
{
    permuted[i] = list[inverted[i]];
}

请注意,所有处理常规排列的算法复杂度为O(n),而在我们的形式中应用排列的复杂度为O(n²)。如果需要多次应用排列,请先将其转换为常规表示形式。


6
在“使用索引序列对列表进行排列”一文中,您提到了一个二次算法。这是可以接受的,因为n可能非常小。但是,可以通过使用顺序统计树(http://pine.cs.yale.edu/pinewiki/OrderStatisticsTree)将其轻松降至O(nlogn)。顺序统计树是红黑树,最初包含值0、1、2、...、n-1,每个节点包含其下方的后代数量。使用此方法,可以在O(logn)时间内找到/删除第k个元素。 - Dimitris Andreou
11
这些被称为 Lehmer 码。这个链接也很好地解释了它们,http://www.keithschwarz.com/interesting/code/?dir=factoradic-permutation。 - mihirg
这个算法很棒,但我发现有几种情况是错误的。以字符串“123”为例,第四个排列应该是231,但根据这个算法,它将是312。以1234为例,第四个排列应该是1342,但它会错误地变成“1423”。如果我观察错了,请纠正我。谢谢。 - Isaac Li
@IsaacLi,如果我没记错的话,f(4) = {2, 0, 0} = 231。而且 f'(312) = {1, 1, 0} = 3。对于 1234,f(4) = {0, 2, 0, 0} = 1342。而且 f'(1423) = {0, 1, 1, 0} = 3。这个算法真的很有启发性。我想知道这是否是原作者的作品。我已经研究和分析了一段时间。我相信它是正确的 :) - midnite
如何将从“我们的表示”转换为“通用表示”,{1,2,0,1,0}--> {1,3,0,4,2}?反之亦然?这是否可能?(通过 {1,2,0,1,0} <--> {C,A,E,B,D}之间进行转换,需要O(n ^ 2))。如果“我们的风格”和“通用风格”无法相互转换,则它们实际上是两个不同的东西,是吗?谢谢。 - midnite
显示剩余2条评论

22

我找到了一个在通常的复杂性假设下以O(n)运行的算法,这里有一个简短的解释:http://antoinecomeau.blogspot.ca/2014/07/mapping-between-permutations-and.html.

注意:考虑到除法的计算复杂性,该算法的实际运行时间为O(n^2 log^2(n))。

public static int[] perm(int n, int k)
{
    int i, ind, m=k;
    int[] permuted = new int[n];
    int[] elems = new int[n];

    for(i=0;i<n;i++) elems[i]=i;
           
    for(i=0;i<n;i++)
    {
            ind=m%(n-i);
            m=m/(n-i);
            permuted[i]=elems[ind];
            elems[ind]=elems[n-i-1];
    }
           
    return permuted;
}
   
public static int inv(int[] perm)
{
    int i, k=0, m=1;
    int n=perm.length;
    int[] pos = new int[n];
    int[] elems = new int[n];
           
    for(i=0;i<n;i++) {pos[i]=i; elems[i]=i;}
           
    for(i=0;i<n-1;i++)
    {
            k+=m*pos[perm[i]];
            m=m*(n-i);
            pos[elems[n-i-1]]=pos[perm[i]];
            elems[pos[perm[i]]]=elems[n-i-1];
    }
           
    return k;
}

1
如果我理解你的算法非常好,那么你正在寻找所有编码可能性(在这种情况下应该是n!种可能性)。然后,你会根据编码项映射数字。 - user3378649
2
这非常整洁。我今天自己想出了同样的方法,但我错过了你可以在反向中省略两个赋值的事实。 - fuz
我不知道这是否符合此页面的主要范围,但是可以使用此方法来排列/取消排列偶排列吗?(其中一个值受到两个其他值的交换的影响) - Lucas Sousa
1
如果包括进制转换部分,这并不是真正的O(n)。除法的运行时间不是恒定的:https://en.wikipedia.org/wiki/Computational_complexity_of_mathematical_operations - Quantum Guy 123
正确的表述应该是在大多数情况下,O(n) 是可以接受的,但由于你正在处理可能非常大的数字,因此 O(n) 并不是一个准确的描述。 - Quantum Guy 123
显示剩余4条评论

8
复杂度可以降到n*log(n),请参见fxtbook的第10.1.1节(“Lehmer代码(反演表)”,p.232ff): http://www.jjj.de/fxt/#fxtbook 跳转到第10.1.1.1节(“使用大数组进行计算”,p.235)以获取快速方法。 (GPL,C ++)代码位于同一网页上。

如果您打开您提到的同一本教材的10.1.4节,它会告诉您可以在O(n)时间内完成。这就是@Antoine Comeau的答案所做的。但是:这是基于输入数字以上升(或下降)阶乘为基础的情况。从整数基数转换为阶乘基数比O(n)更昂贵。 - Quantum Guy 123

5
问题已解决。不过我不确定在这些年过去后你是否仍需要解决方案。哈哈,我刚加入这个网站,所以...
请查看我的Java排列类。您可以基于索引获取符号排列,或者给定符号排列,然后获取索引。
以下是我的排列类:
/**
 ****************************************************************************************************************
 * Copyright 2015 Fred Pang fred@pnode.com
 ****************************************************************************************************************
 * A complete list of Permutation base on an index.
 * Algorithm is invented and implemented by Fred Pang fred@pnode.com
 * Created by Fred Pang on 18/11/2015.
 ****************************************************************************************************************
 * LOL this is my first Java project. Therefore, my code is very much like C/C++. The coding itself is not
 * very professional. but...
 *
 * This Permutation Class can be use to generate a complete list of all different permutation of a set of symbols.
 * nPr will be n!/(n-r)!
 * the user can input       n = the number of items,
 *                          r = the number of slots for the items,
 *                          provided n >= r
 *                          and a string of single character symbols
 *
 * the program will generate all possible permutation for the condition.
 *
 * Say if n = 5, r = 3, and the string is "12345", it will generate sll 60 different permutation of the set
 * of 3 character strings.
 *
 * The algorithm I used is base on a bin slot.
 * Just like a human or simply myself to generate a permutation.
 *
 * if there are 5 symbols to chose from, I'll have 5 bin slot to indicate which symbol is taken.
 *
 * Note that, once the Permutation object is initialized, or after the constructor is called, the permutation
 * table and all entries are defined, including an index.
 *
 * eg. if pass in value is 5 chose 3, and say the symbol string is "12345"
 * then all permutation table is logically defined (not physically to save memory).
 * It will be a table as follows
 *  index  output
 *      0   123
 *      1   124
 *      2   125
 *      3   132
 *      4   134
 *      5   135
 *      6   143
 *      7   145
 *      :     :
 *      58  542
 *      59  543
 *
 * all you need to do is call the "String PermGetString(int iIndex)" or the "int[] PermGetIntArray(int iIndex)"
 * function or method with an increasing iIndex, starting from 0 to getiMaxIndex() - 1. It will return the string
 * or the integer array corresponding to the index.
 *
 * Also notice that in the input string is "12345" of  position 01234, and the output is always in accenting order
 * this is how the permutation is generated.
 *
 * ***************************************************************************************************************
 * ====  W a r n i n g  ====
 * ***************************************************************************************************************
 *
 * There is very limited error checking in this class
 *
 * Especially the  int PermGetIndex(int[] iInputArray)  method
 * if the input integer array contains invalid index, it WILL crash the system
 *
 * the other is the string of symbol pass in when the object is created, not sure what will happen if the
 * string is invalid.
 * ***************************************************************************************************************
 *
 */
public class Permutation
{
    private boolean bGoodToGo = false;      // object status
    private boolean bNoSymbol = true;
    private BinSlot slot;                   // a bin slot of size n (input)
    private int nTotal;                     // n number for permutation
    private int rChose;                     // r position to chose
    private String sSymbol;                 // character string for symbol of each choice
    private String sOutStr;
    private int iMaxIndex;                  // maximum index allowed in the Get index function
    private int[] iOutPosition;             // output array
    private int[] iDivisorArray;            // array to do calculation

    public Permutation(int inCount, int irCount, String symbol)
    {
        if (inCount >= irCount)
        {
            // save all input values passed in
            this.nTotal = inCount;
            this.rChose = irCount;
            this.sSymbol = symbol;

            // some error checking
            if (inCount < irCount || irCount <= 0)
                return;                                 // do nothing will not set the bGoodToGo flag

            if (this.sSymbol.length() >= inCount)
            {
                bNoSymbol = false;
            }

            // allocate output storage
            this.iOutPosition = new int[this.rChose];

            // initialize the bin slot with the right size
            this.slot = new BinSlot(this.nTotal);

            // allocate and initialize divid array
            this.iDivisorArray = new int[this.rChose];

            // calculate default values base on n & r
            this.iMaxIndex = CalPremFormula(this.nTotal, this.rChose);

            int i;
            int j = this.nTotal - 1;
            int k = this.rChose - 1;

            for (i = 0; i < this.rChose; i++)
            {
                this.iDivisorArray[i] = CalPremFormula(j--, k--);
            }
            bGoodToGo = true;       // we are ready to go
        }
    }

    public String PermGetString(int iIndex)
    {
        if (!this.bGoodToGo) return "Error: Object not initialized Correctly";
        if (this.bNoSymbol) return "Error: Invalid symbol string";
        if (!this.PermEvaluate(iIndex)) return "Invalid Index";

        sOutStr = "";
        // convert string back to String output
        for (int i = 0; i < this.rChose; i++)
        {
            String sTempStr = this.sSymbol.substring(this.iOutPosition[i], iOutPosition[i] + 1);
            this.sOutStr = this.sOutStr.concat(sTempStr);
        }
        return this.sOutStr;
    }

    public int[] PermGetIntArray(int iIndex)
    {
        if (!this.bGoodToGo) return null;
        if (!this.PermEvaluate(iIndex)) return null ;
        return this.iOutPosition;
    }

    // given an int array, and get the index back.
    //
    //  ====== W A R N I N G ======
    //
    // there is no error check in the array that pass in
    // if any invalid value in the input array, it can cause system crash or other unexpected result
    //
    // function pass in an int array generated by the PermGetIntArray() method
    // then return the index value.
    //
    // this is the reverse of the PermGetIntArray()
    //
    public int PermGetIndex(int[] iInputArray)
    {
        if (!this.bGoodToGo) return -1;
        return PermDoReverse(iInputArray);
    }


    public int getiMaxIndex() {
    return iMaxIndex;
}

    // function to evaluate nPr = n!/(n-r)!
    public int CalPremFormula(int n, int r)
    {
        int j = n;
        int k = 1;
        for (int i = 0; i < r; i++, j--)
        {
            k *= j;
        }
        return k;
    }


//  PermEvaluate function (method) base on an index input, evaluate the correspond permuted symbol location
//  then output it to the iOutPosition array.
//
//  In the iOutPosition[], each array element corresponding to the symbol location in the input string symbol.
//  from location 0 to length of string - 1.

    private boolean PermEvaluate(int iIndex)
    {
        int iCurrentIndex;
        int iCurrentRemainder;
        int iCurrentValue = iIndex;
        int iCurrentOutSlot;
        int iLoopCount;

        if (iIndex >= iMaxIndex)
            return false;

        this.slot.binReset();               // clear bin content
        iLoopCount = 0;
        do {
            // evaluate the table position
            iCurrentIndex = iCurrentValue / this.iDivisorArray[iLoopCount];
            iCurrentRemainder = iCurrentValue % this.iDivisorArray[iLoopCount];

            iCurrentOutSlot = this.slot.FindFreeBin(iCurrentIndex);     // find an available slot
            if (iCurrentOutSlot >= 0)
                this.iOutPosition[iLoopCount] = iCurrentOutSlot;
            else return false;                                          // fail to find a slot, quit now

            this.slot.setStatus(iCurrentOutSlot);                       // set the slot to be taken
            iCurrentValue = iCurrentRemainder;                          // set new value for current value.
            iLoopCount++;                                               // increase counter
        } while (iLoopCount < this.rChose);

        // the output is ready in iOutPosition[]
        return true;
    }

    //
    // this function is doing the reverse of the permutation
    // the input is a permutation and will find the correspond index value for that entry
    // which is doing the opposit of the PermEvaluate() method
    //
    private int PermDoReverse(int[] iInputArray)
    {
        int iReturnValue = 0;
        int iLoopIndex;
        int iCurrentValue;
        int iBinLocation;

        this.slot.binReset();               // clear bin content

        for (iLoopIndex = 0; iLoopIndex < this.rChose; iLoopIndex++)
        {
            iCurrentValue = iInputArray[iLoopIndex];
            iBinLocation = this.slot.BinCountFree(iCurrentValue);
            this.slot.setStatus(iCurrentValue);                          // set the slot to be taken
            iReturnValue = iReturnValue + iBinLocation * this.iDivisorArray[iLoopIndex];
        }
        return iReturnValue;
    }


    /*******************************************************************************************************************
     *******************************************************************************************************************
     * Created by Fred on 18/11/2015.   fred@pnode.com
     *
     * *****************************************************************************************************************
     */
    private static class BinSlot
    {
        private int iBinSize;       // size of array
        private short[] eStatus;    // the status array must have length iBinSize

        private BinSlot(int iBinSize)
        {
            this.iBinSize = iBinSize;               // save bin size
            this.eStatus = new short[iBinSize];     // llocate status array
        }

        // reset the bin content. no symbol is in use
        private void binReset()
        {
            // reset the bin's content
            for (int i = 0; i < this.iBinSize; i++) this.eStatus[i] = 0;
        }

        // set the bin position as taken or the number is already used, cannot be use again.
        private void  setStatus(int iIndex) { this.eStatus[iIndex]= 1; }

        //
        // to search for the iIndex th unused symbol
        // this is important to search through the iindex th symbol
        // because this is how the table is setup. (or the remainder means)
        // note: iIndex is the remainder of the calculation
        //
        // for example:
        // in a 5 choose 3 permutation symbols "12345",
        // the index 7 item (count starting from 0) element is "1 4 3"
        // then comes the index 8, 8/12 result 0 -> 0th symbol in symbol string = '1'
        // remainder 8. then 8/3 = 2, now we need to scan the Bin and skip 2 unused bins
        //              current the bin looks 0 1 2 3 4
        //                                    x o o o o     x -> in use; o -> free only 0 is being used
        //                                      s s ^       skipped 2 bins (bin 1 and 2), we get to bin 3
        //                                                  and bin 3 is the bin needed. Thus symbol "4" is pick
        // in 8/3, there is a remainder 2 comes in this function as 2/1 = 2, now we have to pick the empty slot
        // for the new 2.
        // the bin now looks 0 1 2 3 4
        //                   x 0 0 x 0      as bin 3 was used by the last value
        //                     s s   ^      we skip 2 free bins and the next free bin is bin 4
        //                                  therefor the symbol "5" at the symbol array is pick.
        //
        // Thus, for index 8  "1 4 5" is the symbols.
        //
        //
        private int FindFreeBin(int iIndex)
        {
            int j = iIndex;

            if (j < 0 || j > this.iBinSize) return -1;               // invalid index

            for (int i = 0; i < this.iBinSize; i++)
            {
                if (this.eStatus[i] == 0)       // is it used
                {
                    // found an empty slot
                    if (j == 0)                 // this is a free one we want?
                        return i;               // yes, found and return it.
                    else                        // we have to skip this one
                        j--;                    // else, keep looking and count the skipped one
                }
            }
            assert(true);           // something is wrong
            return -1;              // fail to find the bin we wanted
        }

        //
        // this function is to help the PermDoReverse() to find out what is the corresponding
        // value during should be added to the index value.
        //
        // it is doing the opposite of int FindFreeBin(int iIndex) method. You need to know how this
        // FindFreeBin() works before looking into this function.
        //
        private int BinCountFree(int iIndex)
        {
            int iRetVal = 0;
            for (int i = iIndex; i > 0; i--)
            {
                if (this.eStatus[i-1] == 0)       // it is free
                {
                    iRetVal++;
                }
            }
            return iRetVal;
        }
    }
}
// End of file - Permutation.java

这是我的主要类,用于展示如何使用该类。

/*
 * copyright 2015 Fred Pang
 *
 * This is the main test program for testing the Permutation Class I created.
 * It can be use to demonstrate how to use the Permutation Class and its methods to generate a complete
 * list of a permutation. It also support function to get back the index value as pass in a permutation.
 *
 * As you can see my Java is not very good. :)
 * This is my 1st Java project I created. As I am a C/C++ programmer for years.
 *
 * I still have problem with the Scanner class and the System class.
 * Note that there is only very limited error checking
 *
 *
 */

import java.util.Scanner;

public class Main
{
    private static Scanner scanner = new Scanner(System.in);

    public static void main(String[] args)
    {
        Permutation perm;       // declear the object
        String sOutString = "";
        int nCount;
        int rCount;
        int iMaxIndex;

        // Get user input
        System.out.println("Enter n: ");
        nCount = scanner.nextInt();

        System.out.println("Enter r: ");
        rCount = scanner.nextInt();

        System.out.println("Enter Symbol: ");
        sOutString = scanner.next();

        if (sOutString.length() < rCount)
        {
            System.out.println("String too short, default to numbers");
            sOutString = "";
        }

        // create object with user requirement
        perm = new Permutation(nCount, rCount, sOutString);

        // and print the maximum count
        iMaxIndex = perm.getiMaxIndex();
        System.out.println("Max count is:" + iMaxIndex);

        if (!sOutString.isEmpty())
        {
            for (int i = 0; i < iMaxIndex; i++)
            {   // print out the return permutation symbol string
                System.out.println(i + " " + perm.PermGetString(i));
            }
        }
        else
        {
            for (int i = 0; i < iMaxIndex; i++)
            {
                System.out.print(i + " ->");

                // Get the permutation array
                int[] iTemp = perm.PermGetIntArray(i);

                // print out the permutation
                for (int j = 0; j < rCount; j++)
                {
                    System.out.print(' ');
                    System.out.print(iTemp[j]);
                }

                // to verify my PermGetIndex() works. :)
                if (perm.PermGetIndex(iTemp)== i)
                {
                    System.out.println(" .");
                }
                else
                {   // oops something is wrong :(
                    System.out.println(" ***************** F A I L E D *************************");
                    assert(true);
                    break;
                }
            }
        }
    }
}
//
// End of file - Main.java

玩得开心。 :)


4

这恰好是 J 内置的函数:

   A. 1 2 3 4 5 6 7
0
   0 A. 1 2 3 4 5 6 7
1 2 3 4 5 6 7

   ?!7
5011
   5011 A. 1 2 3 4 5 6 7
7 6 4 5 1 3 2
   A. 7 6 4 5 1 3 2
5011

4

每个元素可以处于七个位置之一。要描述一个元素的位置,您需要三位比特。这意味着您可以将所有元素的位置存储在32位值中。这远非高效,因为此表示甚至允许所有元素处于相同位置,但我相信比特掩码应该是相当快的。

然而,如果有超过8个位置,则需要更巧妙的方法。


这意味着如果 OP 不介意枚举实际上从 0 到 5039,对吗?如果可以的话,这似乎是一个很好的解决方案。 - Troubadour

3

您可以使用递归算法编码排列。如果N-排列(数值{0,..,N-1}的某种排序)的形式为{x,...},则将其编码为x + N *表示"..."上数字{0,N-1} - {x}的(N-1)-排列的编码。听起来有些复杂,这里是一些代码:

// perm[0]..perm[n-1] must contain the numbers in {0,..,n-1} in any order.
int permToNumber(int *perm, int n) {
  // base case
  if (n == 1) return 0;

  // fix up perm[1]..perm[n-1] to be a permutation on {0,..,n-2}.
  for (int i = 1; i < n; i++) {
    if (perm[i] > perm[0]) perm[i]--;
  }

  // recursively compute
  return perm[0] + n * permToNumber(perm + 1, n - 1);
}

// number must be >=0, < n!
void numberToPerm(int number, int *perm, int n) {
  if (n == 1) {
    perm[0] = 0;
    return;
  }
  perm[0] = number % n;
  numberToPerm(number / n, perm + 1, n - 1);

  // fix up perm[1] .. perm[n-1]
  for (int i = 1; i < n; i++) {
    if (perm[i] >= perm[0]) perm[i]++;
  }
}

这个算法的时间复杂度是O(n^2)。如果有人能提供一个O(n)的算法,那就更好了。


2

我曾经遇到过这个问题,想提供我的Python解决方案。它的时间复杂度为O(n^2)。

import copy

def permute(string, num):
    ''' generates a permutation '''
    def build_s(factoradic): # Build string from factoradic in list form
        string0 = copy.copy(string)
        n = []
        for i in range(len(factoradic)):
            n.append(string0[factoradic[i]])
            del string0[factoradic[i]]
        return n

    f = len(string)
    factoradic = []
    while(f != 0): # Generate factoradic number list
        factoradic.append(num % f)
        num = (num - factoradic[-1])//f
        f -= 1

    return build_s(factoradic)

s = set()
# Print 120 permutations of this string
for i in range(120):
    m = permute(list('abcde'), i)
    s.add(''.join(m))

print(len(s)) # Check that we have 120 unique permutations

这很简单,生成数字的阶乘表示后,我只需从字符串中选择并删除字符。从字符串中删除是这个O(n^2)解决方案的原因。
Antoine 的解决方案性能更好。

1

有趣。我在想这个是否比我的答案更快。对这两个算法进行基准测试将会很有趣。 - Antoine Comeau
它们本质上是相同的算法,但我还没有比较过这两个。在Python中,使用divmod实现算法而不是单独的模数和除法操作可以提高性能(我进行了一些基准测试)。如果您在2000年之前发现了这个算法,我会非常印象深刻,并且它将值得发表(现在可能已经有超过100次引用)。虽然我想象您在那一年之前没有写过一行计算机代码。 - Quantum Guy 123
谢谢,我想这个问题应该是众所周知的,因为修复方法很简单。你能找到一个参考真是太棒了! - Antoine Comeau

1

这是一个非常有趣的问题!

如果你所有的元素都是数字,你可能想考虑将它们从字符串转换为实际的数字。然后,你就可以通过按顺序排列所有排列组合并将它们放入数组中进行排序。之后,你就可以使用各种搜索算法了。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接