Нахождение n-й перестановки без вычисления других - PullRequest
39 голосов
/ 27 октября 2011

Учитывая массив из N элементов, представляющих атомы перестановки, существует ли такой алгоритм:

function getNthPermutation( $atoms, $permutation_index, $size )

, где $atoms - массив элементов, $permutation_index - индекс перестановки и$size - это размер перестановки.

Например:

$atoms = array( 'A', 'B', 'C' );
// getting third permutation of 2 elements
$perm = getNthPermutation( $atoms, 3, 2 );

echo implode( ', ', $perm )."\n";

Выведет:

B, A

Без вычисления каждой перестановки до $ permutation_index?

Я слышал что-то о факторических перестановках, но каждая найденная реализация дает в результате перестановку с одинаковым размером V, что не в моем случае.

Спасибо.

Ответы [ 8 ]

43 голосов
/ 27 октября 2011

Как заявил RickyBobby, при рассмотрении лексикографического порядка перестановок вы должны использовать факториальную декомпозицию в ваших интересах.

С практической точки зрения, это то, как я ее вижу:

  • Выполните своего рода евклидово деление, за исключением того, что вы делаете это с факториальными числами, начиная с (n-1)!, (n-2)! и т. Д.
  • Сохраняйте коэффициенты в массиве.i-й фактор должен быть числом от 0 до n-i-1 включительно, где i идет от 0 до n-1.
  • Этот массив равен ваша перестановка.Проблема в том, что каждый фактор не заботится о предыдущих значениях, поэтому вам нужно настроить их.Более конкретно, вам нужно увеличивать каждое значение столько раз, сколько предыдущих значений меньше или равны.

Следующий код C должен дать вам представление о том, как это работает (nколичество записей, а i - индекс перестановки):

/**
 * @param n The number of entries
 * @param i The index of the permutation
 */
void ithPermutation(const int n, int i)
{
   int j, k = 0;
   int *fact = (int *)calloc(n, sizeof(int));
   int *perm = (int *)calloc(n, sizeof(int));

   // compute factorial numbers
   fact[k] = 1;
   while (++k < n)
      fact[k] = fact[k - 1] * k;

   // compute factorial code
   for (k = 0; k < n; ++k)
   {
      perm[k] = i / fact[n - 1 - k];
      i = i % fact[n - 1 - k];
   }

   // readjust values to obtain the permutation
   // start from the end and check if preceding values are lower
   for (k = n - 1; k > 0; --k)
      for (j = k - 1; j >= 0; --j)
         if (perm[j] <= perm[k])
            perm[k]++;

   // print permutation
   for (k = 0; k < n; ++k)
      printf("%d ", perm[k]);
   printf("\n");

   free(fact);
   free(perm);
}

Например, ithPermutation(10, 3628799) печатает, как и ожидалось, последнюю перестановку из десяти элементов:

9 8 7 6 5 4 3 2 1 0
29 голосов
/ 17 июня 2014

Вот решение, которое позволяет выбрать размер перестановки. Например, помимо возможности генерировать все перестановки из 10 элементов, он может генерировать перестановки пар из 10 элементов. Также он переставляет списки произвольных объектов, а не только целых чисел.

Это PHP, но есть также JavaScript и Haskell имплементация.

function nth_permutation($atoms, $index, $size) {
    for ($i = 0; $i < $size; $i++) {
        $item = $index % count($atoms);
        $index = floor($index / count($atoms));
        $result[] = $atoms[$item];
        array_splice($atoms, $item, 1);
    }
    return $result;
}

Пример использования:

for ($i = 0; $i < 6; $i++) {
    print_r(nth_permutation(['A', 'B', 'C'], $i, 2));
}
// => AB, BA, CA, AC, BC, CB

Как это работает?

За этим стоит очень интересная идея. Давайте возьмем список A, B, C, D. Мы можем построить перестановку, вытягивая из нее элементы, как из колоды карт. Изначально мы можем нарисовать один из четырех элементов. Затем один из трех оставшихся элементов и так далее, пока, наконец, у нас ничего не останется.

Decision tree for permutations of 4 elements

Вот одна из возможных последовательностей выборов. Начиная сверху мы идем по третьему пути, затем по первому, второму и, наконец, по первому. И это наша перестановка № 13.

Подумайте о том, как при данной последовательности выборов вы получите алгоритмически число тринадцать. Затем измените алгоритм, и вы сможете восстановить последовательность из целого числа.

Попробуем найти общую схему для упаковки последовательности выборов в целое число без избыточности и ее распаковки обратно.

Одна интересная схема называется десятичной системой счисления. «27» можно рассматривать как выбор пути № 2 из 10, а затем выбор пути № 7 из 10.

