蓝桥杯算法学习纪实——费解的开关

95. 费解的开关 - AcWing题库

目录

一、初步分析

二、进一步优化

三、遇到的问题、收获的经验、心得

四、其他收获

五、利用二分支递归和二进制的相似性进一步优化

六、其他收获

七、感想 


一、初步分析

本道题可以理解为一个寻找最优解的问题,寻找步数花费最小的“按开关方案”。本题有25个开关,全面来说一共有 2^25 = 33,554,432 种方案,数量级为 10^7 。我们先将这个问题初步实现一下:

这道题有几个关键点:保存最小步数、按开关的连锁反应、计算一组方阵中使其全亮的步数、最小结果判定、多组数据输入。

        “保存最小步数”:每组数据都会有 2^5 种方案,有些能实现目标有些则不能,能实现的步数也有很多种,最后只取最小的。可设定一个变量,每次res依次迭代保存一组方阵中所有方案所需步数,即,每次只保存已枚举方案中的最小值,这个可以用#include <algorithm>中的min() 实现。
        “按开关的连锁反应”:这个按的动作可以用一个函数来实现,按一个开关,同时也改变上下左右的灯状态。
        “计算一组方阵中使其全亮所有方案的步数”:这点是本题的核心算法,可设定另一个变量step累加保存当前步数,按一次开关,step ++,直至当前方案结束后归零;如果最后发现该方案能使得灯全亮,保存最终步数到res中:res = min(step, res);,不能则不保存,进入下一个方案,step重新初始化为0。
       “最小结果判定”:在一组方阵的最小步数出来后,如果最小步数大于6,则输出 -1 。
       “多组数据输入”:while(T--)即可实现上述的重复操作。

现在的问题是:如何枚举所有 2^25 共 33,554,432 种方案。而这个就是指数型枚举的问题,即用递归树实现,每次递归的分支数为 2。以下是枚举函数的雏形:

void dfs(int u)//u从0开始,u=[0~24]
{
	if(u == 25)//触底条件
	{
		//根据完整的方案按完所有要按的灯泡
		//同时计算所用步数
		//保存能完成目标的步数,操作方案无需保存
		//回溯,枚举下一种情况,重复上述操作
	}
	else
	{
		//用数组记录此位置要按
		dfs(u+1);
		//用数组记录此位置不按
		dfs(u+1);
	};
}

二、进一步优化

但是,按照规律,C++代码操作次数控制在 10^7~10^8 为最佳,因后续还有"按开关函数"等操作,所以全部枚举很容易超时(不过也是可以试一试的,具体实现待后续有时间再讨论);故,枚举所有情况显然不行。

此时,就需要剪枝

确定最优解,肯定需要知道全部方案数的情况,从中找出最少步数。而类似数学里函数的最小值,我们也需要知道函数所有点的情况,但数学函数有个简洁的表示方法:“表达式”,例如:y = x^3+x^2+x 就很好的描述了该函数所包含所有点的位置;现在对其求给定区间的最小值,是无需比较它所有的点的,只需比较区间端点和区间内极小值点最多 3 个点,这是在研究了该数学函数的性质后得出的结论,减少了大量比较工作。

受此启发,本题的 33,554,432 种方案其实也无需全部考虑,可以先研究一下此问题的性质:

本题目标:
使灯泡全亮,得到使灯泡全亮的各种步数,最后取最小值。

本题性质:
连带改变灯泡状态->如果按行来看的话,每行的操作方式穷举完,有 32 种->在操作完一行后,此时紧跟着全亮的目标走,因为是穷举,先按完第一行的32种操作方式后(按,不按,按,不按,按……;或10101、00000、00001、00010、00011……),这一行灯泡未必都是亮的,因为可能有亮的被按灭(为了达成目标也可能需要这种操作,因为是连带效应),有灭的没有按;所以在操作第二行时,因为牵制关系或叫它连带效应,我们现在有了上一行的依据,就无需再“无脑”暴力枚举了,而是根据上一行此时的状态来判断现在这行同一列的开关是否需要按,依次类推,直至按完所有 5 行->因为第五行没有下一行,所以此时若第五行还有灭的灯泡,那就说明对应第一行“无脑”暴力枚举的方案往下走是无解的,而如果第五行根据上一行操作完后同时自己这一行也全亮了,就说明对应第一行“无脑”暴力枚举的方案往下走有解。

