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


    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});
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    不难想到用数组表示序列A,而集合S根本不用保存,因为它可以由序列A完全确定——A中没有出现的元素都是可选的。C语言中的函数在接受数组参数时无法的值数组的元素个数,所以需要传一个已经填好的位置个数,或者当前需要确定的元素位置 cur

    代码

    /*************************************************************************
    	> File Name: 生成1~n的排列.cpp
    	> Author: Maureen 
    	> Mail: Maureen@qq.com 
    	> 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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    输入 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: Maureen@qq.com 
    	> 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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

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

    4、小结

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

  • 相关阅读:
    Route53 – Part 1
    第4章 文件管理
    大学生第一款浏览器怎么选,这款浏览器适合学生用
    自媒体视频剪辑中的那些素材到哪里找?
    Rollup failed to resolve import
    C# Winform编程(5)菜单栏和工具栏
    Java 数据结构与算法 堆
    ElasticSearch新增IK扩展词后,让历史数据生效方法
    【温度检测】基于matlab GUI热红外图像温度检测系统【含Matlab源码 1920期】
    力扣每日一道系列 --- LeetCode 160. 相交链表
  • 原文地址:https://blog.csdn.net/u011386173/article/details/134082583