Decision three for number 27 in decimal

Но каждая цифра может кодировать варианты только из 10 альтернатив. Другие системы с фиксированным основанием, такие как двоичные и шестнадцатеричные, также могут кодировать только последовательности выбора из фиксированного числа альтернатив. Нам нужна система с переменным основанием, вроде единиц времени: «14:05:29» - это час 14 из 24, минута 5 из 60, секунда 29 из 60.

Что, если мы возьмем общие функции для преобразования числа в строку и для преобразования строки в число и введем их в заблуждение с использованием смешанных оснований? Вместо того, чтобы принимать один ось, например, parseInt ('beef', 16) и (48879) .toString (16) , они будут принимать одно основание для каждой цифры.

function pack(digits, radixes) {
    var n = 0;
    for (var i = 0; i < digits.length; i++) {
        n = n * radixes[i] + digits[i];
    }
    return n;
}

function unpack(n, radixes) {
    var digits = [];
    for (var i = radixes.length - 1; i >= 0; i--) {
        digits.unshift(n % radixes[i]);
        n = Math.floor(n / radixes[i]);
    }
    return digits;
}

Это вообще работает?

// Decimal system
pack([4, 2], [10, 10]); // => 42

// Binary system
pack([1, 0, 1, 0, 1, 0], [2, 2, 2, 2, 2, 2]); // => 42

// Factorial system
pack([1, 3, 0, 0, 0], [5, 4, 3, 2, 1]); // => 42

А теперь задом наперед:

unpack(42, [10, 10]); // => [4, 2]

unpack(42, [5, 4, 3, 2, 1]); // => [1, 3, 0, 0, 0]

Это так прекрасно. Теперь давайте применим эту параметрическую систему счисления к проблеме перестановок. Мы рассмотрим перестановки длины 2 A, B, C, D. Сколько их всего? Давайте посмотрим: сначала мы рисуем один из 4 предметов, затем один из оставшихся 3, это 4 * 3 = 12 способов нарисовать 2 предмета. Эти 12 способов могут быть упакованы в целые числа [0..11]. Итак, давайте представим, что мы уже упаковали их, и попробуем распаковать:

for (var i = 0; i < 12; i++) {
    console.log(unpack(i, [4, 3]));
}

// [0, 0], [0, 1], [0, 2],
// [1, 0], [1, 1], [1, 2],
// [2, 0], [2, 1], [2, 2],
// [3, 0], [3, 1], [3, 2]

Эти числа представляют варианты, а не индексы в исходном массиве. [0, 0] не означает взятие A, A, это означает, что нужно взять элемент № 0 из A, B, C, D (это A), а затем пункт # 0 из оставшегося списка B, C, D (это B). И в результате перестановка будет A, B.

Другой пример: [3, 2] означает получение элемента № 3 из A, B, C, D (это D) и затем пункта № 2 из оставшегося списка A, B, C (это C). И в результате перестановка будет D, C.

Это отображение называется Lehmer code . Давайте сопоставим все эти коды Лемера с перестановками:

AB, AC, AD, BA, BC, BD, CA, CB, CD, DA, DB, DC

Это именно то, что нам нужно. Но если вы посмотрите на функцию unpack, вы заметите, что она производит цифры справа налево (чтобы изменить действия pack). Выбор из 3 распаковывается перед выбором из 4. Это неудачно, потому что мы хотим выбрать из 4 элементов, прежде чем выбрать из 3. Не имея возможности сделать это, мы должны сначала вычислить код Лемера, накопить его во временный массив, а затем примените его к массиву элементов для вычисления фактической перестановки.

Но если мы не заботимся о лексикографическом порядке, мы можем притвориться, что мы хотим выбрать из 3 элементов, прежде чем выбрать из 4. Затем выбор из 4 выйдет из unpack первым. Другими словами, мы будем использовать unpack(n, [3, 4]) вместо unpack(n, [4, 3]). Этот трюк позволяет вычислить следующую цифру кода Лемера и немедленно применить ее к списку. И именно так работает nth_permutation().

Последнее, что я хочу упомянуть, это то, что unpack(i, [4, 3]) тесно связано с системой факторных чисел. Посмотрите на это первое дерево еще раз, если мы хотим перестановки длины 2 без дубликатов, мы можем просто пропустить каждый второй индекс перестановки. Это даст нам 12 перестановок длины 4, которые можно обрезать до длины 2.

for (var i = 0; i < 12; i++) {
    var lehmer = unpack(i * 2, [4, 3, 2, 1]); // Factorial number system
    console.log(lehmer.slice(0, 2));
}
15 голосов
/ 27 октября 2011

Это зависит от того, как вы «сортируете» свои перестановки (например, лексикографический порядок).