这里穷举完一行五个所有情况后,不再继续“无脑”暴力枚举,是因为发现了这题的性质:

每一行的操作,根据目标,完全由上一行灯的状态所决定。

一开始暴力枚举,是因为,为了实现目标,我们没有根据来指导做出第一步和下一步,不知道怎样的方案才能使得灯全被按亮,所以采用穷举所有方案的方式,逐一判断哪些可以,再取其中步数最小的;而第二行开始不再继续暴力枚举,是因为下一步有了根据根据本题的性质操作最终就能实现全局的目标。所以其中也有局部最优解的思想,即,贪心;同时,完成 5 次分支后,不再进行不必要的分支,这也是剪枝的思想。

故,本题只需要进行 5 次递归二分支,穷举第一行的32种情况(走一步算一步,后面有发现就做出改动)。每个分支底部得到一个五元序列,记录穷举的情况;到底部后据此操作第一行,到此有了第一行的状态后,按照性质递推操作第二到五行;第五行操作完后,判断此时第五行是否全亮,如果恰好全亮,说明达成目标,记录此时步数,与32种方案中的上一结果比较,保存最小的,直至32种枚举判断完毕。此时解出了一组矩阵的答案,判断输出后,通过while(T--)进入下一个矩阵的计算。据此可完善上述多分枝递归函数:

void dfs(int u)//u从0开始递归
{
	//出口一:底:u = 4:第一行第五个灯(0~4)
	if(u == 5)//第二行开始不用递归了,后面有用的方案已经确定,无需枚举判断无用的方案
	{
		memcpy(backup, g, sizeof(g));//备份初始用户输入
		int step = 0;
		//根据所有可能方案,操作第一行,共2^5种
		for(int i = 0; i < 5; i++)
		{
			if(option[i] == 1)//第1行第i个需要按
			{
				step ++;
				turn(0,i);
			}
		}
		//根据点亮所有灯这个目标,操作第二三四五行,有目的的选择性操作,贪心策略
		for(int i = 0; i < 4; i ++)//操作第二行需要以第一行的状态为标准,第五行没有下一行,所以遍历一到四行
		{
			for(int j = 0; j < 5; j ++)
			{
				if(g[i][j] == '0')//如果第一行还有灯是灭的,那么只用按下一行同一列的开关,三四五行以此类推
				{
					step ++;
					turn(i+1,j);
				}
			}
		}
		//判断方案是否可行
		bool dark = false;
		for(int i = 0; i < 5; i ++)
		{
			if(g[4][i] == '0')//如果第五行还有灭的,则该开关方阵无解
			{
				//res = -1;
				dark = true;
				break;
			}
		}
		if(!dark)//如果第五行全部已点亮,则全部灯泡点亮,保存步数取最小,若最小step>res的初值7,最终结果就为7
		{
			res = min(res,step);
			cout<<"该方案"<<k<<":"<<step<<"步;此时最小步数为:";
			cout<<res<<endl;
		}else{
			cout<<"该方案"<<k<<":"<<step<<"步"<<",不能实现全亮"<<endl;
		}
		k ++;
		memcpy(g,backup,sizeof backup);//恢复用户初始数据,进行下一次枚举
	}
	//出口二:对所在位置选择做哪一种操作:按、不按
	else
	{
		option[u] = 1;   //第u-1盏灯按
		dfs(u+1);       //对下一盏做选择
		
		option[u] = 0;   //第u-1盏灯不按
		dfs(u+1);       //对下一盏做选择
	}                   //(递归函数最终出口)
	//核验
	//if(res > 6) res = -1;
}

以下是完整代码:

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 6;
char g[N][N];       //坐标从0开始,多一位保存字符串的'\0'
char backup[N][N];  //执行质变算法前先做个备份,以保证其他情况的枚举:恢复现场
//int dxy[5][5]={{0,-1},{0,0},{-1,0},{0,1},{1,0}};//上边、原位、左边、下边、右边
int option[5];      //保存所有可能的方案,只需要第一行,其余剪去
int res = 7;        //保存最小结果,初始值必须大于6,否则影响最终结果
//全局变量
// void turn(int x, int y)
// {
// 	for(int i = 0; i < 5; i++)
// 	{
// 		int a = x + dxy[i][0];//依次取横坐标,移动
// 		int b = y + dxy[i][1];//依次取纵坐标,移动
// 		if(a < 0 || a >= 5 || b < 0 || b >= 5) continue;//范围[0~4]外的就跳过不操作
// 		//分别按5个在范围内的开关,0变1,1变0
// // 		if(g[a][b] == '1') g[a][b] = '0';
// // 		else g[a][b] = '1';
// 		//位运算优化
// 		g[a][b] ^= 1;       //与1异或值反转:与1相同为0,相异为1
// 	}
// }

int dx[5] = {-1, 0, 1, 0, 0}, dy[5] = {0, 1, 0, -1, 0};//偏移量优化
//二维数组使用的更慢
void turn(int x, int y)
{
	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) continue;  
		g[a][b] ^= 1;
	}
}
void dfs(int u)//u从0开始递归
{
	//出口一:底:u = 4:第一行第五个灯(0~4)
	if(u == 5)//第二行开始不用递归了,后面有用的方案已经确定,无需枚举判断无用的方案
	{
		memcpy(backup, g, sizeof(g));//备份初始用户输入
		int step = 0;
		//根据所有可能方案,操作第一行,共2^5种
		for(int i = 0; i < 5; i++)
		{
			if(option[i] == 1)//第1行第i个需要按
			{
				step ++;
				turn(0,i);
			}
		}
		//根据点亮所有灯这个目标,操作第二三四五行,有目的的选择性操作,贪心策略
		for(int i = 0; i < 4; i ++)//操作第二行需要以第一行的状态为标准,第五行没有下一行,所以遍历一到四行
		{
			for(int j = 0; j < 5; j ++)
			{
				if(g[i][j] == '0')//如果第一行还有灯是灭的,那么只用按下一行同一列的开关,三四五行以此类推
				{
					step ++;
					turn(i+1,j);
				}
			}
		}
		//判断方案是否可行
		bool dark = false;
		for(int i = 0; i < 5; i ++)
		{
			if(g[4][i] == '0')//如果第五行还有灭的,则该开关方阵无解
			{
				//res = -1;
				dark = true;
				break;
			}
		}
		if(!dark) res = min(res,step);//如果第五行全部已点亮,则全部灯泡点亮,保存步数取最小,若最小step>res的初值7,最终结果就为7
		memcpy(g,backup,sizeof backup);//恢复用户初始数据,进行下一次枚举
	}
	//出口二:对所在位置选择做哪一种操作:按、不按
	else
	{
		option[u] = 1;   //第u-1盏灯按
		dfs(u+1);       //对下一盏做选择
		
		option[u] = 0;   //第u-1盏灯不按
		dfs(u+1);       //对下一盏做选择
	}                   //(递归函数最终出口)
	//核验
	//if(res > 6) res = -1;
}

int main()
{
	int T;
	cin >> T;
	while(T --)
	{
		for(int i = 0; i < 5; i ++) cin >> g[i];   //写入用户数据
		dfs(0);
		//核验
		//只能dfs全部完成后再对res做判断
		if(res > 6) res = -1;
		cout<<res<<endl;
		res = 7;
	}
	return 0;
}

三、遇到的问题、收获的经验、心得

现在来理理我在这之中遇到的问题:

