全排列算法(Full Permutation)
定义
给定无重复元素序列 ,即满足:
从中任取 ()个元素并按顺序排成一列,得到的所有结果称为排列(Permutation / Arrangement)。当 时,排列数为
当 时称为全排列(Full Permutation),排列数为
全排列的实现可分为递归与非递归两类。下文示例默认序列元素为 的整数;换成字符等其它可比较类型亦可。
递归实现
1. 交换法
由 :将 中每个元素依次固定到某一位置(通常是当前位置 ),再对剩余 个元素求全排列,直到只剩一个元素。
以 为例,先固定首位:
| 交换 | 结果 |
|---|---|
再对每个结果的后两位继续交换,最终得到全部 种排列。
步骤:
- 固定位索引 ;
- 对所有 ,交换 与 ,再递归处理 ;
- 递归返回后交换回来(回溯);
- 当 时输出一种排列。
#include <iostream>using namespace std;
int n;
void swap(int *a, int *b) { *a ^= *b; *b ^= *a; *a ^= *b;}
void permutation(int array[], int k) { if (k == n - 1) { for (int i = 0; i < n; i++) { cout << array[i] << ' '; } cout << endl; } else { for (int i = k; i < n; i++) { swap(&array[i], &array[k]); permutation(array, k + 1); swap(&array[i], &array[k]); } }}
int main() { cin >> n; int a[n]; for (int i = 0; i < n; ++i) { a[i] = i + 1; } permutation(a, 0); return 0;}注意 即便输入已是字典序,交换法得到的输出顺序一般也不是字典序。
复杂度: 递归会到达 个叶结点;若每次输出花费 ,生成全部全排列的时间为 。递归深度为 ,除输入数组外额外栈空间为 。
2. 递归生成树法
另一视角:。维护一个栈:每次从剩余元素中取一个入栈,递归;返回后出栈并把元素放回。对序列 {1,2,3}$ 可画出递归生成树,根到叶的每条路径对应一种排列:
用 vector 容器同时表示剩余序列与结果栈,即可实现递归生成树法:
#include <iostream>#include <vector>using namespace std;
vector<int> res;vector<int> a;int n;
void permutation(int k) { if (k == 0) { for (int i = 0; i < n; ++i) { cout << res[i] << ' '; } cout << endl; } else { for (int i = 0; i < k; ++i) { res.push_back(a[i]); a.erase(a.begin() + i); permutation(k - 1); a.insert(a.begin() + i, res.back()); res.pop_back(); } }}
int main() { cin >> n; for (int i = 0; i < n; ++i) { a.push_back(i + 1); } permutation(n); return 0;}vector 的 erase / insert 会使后续元素整体移动,常数较大;可用链表降低移动开销,但实现更繁琐。生成全部排列并输出时,时间仍至少是 量级,额外空间 。
非递归实现
3. 穷举法
穷举法的思路与递归生成树类似:用 层嵌套循环依次选位置,并用条件排除重复。但其缺陷也很明显:代码的循环层数绑死 ,一般只适用于 固定的情况。
时示意代码如下:
#include <iostream>using namespace std;
int n = 5;
int main() { int a[n]; for (int i = 0; i < n; ++i) { a[i] = i + 1; } for (int i1 = 0; i1 < n; ++i1) { for (int i2 = 0; i2 < n; ++i2) { if (a[i1] == a[i2]) continue; for (int i3 = 0; i3 < n; ++i3) { if (a[i1] == a[i3] || a[i2] == a[i3]) continue; for (int i4 = 0; i4 < n; ++i4) { if (a[i1] == a[i4] || a[i2] == a[i4] || a[i3] == a[i4]) continue; for (int i5 = 0; i5 < n; ++i5) { if (a[i1] == a[i5] || a[i2] == a[i5] || a[i3] == a[i5] || a[i4] == a[i5]) continue; cout << a[i1] << ' ' << a[i2] << ' ' << a[i3] << ' ' << a[i4] << ' ' << a[i5] << endl; } } } } } return 0;}若输入无重复且按字典序给出,输出也会按字典序。嵌套循环的搜索规模约为 量级,额外空间 。
4. 字典序法(next_permutation / prev_permutation)
种全排列可排成字典序序列 :
- :原序列的字典序最小排列;
- :反字典序(字典序最大);
- 可由 通过求下一个排列得到。
例如 :,,…,。
C++ <algorithm> 提供 next_permutation / prev_permutation。常见用法:
- 先
sort成升序,再循环next_permutation直到返回false; - 或先变成降序,再循环
prev_permutation; - 也可从任意排列双向扫完所有排列。
#include <iostream>#include <algorithm>#include <string>using namespace std;
void print(const string &s) { for (char c : s) { cout << c << ' '; } cout << endl;}
void LFP(string str) { // Lexicographical Full Permutation sort(str.begin(), str.end()); do { print(str); } while (next_permutation(str.begin(), str.end()));}
void RLFP(string str) { // Reverse Lexicographical Full Permutation sort(str.begin(), str.end()); reverse(str.begin(), str.end()); do { print(str); } while (prev_permutation(str.begin(), str.end()));}
int main() { string str; cin >> str; LFP(str); RLFP(str); return 0;}next_permutation 步骤(有重复元素时用 / 亦可):
- 从右往左找最大下标 ,使 (找不到则已是最后一项);
- 从右往左找最大下标 ,使 ;
- 交换 与 ;
- 将区间 反转,得到下一个字典序排列。
prev_permutation 将上述比较方向反过来即可。对应实现如下:
#include <iostream>using namespace std;
void swap(int *a, int *b) { *a ^= *b; *b ^= *a; *a ^= *b;}
int main() { int n; cin >> n; int a[n]; for (int i = 0; i < n; ++i) { a[i] = i + 1; } while (true) { for (int i = 0; i < n; i++) { cout << a[i] << ' '; } cout << endl;
int k = n - 2; int j = n - 1; // prev_permutation:把 >= 改成 <= for (; k >= 0 && a[k] >= a[k + 1]; --k) {} if (k == -1) { break; } for (; j > k && a[k] >= a[j]; --j) {} swap(&a[j], &a[k]); for (int i = 0; i < (n - 1 - k) / 2; ++i) { swap(&a[k + 1 + i], &a[n - 1 - i]); } } return 0;}单次 next_permutation 为 ,枚举全部为 ,额外空间 (原地)。
5. Steinhaus–Johnson–Trotter(SJT)算法
SJT 又称最小改动排列生成算法,其通过相邻交换依次得到下一个排列:为每个元素维护移动方向;每次找出可合法移动的最大元素,与其方向上的相邻元素交换,并翻转所有更大元素的方向,直到没有元素可移动。其具体实现如下:
#include <iostream>using namespace std;
void swap(int &a, int &b) { a ^= b; b ^= a; a ^= b;}
void print(int a[], int n) { for (int i = 0; i < n; ++i) { cout << a[i] << ' '; } cout << endl;}
int main() { int n; cin >> n; int a[n], d[n]; for (int i = 0; i < n; ++i) { a[i] = i + 1; d[i] = -1; // -1 向左,+1 向右 } bool flag = true; while (flag) { flag = false; print(a, n); int mx = 0; int max_index = 0; for (int i = 0; i < n; ++i) { if ((d[i] < 0 && i > 0 && a[i] > a[i - 1]) || (d[i] > 0 && i < n - 1 && a[i] > a[i + 1])) { if (a[i] > mx) { mx = a[i]; max_index = i; flag = true; } } } if (!flag) { break; } swap(a[max_index], a[max_index + d[max_index]]); swap(d[max_index], d[max_index + d[max_index]]); for (int i = 0; i < n; ++i) { if (a[i] > mx) { d[i] = -d[i]; } } } return 0;}初始为字典序时,SJT 的生成顺序通常也不是字典序。时间 ,空间 (含方向数组)。
应用:填数问题
全排列本质是穷举。若必须枚举全部排列,时间至少 ,只适合规模较小的问题。在解题经验上,时限约 、 量级时, 是一般而言可行的规模上限;若剪枝得当有时 可到 或 。
例题: 2016 年第七届蓝桥杯省赛 C 语言 A 组第 3 题——在如下十格填入 –(各用一次),要求任意两个连续整数(差的绝对值为 )所在格不能相邻(含斜向)。
该问题本质是图上的约束计数,一般用 DFS 来解决。但考虑到本题的规模,也可先求全排列,再通过特殊的判定条件筛选出满足条件的排列。下面用字典序法枚举所有排列并逐个判断是否满足条件。
#include <iostream>#define n 10#define abs(a) ((a) > 0 ? (a) : -(a))using namespace std;
void swap(int *a, int *b) { *a ^= *b; *b ^= *a; *a ^= *b;}
int main() { int *a = new int[n]; for (int i = 0; i < n; ++i) { a[i] = i; } int count = 0; while (true) { if (abs(a[0] - a[1]) != 1 && abs(a[1] - a[2]) != 1 && abs(a[3] - a[4]) != 1 && abs(a[4] - a[5]) != 1 && abs(a[5] - a[6]) != 1 && abs(a[7] - a[8]) != 1 && abs(a[8] - a[9]) != 1 && abs(a[0] - a[4]) != 1 && abs(a[1] - a[5]) != 1 && abs(a[2] - a[6]) != 1 && abs(a[3] - a[7]) != 1 && abs(a[4] - a[8]) != 1 && abs(a[5] - a[9]) != 1 && abs(a[0] - a[5]) != 1 && abs(a[1] - a[6]) != 1 && abs(a[3] - a[8]) != 1 && abs(a[4] - a[9]) != 1 && abs(a[0] - a[3]) != 1 && abs(a[1] - a[4]) != 1 && abs(a[2] - a[5]) != 1 && abs(a[4] - a[7]) != 1 && abs(a[5] - a[8]) != 1 && abs(a[6] - a[9]) != 1) { count++; } int k = n - 2, j = n - 1; for (; k >= 0 && a[k] > a[k + 1]; --k) {} if (k == -1) { break; } for (; j > k && a[k] > a[j]; --j) {} swap(&a[j], &a[k]); for (int i = 0; i < (n - 1 - k) / 2; ++i) { swap(&a[k + 1 + i], &a[n - 1 - i]); } } cout << count << endl; delete[] a; return 0;}答案为 1580。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!


