如何直接获得序列的第n个排列的第i个元素(没有任何递归性)?

时间:2019-01-03 07:55:23

标签: python algorithm permutation

假设我们有一个字符串“ ABCD”,我们想从字符串的第n个排列中检索第i个位置的字母。

在此示例中,我知道存在阶乘(4)= 24个排列,并且可以使用itertools.permutations轻松检索列表,这将给出以下信息:

  

['ABCD','ABDC','ACBD','ACDB','ADBC','ADCB','BACD','BADC','BCAD','BCDA','BDAC','BDCA ','CABD','CADB','CBAD','CBDA','CDAB','CDBA','DABC','DACB','DBAC','DBCA','DCAB','DCBA']

因此我要查找的函数f应该返回:

  

f(0,n)== ['A','A','A','A','A','A','B','B','B','B ','B','B','C','C','C','C','C','C','D','D','D','D', 'D','D'] [n]

     

f(1,n)== ['B','B','C','C','D','D','A','A','C','C ','D','D','A','A','B','B','D','D','A','A','B','B', 'C','C'] [n]

     

f(2,n)== ['C','D','B','D','B','C','C','D','A','D ','A','C','B','D','A','D','A','B','B','C','A','C', 'A','B'] [n]

     

f(3,n)== ['D','C','D','B','C','B','D','C','D','A ','C','A','D','B','D','A','B','A','C','B','C','A', 'B','A'] [n]

