《算法竞赛从入门到国奖》算法基础:入门篇-枚举

💡Yupureki:个人主页

✨个人专栏:《C++》 《算法》


🌸Yupureki🌸的简介:


目录

前言

[1. 普通枚举](#1. 普通枚举)

[1.1 铺地毯](#1.1 铺地毯)

算法原理

实操代码

[1.2 回文日期](#1.2 回文日期)

算法原理

实操代码

[1.3 扫雷](#1.3 扫雷)

算法原理

实操代码

[2. 二进制枚举](#2. 二进制枚举)

[2.1 子集](#2.1 子集)

算法原理

实操代码

[2.2 费解的开关](#2.2 费解的开关)

算法原理

实操代码


前言

枚举,顾名思义,就是把所有可能的条件统统列出来并判断。因此这是个比较暴力的解法。如果数据范围较大,极有可能超时,因此我们常常需要优化算法,减少不必要的计算。

1. 普通枚举

1.1 铺地毯

题目链接:

P1003 [NOIP 2011 提高组] 铺地毯 - 洛谷

算法原理

如果地毯覆盖某个点,那么我们按照初中数学学过的知识,其实就很容易推导出来

题目要我们找出最后一个覆盖的地毯,那么只需要从后往前遍历即可

实操代码

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

int main()
{
    int num;cin>>num;
    vector<int> x(num),y(num),lx(num),ly(num);
    for(int i = 0;i<num;i++)
    {
        cin>>x[i]>>y[i]>>lx[i]>>ly[i];
    }
    int n,m;cin>>n>>m;
    for(int i = num-1;i>=0;i--)//从后往前遍历
    {
        if(n>=x[i] && n<=x[i] + lx[i] && m>=y[i] && m<=y[i] + ly[i])
        {
            cout<<i+1;
            return 0;
        }
    }
    cout<<"-1";
    return 0;
}

1.2 回文日期

题目链接:

P2010 [NOIP 2016 普及组] 回文日期 - 洛谷

算法原理

关于回文日期,其实就是回文字符串

所谓回文字符串,即一个字符串轴对称,例如"abba"是中间对称的,而"abab"则不是

那么对于日期,其实也就是个字符串,"20111102"是对称的,但"20121102"则不是对称的

随后题目要我们从两个日期之间,找出所有的回文日期的个数

咋一看,我们似乎需要从开始的日期一天天地数,直到最后的那天

从20000101开始

20000102...20000103.......20000201

真的需要这样吗?我们根据回文字符串的性质,我们假设前面四个字母已经确定了,就是2000,那么倒置过来就是0002,拼在一起就是20000002,所以前四个字母就已经确定了这四个字母开头的那个回文字符串,其余的皆不是。那么我们只需枚举年数即可。

当然如果得到的字符串如果压根不是个日期,例如20131302,直接干到13月去了,那么就不合适

实操代码

cpp 复制代码
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

bool isleapyear(int year)//判断是否为闰年
{
    if(year % 4 == 0)
    {
        if(year % 100 == 0)
        {
            if(year % 400 == 0)
                return true;
            else
                return false;
        }
        else
            return true;
    }
    else
        return false;
}

int day[] = {31,28,31,30,31,30,31,31,30,31,30,31};//每个月的总天数

int getday(int year,int month)//拿到每个月的总天数
{
    if(month == 2)
    {
        if(isleapyear(year))
            return 29;
        else
            return 28;
    }
    else
        return day[month-1];
}

int main()
{
    string a, b; cin >> a >> b;
    int count = 0;
    int year1 = stoi(string(a.begin(), a.begin() + 4));
    int year2 = stoi(string(b.begin(), b.begin() + 4));
    int day1 = stoi(a); int day2 = stoi(b);
    while (year1 <= year2)//枚举年数
    {
        string tmp = to_string(year1);
        string tmp2 = tmp;
        reverse(tmp2.begin(), tmp2.end());//反转年份(前4个字母)
        tmp += tmp2;//由年份得到回文字符串
        int date = stoi(tmp);//由字符串得到整型
        if (date >= day1 && date <= day2)//判断是否是有效日期
        {
            int tmpday = date % 100;
            date /= 100;
            int tmpmonth = date % 100;
            if (tmpmonth >= 1 && tmpmonth <= 12 && tmpday <= getday(year1, tmpmonth))
                count++;
        }
        year1++;
    }
    cout << count;

    return 0;
}

1.3 扫雷

题目链接:

P2327 [SCOI2005] 扫雷 - 洛谷

算法原理

第一眼,这个题目似乎枚举的种类好像还很多,也不好枚举,没事,我们由繁入简,从最简单的开始

对于下面的情况,我们假设第一个格子有雷

根据扫雷的性质,我们似乎可以一个一个往下推,推出后面每一个格子的雷的情况,推到后面发现有一个格子的雷数出现了2个,而一个格子有且仅有一个雷,因此这个情况不成立,当然如果雷数为负值也不成立

我们再假设第一个格子没有雷,也能顺理成章地推出来,是有解的

因此我们只需枚举第一个格子有没有雷即可

实操代码

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

int main()
{
    int n;cin>>n;
    vector<int> a(n+2,0);vector<int> b(n+2,0);
    for(int i = 1;i<=n+1;i++)
    {
        cin>>b[i];
    }
    int count = 0;
    for(int i = 0;i<=1;i++)//枚举第一个格子有没有雷
    {
        a[1] = i;
        int r = 1;
        for(int j = 2;j<=n+1;j++)//递推
        {
            a[j] = b[j-1] - a[j-1] - a[j-2];
            if(a[j]<0||a[j]>1)//雷数非法
            {
                r = 0;
                break;
            }
        }
        if(r && a[n+1] == 0)
            count++;
    }
    cout<<count;
    return 0;
}

2. 二进制枚举

2.1 子集

题目链接:

78. 子集 - 力扣(LeetCode)

算法原理

这个题目其实很像高中数学学的组合数学,即从一堆数种选多个数,问有多少种选法

那么这里我们引入一个新的算法技巧,即二进制枚举

在这里,nums有1,2,3 三个数,我们对于这三个数中的每一个数,要么选,要么不选

我们用1表示选,0表示不选

这个三个1和0组成的序列我们可以当作是某个数的二进制,如果我们观察从0到7,他们的二进制序列其实正好对应这所有的选择情况,因此这就是二进制序列

而这是三个数,2的3次方是8,从1到8-1的序列正好对应其所有的选择情况

因此我们可以把二进制枚举当作一个巧合,一个天才之举,巧妙地把0和1当作选和不选的情况

所以我们枚举从0 到2^n - 1的所有数的二进制枚举,把该数二进制的每一位当作选择的情况即可

实操代码

cpp 复制代码
class Solution {
public:
    vector<vector<int>> subsets(vector<int>& nums) {
        int n = nums.size();
        vector<vector<int>> ret;
        for(int i = 0;i<(1<<n);i++)//枚举从0到2^n - 1的数
        {
            vector<int> tmp;
            for(int j = 0;j<n;j++)
            {
                if((i>>j) & 1)//判断二进制的情况,如果是1,看作选择了对应这个位置的数
                    tmp.push_back(nums[j]);
            }
            ret.push_back(tmp);
        }
        return ret;
    }
};

2.2 费解的开关

题目链接:

P10449 费解的开关 - 洛谷

算法原理

由于又是选与不选,因此我们也可以往二进制枚举那思考

我们先对第一行处理,我随便更改某些灯的状态

如果我们要使所有的灯都是亮着的,那么我们最起码得要前几行的灯是亮着的,那么第一行的灯就必须得亮着

所以我们对于第二行的改变只能是0下面的那些位置,这样就能让第一行的0变成1

同时你也不能改变其他的位置,因为这样就会更改第一行的1

以此类推,第三行,第四行,第五行的更改方式就是要把上一行的灯全变亮

所以当我们枚举,确定了第一行的更改状态,就会确定后面所有行的更改情况

最后确定第五行的灯是不是全亮的,以及操作次数是不是小于等于6的即可

到了具体代码上,我选择的是让一个数的二进制位表示这些灯的表示情况,由于有5行灯,为了防止越界访问,我们创造一个大小为6的数组 a[6],a[i]的二进制序列表示第i + 1行的灯的开关情况

这里也有个小细节,如果更改最左边的灯的情况,那么对应到二进制中,会改变第6位的数字,然而一行不存在6个灯。因此我们需要每一行修改后,a[i] &= (1<<5) - 1,这样会将第6位的数字改为0

实操代码

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

int main()
{
    int group; cin >> group;
    while (group--)
    {
        vector<int> a(6);//用一个数字的二进制表示每一行灯的 0 1状况
        for (int j = 0; j < 5; j++)
        {
            for (int i = 1; i <= 5; i++)
            {
                char n; cin >> n;
                if (n - '0' == 1)
                    a[j] |= (1 << (5 - i));
            }
        }
        int min = 0xffffff;
        for (int i = 0; i < (1 << 5); i++)
        {
            vector<int> b = a;
            int push = i;//i的二进制序列的1表示选择,0表示不选,用push存储
            int count = 0;
            for (int j = 0; j < 5; j++)
            {
                for (int n = 0; n < 5; n++)
                {
                    if (push & (1 << n))
                        count++;//统计改变次数
                }
                b[j] = b[j] ^ push ^ (push << 1) ^ (push >> 1);//改变这一行的状态
                b[j] &= ((1 << 5) - 1);
                b[j + 1] = b[j + 1] ^ push;//改变下一行的状态
                push = b[j] ^ ((1 << 5) - 1);
            }
            if (b[4] == ((1 << 5) - 1) && count < min && count <= 6)
                min = count;
        }
        if (min == 0xffffff)
            cout << "-1" << endl;
        else
            cout << min << endl;
    }
    return 0;
}
相关推荐
9ilk38 分钟前
【C++】--- 类型转换
开发语言·c++
雨季66639 分钟前
蓝桥杯试题及详解文档:统计子矩阵的和等于目标值的数量
算法
ULTRA??44 分钟前
C++两个数组连接类似python的list相加
c++·python
MicroTech202544 分钟前
微算法科技(NASDAQ MLGO)采用混合深度学习赋能区块链:打造智慧城市安全新范式
科技·深度学习·算法
宵时待雨1 小时前
C语言笔记归纳17:数据的存储
c语言·开发语言·笔记
Yupureki1 小时前
《算法竞赛从入门到国奖》算法基础:入门篇-前缀和
c语言·数据结构·c++·算法·1024程序员节
啊吧怪不啊吧1 小时前
算法王冠上的明珠——动态规划之路径问题(第一篇)
大数据·算法·贪心算法·动态规划
承渊政道1 小时前
C++学习之旅【C++类和对象(中)】
c语言·c++·visual studio
崇山峻岭之间1 小时前
C++ Prime Plus 学习笔记037
c++·笔记·学习