目录
一、前言
今天是元旦节,首先祝大家元旦节快乐鸭!组合以及全排列相信大家在高中的时候就有所耳闻了,那么在程序设计中呢,经常会遇到组合与全排列的相关问题。首先先简单的介绍一下组合和全排列的相关概念。
①全排列
如果老师让编号为1,2,3的三个人进行排队,试问这三个人排队的顺序是怎么样的?大家关注到,对于这三个人来说,因为每个人都是不同的,因此1在前,2在后和1在后,2在前就是两种不同的顺序。在这题里面,也就是说有:123,132,213,231,312,321六种不同的顺序,而这6个便是大家所熟悉的公式A33=6(种)。
②组合
那如果是组合呢,如果老师让你从编号为1,2,3的三个人里面任意选择两个人进行pk,请问一共可以进行多少轮不一样的1v1比赛呢?大家肯定会知道那不就是:12,13,23各自进行一轮,一共有三轮嘛。这里的三轮,便是C32=3得出的结果。至此我们发现了全排列和组合的区别就在于两者是否在选择上分先后顺序,也就是说12和21是算一个结果还是算两个结果?
题目描述
有一个数组a,请你从屏幕中输入两个整型n,m(n>=m),并输入任意n个数字到a数组里面,最后按照下面两种方式输出相应的所有可能(没有顺序上的要求)。
①在n个数字里面选择m个进行全排列的所有可能。
②在n个数字里面选择m个进行组合的所有可能。
二、解题思路
①确定所用算法
熟悉排列组合算法的同学在本处会很自然的想到利用深度优先搜索的算法,也就是大名鼎鼎的DFS,但是其实在计算组合的可能性的时候,还可以利用到位运算的方法。后续会提到。
②深搜的模板
void dfs(int x)
{
if(......) //终止条件
{
操作1;
操作2;
......;
return; //返回上一层,否则会进行很多重复步骤甚至无法走出递归。
}
else
{
操作1;
操作2;
vis【】=1; //代表某位置被访问。
dfs(x+1);
vis【】=0; //回溯操作,便于产生更多的可能性。
}
}
以上是深搜的简单模板,其实主要利用的是回溯的思想,其中的操作需要根据题目来进行,其实这样看下来,是不是觉得会很简单呢?其实不然,在深搜里需要注意很多终止条件的使用,以及操作的可行性还有的就是及时的记录下答案和答案的坐标噢,以后会通过相关的题目进行此处的注意。
三、深搜的最终代码
①进行全排列
#include<bits/stdc++.h>
using namespace std;
int a[100005],vis[100005],ans[100005]; //a代输入的数字存储,vis代表访问数组,ans记录访问的答案
int n,m; //n代表输入数组的总长度,m代表选取多少个数字进行排列
//进行全排列的深搜
void dfs_qpl(int x) //x代表搜索到的一个宽度
{
if(x>m)
{
for(int i=1;i<=m;i++)
{
cout<<ans[i]<<" ";
}
cout<<endl;
return;
}
for(int i=1;i<=n;i++)
{
if(!vis[i])
{
vis[i]=1;
ans[x]=a[i];
dfs_qpl(x+1);
vis[i]=0;
}
}
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
dfs_qpl(1);
return 0;
}
最终实现①
②进行组合
#include<bits/stdc++.h>
using namespace std;
int a[100005],vis[100005],ans[100005]; //a代输入的数字存储,vis代表访问数组,ans记录访问的答案
int pos[100005]; //边搜索边记录下已经搜索过的位置
int n,m; //n代表输入数组的总长度,m代表选取多少个数字进行排列
//进行全排列的深搜
void dfs_qpl(int x) //x代表搜索到的一个宽度
{
if(x>m)
{
for(int i=1;i<=m;i++)
{
cout<<ans[i]<<" ";
}
cout<<endl;
return;
}
for(int i=1;i<=n;i++)
{
if(!vis[i]&&i>pos[x-1]) //代表循坏到的下标要比搜索的前一个大
{
pos[x]=i;
vis[i]=1;
ans[x]=a[i];
dfs_qpl(x+1);
vis[i]=0;
}
}
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
dfs_qpl(1); //从下标为1的地方开始搜索
return 0;
}
最终实现③
两个深搜的总结
大家可以在实践的时候,或者观看此篇博客的时候认真地对比一下两个代码之间的差别,大家可以发现,其实只是很细微上的差别,但是却导致了两个代码在执行上的不同,这也就是搜索上需要我们进行各种操作的基本功,然后大家一定要多练习几遍噢,这个其实已经相当于模板类的题目了,下面,想要就组合展开进一步的位运算优化,这个优化的思想也是通过洛谷官网自己出的一本新手程序书上学到的哦,那本书真的挺不错的,所以在这里就进行一个广告啦!
四、位运算组合算法
①水话
只能说位运算其实一直是一个程序设计上优化各种时间复杂度的良药了,因为很多人包括蒟蒻在内的同学们,对二进制利用或者了解的不够多,因此在使用起来会感觉到乏力,因此就从这一刻开始吧,希望同学们还有自己能够在遇到位运算相关的题目的时候,能够多多积累这样类似的算法,至于一些位运算的操作,大家可以关注一些神犇写的博客。在利用位运算的时候一定要关注题目的数据大小,毕竟int只能存32位的数嘛!
②思路产生
还是以1,5,8为例子,这里存在了三个数字,如果我们利用1代表“有”,0代表“无”,那么也就是说,若1,5,8均存在,我们便利用111代表全集。而如果要在里面找两个进行组合,如图所示,便有110,101,011三种可能,而这三种可能所代表的数比111小,也就是说如果我们利用一个循环从0到111(这里均是二进制的意思)进行搜索,那么我们一定会搜到这三种可能,然后再利用其他变量记录1出现的次数,如果出现了两次,我们就把那一次循环所记录下来的结果输出就可以了。下面大家先看一下代码!
位运算组合的完整代码
#include<bits/stdc++.h>
using namespace std;
int a[100005],ans[100005];
int n,m; //n代表输入数组的总长度,m代表选取多少个数字进行排列
int main()
{
cin>>n>>m;
for(int i=0;i<n;i++)
{
cin>>a[i];
}
int U=(1<<n)-1; //U代表全集
int k=0; //k记录是否满足输出条件m
int p=1; //用p为1下标便于记录下答案
for(int i=0;i<=U;i++) //从0一直枚举到全集
{
for(int j=0;j<=n;j++)
{
if((1<<j)&i) //代表这个数字是“有”
{
k++;
ans[p++]=a[j];
}
}
if(k==m)
{
for(int s=1;s<=p-1;s++)
cout<<ans[s]<<" ";
cout<<endl;
k=0;
p=1; //k和p同时进行复原,便于下一个答案的查找。
}
k=0; //外层循环也别忘了重置k和p
p=1; //k和p同时进行复原,便于下一个答案的查找。
}
}
③最终实现
④位运算内的细节讲解
1.全集U的来历:因为158是三个数,那么我们让1左移三位以后会从0001变成1000,然后我们再进行减1的操作便可以得到0111(也就是111)的全集我们想要的结果。
2.if进行筛选的操作(if((1<<j)&i)),这一句话是什么意思呢,让一个j从0到n进行循坏,不断的让1左移j位,其实就相当于产生了001,010,100三个可能之后在&U,就是可以知道,在循坏到i的时候,哪一个位置上是1(也就是“有”)。例如在循环到110的时候,我们便可以让if里面进行两次的操作,最后的k=2,就满足输出的条件了。
3.k,p的重置。两个变量的重置也是很关键的,如果没有及时的重置,我们便不会得到更多的可能性,当然在STL库里面对于查找一个i中有多少个1是有相对应的函数的,但是为了让大家了解这个过程,在这里就不多加赘述了,感兴趣的读者可以阅读洛谷上的那本书进行相关的学习!
五、子集的位运算实现
①水话
其实看到此处并好好理解了上面算法的实现的读者,相信已经发现了位运算可能会在子集上有妙用,因为子集也是经常考的算法题目,如果我们把题目变成,请输出a数组里面包含的所有子集,相信读者能够利用位运算的思想很好的解决,而且后续会有相关的题目利用到这样的思想,尽情期待哦!
②思路产生
因为全集是111,那么在0-111的过程中,1的数量和位置都是不固定的,但是我们惊奇的发现,在循环的过程中永远不会出现一个重复的二进制,这是很显然的,因为每个数的二进制是不同的,而子集其实就是在查找从n个数选0-n的组合,所以很显然少了很多的约束条件,也就是我们在循环中,不断的输入“有”的数就好了!
位运算子集的完整代码
#include<bits/stdc++.h>
using namespace std;
int a[100005];
int n,m; //n代表输入数组的总长度,m代表选取多少个数字进行排列
int main()
{
cin>>n;
for(int i=0;i<n;i++)
{
cin>>a[i];
}
int U=(1<<n)-1; //U代表全集
for(int i=0;i<=U;i++) //从0一直枚举到全集
{
for(int j=0;j<=n;j++)
{
if((1<<j)&i) //代表这个数字是“有”
{
cout<<a[j]<<" ";
}
}
cout<<endl;
}
}
③最终实现
至于这里的158下面为什么会多一行呢?因为一个集合的子集包含空集呢!
新年祝语以及愿景(2022年)
新的2022年,虽然感觉并没有跨年的气氛了,但是确实在头一天写这篇博客的时候充满了斗志,而且本蒟蒻认为此篇也能够顺利帮助在刚接触相关算法的读者们顺利度过这一段迷惑期,因为自己也是这么过来的!我坚持了下来,并且能够坐在电脑之前写着以前我压根就不会写的题目和算法,虽然自己现在的水平还是很低很低,但是我觉得自己很充实。新的一年里,我希望自己能够忘掉2021年的不愉快,忘记很多情感上的伤痛。然后因为自己是计算机的学生,当然希望自己不要掉太多的头发,然后希望新的一年能够接触到更多的算法和数据结构,自己能够变得越来越强,让一些可能开始觉得我不行的那一些人,让他们刮目相看。然后希望今年交大的校赛能够取得好的成绩,顺利的以一个合格的身份进入到校ACM队里面,为我们的学校取得更多的荣誉。希望自己身边的人都健健康康,开开心心每一天。希望今年生日的时候能够亲手把礼物送给和我同一天生日的学姐手上去,向她表示这么多年来的感谢,希望跟她成为好朋友,而她的存在也必将作为我的动力,继续让我朝着一个更好的方向迈进。希望新的一年能够结识到更多一路同行的兄弟们,在2021年的尾声,遇到了很多很多很好的人,虽然有的人来了又离开,但是还是非常感谢这一份遇见,因为遇见,也许我们也成为了更好的我们,最后当然就是希望自己能够不忘初心,继续坚持学算法和数据结构,继续坚持写博客,继续提升自己,继续开开心心地过好2022年的每一天,继续在开心中get到更多更多的新技能。也祝愿所有看到此篇文章的人新年快乐,天天开心!并且表示本蒟蒻由衷的感谢嘻嘻!最后:再见2021年,我来了2022年!