一、结果检查的位置有问题:
在递归函数末尾检查修改res,导致结果全为-1:这样会使得如果上一个分支结果大于  6,下一个分支在执行min(res, step)时,结果必为-1,使得本小于等于6的结果被屏蔽。
     其实在32种情况没结束前,都不需要对res做出题给的限制,最后输出前做一个特判就好了,res是用来迭代的,中间不能擅自修改它的值,因为后续的迭代都要参考前一次的结果(min函数)。
二、循环使用的变量,循环内无重新初始化操作
while(T--)循环内重复使用的变量res,进入下一次循环前没有初始化:这样导致如果前一组数据最终答案res=2,即<=6,循环计算下一组时,res的初值被修改为了2,不满足进入计算的要求,如果下一组结果本为3,那么res将只保存最小的初值2。
int res = 7;        //保存最小结果,初始值必须大于6,否则影响最终结果

所以循环内重复使用的变量,进入下一次循环前,需重新初始化,以保证初值始终符合条件。

我的问题是在参考了其他正确代码之后发现的,下面是我参考的两份代码(为方便debug,有部分修改和添加):

1.dfs加引用:

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

char g[6][6],backup[6][6];
bool sel[5];

int dx[5] = {-1, 0, 1, 0,0}, dy[5] = {0, 1, 0, -1,0};


int k=1;//编号


void turn(int x, int y)  //偏移量技巧
{
	for (int i = 0; i < 5; i ++ )
	{
		int a = x + dx[i], b = y + dy[i];   //a和b表示偏移量之后的位置
		if (a < 0 || a >= 5 || b < 0 || b >= 5) continue;   // 判断偏移之后是否出界,在边界外,直接忽略即可
		g[a][b] ^= 1;  //亦或小技巧
	}
}

void dfs(int u,int &m_res)	//m_res是一个局部引用变量,调用时该引用变量成为实参的别名
{							//递归到底部后,每一个函数内部都有一个res的别名
	if(u>=5)				//实际使用的是第六个别名,使得main函数里的res能够
	{						//直接保存每一种情况的结果,而不会在回溯后因下层递归函数结束,释放内存而丢失结果
		memcpy(backup, g, sizeof g); 
		int step = 0;
		for(int i = 0; i < 5;i++)
		{
			if(sel[i])
			{
				step++;
				turn(0,i);
			}
			
		}
		
		for (int i = 0; i < 4; i ++ ) 
			for (int j = 0; j < 5; j ++ )
				if (g[i][j] == '0')   
				{
					step ++ ;
					turn(i + 1, j);
				}
		
		bool dark = false;   
		for (int i = 0; i < 5; i ++ )
			if (g[4][i] == '0')   
			{
				dark = true;
				break;
			}
		
		if (!dark) 
		{
			m_res = min(m_res,step);
//			cout.width(2);
//			cout<<"该方案"<<k<<":";
			//打桩查看每一步结果
			printf("该方案%-2d:step=%2d步",k,step);
			printf(",可实现;此时m_res=%d\n",m_res);
	
		}else{
//			cout.width(2);
//			cout<<"该方案"<<k<<":";
			//m_res = -1;//只能dfs全部完成后再对res做判断
			printf("该方案%-2d:step=%2d步",k,step);
			printf(",不能实现全亮;此时m_res=%d\n",m_res);
		}
		k ++;
		memcpy(g, backup, sizeof g);
		
		return ;
	}
	
	sel[u] = true;
	dfs(u+1,m_res);
	sel[u] = false;
	dfs(u+1,m_res);
	
	//if (m_res > 6) m_res = -1;//只能dfs全部完成后再对res做判断
}

int main()
{
	int T;
	cin >> T;
	while (T -- )
	{
		for (int i = 0; i < 5; i ++ ) cin >> g[i];
		int res = 10;//每次循环都会初始化
		//main函数内的局部变量,只能在本函数内使用
		//dfs函数无法识别mian函数内的res;
		//除非将mian里的res按引用传给dfs里的m_res
		dfs(0,res);
		
		
		//if (res > 6) res = -1;
		//只能dfs全部完成后再对res做判断
		cout << "最终结果为:" <<res << endl;
		k = 1;
	
	}
	
}

//作者:lieflat
//链接:https://www.acwing.com/file_system/file/content/whole/index/content/3651637/
//来源:AcWing
//著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

区别就在于:
1.该作者的res是函数内的局部变量,且直接在循环里,实现了每次循环初始化res的要求;
2.因为res是局部变量故dfs需要传参,且是按引用传参,回归后进入下一个分支使用的引用变量m_res和上一个分支使用的引用变量m_res虽然不同,但是都是外部变量res的别名,可以直接修改res的值;
3.在尝试删除&后,会发现res最后都为一开始的初值;

void dfs(int u,int m_res)//按值传递

这就是递归按值传递按引用传递区别:递归内部的结果m_res不会在回溯前向上传递而是出栈被释放,导致每种情况的结果都无法向下一步传递 。但是可以做出如下修改,通过返回值向上一层传递计算结果

int main()
{
	int T;
	cin >> T;
	while (T -- )
	{
		for (int i = 0; i < 5; i ++ ) cin >> g[i];
		int res = 10;//main函数内的局部变量,只能在本函数内使用
		//dfs函数无法识别mian函数内的res;
		//除非将mian里的res传给dfs里的m_res
		
		res = dfs(0,res);

		if (res > 6) res = -1;
		
		cout << "最终结果为:" <<res << endl;
		k = 1;		
	}	
}
int dfs(int u,int m_res)//按值传递,返回结果m_res
	sel[u] = true;
	m_res = dfs(u+1,m_res);
	sel[u] = false;
	m_res = dfs(u+1,m_res);

所以递归中要注意参数的传递问题:递归的结果最好保存在全局变量中而不至于丢失,且每一个分支都能访问该变量;当然也可以按引用传递,通过别名的方式操作同一个变量。

“程序员需保证递归函数 不会随意改变 静态变量和全局变量 的值 ,以避免在递归下降过程中的上层函数出错。程序员还必须确保有一个终止条件来结束递归下降过程,并且返回到顶层。”

2.dfs不加引用:

#include <iostream>
#include <cstring>

using namespace std;

const int N = 5;

char g[N][N], back[N][N];
int num;         //按开关的次数    
int ans = 10;    //按开关的最小次数 ;全局变量
int dx[5] = {-1, 0, 1, 0, 0}, dy[5] = {0, 1, 0, -1, 0};

int k = 1;//方案数

void turn(int x, int y)
{
	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) continue;  
		g[a][b] ^= 1;
	}
}
void dfs(int u)
{
	if(u == 5) 
	{
		bool flag = 1; 
		int t = num;    // 和备份g[][]类似,用t备份num,操作t
		
		memcpy(back, g, sizeof g);   // 备份g
		
		for(int i = 0; i < 4; i ++)
			for(int j = 0; j < 5; j ++)
				if(g[i][j] == '0') 
				{
					t ++ ;         
					turn(i + 1, j);
				}
		
		for(int i = 0; i < 5; i ++) if(g[4][i] == '0') flag = 0;
		if(flag) {
			ans = min(ans, t);
			cout<<"该方案"<<k<<":"<<t<<"步;此时最小步数为:";
			cout<<ans<<endl;
		}else{
			cout<<"该方案"<<k<<":"<<t<<"步"<<",不能实现全亮"<<endl;
		}
		k ++;	
		memcpy(g, back, sizeof g);   // 复原g
		return;
	}
	
	turn(0, u); num ++;        // 选择按开关,num加一
	dfs(u + 1);
	turn(0, u); num --;        // 回溯初态
	
	dfs(u + 1);                // 不按开关,即不做任何操作直接下一层     
}

int main()
{
	int m;
	cin >> m;
	while(m --)
	{
		for(int i = 0; i < 5; i ++) for(int j = 0; j < 5; j ++) cin >> g[i][j];
		dfs(0);
		
		if(ans > 6) ans = -1;//只能dfs全部完成后再对res做判断
		cout << "最终结果为:" <<ans << endl;
		ans = 10;  //非常关键!
		
		k = 0;
	}
	return 0;
}