对于i == 0来说,这很容易,我们有f(0,n)==“ ABCD” [n // 6],但是当i增大时找到模式越来越复杂。

我完全不在乎排列的顺序,因此也许可以轻松找到每个i值的通用模式...

我计划将其与一组256个元素和阶乘(256)排列一起使用,因此计算排列不是一个选择。

编辑:我已经有一个函数,但是它太慢了,我想知道是否可以使用简单的公式(例如按位运算)找到某些等效结果...

Edit-2:由于@rici,这是当前最好的解决方案:

f = [factorial(i) for i in range(256)]

def _getElt(k, i):
    """
    Returns the <i>th element of the <k>th permutation of 0..255
    """
    table = list(range(256))

    for j in range(255, 254-i, -1):
        r, k = divmod(k, f[j])
        perm[j], perm[r] = perm[r], perm[j] 
    return perm[255 - i]

Edit-3:这是另一种使用多项式逼近来重新生成置换的方法,因此问题的另一种表述可能是“如何为第n个置换重新生成多项式的第i个系数?”。 >

这是n = 4的n个排列,多项式系数(最后是从多项式系数重建的排列)的列表:

  

0 [0,1,2,3] [分数(0,1),分数(1,1),分数(0,1),分数(0,1)] [0,1,2,3 ]

     

1 [0,1,3,2] [分数(0,1),分数(-3,4),分数(5,2),分数(-2,3)] [0,1,3 ,2]

     

2 [0,2,1,3] [分数(0,1),分数(11,2),分数(-9,2),分数(1,1)] [0,2,1, 3]

     

3 [0,2,3,1] [分数(0,1),分数(7,4),分数(1,2),分数(-1,3)] [0,2,3, 1]

     

4 [0,3,1,2] [分数(0,1),分数(33,4),分数(-13,2),分数(4,3)] [0,3,1, 2]

     

5 [0,3,2,1] [分数(0,1),分数(19,3),分数(-4,1),分数(2,3)] [0,3,2, 1]

     

6 [1,0,2,3] [分数(1,1),分数(-15,4),分数(7,2),分数(-2,3)] [1,0,2 ,3]

     

7 [1,0,3,2] [分数(1,1),分数(-17,3),分数(6,1),分数(-4,3)] [1,0,3 ,2]

     

8 [1、2、0、3] [分数(1、1),分数(21、4),分数(-11、2),分数(4、3)] [1、2、0, 3]

     

9 [1,2,3,0] [分数(1,1),分数(-1,3),分数(2,1),分数(-2,3)] [1,2,3 ,0]

     

10 [1,3,0,2] [分数(1,1),分数(31,4),分数(-15,2),分数(5,3)] [1,3,0, 2]

     

11 [1、3、2、0] [分数(1、1),分数(17、4),分数(-5、2),分数(1、3)] [1、3、2 0]

     

12 [2,0,1,3] [分数(2,1),分数(-17,4),分数(5,2),分数(-1,3)] [2,0,1 ,3]

     

13 [2,0,3,1] [分数(2,1),分数(-31,4),分数(15,2),分数(-5,3)] [2,0,3 ,1]

     

14 [2,1,0,3] [分数(2,1),分数(1,3),分数(-2,1),分数(2,3)] [2,1,0, 3]

     

15 [2,1,3,0] [分数(2,1),分数(-21,4),分数(11,2),分数(-4,3)] [2,1,3 ,0]

     

16 [2,3,0,1] [分数(2,1),分数(17,3),分数(-6,1),分数(4,3)] [2,3,0, 1]

     

17 [2,3,1,0] [分数(2,1),分数(15,4),分数(-7,2),分数(2,3)] [2,3,1, 0]

     

18 [3,0,1,2] [分数(3,1),分数(-19,3),分数(4,1),分数(-2,3)] [3,0,1 ,2]

     

19 [3,0,2,1] [分数(3,1),分数(-33,4),分数(13,2),分数(-4,3)] [3,0,2 ,1]

     

20 [3,1,0,2] [分数(3,1),分数(-7,4),分数(-1,2),分数(1,3)] [3,1,0 ,2]

     

21 [3,1,2,0] [分数(3,1),分数(-11,2),分数(9,2),分数(-1,1)] [3,1,2 ,0]

     

22 [3,2,0,1] [分数(3,1),分数(3,4),分数(-5,2),分数(2,3)] [3,2,0, 1]

     

23 [3,2,1,0] [分数(3,1),分数(-1,1),分数(0,1),分数(0,1)] [3,2,1, 0]

我们可以清楚地看到存在一个对称性:coefs [i] = [3-coefs [23-i] [0]] + [-c对于coefs [23-i] [1:]]这是一种探索的方式,但我不知道这是有可能的。

3 个答案:

答案 0 :(得分:3)

通过进行重复的欧几里德除法(商和余数,又称为divmod)并跟踪选择的字母,可以找到第n个排列。然后,您可以从该排列中选择第i个字母。

S是初始字符串的副本,L是该字符串的长度,P是排列次数(L!)。 T将是逐步构建的n的第S个置换。
示例:S = "ABCD"L = 4P = 24。让我们以n = 15为例。 T应该以{{1​​}}结尾。

设置"CBDA"。计算P = P/L,令divmod(n, P)为商(q),使n/P为余数(r)。从n%P中删除第q个字母,并将其附加到S。设置T,递减n = r,然后重复进行直到L
示例:
1)L = 0P = 24/4 = 6q = 15/6 = 2r = 15%6 = 3S = "ABD"T = "C"n = r = 3
2)L = 3P = 6/3 = 2q = 3/2 = 1r = 3%2 = 1S = "AD"T = "CB"n = r = 1
3)L = 2P = 2/2 = 1q = 1/1 = 1r = 1%1 = 0S = "A"T = "CBD"n = r = 0
4)L = 1P = 1/1 = 1q = 0/1 = 0r = 0%1 = 0S = ""T = "CBDA"n = r = 0

由于只需要第L = 0个字母,因此只要i的长度等于T,就可以停下来,取最后一个字母。

我不会尝试用Python编写代码,因为自从接触Python以来已经太久了,但是here is a demo in C++

答案 1 :(得分:2)

对不起,对于我的Python。这个想法是每个位置的字母重复(number_of_pos - pos)!次。

例如ABCD,第一个位置的字母将重复3!次。因此,通过将n除以阶乘,我们现在可以得出哪个字母在第一位置。要在第二个位置查找字母,我们需要将该字母从集合中删除(改为分配0),并用n更新n - n % 3!到现在第二个位置的字母索引。应用此索引时,我们需要注意不要计算在第一位置的字母。

复杂度为O(kPosCount * kPosCount)

#include <array>
#include <iostream>

const int kPosCount = 4;
const std::array<int, kPosCount> factorial = { 1,1,2,6 };
const std::array<int, kPosCount> kLetters = { 'A', 'B', 'C', 'D' };