Один из способов сделать это - система факторных чисел , она дает вам биекцию между [0, n!] И всеми перестановками.

Тогда для любого числа i в [0, n!] Вы можете вычислить i-ю перестановку, не вычисляя другие.

Эта факторная запись основана на том факте, что любое число от [0 до n!] Может быть записано как:

SUM( ai.(i!) for i in range [0,n-1]) where ai <i 

(это очень похоже на разложение базы)

для получения дополнительной информации об этой декомпозиции, посмотрите на эту ветку: https://math.stackexchange.com/questions/53262/factorial-decomposition-of-integers

надеюсь, это поможет


Как указано в этой статье в википедии , этот подход эквивалентен вычислению лемерного кода :

Очевидный способ генерировать перестановки n - генерировать значения для код Лемера (возможно, с использованием системы факторных чисел представление целых чисел до n!), и преобразовать их в соответствующие перестановки. Однако последний шаг, в то время как просто, сложно реализовать эффективно, потому что это требует n операций по выбору из последовательности и удалению из нее, в произвольном положении; из очевидных представлений последовательность как массив или связанный список, оба требуют (для разных причины) о n2 / 4 операциях по выполнению преобразования. С п скорее всего, будет довольно маленьким (особенно если поколение всех перестановки необходимы) это не слишком большая проблема, но это Оказывается, что как для случайной, так и для систематической генерации существуют простые альтернативы, которые значительно лучше. По этой причине это не представляется полезным, хотя, безусловно, возможно, использовать специальные структура данных, которая позволила бы выполнить преобразование из Лемера код перестановки за O (n log n) времени.

Таким образом, лучшее, что вы можете сделать для набора из n элементов, - это O (n ln (n)) с адаптированной структурой данных.

7 голосов
/ 05 октября 2014

Вот алгоритм для преобразования между перестановками и рангами в линейном времени.Однако используемый им рейтинг не является лексикографическим.Это странно, но последовательно.Я собираюсь дать две функции, одну, которая преобразует из ранга в перестановку, и другую, которая делает обратное.

Во-первых, чтобы отменить (перейти от ранга к перестановке)

Initialize:
n = length(permutation)
r = desired rank
p = identity permutation of n elements [0, 1, ..., n]

unrank(n, r, p)
  if n > 0 then
    swap(p[n-1], p[r mod n])
    unrank(n-1, floor(r/n), p)
  fi
end

Далее, чтобы оценить:

Initialize:
p = input permutation
q = inverse input permutation (in linear time, q[p[i]] = i for 0 <= i < n)
n = length(p)

rank(n, p, q)
  if n=1 then return 0 fi
  s = p[n-1]
  swap(p[n-1], p[q[n-1]])
  swap(q[s], q[n-1])
  return s + n * rank(n-1, p, q)
end

Время выполнения обоих из них - O (n).

Есть хорошая, удобочитаемая статья, объясняющая, почему это работает: ранжирование и расстановка вариантовв линейное время, Myrvold & Ruskey, Письма для обработки информации, том 79, выпуск 6, 30 сентября 2001 года, страницы 281–284.

http://webhome.cs.uvic.ca/~ruskey/Publications/RankPerm/MyrvoldRuskey.pdf

5 голосов
/ 22 августа 2014

Вот короткое и очень быстрое (линейное по количеству элементов) решение в python, работающее для любого списка элементов (13 первых букв в приведенном ниже примере):

from math import factorial

def nthPerm(n,elems):#with n from 0
    if(len(elems) == 1):
        return elems[0]
    sizeGroup = factorial(len(elems)-1)
    q,r = divmod(n,sizeGroup)
    v = elems[q]
    elems.remove(v)
    return v + ", " + ithPerm(r,elems)

Примеры:

letters = ['a','b','c','d','e','f','g','h','i','j','k','l','m']

ithPerm(0,letters[:])          #--> a, b, c, d, e, f, g, h, i, j, k, l, m
ithPerm(4,letters[:])          #--> a, b, c, d, e, f, g, h, i, j, m, k, l
ithPerm(3587542868,letters[:]) #--> h, f, l, i, c, k, a, e, g, m, d, b, j

Примечание: я даю letters[:] (копия letters), а не буквы, потому что функция изменяет свой параметр elems (удаляет выбранный элемент)

2 голосов
/ 22 октября 2018

Следующий код вычисляет k-ю перестановку для данного n.

, т. Е. N = 3.Различные перестановки: 123 132 213 231 312 321