//作者:段嘉许_
//链接:https://www.acwing.com/file_system/file/content/whole/index/content/3528152/
//来源:AcWing
//著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

这位作者在”无脑“暴力枚举时不是先记录方案,而是直接操作,使dfs到底后直接开始根据第一行按第二行;且步数在递归枚举中做了回溯操作(恢复现场)。

四、其他收获

计算机对位运算操作更快
计算机操作一维数组比二维数组更快

原turn函数:

int dxy[5][5]={{0,-1},{0,0},{-1,0},{0,1},{1,0}};//上边、原位、左边、下边、右边
 void turn(int x, int y)
 {
 	for(int i = 0; i < 5; i++)
 	{
 		int a = x + dxy[i][0];//依次取横坐标,移动
 		int b = y + dxy[i][1];//依次取纵坐标,移动
 		if(a < 0 || a >= 5 || b < 0 || b >= 5) continue;//范围[0~4]外的就跳过不操作
 		//分别按5个在范围内的开关,0变1,1变0
 // 		if(g[a][b] == '1') g[a][b] = '0';
 // 		else g[a][b] = '1';
 		//位运算优化
 		g[a][b] ^= 1;       //与1异或值反转:与1相同为0,相异为1
 	}
 }

上面三组数据从下往上依次是:位运算和数组偏移量优化前、 位运算优化后(-32)、位运算和数组偏移量优化后(-32 + -15)

故,位运算可显著优化32ms,二维数组换用一维数组可优化15ms。

五、利用二分支递归和二进制的相似性进一步优化

在开始暴力枚举五个节点、两个选择时,每个分支所列情况表示如下:

(按,不按,按,不按,按)、(……)……;

10101、00000、00001、00010、00011……;

一开始每个分支是用一个长度为5的数组储存每种初步方案,用 1 表示按,0 不按,重复使用32次。 总共就是32个5元01序列,而这恰好对应了 0~31 的二进制右边5位,所以 0~31 这32个数就存储了题目的32种情况,原本需要递归得到的5元序列现在只需要循环就可逐一罗列出来,进而直接往下判断。

同时也有发现:

二进制的1、2、4、8、16……位,从0开始,0 和 1 都分别依次连续交替出现1、2、4、8、16……次。

  1. "二进制: 00000, 十进制: 0"
  2. "二进制: 00001, 十进制: 1"
  3. "二进制: 00010, 十进制: 2"
  4. "二进制: 00011, 十进制: 3"
  5. "二进制: 00100, 十进制: 4"
  6. "二进制: 00101, 十进制: 5"
  7. "二进制: 00110, 十进制: 6"
  8. "二进制: 00111, 十进制: 7"
  9. "二进制: 01000, 十进制: 8"
  10. "二进制: 01001, 十进制: 9"
  11. "二进制: 01010, 十进制: 10"
  12. "二进制: 01011, 十进制: 11"
  13. "二进制: 01100, 十进制: 12"
  14. "二进制: 01101, 十进制: 13"
  15. "二进制: 01110, 十进制: 14"
  16. "二进制: 01111, 十进制: 15"
  17. "二进制: 10000, 十进制: 16"
  18. "二进制: 10001, 十进制: 17"
  19. "二进制: 10010, 十进制: 18"
  20. "二进制: 10011, 十进制: 19"
  21. "二进制: 10100, 十进制: 20"
  22. "二进制: 10101, 十进制: 21"
  23. "二进制: 10110, 十进制: 22"
  24. "二进制: 10111, 十进制: 23"
  25. "二进制: 11000, 十进制: 24"
  26. "二进制: 11001, 十进制: 25"
  27. "二进制: 11010, 十进制: 26"
  28. "二进制: 11011, 十进制: 27"
  29. "二进制: 11100, 十进制: 28"
  30. "二进制: 11101, 十进制: 29"
  31. "二进制: 11110, 十进制: 30"
  32. "二进制: 11111, 十进制: 31"

