枚举算法练习
1.枚举
1.1P1003 [NOIP 2011 提高组] 铺地毯
链接: link
题目描述
为了准备一个独特的颁奖典礼,组织者在会场的一片矩形区域(可看做是平面直角坐标系的第一象限)铺上一些矩形地毯。一共有 n 张地毯,编号从 1 到 n。现在将这些地毯按照编号从小到大的顺序平行于坐标轴先后铺设,后铺的地毯覆盖在前面已经铺好的地毯之上。地毯铺设完成后,组织者想知道覆盖地面某个点的最上面的那张地毯的编号。注意:在矩形地毯边界和四个顶点上的点也算被地毯覆盖。
输入格式
输入共 n+2 行。第一行,一个整数 n,表示总共有 n 张地毯。
接下来的 n 行中,第 i+1 行表示编号 i 的地毯的信息,包含四个整数 a,b,g,k,每两个整数之间用一个空格隔开,分别表示铺设地毯的左下角的坐标 (a,b) 以及地毯在 x 轴和 y 轴方向的长度。
第 n+2 行包含两个整数 x 和 y,表示所求的地面的点的坐标 (x,y)。
输出格式
输出共 1 行,一个整数,表示所求的地毯的编号;若此处没有被地.毯覆盖则输出 -1。
输入输出样例
输入
3
1 0 2 3
0 2 3 3
2 1 3 3
2 2
输出
3
输入
3
1 0 2 3
0 2 3 3
2 1 3 3
4 5
输出
-1
说明/提示
【样例解释 1】
如下图,1 号地毯用实线表示,2 号地毯用虚线表示,3 号用双实线表示,覆盖点 (2,2) 的最上面一张地毯是 3 号地毯。
【数据范围】
对于 30% 的数据,有 n≤2。
对于 50% 的数据,0≤a,b,g,k≤100。
对于 100% 的数据,有 0≤n≤104, 0≤a,b,g,k≤105
NOIP2011 提高组 day1 第 1 题。
1.1.1思路:
这道题使用暴力枚举不会超时,因为题目中要的是最上面那个覆盖的地毯,所以可以倒着枚举。
1.1.2代码
#include <iostream>
using namespace std;
const int N = 1e4 + 10;
struct node
{
int a, b;
int g, k;
}a[N];
int main()
{
int n;
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> a[i].a >> a[i].b >> a[i].g >> a[i].k;
}
int x, y;
cin >> x >> y;
for (int i = n; i >= 1; i--)
{
if (x >= a[i].a && y >= a[i].b && a[i].a + a[i].g >= x && a[i].b + a[i].k >= y)
{
cout << i << endl;
return 0;
}
}
cout << -1 << endl;
return 0;
}
1.2P2010 [NOIP 2016 普及组] 回文日期
链接:link
题目描述
在日常生活中,通过年、月、日这三个要素可以表示出一个唯一确定的日期。
牛牛习惯用 8 位数字表示一个日期,其中,前 4 位代表年份,接下来 2 位代表月份,最后 2 位代表日期。显然:一个日期只有一种表示方法,而两个不同的日期的表 示方法不会相同。
牛牛认为,一个日期是回文的,当且仅当表示这个日期的 8 位数字是回文的。现在,牛牛想知道:在他指定的两个日期之间(包含这两个日期本身),有多少个真实存在的日期是回文的。
一个 8 位数字是回文的,当且仅当对于所有的 i(1≤i≤8)从左向右数的第 i 个数字和第 9−i 个数字(即从右向左数的第 i 个数字)是相同的。
例如:
对于 2016 年 11 月 19 日,用 8 位数字 20161119 表示,它不是回文的。
对于 2010 年 1 月 2 日,用 8 位数字 20100102 表示,它是回文的。
对于 2010 年 10 月 2 日,用 8 位数字 20101002 表示,它不是回文的。
每一年中都有 12 个月份:
其中,1,3,5,7,8,10,12 月每个月有 31 天;4,6,9,11 月每个月有 30 天;而对于 2 月,闰年时有 29 天,平年时有 28 天。
一个年份是闰年当且仅当它满足下列两种情况其中的一种:
这个年份是 4 的整数倍,但不是 100 的整数倍;
这个年份是 400 的整数倍。
例如:
以下几个年份都是闰年:2000,2012,2016。
以下几个年份是平年:1900,2011,2014。
输入格式
两行,每行包括一个 8 位数字。
第一行表示牛牛指定的起始日期。
第二行表示牛牛指定的终止日期。
保证 date1和 date2都是真实存在的日期,且年份部分一定为 4 位数字,且首位数字不为 0。
保证 date1一定不晚于 date2
输出格式
一个整数,表示在 date1和 date2之间,有多少个日期是回文的。
输入输出样例
输入
20110101
20111231
输出
1
输入
20000101
20101231
输出
2
说明/提示
【样例说明】
对于样例 1,符合条件的日期是 20111102。
对于样例 2,符合条件的日期是 20011002 和 20100102。
【子任务】
对于 60% 的数据,满足 date1=date2
1.2.1思路:
利用枚举
策略一:
枚举x到y的所有数字,判断是否回文,如果回文,拆分成年月日,判断是否是合法日期即可。(最麻烦的一种方法,但也很锻炼代码能力)
时间复杂度:最多是从00000000-99999999 即1×108也是不会超时的
1.2.2代码
#include <iostream>
using namespace std;
// 存储每个月的天数,默认 2 月 28 天
int days[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
// 判断是否为闰年
bool isLeapYear(int year)
{
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
// 判断是否为回文数
bool isPalindrome(int num)
{
int original = num;
int reversed = 0;
while (num > 0)
{
reversed = reversed * 10 + num % 10;
num /= 10;
}
return original == reversed;
}
// 判断日期是否合法
bool isValidDate(int year, int month, int day)
{
if (month < 1 || month > 12) return false;
if (month == 2 && isLeapYear(year))
{
return day >= 1 && day <= 29;
}
return day >= 1 && day <= days[month];
}
int main()
{
int x, y;
cin >> x >> y;
int ret = 0;
for (int i = x; i <= y; i++)
{
if (isPalindrome(i))
{
int year = i / 10000;
int month = (i / 100) % 100;
int day = i % 100;
if (isValidDate(year, month, day))
{
ret++;
}
}
}
cout << ret << endl;
return 0;
}
策略二:
枚举年份,拆分成回文形式的日月,然后判断是否合法即可
时间复杂度:1 × 104
#include <iostream>
using namespace std;
int x, y, ret;
int days[] = {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
void Reverse(int& num)
{
int num1 = 0;
while(num > 0 )
{
num1 = num1 * 10 + num % 10;
num /= 10;
}
num = num1;
}
int main()
{
cin >> x >> y;
int a = x / 10000, b = y / 10000;
for(int i = a;i <= b ;i++)
{
int tmp = i;
Reverse(tmp);
int month = tmp / 100;
int day = tmp % 100;
if(month >= 1&& month <= 12&& day >= 1&& day <= days[month])
{
ret++;
}
}
cout << ret << endl;
return 0;
}
策略三:(最简单的一种方法)
用i表示月份,j表示日,用两层for循环来枚举,再转换成对应的回文年份,判断是否在x和y之间。
时间复杂度:12×31 ≈ 4*102
#include <iostream>
using namespace std;
int x, y;
int day[] = { 0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
int main()
{
cin >> x >> y;
int ret = 0;
for (int i = 1; i <= 12; i++)
{
for (int j = 1; j <= day[i]; j++)
{
int k = j % 10 * 1000 + j / 10 * 100 + i % 10 * 10 + i / 10;
int num = k * 10000 + i * 100 + j;
if (x <= num && num <= y) ret++;
}
}
cout << ret << endl;
return 0;
}
1.3P2327 [SCOI2005] 扫雷
链接: link
P2327 [SCOI2005] 扫雷
题目描述
相信大家都玩过扫雷的游戏。那是在一个 n × m n\times m n×m 的矩阵里面有一些雷,要你根据一些信息找出雷来。万圣节到了,“余”人国流行起了一种简单的扫雷游戏,这个游戏规则和扫雷一样,如果某个格子没有雷,那么它里面的数字表示和它 8 8 8 连通的格子里面雷的数目。现在棋盘是 n × 2 n\times 2 n×2 的,第一列里面某些格子是雷,而第二列没有雷,如下图:
由于第一列的雷可能有多种方案满足第二列的数的限制,你的任务即根据第二列的信息确定第一列雷有多少种摆放方案。
输入格式
第一行为 N N N,第二行有 N N N 个数,依次为第二列的格子中的数。( 1 ≤ N ≤ 10000 1\le N\le10000 1≤N≤10000)
输出格式
一个数,即第一列中雷的摆放方案数。
输入输出样例
输入2 1 1
输出
2
1.3.1思路:
因为每个格子只有两种情况:放雷或者不放雷,所以枚举第一格子雷的情况。
下标从1开始,防止出现负数。
1.3.2代码
#include <iostream>
using namespace std;
const int N = 1e4 + 10;
int a[N], b[N];
int n;
int check1()
{
a[1] = 0;
for (int i = 2; i <= n + 1; i++)
{
a[i] = b[i - 1] - a[i - 2] - a[i - 1];
if (a[i] < 0 || a[i] > 1) return 0;
}
if (a[n + 1] == 0)return 1;
else return 0;
}
int check2()
{
a[1] = 1;
for (int i = 2; i <= n + 1; i++)
{
a[i] = b[i - 1] - a[i - 2] - a[i - 1];
if (a[i] < 0 || a[i] > 1) return 0;
}
if (a[n + 1] == 0)return 1;
else return 0;
}
int main()
{
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> b[i];
}
int ret = 0;
ret += check1();
ret += check2();
cout << ret << endl;
return 0;
}
2.二进制枚举
2.1子集
链接: link
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2:
输入:nums = [0]
输出:[[],[0]]
提示:
1 <= nums.length <= 10
-10 <= nums[i] <= 10
nums 中的所有元素 互不相同
2.1.1代码
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums)
{
vector<vector<int>> ret;
int n = nums.size();
for(int st = 0;st < (1 << n); st++)
{
vector<int> tmp;
for(int i = 0;i < n;i++)
{
if((st>>i) & 1) tmp.push_back(nums[i]);
}
ret.push_back(tmp);
}
return ret;
}
};
2.2 P10449 费解的开关
链接: link
题目描述
你玩过“拉灯”游戏吗?
25 25 25 盏灯排成一个 5 × 5 5 \times 5 5×5 的方形。
每一个灯都有一个开关,游戏者可以改变它的状态。
每一步,游戏者可以改变某一个灯的状态。
游戏者改变一个灯的状态会产生连锁反应:和这个灯上下左右相邻的灯也要相应地改变其状态。
我们用数字 1 1 1 表示一盏开着的灯,用数字 0 0 0 表示关着的灯。
下面这种状态
10111
01101
10111
10000
11011
在改变了最左上角的灯的状态后将变成:
01111
11101
10111
10000
11011
再改变它正中间的灯后状态将变成:
01111
11001
11001
10100
11011
给定一些游戏的初始状态,编写程序判断游戏者是否可能在 6 6 6 步以内使所有的灯都变亮。
输入格式
第一行输入正整数 n n n,代表数据中共有 n n n 个待解决的游戏初始状态。
以下若干行数据分为 n n n 组,每组数据有 5 5 5 行,每行 5 5 5 个字符。
每组数据描述了一个游戏的初始状态。
各组数据间用一个空行分隔。
输出格式
一共输出 n n n 行数据,每行有一个小于等于 6 6 6 的整数,它表示对于输入数据中对应的游戏状态最少需要几步才能使所有灯变亮。
对于某一个游戏初始状态,若 6 6 6 步以内无法使所有灯变亮,则输出-1
。
输入输出样例
输入3 00111 01011 10001 11010 11100 11101 11101 11110 11111 11111 01111 11111 11111 11111 11111
输出
3 2 -1
说明/提示
测试数据满足 0 < n ≤ 500 0 < n \le 500 0<n≤500。
2.2.1思路
性质:
1.每一盏灯,最多只会按一次,对于一盏灯而言,只会有按或不按两种状态(因为一个灯按偶数次相当于没按,奇数次跟按一次结果是一样的)
2.按法的先后顺序,是不会影响最终结果的,不用关心按的顺序,只用关心按了什么。
3.第一行的按法确定之后,后续灯的按法就跟着确定了。
解法:
1.暴力枚举第一行所有的按法。
2.根据第一行的按法,计算出当前行以及下一行被按之后的结果;
3.根据第一行的结果,推导出第二行的按法;重复2-3过程。
4.直到按到最后一行,然后判断所有灯是否全亮。
如何实现
1.如何枚举出第一行所有的按法呢?
用二进制枚举所有的状态, 0~(1 << 5)-1
2.如何计算出,一共按了多少次?
求一个数的二进制序列中有几个 1 ,可以用lowbit即 x = x & (x-1)
3.用二进制表示,来存储灯的初始状态
存的时候,把0->1,把1->0,此时,题目就从全亮变成全灭(容易判断,只需判断最后一行是否为0)
4.如何根据push这个按法,计算出当前行a[i]以及下一行a[i+1]被按之后的状态?
根据位元算的知识,快速计算出被按之后的状态
a[i] = a[i] ˆ push ˆ (push >> 1) ˆ ((push << 1);
push << 1 有可能会让第 5 位变成 1 ,这一位是一个「非法」的位置,有可能影响
后续判断,我们要「截断高位」: (push << 1) ˆ ((1 << 5) − 1) ;
a[i] &= (1 << n) - 1;
下一行:当前行的 push 只会对下一行对应的位置做修改: a[i + 1] = a[i + 1] ˆ push ;
当前行怎么亮,下一行就怎么按,这样就可以把当前行亮的位置暗灭:
,注意此时的 a[i] 是被按了之后的状态。
nextpush = a[i]
最后判断最后一行是否全灭:此时反着存储的好处就体现了出来。
2.2.2代码
#include <iostream>
#include <cstring>
using namespace std;
const int N = 10;
int a[N];// 用二进制表示,来存储灯的状态
int n = 5;
int t[N];// 备份 a 数组
int calc(int x)
{
int cnt = 0;
while (x)
{
cnt++;
x &= x - 1;
}
return cnt;
}
int main()
{
int T;
cin >> T;
while (T--)
{
//多组数据,一定要注意清空之前的数据;
memset(a, 0, sizeof(a));
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
char ch; cin >> ch;
//反向存储
if (ch == '0') a[i] |= 1 << j;
}
}
int ret = 0x3f3f3f3f;
// 枚举第一行的所有按法
for (int st = 0; st < (1 << n); st++)
{
memcpy(t, a, sizeof(a)); // 备份原始状态
int push = st; // 当前行的按法
int cnt = 0; // 记录总按次数
for (int i = 0; i < n; i++)
{
cnt += calc(push);// 累加当前行的按次数
// 处理当前行的灯状态变化
t[i] = t[i] ^ push ^ (push << 1) ^ (push >> 1);
t[i] &= (1 << n) - 1;
//修改下一行的状态
t[i + 1] ^= push;
//下一行的状态
push = t[i];
}
// 如果最后一行全部熄灭,更新最小次数
if (t[n - 1] == 0) ret = min(ret, cnt);
}
if (ret > 6) cout << -1 << endl;
else cout << ret << endl;
}
return 0;
}
2.3 UVA11464 Even Parity
链接: link
UVA11464 Even Parity
题目描述
给你一个 n × n n \times n n×n 的 01 01 01 矩阵(每个元素非 0 0 0 即 1 1 1),你的任务是把尽量少的 0 0 0 变成 1 1 1,使得原矩阵便为偶数矩阵(矩阵中每个元素的上、下、左、右的元素(如果存在的话)之和均为偶数)。
输入格式
输入的第一行为数据组数 T T T( T ≤ 30 T \le 30 T≤30)。每组数据:第一行为正整数 n n n( 1 ≤ n ≤ 15 1 \le n \le 15 1≤n≤15);接下来的 n n n 行每行包含 n n n 个非 0 0 0 即 1 1 1 的整数,相邻整数间用一个空格隔开。
输出格式
对于每组数据,输出被改变的元素的最小个数。如果无解,输出 − 1 -1 −1。
输入输出样例
输入3 3 0 0 0 0 0 0 0 0 0 3 0 0 0 1 0 0 0 0 0 3 1 1 1 1 1 1 0 0 0
输出
Case 1: 0 Case 2: 3 Case 3: -1
2.3.1思路
性质:
1.针对每一个0,只有变1次或者不变两种情况,因此,每一个0都有两种状态。
2.改变的先后顺序,是不会影响最终结果的,不用关心顺序,只用关心哪些0最终被改变。
3.第一行的“最终结果”确定之后,后续行的“最终结果”就跟着确定了。
解法:
1.枚举 1 ∼ (1 << n) − 1 之间所有的数,「每⼀个数」就是第一行的最终状态;
2. 由于本题只能 0 变 1 ,所以我们还要「判断」每一行的最终状态 y 「是否合法」:很简单,比较初始状态 x 以及最终状态 y 中「二进制表示的每一位」,如果是 0 变 1 ,就是「合法」操作,计数;如果是 1 变 0 ,「非法」操作,直接「跳出本次循环」,枚举第一行的下一个状态;
3. 当前行的最终状态 a[i] 确定之后,如何「递推」下一行的最终状态 a[i + 1] :
规则是当前位置「上下左右」 1 的个数之和是「偶数」,根据「异或」运算「无进位相加」的特性,正好就是上下左右位置「异或」的结果是 0 。那么下一行对应位置的状态就是「当前行右移一位」与「当前行左移一位」与「上一行对应位置」异或的结果:
a[i + 1] = (a[i] >> 1) ˆ (a[i] << 1) ˆ a[i − 1]
其中 a[i] << 1 会造成不合法的位置是 1 的情况,注意「高位截断」:
(a[i] << 1) & ((1 << n) − 1)
2.3.2代码
#include <iostream>
#include <cstring>
using namespace std;
const int N = 20;
int a[N];
int t[N];
int n;
// 判断 x->y 是否合法
// 返回 -1,表示不合法
// 其余的数,表示合法,并且表示表示 0->1 的次数
int calc(int x, int y)
{
int sum = 0;
for (int i = 0; i < n; i++)
{
if (((x >> i) & 1) == 0 && ((y >> i) & 1) == 1) sum++;
if (((x >> i) & 1) == 1 && ((y >> i) & 1) == 0) return -1;
}
return sum;
}
int solve()
{
int ret = 0x3f3f3f3f;//记录最小的改变次数
// 枚举第一行的最终状态
for (int st = 0; st < (1 << n); st++)
{
memcpy(t, a, sizeof(a));
int change = st;
int cnt = 0;// 统计 0->1 的次数
bool flag = 1;
for (int i = 1; i <= n; i++)
{
// 先判断 change 是否合法
int c = calc(t[i], change);
if (c == -1)
{
flag = 0;
break;
}
cnt += c;// 累加次数
// 当前⾏的最终状态
t[i] = change;
//计算下一行的最终状态
change = t[i - 1] ^ (t[i] << 1) ^ (t[i] >> 1);
change &= (1 << n) - 1;
}
if (flag)
{
ret = min(ret, cnt);
}
}
if (ret == 0x3f3f3f3f) return -1;
else return ret;
}
int main()
{
int T;
cin >> T;
for (int k = 1; k <= T; k++)
{
// 多组测试数据,记得清空
memset(a, 0, sizeof(a));
cin >> n;
for (int i = 1; i <= n; i++)// 避免越界访问
{
for (int j = 0; j < n; j++)
{
int x; cin >> x;
if (x) a[i] |= 1 << j;
}
}
printf("Case %d: %d\n", k, solve());
}
return 0;
}