C++中的排列组合算法详解
引言:排列组合的重要性
排列组合是计算机科学中的基础算法,广泛应用于密码学、数据分析、游戏开发等领域。在C++中,我们通常使用深度优先搜索(DFS)和回溯法来实现排列组合算法。本文将详细解析全排列、组合及其变种的实现原理和代码。
一、全排列算法
1.基本概念:
全排列是指从n个不同元素中取出n个元素,按照一定的顺序排列的所有可能情况。例如,元素{1,2,3}的全排列有6种:123、132、213、231、312、321。
2.算法实现:
#include <iostream> using namespace std; const int N = 101; // 数据范围 int a[N]; // 存储待排列的数 int show[N]; // 存储已排列的数 int n; // 数的个数 bool check[N]; // 标记数字是否已使用void dfs(int showid) {if (showid == n + 1) { // 完成一种排列for (int i = 1; i <= n; i++)cout << show[i] << " ";cout << endl;return;}for (int i = 1; i <= n; i++) {if (!check[i]) { // 选择未使用的数字check[i] = true; // 标记为已使用show[showid] = a[i]; // 放入当前位置dfs(showid + 1); // 递归处理下一个位置check[i] = false; // 回溯,取消标记}} }int main() {cin >> n;for (int i = 1; i <= n; i++)cin >> a[i];dfs(1); // 从第一个位置开始return 0; }
3.算法解析:
-
DFS框架:使用递归实现深度优先搜索
-
标记数组:
check[]
记录数字使用状态 -
回溯机制:递归返回后撤销选择
-
时间复杂度:O(n!) - 阶乘复杂度
二、有重复数字的全排列
1.问题特点:
当输入数组包含重复数字时,基本全排列算法会产生重复结果。例如,输入{1,1,2}会产生3种排列而不是2种。
2.优化实现:
#include <iostream> #include <algorithm> using namespace std; const int N = 101; int a[N]; int show[N]; int n; bool check[N];void dfs(int showid) {if (showid == n + 1) {for (int i = 1; i <= n; i++)cout << show[i] << " ";cout << endl;return;}for (int i = 1; i <= n; i++) {// 关键去重逻辑if (i > 1 && a[i] == a[i - 1] && !check[i - 1])continue;if (!check[i]) {check[i] = true;show[showid] = a[i];dfs(showid + 1);check[i] = false;}} }int main() {cin >> n;for (int i = 1; i <= n; i++)cin >> a[i];sort(a + 1, a + 1 + n); // 必须先排序dfs(1);return 0; }
3.去重关键:
-
排序预处理:确保相同元素相邻
-
跳过条件:
if (i>1 && a[i]==a[i-1] && !check[i-1])
-
当前元素与前一个相同
-
前一个元素未被使用(说明是新层级)
-
-
时间复杂度:最坏情况仍为O(n!)
三、组合算法
1.基本概念:
组合是指从n个不同元素中取出r个元素,不考虑顺序。例如,从{1,2,3,4}中取2个元素的组合有6种:{1,2}、{1,3}、{1,4}、{2,3}、{2,4}、{3,4}。
2.算法实现:
#include <iostream> #include <algorithm> using namespace std; const int N = 101; int a[N]; int show[N]; bool check[N]; int n, r; // n个数中取r个void dfs(int showid) {if (showid == r + 1) { // 选够r个数for (int i = 1; i <= r; i++)cout << show[i] << " ";cout << endl;return;}for (int i = 1; i <= n; i++) {// 避免重复组合的关键条件if (!check[i] && a[i] > show[showid - 1]) {check[i] = true;show[showid] = a[i];dfs(showid + 1);check[i] = false;}} }int main() {cin >> n >> r;for (int i = 1; i <= n; i++)cin >> a[i];sort(a + 1, a + 1 + n); // 排序确保升序show[0] = -1; // 初始值小于所有可能数dfs(1);return 0; }
3.组合关键点:
-
避免重复:只选择比前一个元素大的数
-
排序预处理:确保数组有序
-
初始值设置:
show[0] = -1
作为哨兵值 -
时间复杂度:O(C(n,r)) - 组合数复杂度
四、带重复数字的组合
1.问题特点:
当输入包含重复数字时,需要避免生成重复的组合结果。
2.优化实现:
#include <iostream> #include <algorithm> using namespace std; const int N = 101; int a[N]; int show[N]; bool check[N]; int n, r;void dfs(int showid) {if (showid == r + 1) {for (int i = 1; i <= r; i++)cout << show[i] << " ";cout << endl;return;}for (int i = 1; i <= n; i++) {// 去重逻辑if (i > 1 && a[i] == a[i - 1] && !check[i - 1])continue;// 组合条件if (!check[i] && a[i] >= show[showid - 1]) {check[i] = true;show[showid] = a[i];dfs(showid + 1);check[i] = false;}} }int main() {cin >> n >> r;for (int i = 1; i <= n; i++)cin >> a[i];sort(a + 1, a + 1 + n);show[0] = -1; // 初始哨兵值dfs(1);return 0; }
3.去重关键:
-
排序预处理:确保相同元素相邻
-
跳过条件:
if (i>1 && a[i]==a[i-1] && !check[i-1])
-
组合条件:
a[i] >= show[showid-1]
(允许相等)
五、组合的高效实现
1.优化思路:
使用起始索引避免重复选择和标记数组
#include<iostream> #define N 30 using namespace std; int n, r; int a[N]; // 存储当前组合 int vis[N]; // 标记数组void dfs(int step) {if (step == r + 1) { // 完成组合for (int i = 1; i <= r; i++)cout << a[i] << " ";cout << endl;return;}// 从上一个数开始选择for (int i = a[step - 1]; i <= n; i++) {if (!vis[i]) {a[step] = i;vis[i] = 1;dfs(step + 1);vis[i] = 0;}} }int main() {cin >> n >> r;a[0] = 1; // 初始化起始值dfs(1);return 0; }
2.算法优势:
-
避免重复:通过
i = a[step-1]
确保顺序选择 -
减少循环:不需要遍历所有数字
-
空间优化:不需要存储原始数组
六、排列组合算法对比
算法类型 | 特点 | 时间复杂度 | 空间复杂度 | 关键技巧 |
---|---|---|---|---|
全排列 | 所有元素排列 | O(n!) | O(n) | 标记数组、回溯 |
有重复全排列 | 处理重复元素 | O(n!) | O(n) | 排序、跳过条件 |
组合 | 选择r个元素 | O(C(n,r)) | O(n) | 排序、升序选择 |
有重复组合 | 处理重复元素 | O(C(n,r)) | O(n) | 排序、双重条件 |
高效组合 | 优化选择过程 | O(C(n,r)) | O(n) | 起始索引 |