搜索与回溯算法专题--子集和排列的枚举
枚举排列:
不重集排列:
思路:
用cur表示以cur为当前位置填元素
当cur==n时排列生成完成,输出
然后枚举b数组,尝试在cur上填每一个数
填之前检查一下当前要填的数与a数组之前的数是否相同,不相同代表排列合法,继续搜索
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int n,b[15],a[15];
inline void pr_per(int cur){
if(cur==n){
for(int i=0;i<cur;++i){
cout<<a[i]<<" ";
}
cout<<endl;
return ;
}
for(int i=0;i<n;++i){
int ok=1;
for(int j=0;j<cur;++j){
if(a[j]==b[i]){
ok=0;
break;
}
}
if(ok){
a[cur]=b[i];
pr_per(cur+1);
}
}
}
int main(){
cin>>n;
for(int i=0;i<n;++i){
cin>>b[i];
}
pr_per(0);
return 0;
}
可重集排列:
不能用不重集排列的方法了,因为本来b数组中就有相同的元素,这样的话判重方法就失效了
思路:
cur仍然表示当前位置进行填元素
输出与不重集排列相同
然后枚举b数组的方法要改变一下
在cur上填一个数之后不能判重,而要统计输入数组里这个数出现的次数c2,然后统计排列数组里这个数出现的次数c1,算上现在填的数的话如果c1<=c2说明这个排列合法,可以继续搜索,如果不算这个现在填的数的话c1<=c2-1,但是c1和c2都是整数,所以\(c1<c2\)
但是与此而来的问题是由于原输入数组中有重复的,所以每一次填数是一个数都会填多次,而下一阶段填数每个数也会填多次,造成重复
那么怎么解决呢?
可以看出如果输入数组的重复元素是紧挨着的话,那么可以快速的去除重复元素,去除重复元素后每一次填数一个数只会填一次,下一阶段填数才会再填这个元素,即一个阶段中一个数最多填一次,因为这些相同的数交换顺序也只是一种排列,所以既不会重复,该填的次数也会填满,直到c1>=c2时才会停止,因此也不会漏解
这样的话快速去重就简单了,如果枚举到这个输入数组的第一个数那么应该填它,如果不是第一个元素就要看它和前一个元素是否相同,若相同则不填,不同则填,因为相同的数紧挨在一起
注意:输入数组的相同元素必须紧挨在一起,但可以无序!
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int n,b[15],a[15];
inline void pr_per(int cur){
if(cur==n){
for(int i=0;i<cur;++i){
cout<<a[i]<<" ";
}
cout<<endl;
return ;
}
for(int i=0;i<n;++i)if(!i||b[i]!=b[i-1]){//判断输入数组而不是排列数组
int c1=0,c2=0;
for(int j=0;j<cur;++j){
if(a[j]==b[i])++c1;
}
for(int j=0;j<n;++j){
if(b[j]==b[i])++c2;
}
if(c1<c2)a[cur]=b[i],pr_per(cur+1);//别忘了填数
}
}
int main(){
cin>>n;
for(int i=0;i<n;++i){
cin>>b[i];
}
pr_per(0);
return 0;
}
位运算枚举子集:
这不是递归搜索
这是用二进制表示集合
从右往左数,最右边下标为0,依次类推,每一位上的下标表示该元素,二进制数1表示该元素在集合中,0表示该元素不在集合中,二进制串长度表示集合元素个数
如
00101
5个元素,最右边下标0,0在集合中,2也在集合中,其余都不在
则a与b集合的交集为a&b(因为a、b的某个元素都在集合中时交集才算上它们,只要a/b有一个不包含该元素,那么它们的交集也不包含该元素)
a与b的并集为a|b(因为a、b的某个元素只要在a或b任意一个集合中,并集就包括它们,都不在集合时,并集才不包含它们,同时这样做能保持集合的互异性)
a与b的对称差为a^b(即ab都有的元素或都没有的元素不算在内,ab中只有一个集合有的元素的集合为ab的对称差)
n个元素从0到n-1的全集为二进制数11111……(n个1),即为2^n -1
如何判断某一位是否在集合中?
只要看这一位是否是1即可,1在集合中,0不在
如何用位运算判断?
因为要判断某一位是否为1,所以可以把1和这一位与运算,如果结果为1说明这一位为1,否则这一位为0
所以只要让这个二进制串与一个只有要求的第k位(从右往左从0开始)为1其余位为0的一个二进制串进行逻辑与运算判断结果是否为1即可
求那个二进制串要用<< ,第0位为1,第一位为10,第二位为100,要第k为为1就要左移k位填上k个0
由于n个元素集合有n位,一位有两种0或1,所以能表示2^n个二进制数,数从0
开始到2^n -1,刚好表示了这个集合的所有子集,所以可以从0开始枚举到2^n -1,每一个数都是一个子集,然后把这个子集判断每一位上是否为1,找出在集合中的元素,并输出,即可枚举子集
但这种方法不按字典序
因为按00/01/10/11的顺序枚举,显然不按字典序
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
//#include<iostream
using namespace std;
int main(){
int n;
cin>>n;
for(int i=0;i<(1<<n);++i){
for(int j=0;j<n;++j){
if(i&(1<<j))cout<<j<<" ";
}
cout<<endl;
}
return 0;
}
增量构造法枚举子集:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
//#include<iostream
using namespace std;
int n,a[25];
inline void print_subset(int cur){//cur表示当前位置该填什么元素
for(int i=0;i<cur;++i){//从0到上一次的cur输出集合中元素
cout<<a[i]<<" ";
}
cout<<endl;
int ii=cur?a[cur-1]+1:0;//当cur为0时是第一次枚举,从0开始,当cur不为0时为避免重复应该从上一次填的元素+1开始枚举
for(int i=ii;i<n;++i){
a[cur]=i;
print_subset(cur+1);
}
}
int main(){
cin>>n;
print_subset(0);
return 0;
}
这样按字典序,因为是从少到多,从小到大的顺序枚举的
位向量法枚举子集:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
//#include<iostream
using namespace std;
int n,a[25];
inline void print_subset(int cur){//cur表示当前枚举的元素
if(cur==n){//当n个元素是否在集合中全部确定时才输出
for(int i=0;i<n;++i){
if(a[i])cout<<i<<" ";
}
cout<<endl;
return;
}
a[cur]=1;//当cur元素在集合中时枚举一遍子集
print_subset(cur+1);
a[cur]=0;//当cur元素不在集合中时枚举一遍子集
print_subset(cur+1);
}
int main(){
memset(a,0,sizeof a);
cin>>n;
print_subset(0);
return 0;
}
显然不按字典序
因为先输出的是元素全在集合中时的子集,然后才输出有元素不在集合中时的子集
#nextpermutation手写
已知一个排列\(a\),求它的下一个排列
首先倒着找从右边第一位连续算起满足正着从大到小排列的最小下标,因为这些已经逆序排好了,说明它们已经是最大排列了,下一个排列肯定是改前一个数
如果最小下标是\(1\)(下标从\(1\)开始),说明已经 没有下一个排列了,返回\(0\)
把最小下标的前一个数和右边有序序列中大于它且最小的元素,然后交换这两个元素,这样前一个数就增大了尽可能小的数,保证了字典序
但是右边有序序列需要从小到大排序,如果sort \(nlogn\)
所以可以用插入
因为原来逆序排列,所以只需要倒转这个序列,然后除了交换过的元素其余保持有序
因为那个交换过的元素变小了,所以只需要向左不断插入,找到合适位置 \(O(n)¥
单次总复杂度\)O(n)$
同样适用于可重序列
因为找从大到小的序列是是前面大于等于后面就行,所以找到的最小下标的前一个数一定严格小,然后找最小值时是优先找前面的值
找到后交换/排序,然后就到下一个排列了,不会重复/遗漏
可重集的枚举
就是输入一个集合的元素(从小到大排列),可能有相同的,要求不重复,不遗漏的输出所有子集(空集除外)
如 1 1 1
1
1 1
1 1 1
那么我们首先先考虑递归
考虑增量构造法,像枚举排列一样,把a数组里填上p数组的值(p数组是输入数组),但是问题来了,转移时需要从比\(cur-1\)时所填的元素刚好大开始枚举,那么无法转移
因为从小到大
我还可以像原来一样枚举,只是输出时输出p【a【i】】,但是相同元素如何处理呢
1 2 2为例
1 2 3下标
先填1,输出1
然后第二个位置填2,输出 1 2
然后第三个位置填3 输出1 2 2
然后第二个位置填3 输出 1 2
然后第一个位置填2 输出2
第二个位置填3 输出2 2
然后第1个位置填3 输出2
可见重复
为什么呢因为第一个位置填1时第二个位置填2和3是相同的,所以只能填一个
看第一个位置填2和3是相同的,所以只能填一个
而第一个位置填2时,第二个位置是填3,虽然2和3一样,但却又可以填
所以,在枚举这一次填的元素时,要结合p数组判断,如果这是进入循环的第一次,就都应该填并递归,不管它和它前面的元素p数组中是否冲突
如果不是第一次,那么只有p数组中这个元素和前一个元素不等时,才填并递归
这样保证每一类元素都恰好填一次
bfs版的是同理的,扩展节点时要用下标拓展,同时也要相同的方式判断p数组
黄粱一梦,终是一空
本文来自博客园,作者:hicode002,转载请注明原文链接:https://www.cnblogs.com/hicode002/p/19526821

浙公网安备 33010602011771号