全组合算法(Full Combination)

1072 字
5 分钟
全组合算法(Full Combination)

定义#

从含 nn 个元素的集合 A={a1,,an}A = \{a_1, \cdots, a_n\} 中任取 mm0mn0 \leq m \leq n)个元素,组成的子集称为组合(Combination)。当 m<nm < n 时,组合数为

Cnm=(nm)=n!m!(nm)!.C_n^m = \binom{n}{m} = \frac{n!}{m!(n-m)!}.

给定集合 AAmm 时,所有可能的组合构成的集合称为全组合(Full Combination)。全组合的实现同样可分递归(回溯)非递归。下文默认集合元素为 0,,N10, \cdots, N-1(与代码一致);若要使用 1,,N1, \cdots, N,初始化时改为 a[i] = i + 1 即可。

递归实现#

1. 递归回溯法#

由组合恒等式出发

Cnm=Cn1m+Cn1m1C_n^m = C_{n-1}^m + C_{n-1}^{m-1}

可把从 nn 个元素中取 mm 个元素拆解成一系列与单一元素 aia_i 相关的两个子问题:

  1. 不选 aia_i:在剩余 n1n-1 个中取 mmCn1m\Rightarrow C_{n-1}^m
  2. aia_i:在剩余 n1n-1 个中再取 m1m-1Cn1m1\Rightarrow C_{n-1}^{m-1}

递归边界:已取满 mm 个则输出;下标越界且未取满则失败返回。实现上是回溯:先试探选中当前元素,再试探不选;对应递归调用中,先递归调用不选,再递归调用选。

#include <iostream>
#include <vector>
using namespace std;
vector<int> res;
int N, M;
int *a;
void print() {
for (int i = 0; i < (int)res.size(); ++i) {
cout << res[i] << ' ';
}
cout << endl;
}
// k:当前考虑的下标;m:还需要再取多少个(此处用 res.size() 与 M 判断亦可)
void combination(int k, int m) {
if ((int)res.size() == M) {
print();
return;
}
if (k >= N) {
return;
}
res.push_back(a[k]);
combination(k + 1, m - 1); // 选
res.pop_back();
combination(k + 1, m); // 不选
}
int main() {
cin >> N >> M;
a = new int[N];
for (int i = 0; i < N; ++i) {
a[i] = i;
}
combination(0, M);
delete[] a;
return 0;
}

2. 顺序法(递归)#

更常见的写法:强制按递增下标选取,避免同一子集因顺序不同被重复枚举。先取第 11 个元素可选范围为下标 [0,NM][0, N-M],再递归取后续;当还剩 11 个名额时,遍历剩余元素并输出。

#include <iostream>
#include <vector>
using namespace std;
vector<int> res;
int N, M;
int *a;
void print() {
for (int i = 0; i < (int)res.size(); ++i) {
cout << res[i] << ' ';
}
cout << endl;
}
void combination(int k, int m) {
if (m == 1) {
for (int i = k; i < N; ++i) {
res.push_back(a[i]);
print();
res.pop_back();
}
return;
}
for (int i = k; i <= N - m; ++i) {
res.push_back(a[i]);
combination(i + 1, m - 1);
res.pop_back();
}
}
int main() {
cin >> N >> M;
a = new int[N];
for (int i = 0; i < N; ++i) {
a[i] = i;
}
combination(0, M);
delete[] a;
return 0;
}

非递归实现#

3. 顺序法(嵌套循环)#

mm 层循环,每层选一个严格递增的下标。与全排列里的蛮力法一样:层数绑死 mm,改 mm 就要改代码。

m=3m = 3 为例:

#include <iostream>
#include <vector>
using namespace std;
vector<int> res;
int N;
int M = 3;
int *a;
void print() {
for (int i = 0; i < (int)res.size(); ++i) {
cout << res[i] << ' ';
}
cout << endl;
}
int main() {
cin >> N;
a = new int[N];
for (int i = 0; i < N; ++i) {
a[i] = i;
}
for (int i1 = 0; i1 < N - 2; ++i1) {
res.push_back(a[i1]);
for (int i2 = i1 + 1; i2 < N - 1; ++i2) {
res.push_back(a[i2]);
for (int i3 = i2 + 1; i3 < N; ++i3) {
res.push_back(a[i3]);
print();
res.pop_back();
}
res.pop_back();
}
res.pop_back();
}
delete[] a;
return 0;
}

4. 状态序列 + 全排列#

对于每个元素 aia_i,只有不在子集中两种状态。可构造长度为 nn、恰含 mm1nmn-m0 的状态串 CCCC 的每一种不同排列对应一种组合。那么问题就转换为了求 CC 的互异全排列问题。

例:A={1,2,3,4}A=\{1,2,3,4\}m=2m=2,初始 C={1,1,0,0}C=\{1,1,0,0\},用字典序/反字典序枚举 CC 的互异排列:

状态 CC取出的元素
1,1,0,01,1,0,01,21,2
1,0,1,01,0,1,01,31,3
1,0,0,11,0,0,11,41,4
0,1,1,00,1,1,02,32,3
0,1,0,10,1,0,12,42,4
0,0,1,10,0,1,13,43,4

全排列算法见 全排列一文。非递归时可用 prev_permutation / next_permutation 枚举状态串:

#include <iostream>
#include <algorithm>
#include <vector>
#include <string>
using namespace std;
vector<int> res;
int N, M;
int *a;
string b;
void print() {
for (int i = 0; i < (int)res.size(); ++i) {
cout << res[i] << ' ';
}
cout << endl;
}
int main() {
cin >> N >> M;
a = new int[N];
b.append(M, '1').append(N - M, '0');
for (int i = 0; i < N; ++i) {
a[i] = i;
}
do {
for (int i = 0; i < N; ++i) {
if (b[i] == '1') {
res.push_back(a[i]);
}
}
print();
res.clear();
} while (prev_permutation(b.begin(), b.end()));
delete[] a;
return 0;
}
提示

next_permutation / prev_permutation 在有重复元素时只会生成互异排列,因此对 mm11nmn-m00 的串,枚举次数是 CnmC_n^m 而非 n!n!

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

全组合算法(Full Combination)
https://blog.scxs-studio.com/posts/full-combination/
作者
R. Z.
发布于
2019-05-17
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
R. Z.
Suffering from acute coke overdose
分类
标签
站点统计
文章
7
分类
4
标签
16
总字数
57,527
运行时长
0
最后活动
0 天前
站点信息
构建平台
EdgeOne Pages
博客版本
Firefly v6.15.3
文章许可
CC BY-NC-SA 4.0