组合算法(二进制辅助法)
1.什么是数学中的组合
组合和排列不同,组合不需要考虑选择的元素的顺序,而排列需要。
-
组合:从n个数中选择k个数组成一个组合,看有多少种不同的组合,每个数选择的先后顺序不受限制
C n k = A n k k ! = n ! k ! ( n ! − k ! ) C^k_n = \frac{A_n^k}{k!} = \frac{n!}{k!(n!-k!)} Cnk=k!Ank=k!(n!−k!)n! -
排列:从n个数中选择k个数排成一个队列,看有多少种不同的排列,每个数选择的先后顺序不同则所组成的排列不同。
A n k = n ( n − 1 ) ( n − 2 ) . . . ( n − k + 1 ) = n ! ( n − k ) ! A^k_n=n(n-1)(n-2)...(n-k+1) = \frac{n!}{(n-k)!} Ank=n(n−1)(n−2)...(n−k+1)=(n−k)!n!
以1~5五个数中选三个数为例,总共可以组成的组合有:
1,2,3
1,2,4
1,3,4
2,3,4
1,2,5
1,3,5
2,3,5
1,4,5
2,4,5
3,4,5
2.计算机中如何实现高效的组合算法
这里我分递归跟非递归两种做法!
①递归做法
组合的方式我们可以用实际上的问题来理解
比如一个箱子里面有五个球,你每次从里面随机拿一个球,总共拿三次。
你每拿一次箱子里面的五个球都会有选 与 不选两种可能。
当你拿了一个之后箱子里面就剩下四个球。
这个四个球又会同样有选与不选两种可能。这就会形成一个递归。
直至三个球全部选完就可以看我们选了哪三个球了。
当我们的将所有的可能枚举出来的时候所有的组合可能也就出来了。
代码:
#include<iostream>
using namespace std;
int n, k;
/*
*递归函数。
*参数u:枚举了多少位数
*参数sum:选择了多少
*参数state:用二进制的1和0两种状态表示五个数的选与不选的状态
*k:要选择的数, 5选3 中的3
*n:在多少里面选, 5选3中的5
*/
void dfs(int u, int sum, int state)
{
/* n - u :表示还未枚举的数
* sum:表示已经选择的数
* 整体表示,已选择的数加上最后可选择的数如果还不够总共需要选择的数的话就可以返回了
*/
if(sum + (n - u) < k) return;
//当选择的数够3个了的时候
if (sum == k){
for ( int i = 0; i < n; i++ ) {
if ( state>>i & 1 ){
cout<<i+1<<' ';
}
}
cout<<endl;
return;
}
//当所有数都枚举完了的时候
if ( u == n ) return;
//表示这个数被选的情况
dfs(u+1,sum+1,state | 1<<u);
//表示这个数没有被选的情况
dfs(u+1,sum,state);
}
int main(){
cin>>n>>k;
//将所有的数都遍历一遍,选与被选都考虑了一遍!
dfs(0,0,0);
return 0;
}
运行结果:
②非递归做法
非递归的方式我感觉非常的巧妙而且有意思,而且相对递归会比较容易理解很多,毕竟递归还是比较难的。
思路:
我们还是以五个数里面选择数三个数为例。
由上面递归的做法可以知道,每个数都有选与不选两种可能。
可以假设一个容量为五的数组里面,我们将选的赋值为1,将没选的赋值为0。
我们可以转化成用二进制的形式,那每一种组合就相当于一个数值,因为如果在0~31个范围里面只选取三个数赋值为1的话,在不同的位置赋值对于一个二进制数来说都是不同的数值。我们换算成十进制从左往右算。
1,2,3(1,1,1,0,0)—— 7
1,2,4(1,1,0,1,0)—— 11
1,3,4(1,0,1,1,0)—— 13
2,3,4(0,1,1,1,0)—— 14
1,2,5(1,1,0,0,1)—— 19
1,3,5(1,0,1,0,1)—— 21
2,3,5(0,1,1,0,1)—— 22
1,4,5(1,0,0,1,1)—— 25
2,4,5(0,1,0,1,1)—— 26
3,4,5(0,0,1,1,1)—— 28
我们可以看出如果用二进制的形式来表示选与不选,通过位移1的位置我们可以得到不同的数值,也就是不同的组合,而且如果按照一定的规则位移的话你可以发现他是刚好从小到大的排序的。
这边我们要注意的是我们是从左往右计算,位移的。
所以是右大左小,每最右边的那个1往右位移一位的时候就相当于进一了,不管后面的1怎么放都会比前面的那个组合大,所以后面的1就要从最左边也就是最小的那边开始枚举。
代码实现
- n选k,初始化n的个数,将他们都赋值为0,然后将前面k个数赋值为1,表示最小的二进制数。
- 如果找到下一个比较大的数呢,那就是位移,每次都是只位移一位,所以可以将右边的0赋值为1,将原本的1改为0,就是遇到10的都全部改为01,那么这个数就变大了。
- 每次位移的时候都要将位移的这个1后面的全部1都移到最左边,这样才是最小的数。
代码如下:
#include <iostream>
using namespace std;
#define NUM 5
int M[NUM];
/*输出*/
void output(int m[])
{
for(int i = 0 ;i < NUM; i++){
if(M[i]){
cout<<m[i];
}
//cout<<M[i];
}
cout<<endl;
}
/*将前面的数从新排序,将1都移至最左端*/
void paixu(int k)
{
for(int i = k;i >= 0; i--){
for(int j = i-1; j>=0; j--){
if(M[i]>M[j]){
int tmp = M[i];
M[i] = M[j];
M[j] = tmp;
}
}
}
}
/*判断是否结束*/
int jdgover(int n)
{
for(int i = NUM-n;i < NUM ; i++){
if(M[i] == 0)
return 0;
}
return 1;
}
/*主函数*/
int main()
{
int m[5] = {1,2,3,4,5};
int i,n;
cout<<"请输入你要从五个数中选几个数进行组合:";
cin>>n;
for(int k = 0;k<n;k++){
M[k] = 1;
}
output(m);
/*遍历,直到1从左边全部移到右边*/
for (i = 0;i <NUM ;i++){
if(M[i] == 1 && M[i+1] == 0){
M[i] = 0;
M[i+1] = 1;
paixu(i);
output(m);
i = -1;
}
if(jdgover(n))
break;
}
}
运行结果: