【基础算法】枚举(普通枚举、二进制枚举)

文章目录

  • 一、普通枚举
    • [1. 铺地毯](#1. 铺地毯)
      • [(1) 解题思路](#(1) 解题思路)
      • [(2) 代码实现](#(2) 代码实现)
    • [2. 回文日期](#2. 回文日期)
    • [3. 扫雷](#3. 扫雷)
      • [(2) 解题思路](#(2) 解题思路)
      • [(2) 代码实现](#(2) 代码实现)
  • 二、二进制枚举
    • [1. 子集](#1. 子集)
      • [(1) 解题思路](#(1) 解题思路)
      • [(2) 代码实现](#(2) 代码实现)
    • [2. 费解的开关](#2. 费解的开关)
      • [(1) 解题思路](#(1) 解题思路)
      • [(2) 代码实现](#(2) 代码实现)
    • [3. even parity](#3. even parity)
      • [(1) 解题思路](#(1) 解题思路)
      • [(2) 代码实现](#(2) 代码实现)

一、普通枚举

顾名思义,就是把所有情况全部罗列出来,然后找到符合题目要求的那一个,因此它是一种纯暴力的算法。

一般情况下,枚举策略都是会超时的。此时要先根据题目的数据范围来判断暴力枚举是否可以通过。 如果不行的话,就要考虑用其他各种算法来进行优化(比如⼆分,双指针,前缀和与差分等)。

使用枚举策略时,重点思考枚举的对象(枚举什么),枚举的顺序(正序还是逆序),以及枚举的方式(普通枚举?⼆进制枚举?递归枚举?)

1. 铺地毯

【题目链接】

P1003 [NOIP 2011 提高组\] 铺地毯 - 洛谷](https://www.luogu.com.cn/problem/P1003)

【题目描述】

为了准备一个独特的颁奖典礼,组织者在会场的一片矩形区域(可看做是平面直角坐标系的第一象限)铺上一些矩形地毯。一共有 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 + gb <= y <= b + k


(2) 代码实现

cpp 复制代码
#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 普及组\] 回文日期 - 洛谷](https://www.luogu.com.cn/problem/P2010)

【题目描述】

在日常生活中,通过年、月、日这三个要素可以表示出一个唯一确定的日期。

牛牛习惯用 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 天。

一个年份是闰年当且仅当它满足下列两种情况其中的一种:

  1. 这个年份是 4 4 4 的整数倍,但不是 100 100 100 的整数倍;
  2. 这个年份是 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) 代码实现

cpp 复制代码
#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\] 扫雷 - 洛谷](https://www.luogu.com.cn/problem/P2327)

【题目描述】

相信大家都玩过扫雷的游戏。那是在一个 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. 两数组的下标可以从 1 开始计数 ,因为当第一个位置确定之后我们是从第二个位置开始枚举的,如果从下标为 0 开始计数则对应 a[1],计算时的 a[i - 2] 会发生越界的情况。所以从下标为 1 开始计数可以有效避免这种情况。

  2. 如果一共有 n 个数,那么循环遍历要遍历到下标 n + 1 处,当 a[n + 1] 不为 0 的时候,同样也是不合法的情况。比如当第二列是数字 1, 3 时,实际上是不合法的,这个时候我们可以通过计算 a[3] 的位置是否为 0 来判断。


(2) 代码实现

cpp 复制代码
#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) 代码实现

cpp 复制代码
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
11100

11101
11101
11110
11111
11111

01111
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. 枚举出第一行的按法。(由于灯只有 1 和 0 两种状态,因此使用二进制枚举)
  2. 根据第一行的按法,计算出第一行和第二行被按之后的新状态。
  3. 根据第一行的结果,推导出第二行的按法,第三、四、五行同理。
  4. 按完最后一行,判断所有灯是否全亮。

  • 如何枚举出第一行所有的按法?

由于每一行有五盏灯,所以二进制枚举的时候只需从 00000 枚举到 11111( 2 5 − 1 2^5 - 1 25−1)即可。0 代表这个位置不按,1 代表按。

  • 对于一个二进制数(按法),如何计算出有多少个 1(按了多少次)?

只需下面的方式即可:

cpp 复制代码
// 计算二进制数中 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) 代码实现

cpp 复制代码
#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) 代码实现

cpp 复制代码
#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 则不合法,返回-1
        if(((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;
}
相关推荐
Rocky4015 分钟前
JAVAEE->多线程:锁策略
java·开发语言·jvm
C137的本贾尼11 分钟前
(每日一道算法题)二叉树剪枝
算法·机器学习·剪枝
JSUITDLWXL31 分钟前
ideal2022.3.1版本编译项目报java: OutOfMemoryError: insufficient memory
java·开发语言
magic 24534 分钟前
Java建造者模式(Builder Pattern)详解与实践
java·开发语言·建造者模式
前端小崔36 分钟前
前端面试题之ES6保姆级教程
开发语言·前端·javascript·面试·职场和发展·ecmascript·es6
Love__Tay1 小时前
【学习笔记】Python金融基础
开发语言·笔记·python·学习·金融
Lilith的AI学习日记1 小时前
什么是预训练?深入解读大模型AI的“高考集训”
开发语言·人工智能·深度学习·神经网络·机器学习·ai编程
BUG收容所所长2 小时前
栈的奇妙世界:从冰棒到算法的华丽转身
前端·javascript·算法
wangjinjin1802 小时前
Python Excel 文件处理:openpyxl 与 pandas 库完全指南
开发语言·python
愚润求学2 小时前
【C++】类型转换
开发语言·c++