初学枚举——特殊密码锁与熄灯问题的理解

特殊密码锁与熄灯问题的理解


一、特殊密码锁

1. 问题简述
有一种特殊的二进制密码锁,由n个相连的按钮组成(n<30),按钮有凹/凸两种状态,用手按某个按钮时,它及其相邻的两个按钮状态会反转。当按的是最左或者最右边的按钮时,该按钮只会影响到跟它相邻的一个按钮。当前密码锁状态已知,求解至少需要按多少次按钮,才能将密码锁转变为所期望的目标状态。
输入:两行,给出两个由0、1组成的等长字符串,表示当前/目标密码锁状态,其中0代表凹,1代表凸。
输出:至少需要进行的按按钮操作次数,如果无法实现转变,则输出impossible。

2. 问题分析
考虑使用枚举法解题,每一个按钮有按与不按两种情况,如果对所有情况都进行枚举的话,n个按钮则会产生2n种情况,时间成本是较高的。
在分析问题前,我们要明白一点:按钮的先后顺序是不影响密码锁最终状态的,比如初始锁为001,分别进行以下两种按法:
第一种:按第一个键 --> 按第三个键,对应 001 --> 111 --> 100;
第二种:按第三个键 --> 按第一个键,对应 001 --> 010 --> 100;
基于此,我们不妨从左往右依次进行枚举,而且,同一个按钮不能按两次,因为这样效果会抵消。
我们的基本思路是利用一些已知信息减少枚举的次数。其实,在我们进行枚举的时候,可以尝试寻找某个局部,一旦该局部被确定了,剩余部分的状态也就随之确定。比如对于密码锁问题
初始锁状态为 010
目标锁状态为 101
如果我们已经确定了第一个按钮 按下,当前状态变为100,那么我们按钮2的操作是什么呢?还需要进行枚举吗?显然此时应 不按。因为如果按下后就会使当前状态变为011,而按钮1的目标状态是1,也就是说,要达到目标,我们还需要按一次按钮1或者按钮2,有一个按钮会按两次,这在前面已经分析到是无意义的。同理,按钮3的状态也可以确定。
很显然,按钮1就是我们要找的局部,确定了该局部,就能从左往右依次确定每个按钮的操作,这对n个按钮的情况也是成立的,230的问题就能简化为22的问题了。
从局部确定剩余部分状态的原理是什么呢?那就是前面提到的已知信息,因为我们已经知道了每一个按钮的目标状态,在选择按与不按的时候就有了倾向性。

情形1中盲人就好比“暴力”枚举法,因为不知道走每条路的后果,只能去逐一尝试右、上、下三条路,在找到正确的路前,可能会碰到老虎和狼;而情形二中的人,知道了走某些路径不会到达生路,只会选择合适的那一条。正如密码锁问题里,已经知道了 按下 按钮2不会使按钮1变为目标状态,选择 不按 就是唯一确定的。

生路====》密码锁目标状态
路径====》对每个按钮按或者不按的选择
走不同路产生的结果====》按或者不按后按钮的状态

3. 代码实现
(1)输入
getline()函数,读入未知长度的字符串。
(2)密码锁存储及操作
可用 一维数组实现,但因为密码锁只有0、1两种状态,可用二进制数来表示,节省空间。以下用数字c表示整个密码锁状态,相关操作如下:

调整第i个按钮状态,v只有1或0两个值:

void SetBit(int& c, int i, int v)
{
	//调整数字c的第i位
	if (v)
		c |= (1 << i);
	else
		c &= ~(1 << i);
}

读取第i个按钮状态

int GetBit(int c, int i)
{
	//取数字c的第i位
	return (c >> i) & 1;
}

按下第i个按钮改变状态

void FlipBit(int& c, int i)
{
	//转换第i位,与1做异或
	c ^=(1 << i);
}

源代码

#include <iostream>
#include<algorithm>
#include<string>

using namespace std;


int GetBit(int c, int i)
{
	//取数字c的第i位
	return (c >> i) & 1;
}

void SetBit(int& c, int i, int v)
{
	//调整数字c的第i位
	if (v)
		c |= (1 << i);
	else
		c &= ~(1 << i);
}

void FlipBit(int& c, int i)
{
	//转换第i位,与1做异或
	c ^=(1 << i);
}

int main()
{
	string LockLine;

	int OriLock = 0; //初始密码锁状态
	int DesLock = 0; //目标密码锁状态
	int CurLock = 0; //当前密码锁状态

	//设置初始密码锁每个按钮的状态
	getline(cin,LockLine);
	int len = LockLine.size();
	for (int i = 0; i < len; ++i)
	{
		SetBit(OriLock, i, LockLine[i]-'0');
	}
	
	//设置目标密码锁每个按钮的状态
	getline(cin, LockLine);
	for (int i = 0; i < len; ++i)
	{
		SetBit(DesLock, i, LockLine[i]-'0');
	}

	int mintimes =len;
	for (int i = 0; i < 2; i++)//第一个按钮只有0、1两种状态
	{
		CurLock = OriLock;
		int switchs = i;//对按钮进行的操作
		int times = 0;
		for (int j = 0; j < len; j++)
		{
			//依次对每个按钮进行处理
			if (switchs)
			{
				times++;
				if (j > 0)
					FlipBit(CurLock, j-1);
				if (j < len - 1)
					FlipBit(CurLock, j + 1);
				FlipBit(CurLock, j);
			}
			if (j < len - 1) //当前按钮与目标状态一致,则switchs=0,否则1
				switchs = GetBit(CurLock, j) ^ GetBit(DesLock, j);
		}
		if (CurLock == DesLock)//判断当前密码锁状态与目标状态是否一致
			mintimes = min(mintimes, times);
	}
	if(mintimes==len)
		cout<<"impossible"<<endl;
	else
		cout<<mintimes<<endl;
	return 0;
}

