【基础算法】枚举(普通枚举、二进制枚举)
文章目录
- 一、普通枚举
- 1. 铺地毯
- (1) 解题思路
- (2) 代码实现
- 2. 回文日期
- (1) 解题思路
- 思路一:暴力枚举
- 思路二:枚举年份
- 思路三:枚举月日
- (2) 代码实现
- 3. 扫雷
- (2) 解题思路
- (2) 代码实现
- 二、二进制枚举
- 1. 子集
- (1) 解题思路
- (2) 代码实现
- 2. 费解的开关
- (1) 解题思路
- (2) 代码实现
- 3. even parity
- (1) 解题思路
- (2) 代码实现
一、普通枚举
顾名思义,就是把所有情况全部罗列出来,然后找到符合题目要求的那一个,因此它是一种纯暴力的算法。
一般情况下,枚举策略都是会超时的。此时要先根据题目的数据范围来判断暴力枚举是否可以通过。 如果不行的话,就要考虑用其他各种算法来进行优化(比如⼆分,双指针,前缀和与差分等)。
使用枚举策略时,重点思考枚举的对象(枚举什么),枚举的顺序(正序还是逆序),以及枚举的方式(普通枚举?⼆进制枚举?递归枚举?)。
1. 铺地毯
【题目链接】
[P1003 NOIP 2011 提高组] 铺地毯 - 洛谷
【题目描述】
为了准备一个独特的颁奖典礼,组织者在会场的一片矩形区域(可看做是平面直角坐标系的第一象限)铺上一些矩形地毯。一共有 n n n 张地毯,编号从 1 1 1 到 n n n。现在将这些地毯按照编号从小到大的顺序平行于坐标轴先后铺设,后铺的地毯覆盖在前面已经铺好的地毯之上。
地毯铺设完成后,组织者想知道覆盖地面某个点的最上面的那张地毯的编号。注意:在矩形地毯边界和四个顶点上的点也算被地毯覆盖。
【输入格式】
输入共 n + 2 n + 2 n+2 行。
第一行,一个整数 n n n,表示总共有 n n n 张地毯。
接下来的 n n n 行中,第 i + 1 i+1 i+1 行表示编号 i i i 的地毯的信息,包含四个整数 a , b , g , k a ,b ,g ,k a,b,g,k,每两个整数之间用一个空格隔开,分别表示铺设地毯的左下角的坐标 ( a , b ) (a, b) (a,b) 以及地毯在 x x x 轴和 y y y 轴方向的长度。
第 n + 2 n + 2 n+2 行包含两个整数 x x x 和 y y y,表示所求的地面的点的坐标 ( x , y ) (x, y) (x,y)。
【输出格式】
输出共 1 1 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 1 1 号地毯用实线表示, 2 2 2 号地毯用虚线表示, 3 3 3 号用双实线表示,覆盖点 ( 2 , 2 ) (2,2) (2,2) 的最上面一张地毯是 3 3 3 号地毯。
【数据范围】
对于 30 % 30\% 30% 的数据,有 n ≤ 2 n \le 2 n≤2。
对于 50 % 50\% 50% 的数据, 0 ≤ a , b , g , k ≤ 100 0 \le a, b, g, k \le 100 0≤a,b,g,k≤100。
对于 100 % 100\% 100% 的数据,有 0 ≤ n ≤ 10 4 0 \le n \le 10^4 0≤n≤104, 0 ≤ a , b , g , k ≤ 10 5 0 \le a, b, g, k \le {10}^5 0≤a,b,g,k≤105。noip2011 提高组 day1 第 1 1 1 题。
(1) 解题思路
枚举每一个地毯,判断该该点是否被这个地毯覆盖。注意枚举的时候最好逆序枚举。因为我们需要找的是最上面的地毯,如果顺序枚举的话那么必须要枚举完所有情况才能找到最上面的地毯,而逆序枚举的话所找到第一个覆盖的地毯一定是最上面的地毯。
在判断一个点是否被一个地毯覆盖的时候只需要用到简单的数学知识即可。假设地毯左下角点的坐标为 (a, b)
,由题意,这张地毯的右上角点的坐标为 (a + g, b + k)
。那么如果一个点 (x, y)
被覆盖,一定有 a <= x <= a + g
且 b <= y <= b + k
。
(2) 代码实现
#include<iostream>using namespace std;const int N = 1e4 + 10;
int a[N], b[N], g[N], k[N];
int n, x, y;// 寻找最上面的地毯并返回其下标,没找到返回-1
int find()
{for(int i = n; i >= 1; --i) // 逆序枚举每一个地毯{if(x >= a[i] && x <= a[i] + g[i]&& y >= b[i] && y <= b[i] + k[i]){return i;}}return -1;
}int main()
{cin >> n;for(int i = 1; i <= n; ++i)cin >> a[i] >> b[i] >> g[i] >> k[i];cin >> x >> y;cout << find();return 0;
}
2. 回文日期
【题目链接】
[P2010 NOIP 2016 普及组] 回文日期 - 洛谷
【题目描述】
在日常生活中,通过年、月、日这三个要素可以表示出一个唯一确定的日期。
牛牛习惯用 8 8 8 位数字表示一个日期,其中,前 4 4 4 位代表年份,接下来 2 2 2 位代表月份,最后 2 2 2 位代表日期。显然:一个日期只有一种表示方法,而两个不同的日期的表 示方法不会相同。
牛牛认为,一个日期是回文的,当且仅当表示这个日期的 8 8 8 位数字是回文的。现在,牛牛想知道:在他指定的两个日期之间(包含这两个日期本身),有多少个真实存在的日期是回文的。
一个 8 8 8 位数字是回文的,当且仅当对于所有的 i i i( 1 ≤ i ≤ 8 1 \le i \le 8 1≤i≤8)从左向右数的第 i i i 个数字和第 9 − i 9-i 9−i 个数字(即从右向左数的第 i i i 个数字)是相同的。
例如:
- 对于 2016 年 11 月 19 日,用 8 8 8 位数字 20161119 20161119 20161119 表示,它不是回文的。
- 对于 2010 年 1 月 2 日,用 8 8 8 位数字 20100102 20100102 20100102 表示,它是回文的。
- 对于 2010 年 10 月 2 日,用 8 8 8 位数字 20101002 20101002 20101002 表示,它不是回文的。
每一年中都有 12 12 12 个月份:
其中, 1 , 3 , 5 , 7 , 8 , 10 , 12 1, 3, 5, 7, 8, 10, 12 1,3,5,7,8,10,12 月每个月有 31 31 31 天; 4 , 6 , 9 , 11 4, 6, 9, 11 4,6,9,11 月每个月有 30 30 30 天;而对于 2 2 2 月,闰年时有 29 29 29 天,平年时有 28 28 28 天。
一个年份是闰年当且仅当它满足下列两种情况其中的一种:
- 这个年份是 4 4 4 的整数倍,但不是 100 100 100 的整数倍;
- 这个年份是 400 400 400 的整数倍。
例如:
- 以下几个年份都是闰年: 2000 , 2012 , 2016 2000, 2012, 2016 2000,2012,2016。
- 以下几个年份是平年: 1900 , 2011 , 2014 1900, 2011, 2014 1900,2011,2014。
【输入格式】
两行,每行包括一个 8 8 8 位数字。
第一行表示牛牛指定的起始日期。
第二行表示牛牛指定的终止日期。
保证 d a t e 1 \mathit{date}_1 date1 和 d a t e 2 \mathit{date}_2 date2 都是真实存在的日期,且年份部分一定为 4 4 4 位数字,且首位数字不为 0 0 0。
保证 d a t e 1 \mathit{date}_1 date1 一定不晚于 d a t e 2 \mathit{date}_2 date2。
【输出格式】
一个整数,表示在 d a t e 1 \mathit{date}_1 date1 和 d a t e 2 \mathit{date}_2 date2 之间,有多少个日期是回文的。
【示例一】
输入
20110101 20111231
输出
1
【示例二】
输入
20000101 20101231
输出
2
【说明/提示】
【样例说明】
对于样例 1,符合条件的日期是 20111102 20111102 20111102。
对于样例 2,符合条件的日期是 20011002 20011002 20011002 和 20100102 20100102 20100102。
【子任务】
对于 60 % 60 \% 60% 的数据,满足 d a t e 1 = d a t e 2 \mathit{date}_1 = \mathit{date}_2 date1=date2。
(1) 解题思路
最容易想到的,就是设置一个变量 cnt
记录回文日期的个数,枚举两个日期中间的所有数字,判断是否是回文日期,如果是,那么判断是否是一个合法的日期,如果合法那么 cnt++
。
在暴力枚举的过程中,我们实际上枚举了很多没有用的数字。通过观察可以发现,如果一个日期数字的前四位(也就是年份)确定了的话,那么它的后四位也就随之确定了。比如 2010 2010 2010,想要构成回文日期的话那么它的后四位必定是 0102 0102 0102。因此我们仅需枚举前四位(年份)再加判断是否合法即可。
枚举年份的情况有可能还是太多了,我们可以只枚举月日,即枚举后四位,那么年份也就确定了。而在这种情况下我们仅需最多枚举 365 / 366 种情况,将日月组成的数字倒过来拼成年份的数字,最终将组成的回文日期与题目给的日期作比较判断是否在题目给定的范围之内即可,大大减少了时间复杂度。
(2) 代码实现
#include<iostream>using namespace std;int date1, date2;
int day[] = {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};int main()
{cin >> date1 >> date2;int cnt = 0;for(int i = 1; i <= 12; i++){for(int j = 1; j <= day[i]; j++){int y = (j % 10) * 1000 + (j / 10) * 100 + (i % 10) * 10 + (i / 10); // 倒过来拼成年份int num = y * 10000 + i * 100 + j; // 拼成回文日期if(date1 <= num && num <= date2) cnt++; // 判断是否在题目给定的范围内}}cout << cnt;return 0;
}
3. 扫雷
【题目链接】
[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
(2) 解题思路
通过观察可以发现,当我们确定第一列第一个位置是否有雷时,那么第一列雷的排列情况也就全部确定了。所以,我们仅需枚举出第一列第一个位置放或不放地雷两种情况,然后检查这两种情况下的地雷分布是否合法。
具体来说,我们可以用两个数组 a[N]
和 b[N]
来保存第一列地雷分布情况(0 表示无地雷,1 表示有地雷)和第二列的数字。那么第一列的第 i
个位置是否有地雷就可以这样计算:a[i] = b[i - 1] - a[i - 2] - a[i - 1]
,如果 a[i]
小于 0 或大于 1,说明这种情况不合法,即不存在这种情况。
还需要注意两个细节问题:
-
两数组的下标可以从 1 开始计数,因为当第一个位置确定之后我们是从第二个位置开始枚举的,如果从下标为 0 开始计数则对应
a[1]
,计算时的a[i - 2]
会发生越界的情况。所以从下标为 1 开始计数可以有效避免这种情况。 -
如果一共有
n
个数,那么循环遍历要遍历到下标n + 1
处,当a[n + 1]
不为 0 的时候,同样也是不合法的情况。比如当第二列是数字 1, 3 时,实际上是不合法的,这个时候我们可以通过计算a[3]
的位置是否为 0 来判断。
(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 - 1] - a[i - 2];if(a[i] < 0 || a[i] > 1) return 0; // 不合法的情况}if(a[n + 1] == 0) return 1;else return 0; // n + 1 位置不为 0 也是不合法的
}int check2()
{a[1] = 1;for(int i = 2; i <= n + 1; i++){a[i] = b[i - 1] - a[i - 1] - a[i - 2];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 ans = 0;ans += check1(); // a[1] 不放地雷ans += check2(); // a[1] 放地雷cout << ans;return 0;
}
二、二进制枚举
二进制枚举:用一个数二进制表示中的 0/1 表示两种状态,从而达到枚举各种情况。
注:利用二进制枚举时,会用到⼀些位运算的知识。
多说无益,直接实战!
1. 子集
【题目链接】
78. 子集 - 力扣(LeetCode)
【题目描述】
给你⼀个整数数组 nums ,数组中的元素互不相同。返回该数组所有可能的⼦集(幂集)。 解集不能包含重复的⼦集。你可以按任意顺序返回解集。
【示例一】
输⼊
nums = [1,2,3]
输出
[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
(1) 解题思路
可以发现,数组 nums
中的数字都有选或不选两种情况,那么我们可以用 0 和 1 分别来表示不选和选这两种状态。通过二进制的方式来枚举出所有的子集。并且含有 n
个数的集合,其子集一共有 2 ^ n
个,即 1 << n
个。
于是,当我们有了一个二进制数比如 101 时,表示第 1,3 位要放到子集中,形成 [1, 3]
。那么如何用代码实现对应位的取舍呢?我们可以用一个变量 i
遍历二进制的每一位,如果第 i
位为 1,那么表示取,那么对应的 nums[i]
就存放在一个子集中。
判断第 i
位是否为 1:(对应的二进制数 >> i) & 1
。
原理就是把这个二进制数的第 i
位移到最低位,然后和 1 进行按位与操作,如果这个位是 1,那么结果就是 1,否则为 0。
十进制 | 二进制 | 子集 | 判断取舍(用 i 遍历二进制的每一位) |
---|---|---|---|
0 | 000 | [] | 所有位为 0,都不取 |
1 | 001 | [1] | i = 0: 001 >> 0 &1 = 1 (取) |
2 | 010 | [2] | i = 1: 010 >> 1 &1 = 1 (取) |
3 | 011 | [1,2] | i = 0, 1 为真 |
4 | 100 | [3] | i = 2: 100 >> 2 &1 = 1 (取) |
5 | 101 | [1,3] | i = 0, 2 为真 |
6 | 110 | [2,3] | i = 1, 2 为真 |
7 | 111 | [1,2,3] | 所有位为真,都取 |
(2) 代码实现
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++) // 枚举 0~(2^n - 1){vector<int> tmp; // 存储子集for(int i = 0; i < n; i++){// 如果 st 对应的二进制的第 i 位为 1,那么就放进子集中if((st >> i) & 1) tmp.push_back(nums[i]);}ret.push_back(tmp);}return ret;}
};
2. 费解的开关
【题目链接】
P10449 费解的开关 - 洛谷
【题目描述】
你玩过“拉灯”游戏吗?
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 1110011101 11101 11110 11111 1111101111 11111 11111 11111 11111
输出
3 2 -1
【说明/提示】
测试数据满足 0 < n ≤ 500 0 < n \le 500 0<n≤500。
(1) 解题思路
通过思考我们可以发现,按灯这样的操作具有以下三点 性质:
- 每一盏灯,最多只会按一次
这是因为一盏灯只有开或者关两个状态,所以当一盏灯被按两次的时候,与它不被按的情况是等价的;被按三次的时候,与它被按一次是等价的,以此类推。
-
按灯的先后顺序,不会影响最终的结果
-
第一行的按法确定了之后,后续灯的按法就跟着确定了
因为当第一行按了以后,假如说变成 00101,那么第二行只能去按第1, 2, 4 个位置才能使第一行的三个 0 变成 1,后面第三行和第四行是影响不到第一行的状态的。所以第一行确定了,那么其余行也就确定了。
有了这三点性质之后,我们就可以确定我们的解题思路了:
- 枚举出第一行的按法。(由于灯只有 1 和 0 两种状态,因此使用二进制枚举)
- 根据第一行的按法,计算出第一行和第二行被按之后的新状态。
- 根据第一行的结果,推导出第二行的按法,第三、四、五行同理。
- 按完最后一行,判断所有灯是否全亮。
- 如何枚举出第一行所有的按法?
由于每一行有五盏灯,所以二进制枚举的时候只需从 00000 枚举到 11111( 2 5 − 1 2^5 - 1 25−1)即可。0 代表这个位置不按,1 代表按。
- 对于一个二进制数(按法),如何计算出有多少个 1(按了多少次)?
只需下面的方式即可:
// 计算二进制数中 1 的个数
int calc(int x)
{ int cnt = 0;while(x){cnt++;x &= x - 1;}return cnt;
}
可以在草稿纸上试一下,按位与几次就会拿掉几个 1。
- 如何存储灯的初始状态?
我们可以用二进制来存储每一行的灯的开关状态,比如第一行的灯是 00110,那么我们只需要在一个一维数组中的第一位存储一个 00110 这个二进制数即可。
这里还有一个小技巧,就是我们可以把 1 转化成 0,0 转化成 1,这样反过来存储,问题就变成了把灯全部变成 0 要多少步,这样的话,当我们判断是否是全灭的时候,仅需看最后一行对应存储的值是否为 0 即可。当然这样转化还有其他好处,一会儿会提到。
- 如何根据一个按灯方式
push
,计算出当前行a[i]
以及下一行a[i + 1]
被按之后的状态?
对于 a[i]
行来说,我们可以用位运算快速计算得到。对于一行灯的状态 a[i]
如 10111,给定一种按灯方式 push 如 00100,我们仅需使用 a[i] = a[i] ^ push ^ (push << 1) ^ (push >> 1)
就可以让对应位置及其左右两边的灯的状态改变,因为 1、0 这两个数异或 1 就可以实现 1 变 0,0 变 1。
但是这里有一个小小的问题的,就是 push << 1
可能会让第 5 位变成 1,这是一个非法的位置,可能会影响后续的判断,因此我们需要截断高位。这个时候只需让计算后的 a[i]
按位与上 111110 即可,即 (1 << 5) - 1
。
所以计算 a[i]
的公式为:a[i] = a[i] ^ push ^ (push << 1) ^ (push >> 1) & ((1 << n) - 1))
。
计算第 a[i + 1]
行就比较简单了,只需要修改 push 中对应为 1 的位置,不需要管左右,易得 a[i + 1] = a[i + 1] ^ push
。
- 如何求下一行怎么按?
我们的问题已经转化成了变成全灭,因此我们的 a[i + 1]
的需要按的位置只能是上一行 a[i]
亮着的位置,恰好就是 a[i]
,即 next_push = a[i]
。这也是将问题转化成全灭的好处之一。
(2) 代码实现
#include<iostream>
#include<cstring>using namespace std;const int N = 10;
int n = 5;
int a[N]; // 存储每一行灯的状态
int t[N]; // a数组的备份// 计算一个二进制数中有多少个 1
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 == '1') a[i] |= 1 << j;// 现在我们反着来存if(ch == '0') a[i] |= 1 << j;}}int ans = 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) ans = min(ans, cnt); // 如果能全灭,更新最小操作数}if(ans > 6) cout << -1 << endl;else cout << ans << endl;}return 0;
}
3. even parity
【题目链接】
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
(1) 解题思路
这道题与上一道题很相似,我们也可以发现当我们把第一行的改变情况确定了之后,那么后面的改变状态也随之确定。因此,还是可以枚举第一行所有的改变情况。
- 如何枚举出第一行所有的按法?
同上道题一样,我们从 0 枚举到 (1 << n) - 1
。但是需要注意的是,不是所有的情况都是合法的,因为这道题只能从 0 变成 1,因此我们还需要判断每一行的最终情况是否合法。如果都是 0 变 1,则合法,计数;如果不合法,跳出本次循环,枚举下一个状态。
- 如何根据一个改变方式
change
,计算出下一行a[i + 1]
的状态?
如果一个 a[i]
的某一个位置被改变后,我们需要计算 a[i + 1]
对应的位置,也就是 a[i]
的正下方,需要这个位置的上下左右数字之和为偶数,也就是上下左右的 0 的个数为偶数或者 1 的个数为偶数。由异或的性质可知,这个位置的上下左右的异或结果为 0,求下方的数实际上就是上左右三个数的异或结果。所以有 a[i + 1] = a[i - 1] ^ (a[i] << 1) ^ (a[i] >> 1)
。
(2) 代码实现
#include<iostream>
#include<cstring>using namespace std;const int N = 20;
int n;
int a[N];
int t[N];int calc(int x, int y) // t[i], change
{int sum = 0;for(int j = 0; j < n; j++){// 如果 t[i] 的第 j 位是 1 而 change 的第 j 位是 0 则不合法,返回-1if(((x >> j) & 1) == 1 && ((y >> j) & 1) == 0) return -1;// 如果 t[i] 的第 j 位是 0 而 change 的第 j 位是 1 则合法,并统计一次次数if(((x >> j) & 1) == 0 && ((y >> j) & 1) == 1) sum++;}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; // 记录当前情况下的最小操作数bool flag = true;for(int i = 1; i <= n; i++){int c = calc(t[i], change); // 判断是否合法,若合法则计算操作次数if(c == -1) // 如果不合法{flag = false;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;
}
相关文章:

【基础算法】枚举(普通枚举、二进制枚举)
文章目录 一、普通枚举1. 铺地毯(1) 解题思路(2) 代码实现 2. 回文日期(1) 解题思路思路一:暴力枚举思路二:枚举年份思路三:枚举月日 (2) 代码实现 3. 扫雷(2) 解题思路(2) 代码实现 二、二进制枚举1. 子集(1) 解题思路(2) 代码实现 2. 费解的…...

智能对联网页小程序的仓颉之旅
#传统楹联遇上AI智能体:我的Cangjie Magic开发纪实 引言:一场跨越千年的数字对话 "云对雨,雪对风,晚照对晴空"。昨天晚上星空璀璨,当我用仓颉语言写下第一个智能对联网页小程序的Agent DSL代码时࿰…...
Go字符串切片操作详解:str1[:index]
在Go语言中,return str1[:index] 是一个字符串切片操作,它截取字符串的一部分。让我们深入解析这个操作的含义和原理: 基本语法和含义 str1:原始字符串[:index]:切片操作符str1[:index]: 起始…...
JavaScript 本地存储 (localStorage) 完全指南
文章目录 JavaScript 本地存储 (localStorage) 完全指南 🔐一、什么是 localStorage?💡二、如何使用 localStorage?🔧1. 存储数据2. 读取数据3. 删除数据4. 清空所有数据 三、存储对象和数组的技巧 🎨1. 存…...
从golang的sync.pool到linux的slab分配器
最近学习golang的时候,看到golang并发编程中有一个sync.pool,即对象池,猛地一看这不跟linux的slab分配器类似嘛,赶紧学习记录下 这里先总结下设计sync.pool和slab的目的 sync.pool 为了缓解特定类型的对象频繁创建和销毁&#x…...

Python分形几何可视化—— 复数迭代、L系统与生物分形模拟
Python分形几何可视化—— 复数迭代、L系统与生物分形模拟 本节将深入探索分形几何的奇妙世界,实现Mandelbrot集生成器和L系统分形树工具,并通过肺部血管分形案例展示分形在医学领域的应用。我们将使用Python的NumPy进行高效计算,结合Matplo…...

【超详细】英伟达Jetson Orin NX-YOLOv8配置与TensorRT测试
文章主要内容如下: 1、基础运行环境配置 2、Torch-GPU安装 3、ultralytics环境配置 4、Onnx及TensorRT导出详解 5、YOLOv8推理耗时分析 基础库版本:jetpack5.1.3, torch-gpu2.1.0, torchvision0.16.0, ultralytics8.3.146 设备的软件开发包基础信息 需…...

Go语言学习-->项目中引用第三方库方式
Go语言学习–>项目中引用第三方库方式 1 执行 go mod tidy 分析引入的依赖有没有正常放在go.mod里面 找到依赖的包会自动下载到本地 并添加在go.mod里面 执行结果: 2 执行go get XXXX(库的名字)...
Vue Fragment vs React Fragment
文章目录 前言🧩 一、概念对比:Vue Fragment vs React Fragment📦 二、使用示例对比✅ Vue 3 中使用 Fragment✅ React 中使用 Fragment 🔍 三、差异解析1. **使用方式**2. **传递属性(如 key)**3. **插槽系…...
【LRU】 (最近最少使用)
LRU (最近最少使用) 文章目录 LRU (最近最少使用)一、LRU是什么?二、实现1.常规算法2.双栈更替总结 一、LRU是什么? LRU(Least Recently Used)是一种常见的缓存淘汰策略,核心思想是 “淘汰最长时间未被使用的缓存数据…...

每日Prompt:云朵猫
提示词 仰视,城镇的天空,一片形似猫咪的云朵,用黑色的简笔画,勾勒出猫咪的形状,可爱,俏皮,极简...

AI浪潮下的IT行业:威胁、转变与共生之道
目录 前言1 AI在IT行业的具体应用场景1.1 软件开发中的AI助手1.2 运维与监控的智能化1.3 测试自动化与质量保障1.4 安全防护中的智能威胁识别 2 AI对IT从业者的实际影响2.1 工作内容的结构性变化2.2 技能结构的再平衡 3 IT从业者不可替代的能力与价值3.1 复杂系统的架构与抽象能…...

基于功能基团的3D分子生成扩散模型 - D3FG 评测
D3FG 是一个在口袋中基于功能团的3D分子生成扩散模型。与通常分子生成模型直接生成分子坐标和原子类型不同,D3FG 将分子分解为两类组成部分:官能团和连接体,然后使用扩散生成模型学习这些组成部分的类型和几何分布。 一、背景介绍 D3FG 来源…...
Python Cookbook-7.12 在 SQLite 中储存 BLOB
任务 想将 BLOB 存入一个 SQLite 数据库, 解决方案 Python的 PySQLite 扩展提供了 sqlite.encode 函数,它可帮助你在 SOLite 数据库中插入二进制串。可以基于这个函数编写一个小巧的适配器类: import sqlite,cPickle class Blob(object):自动转换二进制串def __init__(self…...

蓝耘服务器与DeepSeek的结合:引领智能化时代的新突破
🌟 嗨,我是Lethehong!🌟 🌍 立志在坚不欲说,成功在久不在速🌍 🚀 欢迎关注:👍点赞⬆️留言收藏🚀 🍀欢迎使用:小智初学…...

无人机光纤FC接口模块技术分析
运行方式 1. 信号转换:在遥控器端,模块接收来自遥控器主控板的电信号。 2.电光转换:模块内部的激光发射器将电信号转换成特定波长的光信号。 3.光纤传输:光信号通过光纤跳线传输。光纤利用全内反射原理将光信号约束在纤芯内进行…...
【LeetCode】3170. 删除星号以后字典序最小的字符串(贪心 | 优先队列)
LeetCode 3170. 删除星号以后字典序最小的字符串(中等) 题目描述解题思路java代码 题目描述 题目链接:3170. 删除星号以后字典序最小的字符串 给你一个字符串 s 。它可能包含任意数量的 * 字符。你的任务是删除所有的 * 字符。 当字符串还…...
Oracle 用户名大小写控制
Oracle 用户名大小写控制 在 Oracle 数据库中,用户名的默认大小写行为和精确控制方法如下: 一 默认用户名大小写行为 不引用的用户名:自动转换为大写 CREATE USER white IDENTIFIED BY oracle123; -- 实际创建的用户名是 "WHITE"…...

作为过来人,浅谈一下高考、考研、读博
写在前面 由于本人正在读博,标题中的三个阶段都经历过或正在经历,本意是闲聊,也算是给将要经历的读者们做个参考、排雷。本文写于2022年,时效性略有落后,不过逻辑上还是值得大家参考,若所述存在偏颇&#…...

立志成为一名优秀测试开发工程师(第十一天)—Postman动态参数/变量、文件上传、断言策略、批量执行及CSV/JSON数据驱动测试
目录 一、Postman接口关联与正则表达式应用 1.正则表达式解析 2.提取鉴权码。 二、Postman内置动态参数以及自定义动态参数 1.常见内置动态参数: 2.自定义动态参数: 3.“编辑”接口练习 三、图片上传 1.文件的上传 2.上传后内容的验证 四、po…...
Global Security Market知识点总结:主经纪商业务
在全球证券市场的复杂体系中,主经纪商业务(Prime Brokerage)占据着独特且关键的位置。这一业务为大型机构投资者提供了一系列至关重要的服务,极大地影响着金融市场的运作与发展。 一、主经纪商业务的定义 主经纪商业务是投资银行…...

算法练习-回溯
今天开始新的章节,关于算法中回溯法的练习,这部分题目的难度还是比较大的,但是十分锻炼人的思维与思考能力。 处理这类题目首先要注意几个基本点: 1.关于递归出口的设置,这是十分关键的,要避免死循环的产…...
AI代码助手需求说明书架构
AI代码助手需求说明书架构 #mermaid-svg-6dtAzH7HjD5rehlu {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-6dtAzH7HjD5rehlu .error-icon{fill:#552222;}#mermaid-svg-6dtAzH7HjD5rehlu .error-text{fill:#552222;s…...
PTC过流保护器件工作原理及选型方法
PTC过流保护器件 (Positive Temperature Coefficient,正温度系数热敏电阻)是一种过流保护元件,其工作原理基于电阻值随温度变化的特性。当电路正常工作时,PTC的阻值很小,电流可以顺畅通过;但当…...
RabbitMQ 在解决数据库高并发问题中的定位和核心机制
RabbitMQ 在解决数据库高并发问题中的定位和核心机制 它是间接但极其有效的解决方案,以下内容聚焦如何最大化发挥 RabbitMQ 的潜力: 一、核心机制落地强化方案 1. 精准的异步化切割 关键原则:区分 “必须同步” 和 “可异步” 操作 #merma…...
VSCode主题定制:CSS个性化你的编程世界
在今天的数字世界,编程环境已成为开发者的第二大脑,而主题正是个性化你的创意空间的关键。本文将指导你如何使用CSS自定义VSCode的主题,让你的IDE不仅功能强大,更具视觉个性。 思路分析 设计思路: 创建主色调基调和…...
Windows 下彻底删除 VsCode
彻底删除 VS Code (Visual Studio Code) 意味着不仅要卸载应用程序本身,还要删除所有相关的配置文件、用户数据、插件和缓存。这可以确保你有一个完全干净的状态,方便你重新安装或只是彻底移除它。 重要提示: 在执行以下操作之前,…...

一文带你入门Java Stream流,太强了,mysqldba面试题及答案
list.add(“世界加油”); list.add(“世界加油”); long count list.stream().distinct().count(); System.out.println(count); distinct() 方法是一个中间操作(去重),它会返回一个新的流(没有共同元素)。 Stre…...

FastAPI安全异常处理:从401到422的奇妙冒险
title: FastAPI安全异常处理:从401到422的奇妙冒险 date: 2025/06/05 21:06:31 updated: 2025/06/05 21:06:31 author: cmdragon excerpt: FastAPI安全异常处理核心原理与实践包括认证失败的标准HTTP响应规范、令牌异常的特殊场景处理以及完整示例代码。HTTP状态码选择原则…...

阿里云 RDS mysql 5.7 怎么 添加白名单 并链接数据库
阿里云 RDS mysql 5.7 怎么 添加白名单 并链接数据库 最近帮朋友 完成一些运维工作 ,这里记录一下。 文章目录 阿里云 RDS mysql 5.7 怎么 添加白名单 并链接数据库最近帮朋友 完成一些运维工作 ,这里记录一下。 阿里云 RDS MySQL 5.7 添加白名单1. 登录…...