代码如下:

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 6;
char g[N][N];		
char backup[N][N];

int dx[5] = { 0, 0, -1, 1, 0};
int dy[5] = {-1, 0,  0, 0, 1};

//int k=1;//编号

void turn(int x, int y)
{
	for(int i = 0; i < 5; i ++)
	{
		int a = x + dx[i];
		int b = y + dy[i];
		if(a < 0 || a >= 5 || b < 0 || b >= 5)
			continue;
		g[a][b]^=1;//'0'变'1','1'变'0',48(110000)互换49(110001),异或1(000001),反转最后一位
	}
}

void not_dfs(int& m_res)//引用变量,作为传入参数的别名
{
	//位置错误!
//	memcpy(backup, g, sizeof(g));	//备份初始状态
//	int step = 0;					//步数累加器
	
	//枚举所有32种方案,取最小的step到res中
	for(int op=0; op<=31; op++)	//每个数隐含五个灯的状态
	{
		//备份初始状态
		memcpy(backup, g, sizeof(g));//内存复制函数,在cstring头文件中
		//初始化步数累加器
		int step = 0;
		//按照第i+1种情况按完第一行
		for(int k=0; k<=4; k++)	//枚举二进制右边五位
		{
			if(op>>k &1)		//提取二进制右数k+1位的数,与1、或0
			{
				//turn(0,5-k);	//如果i对应二进制数右数第k+1位为1,则第一行第5-k个要按
				turn(0,4-k);	//但是下标从0开始,还要-1!
				step++;
			}
		}
		//根据第一行递推按剩下四行
		for(int i=1; i<5; i++)	//从第二行开始
		{
			for(int j=0; j<5; j++)
			{
				if(g[i-1][j] == '0')//如果上方灯是灭的
				{
					turn(i,j);		//按自己,来让上方亮,这一行的操作目的就是使上一行全亮
					step++;
				}
			}
		}
		//第五行没有下一行使它全亮,故此时如果第五行没能全亮就无解
		bool bright = true;
		for(int i=0; i<5; i++)
		{
			if(g[4][i] == '0')
				bright = false;//没能全亮
		}
		if(bright)					
		{
			m_res=min(m_res,step);//全亮则保存到res中,前提是比之前枚举的方案少
//			printf("该方案%-2d:step=%2d步",k,step);
//			printf(",可实现;此时m_res=%d\n",m_res);
		}
//		else{
//			printf("该方案%-2d:step=%2d步",k,step);
//			printf(",不能实现全亮;此时m_res=%d\n",m_res);
//		}
//		k ++;
		memcpy(g, backup, sizeof(g));//恢复初始状态,进行下一种方案的枚举和判断
	}
}

int main()
{
	int T;
	cin>>T;
	int res = 10;	//用于保存每次结果
	while(T --)		//多次计算
	{
		//读取矩阵
		for(int i=0; i<5; i++)cin>>g[i];
		//计算res
		not_dfs(res);
		//结果检验
		if(res>6)res = -1;
		//输出结果
		cout<<res<<endl;
		//重新初始化
		res = 10;//显示多组答案,重复使用需要恢复初值
	}
	return 0;
}

六、其他收获

异或运算的用途:相同为假0,相异为真1

1)翻转指定位:与1相异或值反转

比如将数 X=1010 1110 的低4位进行翻转,只需要另找一个数Y,令Y的低4位为1,其余位为0,即Y=0000 1111,然后将X与Y进行按位异或运算(X^Y=1010 0001)即可得到。

memcpy内存复制函数的用法:
C 库函数  void *memcpy(void *str1, const void *str2, size_t n) ;
从存储区  str2  复制  n  个字节到存储区 str,位于string.h头文件中
C 库函数 – memcpy() | 菜鸟教程 (runoob.com)

七、感想 

基础不牢,真tm难受,还是要扎实好理论知识!

  • 26
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值