char f(int pos, int n) {
    assert(pos < kPosCount);
    std::array<int, kPosCount> letters = kLetters;

    int res = 0;
    for (int i = 0; i <= pos; ++i) {
        const int letter = n / factorial[kPosCount - i - 1];
        int j = 0;
        for (int stillThere = 0; j < kPosCount; ++j) {
            if (letters[j] != 0) {
                if (stillThere == letter) {
                    break;
                }
                ++stillThere;
            }
        }
        letters[j] = 0;
        res = j;
        n %= factorial[kPosCount - i - 1];
    }
    return kLetters[res];
}

int main() {
    const int kMaxN = factorial.back() * kPosCount;
    for (int i = 0; i < kPosCount; ++i) {
        for (int j = 0; j < kMaxN; ++j) {
            std::cout << f(i, j) << " ";
        }
        std::cout << std::endl;
    }
}

答案 2 :(得分:1)

这是函数的版本,具有不同的排列顺序。在这里,我并没有强迫排列的大小为256,所以您必须将其作为第一个参数。

def elt(n,k,i):
  """Returns element i of permutation k from permutations of length n"""
  perm = [*range(n)]
  for j in range(i+1):
    k, r = divmod(k, n-j)
    perm[j], perm[j+r] = perm[j+r], perm[j]
  return perm[i]

与您的代码有两个区别。

首先,数字从右到左从索引中剔除,这意味着bignum divmod都是具有少量股息的divmod。我以为那会比使用bignum股息更快,但事实证明它要慢得多。但是,它的确避免了必须预先计算阶乘。

第二,而不是从表的中间进行一系列pop的操作,因为弹出操作是O(n),这将具有二次复杂性,我只是将每个元素与某些前向交换元件。尽管bignum算术在计算中起着主导作用,但这要快得多。

实际上,elt(n,k,i)使用i的混合基分解作为随机变量,在range(n)的Fisher-Yates混洗中执行了最初的k步骤随机洗牌中的值。由于F-Y随机播放对每个可能的随机值序列产生不同的结果,并且k的混合基分解对k的每个不同的值产生不同的序列,因此将生成所有排列。

这是n = 4时的顺序:

>>> print('\n'.join(' '.join(str(elt(4, k, i)) for i in range(4))
                    for k in range(factorial(4))))
0 1 2 3
1 0 2 3
2 1 0 3
3 1 2 0
0 2 1 3
1 2 0 3
2 0 1 3
3 2 1 0
0 3 2 1
1 3 2 0
2 3 0 1
3 0 2 1
0 1 3 2
1 0 3 2
2 1 3 0
3 1 0 2
0 2 3 1
1 2 3 0
2 0 3 1
3 2 0 1
0 3 1 2
1 3 0 2
2 3 1 0
3 0 1 2

为真正加速该功能,我重新引入了预先计算的阶乘(作为局部变量,可以根据需要扩展)。我还通过消除尽可能多的算法对内部循环进行了微优化,这意味着从后到前进行F-Y随机播放。这导致了另一个排列顺序。参见下面的示例。

最终的优化功能比问题版本快15%(也就是说,进行简单基准测试大约需要85%的时间)。

def elt_opt(n, k, i, fact=[1]):
    """Returns element i of permutation k from permutations of length n.
       Let the last argument default.
    """
    while len(fact) < n: fact.append(fact[-1]*len(fact))
    perm = list(range(n))
    for j in range(n-1, n-2-i, -1):
        r, k = divmod(k, fact[j])
        perm[j], perm[r] = perm[r], perm[j]
    return perm[n-i-1]

排列顺序:

>>> print('\n'.join(' '.join(str(elt_opt(4, k, i)) for i in range(4))
                    for k in range(factorial(4))))
0 3 2 1
0 3 1 2
0 1 3 2
0 1 2 3
0 2 3 1
0 2 1 3
1 0 2 3
1 0 3 2
1 3 0 2
1 3 2 0
1 2 0 3
1 2 3 0
2 0 3 1
2 0 1 3
2 1 0 3
2 1 3 0
2 3 0 1
2 3 1 0
3 0 2 1
3 0 1 2
3 1 0 2
3 1 2 0
3 2 0 1
3 2 1 0