Щоб описати перестановку n елементів, ви бачите, що для позиції, на якій перший елемент закінчується, у вас є n можливостей, тому ви можете описати це числом між 0 і n-1. Для позиції, в якій наступний елемент закінчується, у вас є n-1 інших можливостей, тому ви можете описати це числом між 0 і n-2.
Et cetera, поки у вас не буде n чисел.
В якості прикладу для п = 5, розглянемо перестановку , яка приносить abcde
в caebd
.
a
, перший елемент закінчується на другій позиції, тому ми присвоюємо йому індекс 1 .
b
закінчується на четвертій позиції, яка була б індексом 3, але це третя залишилася, тому ми присвоюємо їй 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 до до повинно бути рівно 1 менше , ніж найменше число , яке може бути представлено тільки з допомогою цифр , до +1. У двійковій формі 0111 повинен бути на один менший ніж 1000. У десятковій частині 099999 має бути на один нижчий за 100000.
Кодування до змінної бази
Розміщення між наступними числами, що дорівнює рівно 1, є важливим правилом. Зрозумівши це, ми можемо представити нашу послідовність індексів числом змінної бази . Основою для кожної цифри є кількість різних можливостей для цієї цифри. Для десяткових знаків кожна цифра має 10 можливостей, для нашої системи найменша права цифра матиме 1 можливість, а найменша ліва матиме n можливостей. Але оскільки найправіша цифра (останнє число в нашій послідовності) завжди 0, ми залишаємо її поза. Це означає, що ми залишилися з основами 2 до n. Загалом, k'-я цифра матиме основу b [k] = k + 2. Найвище значення, дозволене для цифри k, - h [k] = b [k] - 1 = k + 1.
Наше правило щодо ваг w [k] цифр вимагає, щоб сума h [i] * w [i], де i переходить від i = 0 до i = k, дорівнювала 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'th (правий крайній край, починаючи з 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 є дійсними. Їх точно 120, що є n! для n = 5 у нашому прикладі, саме кількість різних перестановок. Таким чином, ви можете побачити наші закодовані номери повністю вказати всі можливі перестановки.
Розшифровка із змінної бази
Декодування схожа на перетворення у двійкове чи десяткове. Загальний алгоритм такий:
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;
}
Загальне представлення перестановок
Зазвичай ви б не представляли перестановку настільки інтуїтивно, як ми це робили, а просто абсолютним положенням кожного елемента після застосування перестановки. Наш приклад {1, 2, 0, 1, 0} for abcde
to caebd
зазвичай представлений {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²). Якщо вам потрібно застосувати перестановку кілька разів, спочатку перетворіть її у загальне представлення.