Что такое перестановка?
Перестановка — это способ расставить все n элементов в некотором порядке.
нужно расположить их в каком-то порядке
Например, для чисел:
1 2 3
Перестановки:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
Сколько всего перестановок?
Если все элементы разные, то количество перестановок равно:
n!
Факториал:
n! = 1 * 2 * 3 * ... * n
Например:
3! = 1 * 2 * 3 = 6
Поэтому у чисел 1, 2, 3 есть 6 перестановок.
Почему n!?
Для первой позиции можно выбрать любой из n элементов.
Для второй позиции остается n - 1 элементов. Для третьей — n - 2.
n - 1 вариантов для второй
n - 2 вариантов для третьей
...
1 вариант для последней
Поэтому:
n * (n - 1) * (n - 2) * ... * 1 = n!
Перестановки, сочетания, размещения
| Тип | Что значит | Порядок важен? |
|---|---|---|
| Перестановки | Расставить все n элементов | Да |
| Сочетания | Выбрать k из n | Нет |
| Размещения | Выбрать k из n и расставить | Да |
Задача генерации
Дано число n. Нужно вывести все перестановки чисел от 1 до n.
Input
3
Output
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
Идея генерации через рекурсию
Будем строить перестановку слева направо.
На каждой позиции выбираем число, которое еще не использовали.
used[x] — использовали ли число x
если cur.size() == n, выводим cur
Код: генерация перестановок
#include <bits/stdc++.h>
using namespace std;
int n;
vector<int> cur;
vector<int> used;
void gen() {
if ((int)cur.size() == n) {
for (int x : cur) {
cout << x << ' ';
}
cout << '\n';
return;
}
for (int x = 1; x <= n; x++) {
if (used[x]) {
continue;
}
used[x] = 1;
cur.push_back(x);
gen();
cur.pop_back();
used[x] = 0;
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
used.assign(n + 1, 0);
gen();
return 0;
}
Как работает рекурсия?
Для n = 3:
потом второе
потом третье
когда длина стала 3, выводим перестановку
Например:
cur = []
берем 1 → cur = [1]
берем 2 → cur = [1, 2]
берем 3 → cur = [1, 2, 3]
выводим 1 2 3
Потом делаем откат:
убираем 3
убираем 2
пробуем другое число
Что такое backtracking?
Backtracking — это метод, где мы:
2. идем глубже рекурсией
3. откатываем выбор
4. пробуем следующий вариант
В коде это выглядит так:
used[x] = 1;
cur.push_back(x);
gen();
cur.pop_back();
used[x] = 0;
Генерация через next_permutation
В C++ есть готовая функция:
next_permutation
Она переставляет массив в следующую лексикографическую перестановку.
Нужно начать с отсортированного массива:
1 2 3 ... n
Код: next_permutation
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<int> a(n);
for (int i = 0; i < n; i++) {
a[i] = i + 1;
}
do {
for (int x : a) {
cout << x << ' ';
}
cout << '\n';
} while (next_permutation(a.begin(), a.end()));
return 0;
}
Лексикографический порядок
Лексикографический порядок — это порядок как в словаре.
Для перестановок чисел:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
Это именно тот порядок, который дает next_permutation, если начать с отсортированного массива.
prev_permutation
Есть и обратная функция:
prev_permutation
Она строит предыдущую перестановку. Для нее нужно начать с убывающего массива.
vector<int> a = {3, 2, 1};
do {
for (int x : a) {
cout << x << ' ';
}
cout << '\n';
} while (prev_permutation(a.begin(), a.end()));
Перестановки строки
Перестановки можно генерировать не только для чисел, но и для строки.
string s;
cin >> s;
sort(s.begin(), s.end());
do {
cout << s << '\n';
} while (next_permutation(s.begin(), s.end()));
Например:
abc
Даст:
abc
acb
bac
bca
cab
cba
Если есть одинаковые элементы
Если в массиве есть одинаковые элементы, то next_permutation не будет выводить одинаковые перестановки повторно.
Например, строка:
aab
Перестановки:
aab
aba
baa
Но важно сначала отсортировать:
sort(s.begin(), s.end());
Код: уникальные перестановки строки
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
string s;
cin >> s;
sort(s.begin(), s.end());
do {
cout << s << '\n';
} while (next_permutation(s.begin(), s.end()));
return 0;
}
Количество перестановок с повторами
Если элементы повторяются, количество уникальных перестановок меньше.
Формула:
n! / (c1! * c2! * ... * ck!)
Где c1, c2, ... — количества одинаковых элементов.
Например:
aab
Здесь:
n = 3
a встречается 2 раза
b встречается 1 раз
Количество:
3! / 2! = 3
Номер перестановки
Иногда нужно найти номер перестановки в лексикографическом порядке.
Например:
n = 3
1: 1 2 3
2: 1 3 2
3: 2 1 3
4: 2 3 1
5: 3 1 2
6: 3 2 1
Перестановка 2 3 1 имеет номер 4.
Идея номера перестановки
Смотрим слева направо. На каждой позиции считаем, сколько меньших неиспользованных чисел можно поставить.
Если на позиции можно поставить меньшее число, то все перестановки с этим меньшим числом идут раньше.
Код: номер перестановки
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<int> p(n);
for (int i = 0; i < n; i++) {
cin >> p[i];
}
vector<long long> fact(n + 1, 1);
for (int i = 1; i <= n; i++) {
fact[i] = fact[i - 1] * i;
}
vector<int> used(n + 1, 0);
long long rank = 1;
for (int i = 0; i < n; i++) {
int cnt = 0;
for (int x = 1; x < p[i]; x++) {
if (!used[x]) {
cnt++;
}
}
rank += cnt * fact[n - i - 1];
used[p[i]] = 1;
}
cout << rank;
return 0;
}
Перестановка по номеру
Обратная задача: дано n и номер k. Нужно найти k-ю перестановку в лексикографическом порядке.
Идея:
по k понимаем, какой элемент поставить первым
потом переходим к следующей позиции
Код: перестановка по номеру
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
long long k;
cin >> n >> k;
vector<long long> fact(n + 1, 1);
for (int i = 1; i <= n; i++) {
fact[i] = fact[i - 1] * i;
}
vector<int> nums;
for (int i = 1; i <= n; i++) {
nums.push_back(i);
}
k--;
vector<int> ans;
for (int pos = 0; pos < n; pos++) {
long long block = fact[n - pos - 1];
long long id = k / block;
ans.push_back(nums[id]);
nums.erase(nums.begin() + id);
k %= block;
}
for (int x : ans) {
cout << x << ' ';
}
return 0;
}
Пример перестановки по номеру
Input
3 4
Output
2 3 1
Потому что:
1: 1 2 3
2: 1 3 2
3: 2 1 3
4: 2 3 1
5: 3 1 2
6: 3 2 1
Проверка, является ли массив перестановкой
Иногда нужно проверить, что массив длины n является перестановкой чисел от 1 до n.
То есть каждое число от 1 до n встречается ровно один раз.
vector<int> cnt(n + 1, 0);
for (int x : a) {
if (x < 1 || x > n) {
cout << "NO";
return 0;
}
cnt[x]++;
}
for (int x = 1; x <= n; x++) {
if (cnt[x] != 1) {
cout << "NO";
return 0;
}
}
cout << "YES";
Полный код проверки
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<int> a(n);
vector<int> cnt(n + 1, 0);
for (int i = 0; i < n; i++) {
cin >> a[i];
if (a[i] < 1 || a[i] > n) {
cout << "NO";
return 0;
}
cnt[a[i]]++;
}
for (int x = 1; x <= n; x++) {
if (cnt[x] != 1) {
cout << "NO";
return 0;
}
}
cout << "YES";
return 0;
}
Циклы в перестановке
Перестановку можно рассматривать как отображение:
i → p[i]
Тогда элементы разбиваются на циклы.
Например:
p = [2, 3, 1, 5, 4]
Циклы:
1 → 2 → 3 → 1
4 → 5 → 4
Код: разложение на циклы
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<int> p(n + 1);
for (int i = 1; i <= n; i++) {
cin >> p[i];
}
vector<int> used(n + 1, 0);
for (int i = 1; i <= n; i++) {
if (!used[i]) {
int v = i;
while (!used[v]) {
used[v] = 1;
cout << v << ' ';
v = p[v];
}
cout << '\n';
}
}
return 0;
}
Асимптотика
| Метод | Время | Память |
|---|---|---|
| Генерация всех перестановок | O(n! * n) | O(n) |
| next_permutation | O(n! * n) | O(n) |
| Номер перестановки | O(n²) | O(n) |
| Перестановка по номеру | O(n²) | O(n) |
| Проверка перестановки | O(n) | O(n) |
| Разложение на циклы | O(n) | O(n) |
Когда какой способ использовать?
| Задача | Способ |
|---|---|
| Вывести все перестановки | Рекурсия или next_permutation |
| Нужен лексикографический порядок | next_permutation |
| Нужно генерировать вручную | Backtracking |
| Найти номер перестановки | Факториалы и used |
| Найти перестановку по номеру | Факториальная система |
| Проверить массив | cnt или used |
Типичные ошибки
- Забыть, что порядок в перестановках важен
- Не сделать used[x] = 0 после рекурсии
- Не сделать cur.pop_back() после рекурсии
- Запустить next_permutation не с отсортированного массива
- Получить overflow в факториалах
- Перепутать 0-indexed и 1-indexed номер перестановки
- Думать, что генерация перестановок работает быстро для больших n
- Не учитывать одинаковые элементы в строке
Короткий шаблон генерации
int n;
vector<int> cur;
vector<int> used;
void gen() {
if ((int)cur.size() == n) {
for (int x : cur) {
cout << x << ' ';
}
cout << '\n';
return;
}
for (int x = 1; x <= n; x++) {
if (used[x]) {
continue;
}
used[x] = 1;
cur.push_back(x);
gen();
cur.pop_back();
used[x] = 0;
}
}
Короткий шаблон next_permutation
vector<int> a(n);
for (int i = 0; i < n; i++) {
a[i] = i + 1;
}
do {
for (int x : a) {
cout << x << ' ';
}
cout << '\n';
} while (next_permutation(a.begin(), a.end()));
Главное запомнить
порядок важен
количество перестановок = n!
генерировать можно рекурсией или next_permutation
для больших n все перестановки вывести невозможно