本文主要内容:递归实现三种类型枚举
- 1~n的排列型枚举:若n=3,一种结果为:123,132,213,231,312,321
- 1~n的组合型枚举:若n=3,m=2,一种结果为:12,13,23
- 1~n的指数型枚举:若n=3,一种结果为:(空),1,12,123,2,23,3
- 关于有重复元素时避免重复枚举的思考
注意:
- 本文的代码仅供参考,可能还有更好的实现方式。
- 关于重复元素造成枚举结果重复如何解决会在第四个模块来讲述~
- 本文的三种类型枚举的枚举结果要求不能重复
- 例题(来源于Acwing):
无重复元素:Acwing94. 递归实现排列型枚举、Acwing93. 递归实现组合型枚举、Acwing92. 递归实现指数型枚举
有重复元素:Acwing1537. 递归实现排列类型枚举 II、 Acwing1573.递归实现组合型枚举 II、Acwing1572.递归实现指数型枚举 II
1~n的排列型枚举
- 利用dfs实现按位枚举,个数达到要求则输出路径结果
//数据范围:1 ≤ n ≤ 9
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 10;
int a[N], n, path[N]; //a[]为待枚举的序列,n为序列元素的个数,path[]存储dfs的路径结果
bool vis[N]; //布尔数组用来判断在dfs过程中序列元素是否已被放入path[]中
void dfs(int u)
{
if(u == n) //满足条件则输出
{
for(int i = 0; i < n; i++)
cout << path[i] << ' ';
cout << endl;
}
for(int i = 0; i < n; i++)
if(!vis[i])
{
if(i && a[i] == a[i - 1] && !vis[i - 1])continue;//用来剪掉重复分支
vis[i] = true;
path[u] = a[i];
dfs(u + 1);
path[u] = 0; //恢复现场
vis[i] = false;
}
}
int main()
{
cin >> n;
for(int i = 0; i < n; i++)cin >> a[i];
sort(a, a + n); //对数组进行排序,使所有相同重复元素相邻
dfs(0);
return 0;
}
1~n的组合型枚举
- 比起排列型枚举,组合型枚举有了个数限制,不一定要序列中所有元素都枚举出来
- 组合型枚举将“无视”顺序的不同,元素种类及对应个数相同的两种不同排列的枚举结果被视为相同
- 组合型枚举也是采用dfs的方式进行枚举,个数到达要求即输出路径结果
- 因为对顺序没有要求,所以我们可以人为让枚举结果中的数字升序排列,这样确保不会因为元素的顺序不同而造成枚举结果重复(但是重复元素造成的枚举结果重复无法解决)
//数据范围:n>0, 0≤m≤n, n+(n−m)≤25, 序列内所有元素均不大于 n。
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 30;
int a[N], n, m; //a[]为待枚举序列,n为其长度、m为单个枚举结果长度
void dfs(int state, int u, int start) //用一个state来代替vis[]和path[]
{
if(u == m) //达到m个就输出
{
for(int i = 0; i < n; i++)
if(state >> i & 1)
cout << a[i] << ' ';
puts("");
return;
}
for(int i = start; i < n; i++) //每次从起点开始枚举、确保所有结果升序排列
if((state >> i & 1) == 0)
{
if(i && a[i] == a[i - 1] && (state >> (i - 1) & 1) == 0)continue;//用来剪掉重复分支
state += 1 << i;
dfs(state, u + 1, i + 1);
state -= 1 << i;
}
}
int main()
{
cin >> n >> m;
for(int i = 0; i < n; i++)cin >> a[i];
sort(a, a + n); //排序后重复元素连续
dfs(0, 0, 0);
return 0;
}
1~n的指数型枚举
- 与组合型枚举一样,枚举的结果中的元素没有顺序要求
- 与组合型枚举相比,指数型枚举的枚举结果长度不一定
- 与组合型枚举类似,也是采用dfs来枚举,所不同的是其一边递归一边输出路径结果
//数据范围:1≤n≤15, 序列内所有元素均不大于 n。
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 20;
int n, a[N];
void dfs(int state, int u, int start) //用一个state来代替vis[]和path[]
{
if(u > n)return;
for(int i = 0; i < n; i++)
if(state >> i & 1)
cout << a[i] << ' ';
puts("");
for(int i = start; i < n; i++)
if((state >> i & 1) == 0)
{
if(a[i] == a[i - 1] && i && (state >> (i - 1) & 1) == 0)continue;//用来剪掉重复分支
state += 1 << i;
dfs(state, u + 1, i + 1);
state -= 1 << i;
}
}
int main()
{
cin >> n;
for(int i = 0; i < n; i++)cin >> a[i];
sort(a, a + n);
dfs(0, 0, 0);
return 0;
}
以上三种递归枚举基本上都是使用dfs来实现,但是递归实现方式不只有dfs,还有其他的方式,不过用dfs来实现比较常见
关于有重复元素时避免重复枚举的思考
避免重复的方法有许多种,例如将重复元素按个数分组,一次选一组放入路径结果然后递归到下一层。这里我们采取另外一种思考方式,上述三个中类型枚举都是基于这种思考方式来处理的,可见其具有一定的通用性。
问题说明:
以序列2 2的的长度为2的组合型枚举为例,若不处理重复元素造成的问题,则结果有两种:2 2和2 2,明显重复枚举了,造成重复的原因是重复元素的不同排列组合均放入枚举结果输出,为了方便理解给重复元素标个号,如:2(1),2(2),刚刚的两种枚举结果为2(1),2(2)和2(2),2(1)。如果一个序列有n个重复元素x,对于所有包含k个x的重复枚举结果数为(忽略其他原因造成的重复):
C
n
k
∗
K
!
{C}_n^k * K!
Cnk∗K!
解决方法:
我们可以人为规定重复元素枚举顺序,所有枚举结果的值相同重复元素必须按照规定来选取(例如相连升序,枚举后面的重复元素之前必须先枚举之前的同值重复元素,即若一个枚举结果中还有k个重复元素x,那么这k个x只能是x(1), x(2), …,x(k)且按位从小到大排列),否则舍弃该枚举结果(例子中的2(2),2(1)),实现方式如下(相连升序):
- 将序列排序,使得所有值相同的相同元素相邻
- 在dfs过程中,若有一个元素与其在序列中的前一个元素值相同且前一个元素并没有放入路径结果path中,则直接continue剪枝(否则会造成枚举结果重复)
- 以上两条只需通过两行代码实现,具体实现方式看上面的代码