Если k = 5, вернуть 312. Другими словами, это дает k-ю лексикографическую перестановку.

    public static String getPermutation(int n, int k) {
        char temp[] = IntStream.range(1, n + 1).mapToObj(i -> "" + i).collect(Collectors.joining()).toCharArray();
        return getPermutationUTIL(temp, k, 0);
    }

    private static String getPermutationUTIL(char temp[], int k, int start) {
        if (k == 1)
            return new String(temp);
        int p = factorial(temp.length - start - 1);
        int q = (int) Math.floor(k / p);
        if (k % p == 0)
            q = q - 1;
        if (p <= k) {
            char a = temp[start + q];
            for (int j = start + q; j > start; j--)
                temp[j] = temp[j - 1];
            temp[start] = a;
        }
        return k - p >= 0 ? getPermutationUTIL(temp, k - (q * p), start + 1) : getPermutationUTIL(temp, k, start + 1);
    }

    private static void swap(char[] arr, int j, int i) {
        char temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    private static int factorial(int n) {
        return n == 0 ? 1 : (n * factorial(n - 1));
    }
0 голосов
/ 30 мая 2018

Это рассчитывается.Это код C #, который делает это за вас.

using System;
using System.Collections.Generic;

namespace WpfPermutations
{
    public class PermutationOuelletLexico3<T>
    {
        // ************************************************************************
        private T[] _sortedValues;

        private bool[] _valueUsed;

        public readonly long MaxIndex; // long to support 20! or less 

        // ************************************************************************
        public PermutationOuelletLexico3(T[] sortedValues)
        {
            if (sortedValues.Length <= 0)
            {
                throw new ArgumentException("sortedValues.Lenght should be greater than 0");
            }

            _sortedValues = sortedValues;
            Result = new T[_sortedValues.Length];
            _valueUsed = new bool[_sortedValues.Length];

            MaxIndex = Factorial.GetFactorial(_sortedValues.Length);
        }

        // ************************************************************************
        public T[] Result { get; private set; }

        // ************************************************************************
        /// <summary>
        /// Return the permutation relative to the index received, according to 
        /// _sortedValues.
        /// Sort Index is 0 based and should be less than MaxIndex. Otherwise you get an exception.
        /// </summary>
        /// <param name="sortIndex"></param>
        /// <param name="result">Value is not used as inpu, only as output. Re-use buffer in order to save memory</param>
        /// <returns></returns>
        public void GetValuesForIndex(long sortIndex)
        {
            int size = _sortedValues.Length;

            if (sortIndex < 0)
            {
                throw new ArgumentException("sortIndex should be greater or equal to 0.");
            }

            if (sortIndex >= MaxIndex)
            {
                throw new ArgumentException("sortIndex should be less than factorial(the lenght of items)");
            }

            for (int n = 0; n < _valueUsed.Length; n++)
            {
                _valueUsed[n] = false;
            }

            long factorielLower = MaxIndex;

            for (int index = 0; index < size; index++)
            {
                long factorielBigger = factorielLower;
                factorielLower = Factorial.GetFactorial(size - index - 1);  //  factorielBigger / inverseIndex;

                int resultItemIndex = (int)(sortIndex % factorielBigger / factorielLower);

                int correctedResultItemIndex = 0;
                for(;;)
                {
                    if (! _valueUsed[correctedResultItemIndex])
                    {
                        resultItemIndex--;
                        if (resultItemIndex < 0)
                        {
                            break;
                        }
                    }
                    correctedResultItemIndex++;
                }

                Result[index] = _sortedValues[correctedResultItemIndex];
                _valueUsed[correctedResultItemIndex] = true;
            }
        }

        // ************************************************************************
        /// <summary>
        /// Calc the index, relative to _sortedValues, of the permutation received
        /// as argument. Returned index is 0 based.
        /// </summary>
        /// <param name="values"></param>
        /// <returns></returns>
        public long GetIndexOfValues(T[] values)
        {
            int size = _sortedValues.Length;
            long valuesIndex = 0;

            List<T> valuesLeft = new List<T>(_sortedValues);

            for (int index = 0; index < size; index++)
            {
                long indexFactorial = Factorial.GetFactorial(size - 1 - index);

                T value = values[index];
                int indexCorrected = valuesLeft.IndexOf(value);
                valuesIndex = valuesIndex + (indexCorrected * indexFactorial);
                valuesLeft.Remove(value);
            }
            return valuesIndex;
        }

        // ************************************************************************
    }
}
0 голосов
/ 27 октября 2011

Если вы храните все перестановки в памяти, например, в массиве, вы должны иметь возможность выводить их обратно по одной за O (1) раз.

Это означает, что вам нужно хранить все перестановки, поэтому, если вычисление всех перестановок занимает слишком много времени или их хранение занимает слишком много места, это может не быть решением.

Мое предложение было бы в любом случае попробовать и вернуться, если оно слишком большое / медленное - нет смысла искать «умное» решение, если наивный будет делать эту работу.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...