Как получить <i> -й элемент <n> -й перестановки последовательности напрямую (без какой-либо рекурсии)?

Допустим, у нас есть строка «ABCD», и мы хотим получить букву в i-й позиции из n-й перестановки строки.

В этом примере я знаю, что есть factorial (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: Вот другой подход, использующий полиномиальные приближения для воссоздания перестановок, поэтому другая формулировка проблемы могла бы быть «как воссоздать i-й коэффициент полинома для n-й перестановки?».

Это список из n, перестановки, коэффициента полинома (и, наконец, перестановки, перестроенной из коэффициентов полинома) для N = 4:

0 [0, 1, 2, 3] [Fraction(0, 1), Fraction(1, 1), Fraction(0, 1), Fraction(0, 1)] [0, 1, 2, 3]

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

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

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

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

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

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

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

8 [1, 2, 0, 3] [Fraction(1, 1), Fraction(21, 4), Fraction(-11, 2), Fraction(4, 3)] [1, 2, 0, 3]

9 [1, 2, 3, 0] [Fraction(1, 1), Fraction(-1, 3), Fraction(2, 1), Fraction(-2, 3)] [1, 2, 3, 0]

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

11 [1, 3, 2, 0] [Fraction(1, 1), Fraction(17, 4), Fraction(-5, 2), Fraction(1, 3)] [1, 3, 2, 0]

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

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

14 [2, 1, 0, 3] [Fraction(2, 1), Fraction(1, 3), Fraction(-2, 1), Fraction(2, 3)] [2, 1, 0, 3]

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

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

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

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

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

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

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

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

23 [3, 2, 1, 0] [Fraction(3, 1), Fraction(-1, 1), Fraction(0, 1), Fraction(0, 1)] [3, 2, 1, 0]

Мы ясно видим, что существует симметрия: coefs [i] = [3 - coefs [23-i] [0]] + [-c for c in coefs [23-i] [1:]], так что это способ исследовать, но я понятия не имею, что это возможно.

Так в чем конкретно ваш вопрос?

ycx 03.01.2019 09:01

Попробуйте не вычислять factorial(f) по три раза за каждый шаг? Если вы установите некоторую переменную fact = factorial(f) в начале, то вы можете просто разделить ее на f на первом этапе, затем на f-1, затем на f-2 и так далее.

Nelfeal 03.01.2019 10:41

@Nelfeal да, это был просто пример, в моих исходных функциях факториалы вычисляются заранее ... Я просто хочу избавиться от всего цикла while.

fbparis 03.01.2019 11:01

Я верю, что ты не можешь

Yola 03.01.2019 11:06
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
3
4
510
3

Ответы 3

Вы можете найти n-ю перестановку, повторяя евклидово деление (частное и остаток, иначе divmod) и отслеживая, какие буквы вы выбираете. Затем вы можете выбрать i-ю букву из этой перестановки.

Пусть S будет копией начальной строки, L - длиной этой строки, а P - количеством перестановок (L!). T будет n-й перестановкой S, построенной шаг за шагом. Пример: S = "ABCD", L = 4 и P = 24. Для примера возьмем n = 15. T в конце должен быть "CBDA".

Установите P = P/L. Вычислите divmod(n, P), пусть q будет частным (n/P), а r остатком (n%P). Удалите q-ю букву из S и добавьте ее в T. Установить n = r, уменьшить L и повторять до L = 0.
Пример:
1) P = 24/4 = 6, q = 15/6 = 2, r = 15%6 = 3, S = "ABD", T = "C", n = r = 3, L = 3.
2) P = 6/3 = 2, q = 3/2 = 1, r = 3%2 = 1, S = "AD", T = "CB", n = r = 1, L = 2.
3) P = 2/2 = 1, q = 1/1 = 1, r = 1%1 = 0, S = "A", T = "CBD", n = r = 0, L = 1.
4) P = 1/1 = 1, q = 0/1 = 0, r = 0%1 = 0, S = "", T = "CBDA", n = r = 0, L = 0.

Поскольку вам нужна только буква i, вы можете остановиться, когда длина T равна i+1, и взять эту последнюю букву.

Я не буду пытаться кодировать это на Python, потому что прошло слишком много времени с тех пор, как я прикасался к Python, но вот демонстрация на C++.

Большое спасибо за ваш ответ. У меня уже есть такая функция в python, мне было интересно, можно ли сделать прямую эквивалентность ...

fbparis 03.01.2019 10:30

Но это прямая эквивалентность. Он имеет только шаги L и может быть отменен (также в шагах L). Я почти уверен, что вы не получите ничего более простого.

Nelfeal 03.01.2019 10:37

Простите за мой 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;
    }
}

Вот версия вашей функции с другим порядком перестановки. Я не устанавливал здесь размер перестановки 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 divmods являются модулями divmods с небольшим делимым. Я думал, что это будет быстрее, чем использование больших дивидендов, но оказалось, что это значительно медленнее. Однако он позволяет избежать предварительного вычисления факториалов.

Во-вторых, вместо того, чтобы выполнять серию pop из середины таблицы, которая будет иметь квадратичную сложность, потому что операция pop - O (n), я просто меняю местами каждый элемент с некоторым прямым элементом. Это значительно быстрее, хотя большая арифметика доминирует в вычислениях.

Фактически, elt(n,k,i) выполняет первые шаги i в тасовке Фишера-Йетса range(n), используя разложение 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

Большое спасибо, материал о Фишере-Йейтсе очень интересен, а также генерирует перестановки без вычисления факториалов! Но после тестирования я обнаружил, что это решение немного медленнее моего. Понятия не имею, почему, возможно, Python 3.7 лучше справляется со списком .pop ()

fbparis 03.01.2019 20:57

@fbparis: оказывается, я ошибался насчет разделения; Python на самом деле примерно на 60% быстрее делит два больших числа с малым частным, чем делит большое число на небольшое делимое. Это подавляет ускорение обмена.

rici 03.01.2019 22:31

@fbparis: Я добавил самую оптимизированную версию, которую смог придумать. Но он не намного быстрее твоего.

rici 04.01.2019 00:07

хорошо сделано (я приблизился к увеличению скорости на 10%), но он все еще не подходит для предполагаемого использования, я думаю, мне нужно что-то <O (n). Я сильно сомневаюсь, что это возможно, но я буду продолжать немного пробовать новые вещи (в настоящее время я тестирую вещи с помощью интерполяции Лагранжа ...)

fbparis 04.01.2019 03:41

@fbparis: я не верю, что сублинейная существует, но вы могли бы как-то переформулировать свою проблему. Например, если вам нужно более одного элемента из одной и той же перестановки, все, кроме одного, будут бесплатными. Так что это может очень помочь

rici 04.01.2019 04:11

есть другой способ улучшить его, используя симметрию перестановок, чтобы мы никогда не зацикливали больше, чем N // 2

fbparis 04.01.2019 04:49

Позвольте нам продолжить обсуждение в чате.

fbparis 04.01.2019 05:36

@fbparis. Конечно, но бигнумы больше в начале цикла, поэтому первая итерация будет самой медленной. Тем не менее, мне трудно представить себе вариант использования, в котором вам когда-либо понадобится только один элемент назначенной перестановки.

rici 04.01.2019 05:38

Другие вопросы по теме