尝试用递归的思想解决:先输出所有以 1 开头的排列(这步是递归调用),然后输出以 2 开头的排列(又是递归调用),接着是以 3 开头的排列… 最后才是以 n n n 开头的排雷。
以 1 开头的排列特点:第一位是 1,后面是 2 ~ 9 的排列。根据字典序的定义,这些 2 ~ 9 的排列也必须按照字典序排列。换言之,需要“按照字典序输出2 ~ 9 的排列”,不过需要注意的是,在输出时,每个排列的最前面要加上“1”。如此一来,所设计的递归函数需要以下参数:
伪代码:
void print_permutation(序列A,集合S)
{
if (S为空)输出序列A;
else 按照从小到大的顺序依次考虑 S 的每个元素v
{
print_permutation(在A的末尾添加v后得到新序列,S-{v});
}
}
不难想到用数组表示序列A,而集合S根本不用保存,因为它可以由序列A完全确定——A中没有出现的元素都是可选的。C语言中的函数在接受数组参数时无法的值数组的元素个数,所以需要传一个已经填好的位置个数,或者当前需要确定的元素位置 cur
。
/*************************************************************************
> File Name: 生成1~n的排列.cpp
> Author: Maureen
> Mail: [email protected]
> Created Time: 五 10/27 19:21:28 2023
************************************************************************/
#include
using namespace std;
int A[101];
void print_permutation(int n, int *A, int cur) {
if (cur == n) { //递归边界
for (int i = 0; i < n; i++) {
printf("%d ", A[i]);
}
printf("\n");
} else {
for (int i = 1; i <= n; i++) { //尝试在A[cur]中填各种整数i
bool ok = true; //检查i是否被用过
for (int j = 0; j < cur; j++) {
if (A[j] == i) {
ok = false; //如果i已经在A[0]~A[cur-1]出现过,则不能再选
}
}
if (ok) {
A[cur] = i;
print_permutation(n, A, cur + 1); //递归调用
}
}
}
}
int main() {
int n;
scanf("%d", &n);
print_permutation(n, A, 0);
return 0;
}
将问题修改成:输入数组 P,并按字典序输出数组P各元素的所有排列。
只需要将 P 加入到 print_permutation
的参数列表,并将 if(A[j] == i)
修改为 if (A[j] == P[i])
,A[cur] = i
修改为 A[cur] = P[i]
。这样,只要将 P 的所有元素从小到大顺序排序,然后调用 print_permutation(n, P, A, 0)
即可。
方法不错,但是有一个小问题:输入 1 1 1
后,程序无输出,原因在于,程序禁止 A 数组中出现重复,而在 P 中本来就有重复元素时,对A数组的限制就是错误的。
一个解决方法是统计 A[0] ~ A[cur - 1] 中 P[i] 出现的次数 c1,以及 P 数组中 P[i] 的出现次数c2。只要 c1 < c2,就能递归调用。
else {
for (int i = 0; i < n; i++) {
int c1 = 0, c2 = 0;
for (int j = 0; j < cur; j++) {
if (A[j] == P[i])
c1++;
}
for (int j = 0; j < n; j++) {
if (P[j] == P[i]) {
c2++;
}
}
if (c1 < c2) {
A[cur] = P[i];
print_permutation(n, P, A, cur + 1);
}
}
}
输入 1 1 1
,结果输出了27个 1 1 1。没有遗漏,但是出现重复:先试着把第 1 个 1作为开头,递归调用结束后再尝试用第 2 个1 作为开头,递归调用结束后再尝试用第 3 个1 作为开头,再一次递归调用。可实际上这3个1是相同的,应只递归一次,而不是三次。
换言之,枚举的下标 i
应不重复、不遗漏地取遍所有 P[i] 值。由于 P 数组已经排好序,所以只需要检查 P 的第一个元素和所有 “与前一个元素不相同”的元素。
/*************************************************************************
> File Name: 可重集的全排列.cpp
> Author: Maureen
> Mail: [email protected]
> Created Time: 五 10/27 19:42:25 2023
************************************************************************/
#include
#include
using namespace std;
int P[101];
int A[101];
// 输出数组P中元素的全排列。数组P中可能有重复元素
void print_permutation(int n, int *P, int *A, int cur) {
if (cur == n) {
for (int i = 0; i < n; i++) {
printf("%d ", A[i]);
}
printf("\n");
} else {
for (int i = 0; i < n; i++) {
if (!i || P[i] != P[i - 1]) {
int c1 = 0, c2 = 0;
for (int j = 0; j < cur; j++) {
if (A[j] == P[i]) c1++;
}
for (int j = 0; j < n; j++) {
if (P[j] == P[i]) c2++;
}
if (c1 < c2) {
A[cur] = P[i];
print_permutation(n, P, A, cur + 1);
}
}
}
}
}
int main() {
int n;
while (scanf("%d", &n) == 1 && n) {
for (int i = 0; i < n; i++) scanf("%d", &P[i]);
sort(P, P + n);
print_permutation(n, P, A, 0);
}
return 0;
}
枚举所有排列的另一方法是从字典序最小排列开始,不停调用 “求下一个排列” 的过程。C++ 的 STL 中提供了一个库函数 next_permutation
。
#include
#include
using namespace std;
int main() {
int n, p[10];
scanf("%d", &n);
for(int i = 0; i < n; i++)
scanf("%d", &p[i]);
sort(p, p+n); // 排序,得到p的最小排列
do {
for(int i = 0; i < n; i++)
printf("%d ", p[i]); // 输出排列p
printf("\n");
} while(next_permutation(p, p+n)); // 求下一个排列
return 0;
}
该代码同样适用于可重集。
枚举排列的常见方法有两种:一是递归枚举,二是用 STL 中的 next_permutation
。