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

本文参考可刘汝佳《算法竞赛入门经典》(第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<iostream>

using namespace std;

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

int main()
{
    int n=4;//共四个元素:0,1,2,3
    for(int i=0;i<(1<<n);i++){  //四个元素的集合共有(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<iostream>

using namespace std;

void print_subset(int n,int s){
    for(int i=0;i<n;i++){
        if(s & (1<<i)) cout<<i<<" "; 
    }
    cout<<endl;
}

//找到集合{1,2}所对应的二进制形式再所对应的整数
int get_supple(int n,int s,int b[],int b_n){
    int cnt=0;
    int a[4];
    for(int i=0;i<n;i++){
        if(s & (1<<i)){
            a[cnt++]=i;
        }
    }
    bool flag=1;
    if(cnt==b_n){
        for(int i=0;i<b_n;i++){
            if(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<iostream>

using namespace std;

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

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<iostream>

using namespace std;

void subset(int n,int *a,int cur){
    for(int i=0;i<cur;i++) cout<<a[i]<<" ";
    cout<<endl;
    for(int i=cur;i<n;i++){
        a[cur]=i;
        subset(n,a,cur+1);
    }
}

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

位向量法

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

#include<iostream>

using namespace std;

void subset(int n,int *a,int cur){
    if(cur==n){ //等所有的元素都标记好之后打印当前所选的子集
        for(int i=0;i<n;i++){
            if(a[i]) cout<<i<<" ";
        }
        cout<<endl;
        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;
}
按照刚刚同样的格式整理总结一下以下内容:幻灯片1 矩阵前缀和 幻灯片2 复习: 前缀和算法  对于一个长为n的序列a = {a[1],a[2],a[3],....,a[n]}  我们可以求出a的前缀和数组s,其中s[i] = a[1]+a[2]+...+a[i]  这样当我们想要求a序列中一段区间的和时,就可以用s[r]-s[l-1]求出a[l]+a[l+1]+...+a[r]的区间和 幻灯片3 问题探究  在一个矩阵上,如果我们想要求出一个子矩阵的和,是否有类似于前缀和的方法?  提示,考虑一下如何定义矩阵上的“前缀” ? 幻灯片4 求面积 思考: 下图的大矩形S,被划分出了四个子矩形A,B,C,D  请用A,B,C,D的面积,进行四则运算,得到大矩形的面积  请用S,A,B,C的面积,通过四则运算,得到矩形D的面积 幻灯片5 启发 通过上面的例子可以想到:  当我们想要算出矩阵中某一块子矩阵的和,例如想要得到矩形D的和,可以先提前求出S,A,B,C的和(就像在一维前缀和算法中求sum[]数组一样)  再通过S-A-B+C得到D的矩阵和  要提前求出S,A,B,C这类矩形的和,就要先归纳出他们的特点。  思考: S,A,B,C这些矩形有什么共性呢? 幻灯片6 前缀矩阵 很容易发现,S、A、B、C都是以矩阵中的某个元素为右下角,以矩阵第一行第一列为左上角的  我们把这些矩阵称为前缀矩阵,在二维前缀和算法中,就是要提前求出sum[i][j]表示以第i行第j列为右下角的前缀矩阵和  下图这些子矩阵就是前缀矩阵: 幻灯片7 练一练 sum[i][j]表示以第i行第j列为右下角的前缀矩阵和  对于右侧矩阵:    求出: sum[2][3] sum[4][2] sum[3][5] 幻灯片8 预处理 想要快速求出所有的前缀矩阵和sum[i][j],就要类似一维前缀和那样找到相应的递推公式,像动态规划一样快速的求出所有的sum值  看看下面的矩阵,S = C+B-A+D ,替换成sum值和矩阵a中的元素就是: sum[i][j] = sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1] + a[i][j]      这就是矩阵前缀和的递推公式,请认真理解并记忆 幻灯片9 预处理—代码实现  读入n,m和n*m的矩阵,求出sum[][]数组,并将其输出  输入样例: 输出样例:     sum[i][j] = sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1] + a[i][j]   幻灯片10 预处理-代码实现 幻灯片11 求出子矩阵的和 看下面一个例子,如何利用sum数组求出矩形D,也就是以(4,4)为左上角、(5,6)为右下角的的子矩阵和           幻灯片12 求出子矩阵的和 看下面一个例子,如何利用sum数组求出矩形D,也就是以(4,4)为左上角、(5,6)为右下角的的子矩阵和        D = S - B - C + A = sum[5][6] - sum[5][3] - sum[3][6] + sum[3][3]   幻灯片13 求出子矩阵的和 更普遍的,如果想要求出以(a,b)为左上角,(c,d)为右下角的子矩阵和  就可以用sum[c][d]-sum[a-1][d]-sum[c][b-1]+sum[a-1][b-1]  例如右图,矩阵D中: 左上角为(4,4),右下角为(5,6) 矩阵D的和为sum[5][6]-sum[3][4]-sum[4][3]+sum[3][3]   幻灯片14 例题: 1717 最大子矩阵 幻灯片15 暴力算法1 简单粗暴的处理方法是:  用双重循环枚举子矩阵左上角(a,b)的坐标  利用双重循环计算这个子矩阵的和  这样做的时间复杂度为O(n^4)  代码实现略,课后布置成作业,每位同学完成并提交后,在群里截图打卡“暴力算法1已完成”   幻灯片16 暴力算法2 简单粗暴的处理方法是:  用双重循环枚举子矩阵左上角(a,b)的坐标  利用一维前缀和,O(n)枚举子矩阵的每一行求和  这样做的时间复杂度为O(n^3)  代码实现略,课后布置成作业,每位同学完成并提交后,在群里截图打卡“暴力算法2已完成”   幻灯片17 标准解法 利用二维前缀和,我们在处理出sum[][]数组后,只要:  枚举子矩阵的左上角(a,b)的坐标,求出右下角(c = a+x-1, d = b+y-1)  利用二位前缀和直接求出子矩阵的和  sum[c][d]-sum[c][b-1]-sum[a-1][d]+sum[a-1][b-1]  幻灯片18 参考核心代码 幻灯片19 例题: 1722 星空 幻灯片20 例题: 1722 星空 幻灯片21 题解  简单分析,会发现每过c+1秒所有星星的亮度又变回来了  所以t时刻是等价于t%(c+1)时刻的  这样的话实际上只有0~c这c+1个不同的时刻  可以用sum[t][][]记录t时刻的星星亮度对应的矩阵前缀和,这样的预处理的复杂度是O(c*n*m)的,对于每次询问,只要计算出等价的0~c中的时刻,并计算矩阵和即可。 幻灯片22 例题 1724 Pond 幻灯片23 题解 二分中位数mid,将大于mid的数设为1,否则设为0  这样一个子矩阵的和就是这个子矩阵中大于mid的数的个数  枚举k*k的正方形子矩阵的右下角,并利用sum数组计算对应的矩阵和  如果找到一个子矩阵的和是≤k*k/2的,说明存在一个正方形子矩阵的中位数≤mid,就朝着小的方向二分,否则朝着大的方向二分 幻灯片24 核心代码 幻灯片25 作业  例题/中等难度题目 1717 1722 1724  较难题目 1720 1721 
03-08
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值