c++
// 思路:我们枚举第一行的点击方法,共32种,完成第一行的点击后,固定第一行,
// 从第一行开始递推,若达到第n行不全为0,说明这种点击方式不合法。
// 在所有合法的点击方式中取点击次数最少的就是答案。
// 对第一行的32次枚举涵盖了该问题的整个状态空间,因此该做法是正确的
//
// 时间复杂度:32*20*5*500
// 对第一行操作有32种可能 * 对前四行有20种操作可能 * 每一次操作都要改变5个灯的状态 * 最多读入的时候可能有500次light矩阵
//
// 最关键的两个性质
// 每一个位置最多只会被点击一次
// 如果固定了第一行,那么满足题意的点击方案最多只有一种
#include <iostream>
#include <vector>
#include <cstring>
#include <limits.h>
//定义全局变量时,需要赋值的时候要慎重考虑
using namespace std;
char light[10][10];
void turn(int x, int y) {
int dx[5] = {0, -1, 1, 0, 0}, dy[5] = {0, 0, 0, -1, 1};
for (int i = 0; i < 5; i ++ ) {
int a = x + dx[i], b = y + dy[i];
if (a >= 0 && a < 5 && b >= 0 && b < 5) {
light[a][b] ^= 1;
}
}
}
int work() { //无参数
int res = INT_MAX; //不能定义为全局变量,每运行一次work,就要重新设置res为最大变量
//遍历32个第一行的操作可能
for (int k = 0; k < 1 << 5; k ++ ) {
int cnt = 0; //不能定义为全局变量,每次新的第一行状态,都要重新开始计算cnt
char backup[10][10];
memcpy(backup, light, sizeof light); //备份,因为下面的操作会改变light
//第一行按灯一共有32种可能,对于每一种可能,我们操作选择后,开始固定,
//此时第一行不一定是全亮的状态,第一行只是32种操作可能的一种
//然后遍历前四行,如果light[i][j] == '0', 那么turn(i+1, j)
//这一步是枚举第一行的点击方法,只要第一行固定了,那么满足题意的点击方法就只有一种了。
//假如第一行是10101, k从0到31进行枚举,如果k = 11000,
//那么代表light矩阵中第一行的第一个和第二个灯要点击一下,
//第一行变为01101,
//之后固定这一行,改变下面的灯看是否能全变亮。
//这也就是为什么我们copy light, 每一次对k的枚举都会改变light。
for (int i = 0; i < 5; i ++ )
if (k >> i & 1) {
turn(0, i); //这里面已经改变了light[0][i]
cnt ++ ;
}
for (int i = 0; i< 4; i ++ )
for (int j = 0; j < 5; j ++ ) {
if (light[i][j] == '0') { //如果前一行 == ‘0’,那么改变下一行
turn(i+1, j);
cnt ++ ;
}
}
//用一个bool来表示最后一行的状态,是全亮的话bool就为true
bool is_successful = true;
for (int i = 0; i < 5; i ++ ) {
if (light[4][i] == '0') {
is_successful = false;
break;
}
}
//对于32种第一行的操作后,每一次或许可以让light的灯都变亮, 或许不可以
//每一次,我们都记录一下操作次数cnt,更新res,直到res是最小的操作数
if (is_successful == true) res = min(res, cnt);
memcpy(light, backup, sizeof light); //备份,使light变为最开始第一行未操作的状态
}
if (res <= 6) return res;
else return -1;
}
int main() {
int n;
cin >> n;
//读入
while (n -- ) {
for (int i = 0; i < 5; i ++ )
for (int j = 0; j < 5; j ++ )
cin >> light[i][j];
cout << work() << endl;
}
return 0;
}
非常感谢楼主。之前一直想不通第0行的那段代码到底是什么意思。找了好几分题解,翻到这里终于开窍了。我也说下我的理解,为后来者多提供一份参考。第0行,是基准行,后面行都要根据固定好的第0行来进行递推,这一点相信大家都理解。问题是,第0行状态如何固定?其实就是一个暴力枚举所有可能的状态。循环中的k就是对第0行进行所有操作的暴力枚举。比如,如果k是二进制的00000,就表示不对第0行进行任何操作,那么就保持输入状态。如果k是11111,就是把第0行的5个灯依次都点一遍。依次类推,k从二进制00000循环到11111,就是对第0行的所有灯各自点还是不点进行了全部枚举。这样,针对每一种操作,第0行固定下来,就可以进行后续行的递推了。
你好,请问为什么第0行为什么是1的时候才按开关呢,不应该是0的时候按吗?因为只有是0的时候灯才是灭的吗?
以楼主代码为例,light数组代表的是灯的状态,0灭1开,其状态由turn这个函数改变。而最外层循环中的k,代表的是,我不管当前light数组第0行是什么状态,把所有可能的改变手段组合穷举一遍。k是对第0行灯的操作穷举,也就是对light第0行可以进行哪些改变,1代表翻转某灯,0代表不动。例如k是00000,代表没有任何操作。11100代表翻转第0行前3个灯状态。如此,每一个k,都代表了一种对第0行灯的改变方式,针对每一种第0行,就可以继续后面行。明白了这个,也就能解释你拷贝的问题。每次k循环,都是尝试一种第0行的改变方案,所以要先把light备份,第0行固定好了,就继续2345行的操作,k循环一轮,就是一个对所有灯改变的完整尝试结果。在进行新的k循环前,自然要把light数组恢复成原始状态。因为我们说的k的所有针对第0行的操作穷举,都是针对light原始状态进行的。
有一句话不确切,修改一下。应该说,我不管light第0行的初始状态是什么,都针对这个初始状态,把所有可能的改变手段组合穷举一遍
非常非常感谢哈哈哈!昨天在这个问题上想了好久也没想出来原因。
666我也懂了
tql,想了半天也没想懂,大佬一点就通
tql,感谢大佬答疑
你对k的这个讲解讲的很透彻,y总在视频里面只是一带而过,感觉其实这一部分才是最容易忽视的,从而导致了很多人没彻底理解。
tql,感谢大佬!!这个枚举32种情况的循环我已经想了两三天了,一直觉得第一行的状态不是输入进去了吗?这样枚举32种状态求出来的步数不就不一定是输入数据说对应的第一行了吗,而且为什么在枚举出来1的时候要按0的时候不按。大佬一点全懂了,orzorz!!
同感,看y总视频那里总是讲的不仔细,然后就这里不懂
for (int i = 0; i < 5; i ) //这里只是对第一行的所有操作可能,1为操作,或者0为操作都行,对第一行共32种操作可能,改成!也是可以的,只要枚举出32操作方案就行
if (!(k >> i & 1)) {
turn(0, i); //这里面已经改变了light[0][i]
cnt ;
}
//这一步是枚举第一行的点击方法,只要第一行固定了,那么满足题意的点击方法就只有一种了。
//假如第一行是10101, k从0到31进行枚举,如果k = 11000,
//那么代表light矩阵中第一行的第一个和第二个灯要点击一下,
//第一行变为01101,
//之后固定这一行,改变下面的灯看是否能全变亮。
//这也就是为什么我们copy light, 每一次对k的枚举都会改变light。
for (int i = 0; i < 5; i )
if (k >> i & 1) {
turn(0, i); //这里面已经改变了light[0][i]
cnt ;
}
大佬有个问题,这里turn按您的说法应该是turn(0,4-i)。不过不影响结果就是了,从前面往后改,和从后往前改是一样的
非常感谢楼主,刚开始想着为什么递推一定是正确的,但是看了楼主写的做题思路是把第一行的情况都枚举一遍终于懂了为啥递推是一定可以的,因为第一行的所有情况总有一种是可行的,然后接下来的递推是趋向于答案的
哎呦,不能定义为全局变量把我蒙住了
思路好清晰
已经清楚了,非常感谢
很清晰了,谢谢
答主的10101,k=11000,第一个灯和第二个灯都点一下结果不是01101吧?而是10001,按第一个灯的时候才是01101吧?
我也算了一下,是这样的呢
10101
dong le
%%%
orz
妙啊
对前4行有20种操作可能 是怎么得出来的呢?
你第一行情况确定下来,是不是有且只有0的位置的第二行的要操作一次。所以就是一行根据上一行的情况操作五次
tql!!!蟹蟹大佬
前面的那些写的都太深了,,,您的解释非常的详细!
你好,请问一下为什么需要拷贝两次数组,感觉拷贝一次就行了,每次将原数组拷贝到副本数组,然后直接对副本数组直接操作不就行了,但是我这样试了一下是错的。。。请问问题出在哪啊。
你瞅瞅turn里面是对什么操作的?
你这想法没错,不过这里是对原数组操作,操作完后再将副本数组(输入状态)复制到原数组
并不能把一个数组直接作为另一个数组的初始值,必须用遍历或字符串
在这里枚举的是操作,第一行状态已经输入了,枚举第一行操作然后针对操作来进行对第一行的状态进行开灯或者关灯,接下来再进行下面行的判断
请问一下假如第一行是10101, k从1到32进行枚举,如果k = 11000,那么要点击矩阵中第一行的第一个和第二个灯,第一次点后变为01101,第二个灯点后变为10101,与最初状态没有发生改变,这是为什么呢?
01101->10001
正解
十分感谢!