二、熄灯问题

1. 问题简述
有一种特有一个由按钮组成的矩阵,其中每行有6个按钮,共5行。每个按钮的位置上有一盏灯。当按下一个按钮后,该按钮以及周围位置(上边、下边、左边、右边)的灯都会改变一次。在矩阵角上的按钮改变3盏灯的状态;在矩阵边上的按钮改变4盏灯的状态;其他的按钮改变5盏灯的状态。0代表灯熄灭,1代表点亮,目标是按下一系列按钮使所有灯都熄灭。

输入:5行由0、1组成的等长字符串,表示灯的初始状态。
输出:5行由0、1组成的等长字符串,1表示按下当前按钮,0表示不需要按。

2. 问题分析
该问题可以说是密码锁问题维度上的升级,由一维变成了二维。首先还是寻找枚举问题的局部,不难发现,当确定了第一行(或列)按钮的操作后,第二行按钮的操作也就确定了。我们只需要对第一行进行局部枚举,该行由6个数字组成共有26种情况。同样,我们使用二进制数进行表示,每一行需用一个数表示,5行共需5个数,整个灯阵列只需一个char[5]的数组就能表示。对第一行,从0枚举到63即可(八位二进制数,足以枚举6个按钮的状态)。

密码锁初始状态====》灯初始状态
密码锁目标状态====》灯全部熄灭
密码锁一个按钮====》灯一行按钮

源代码

#include <iostream>
#include<algorithm>
#include<string>

using namespace std;


int GetBit(char c, int i)
{
	//取数字c的第i位
	return (c >> i) & 1;
}

void SetBit(char& c, int i, int v)
{
	//调整数字c的第i位
	if (v)
		c |= (1 << i);
	else
		c &= ~(1 << i);
}

void FlipBit(char& c, int i)
{
	//转换第i位,与1做异或
	c ^= (1 << i);
}

void OutPut(char result[])//结果输出
{
	for (int i = 0; i < 5; ++i) {
		for (int j = 0; j < 6; ++j) {
			cout << GetBit(result[i], j);
			if (j < 5)
				cout << " ";
		}
		cout << endl;
	}
}

int main()
{
	char OriLight[5]; //初始灯状态
	char CurLight[5]; //当前灯状态
	char Result[5];   //对灯阵列的操作

	//设置初始灯阵列的状态
	memset(OriLight, 0, sizeof(OriLight));
	for (int i = 0; i < 5; ++i)
	{
		string in;
		getline(cin, in);
		for (int j = 0; j < 6; ++j)
		{
			SetBit(OriLight[i], j, in[j]-'0');//一定要-'0'
		}
	}

	for (int i = 0; i < 64; i++)//第一行按钮的64种状态
	{
		memcpy(CurLight, OriLight, sizeof(OriLight));
		char switchs = i;//对按钮进行的操作
		for (int j = 0; j < 5; ++j)//遍历每一行
		{
			Result[j] = switchs;
			for (int k = 0; k < 6; ++k)
			{
				if (GetBit(switchs, k))
				{
					if (k > 0)
						FlipBit(CurLight[j], k - 1);
					if (k < 5)
						FlipBit(CurLight[j], k + 1);
					FlipBit(CurLight[j], k);
				}
			}
			/*if(j>0)   //改变上一行灯,无需进行
				CurLight[j - 1] ^= switchs;*/

			if (j < 4)  //改变下一行灯状态
				CurLight[j + 1] ^= switchs;
			switchs = CurLight[j];//本行的状态决定对下一行的操作
		}
		if (CurLight[4] == 0)//判断最后一行灯是否全灭
		{
			OutPut(Result);
			break;
		}
	}
	return 0;
}

三、总结

特殊密码锁与熄灯问题本质上是相同的,二者仅是维度上的区别。在解这两个问题的过程中,个人主要有以下收获:

1.寻找局部。在枚举问题里,可能存在某个局部,其状态确定之后,其他部分的状态也随之确定或者减少;
2.二进制数表示。在只有0、1两种状态时,利用二进制数来表示,节省空间。而且在熄灯问题里,对第一行的状态也不需要再进行多重循环。


新人初次分享,不当之处还请指正,感谢。
发布了13 篇原创文章 · 获赞 7 · 访问量 601

猜你喜欢

转载自blog.csdn.net/hejnhong/article/details/104432308