排列生成算法:集合的全排列

1、生成 1 ~ n 的排列

思路

尝试用递归的思想解决:先输出所有以 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;
}

2、可重集的全排列(递归)

将问题修改成:输入数组 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;
}

3、可重集的全排列(next_permutation)

枚举所有排列的另一方法是从字典序最小排列开始,不停调用 “求下一个排列” 的过程。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;
}

该代码同样适用于可重集。

4、小结

枚举排列的常见方法有两种:一是递归枚举,二是用 STL 中的 next_permutation

你可能感兴趣的:(算法竞赛入门经典,算法,枚举排列,递归)