紫书第七章-----暴力求解法(枚举子集)

本文参考可刘汝佳《算法竞赛入门经典》(第2版)
谨记:本篇算法都是在求0~n-1构成了n个数的子集

二进制法

/*
    二进制法生成子集。

    先看一个例子,集合{0,1,4,6,7,8,16,18}用32位的二进制数可以表示如下:
    (0代表所对应的数不在集合中,1代表所对应的数在集合中)0000 0000 0000 0101 0000 0001 1101 0011

    下面程序以集合A={0,1,2,3}为例生成它的所有子集。
    只要对着下面的程序手动走一遍,就明白这个算法的内涵了(前提是需要具备计算机如何存储二进制整数的知识),现在仅举一个例子示范,对照下面程序来说,当s=6的时候,对应的32位二进制数是:0000 0000 0000 0000 0000 0000 0000 0110,在print_subset函数中,让s=6分别与1左移0到n-1位进行与运算,如下:
    1)1左移0位是0000 0000 0000 0000 0000 0000 0000 0001,与s与运算后,结果是0000 0000 0000 0000 0000 0000 0000 0000,对应十进制0
    2)1左移1位是0000 0000 0000 0000 0000 0000 0000 0010,与s与运算后,结果是0000 0000 0000 0000 0000 0000 0000 0010,对应十进制2
    3)1左移2位是0000 0000 0000 0000 0000 0000 0000 0100,与s与运算后,结果是0000 0000 0000 0000 0000 0000 0000 0100,对应十进制4
    4)1左移3位是0000 0000 0000 0000 0000 0000 0000 1000,与s与运算后,结果是0000 0000 0000 0000 0000 0000 0000 0000,对应十进制0

经过上面,得到了s=6大集合所对应的一个子集{1,2},同样地,当s=0,s=1,…,s=15,都可以分别求出一个子集,总共求出16个子集。
*/

#include

using namespace std;

void print_subset(int n,int s){
    for(int i=0;iif(s & (1<cout<" "; //注意这里是与的位运算,结果非0则为真
    }
    cout<//如果是空集,则对应于一个空行
}

int main()
{
    int n=4;//共四个元素:0,1,2,3
    for(int i=0;i<(1<//四个元素的集合共有(1<<4)-1个元素
        print_subset(n,i);
    }
    return 0;
}

补充:
A B A&B A|B A^B
二进制 10110 01100 00100 11110 11010
集合 {1,2,4} {2,3} {2} {1,2,3,4} {1,3,4}
交集 并集 对称差
则集合的补集可以用全集和该集合异或得到。

【求补集示例】
求{1,2}对应全集{0,1,2,3}的补集



#include

using namespace std;

void print_subset(int n,int s){
    for(int i=0;iif(s & (1<cout<" "; 
    }
    cout<//找到集合{1,2}所对应的二进制形式再所对应的整数
int get_supple(int n,int s,int b[],int b_n){
    int cnt=0;
    int a[4];
    for(int i=0;iif(s & (1<bool flag=1;
    if(cnt==b_n){
        for(int i=0;iif(a[i]!=b[i]){
                flag=0;break;
            }
        }
        if(flag) return s;
    }
    return -1;
}

int main()
{
    int n=4;//共四个元素:0,1,2,3
    int b[2]={1,2};
    int ALL_BITS=(1<<4)-1;
    for(int i=0;i<(1<<4)-1;i++){
        int tmp=get_supple(n,i,b,2);
        if(tmp!=-1){
            tmp=tmp ^ ALL_BITS;//异或求其补集
            print_subset(n,tmp);
            break;
        }
    }
    return 0;
}

增量构造法

首先假设的是0到n-1这n个数是从小到大的顺序。
本算法的思想和求全排列的思想类似,都是谁打头的问题。不同的是,求子集的时候,比如以0,1,2,3这4个数为例,2打头后,后面再出现的数必须比2大(请牢记这句话,很重要),而全排列算法中,求出0,1,2,3后可以求出0,1,3,2这个排列。下面展示下子集生成的过程。
还是分别是0,1,2,3打头,以1打头为例,1打头后,分为1 2打头,1 3打头,1 2打头后又有1 2 3打头,而1 3打头后,要求后面出现的数必须比3大,这不可能,所以1打头的情况到此结束,其他情况类似分析。

#include

using namespace std;

void subset(int n,int *a,int cur){
    for(int i=0;icout<" ";
    cout<int s=cur?a[cur-1]+1:0; //经过单步调试知道该句代码的重要意义就是
            //前面已经固定的情况下,求出将要打印的子集的最小值
    for(int i=s;i1);
    }
}

int main()
{
    int a[5];
    subset(4,a,0);
    return 0;
}

上面代码比如打印出0 1 3之后,再次打印新的子集的时候,求出的当前最小值是a[2]+1=4,到此结束,而下面的代码会出现a[3]=3的问题,打印出0 1 3 3的不合法子集。建议逐步调试可以快速发现问题。

#include

using namespace std;

void subset(int n,int *a,int cur){
    for(int i=0;icout<" ";
    cout<for(int i=cur;i1);
    }
}

int main()
{
    int a[5];
    subset(4,a,0);
    return 0;
}

位向量法

说白了就是,每个元素都对应选与不选,那我们就暴力枚举所有情形,有的元素标记要选(1),有的标记不选(0),然后把要选的打印出来,就是一个子集。

#include

using namespace std;

void subset(int n,int *a,int cur){
    if(cur==n){ //等所有的元素都标记好之后打印当前所选的子集
        for(int i=0;iif(a[i]) cout<" ";
        }
        cout<return;
    }
    a[cur]=0;//不选择cur这个元素
    subset(n,a,cur+1);
    a[cur]=1;//选择cur这个元素
    subset(n,a,cur+1);
}

int main()
{
    int a[5];
    subset(4,a,0);
    return 0;
}

你可能感兴趣的:(紫书第七章-----暴力求解法(枚举子集))