关于枚举算法的一些心得总结。
题目描述:有9个时钟,排成一个3*3的矩阵。
现在需要用最少的移动,将9个时钟的指针都拨到12点的位置。共允许有9种不同的移动。如下表所示,每个移动会将若干个时钟的指针沿顺时针方向拨动90度。
移动 影响的时钟
1 ABDE
2 ABC
3 BCEF
4 ADG
5 BDEFH
6 CFI
7 DEGH
8 GHI
9 EFHI
输入:
9个整数,表示各时钟指针的起始位置,相邻两个整数之间用单个空格隔开。其中,0=12点、1=3点、2=6点、3=9点。
输出:
输出一个最短的移动序列,使得9个时钟的指针都指向12点。按照移动的序号从小到大输出结果。相邻两个整数之间用单个空格隔开。
样例输入:
3 3 0
2 2 2
2 1 2
样例输出:
4 5 8 9
相比与熄灯问题,拨钟问题更具一般性.
思路如下:
枚举第一行的所有情况,由于每个时钟块可以被拨动不止一次,所以变量应为每个时钟块的拨动次数,而次数有0、1、2、3四种情况。所以需要枚举4^3次。每一次枚举完,确定好第一行每一块的拨动次数后,下一步就是以此为基础,确定全部时钟的拨动次数(熄灯问题也是如此)。那么如何来确定呢?
ABCDEFGHI时钟对应的拨动次数是i1 i2 i3 i4 i5 i6 i7 i8 i9
现在已知i1 i2 i3 且易知,A时钟是否被调至零点只与A B D时钟的拨动次数有关,现在已知了A B的拨动次数,为了将A调零,可以求出D的拨动次数。
即 i4 = (4 - (puzzle[1][1] + i2 + i1) % 4) % 4;
注意,此式代表了两层信息,首先是A时钟可以被调至零,还有就是借此算出了D的拨动次数
(理清这一点很重要,枚举算法的最后其实就是去验证那些在前面没有被验证的元素是否可以被调零。此处A时钟已经被确定可以为零,所以最后不用检验A时钟)
同理可以求出其他所有时钟的拨动次数,但由于最开始假设了三个已知量,所以其实只验证了6个时钟是否可以被调零,还剩下3个没有验证,所以这三个就是算法最后需要去验证的。
这部分具体代码如下
int i1, i2, i3, i4, i5, i6, i7, i8, i9;
i4 = (4 - (puzzle[1][1] + i2 + i1) % 4) % 4;
// 确定了1行1列元素被置零,同时求出i4
i5 = (4 - (puzzle[1][2] + i1 + i2 + i3) % 4) % 4;
// 确定了1行2列元素被置零,同时求出i5
i6 = (4 - (puzzle[1][3] + i2 + i3) % 4) % 4;
// 确定了1行3列元素被置零,同时求出i6
i7 = (4 - (puzzle[2][1] + i4 + i1 + i5) % 4) % 4;
// 确定了2行1列元素被置零,同时求出i7
i8 = (4 - (puzzle[3][1] + i7 + i4) % 4) % 4;
// 确定了3行1列元素被置零,同时求出i8
i9 = (4 - (puzzle[2][2] + i5 + i1 + i3 + i7) % 4) % 4;
// 确定了2行2列元素被置零,同时求出i9
求出所有时钟的拨动次数后,就验证最后三个时钟在现有的“环境”中是否可以被调零
如下:
if ((puzzle[2][3] + i3 + i9 + i5 + i6) % 4 == 0
&& (puzzle[3][2] + i5 + i7 + i8 + i9) % 4 == 0
&& (puzzle[3][3] + i9 + i8 + i6) % 4 == 0)
一旦条件满足(即可以被调零),说明此时枚举的第一行就是可以让所有时钟调零的情况,但是这种情况并不是唯一,还有其他第一行也许也可以,我们是要寻找步骤最少的方法,所以进入该条件语句后还要进一步判断是否是最短路径。
代码如下(sum是一开始设的一个很大的数)
int temp[9] = { i1,i2,i3,i4,i5,i6,i7,i8,i9 };
if (sum > i1 + i2 + i3 + i4 + i5 + i6 + i7 + i8 + i9) //不断找到步数最少的
{
sum = i1 + i2 + i3 + i4 + i5 + i6 + i7 + i8 + i9;
for (int i = 0; i < 9; ++i)
result[i] = temp[i];
return;
}
else
return;
至此,本程序的最主要的代码就写好了。
接下来是main函数和枚举函数enumerate()
程序完整代码如下:
#include <iostream>
using namespace std;
int i1, i2, i3, i4, i5, i6, i7, i8, i9;
int puzzle[4][4] = { 0 };
int result[9] = { 0 };
int sum = 10000;
void guess()
{
i4 = (4 - (puzzle[1][1] + i2 + i1) % 4) % 4;
// 确定了1行1列元素被置零,同时求出i4
i5 = (4 - (puzzle[1][2] + i1 + i2 + i3) % 4) % 4;
// 确定了1行2列元素被置零,同时求出i5
i6 = (4 - (puzzle[1][3] + i2 + i3) % 4) % 4;
// 确定了1行3列元素被置零,同时求出i6
i7 = (4 - (puzzle[2][1] + i4 + i1 + i5) % 4) % 4;
// 确定了2行1列元素被置零,同时求出i7
i8 = (4 - (puzzle[3][1] + i7 + i4) % 4) % 4;
// 确定了3行1列元素被置零,同时求出i8
i9 = (4 - (puzzle[2][2] + i5 + i1 + i3 + i7) % 4) % 4;
// 确定了2行2列元素被置零,同时求出i9
// 到目前为止,还有2行3列、3行2列和3行3列元素没有被置零,我们接下来就判断已知所有元素press的情况下,
// 这三个元素是否会被置零,如果没有被置零说明第一行的枚举情况不对,返回false
if ((puzzle[2][3] + i3 + i9 + i5 + i6) % 4 == 0
&& (puzzle[3][2] + i5 + i7 + i8 + i9) % 4 == 0
&& (puzzle[3][3] + i9 + i8 + i6) % 4 == 0)
{
int temp[9] = { i1,i2,i3,i4,i5,i6,i7,i8,i9 };
if (sum > i1 + i2 + i3 + i4 + i5 + i6 + i7 + i8 + i9) //不断找到步数最少的
{
sum = i1 + i2 + i3 + i4 + i5 + i6 + i7 + i8 + i9;
for (int i = 0; i < 9; ++i)
result[i] = temp[i];
return;
}
else
return;
}
return;
}
void enumerate()
{
for (i1 = 0; i1 < 4; i1++)
for (i2 = 0; i2 < 4; i2++)
for (i3 = 0; i3 < 4; i3++)
guess();
}
int main()
{
for (int i = 1; i <= 3; i++)
for (int j = 1; j <= 3; j++)
cin >> puzzle[i][j];
enumerate();
for (int i = 0; i < 9; i++)
if (result[i] != 0)
for (int c = 0; c < result[i]; c++)
cout << i + 1 << ' ';
return 0;
}
此题与熄灯问题的区别:
1、此题枚举时必须全部枚举完,熄灯问题是枚举到有正确的就输出,因为只有一种熄灯解,所以是while(guess()==false){}因此本题的guess函数返回类型不为bool型而是为空
2、熄灯问题规律性更强,此题由于只有九个时钟,所以guess()函数里确定拨动次数时直接列了6个运算表达式,而熄灯问题中由于有很多行,所有用for循环挨个往下,但他们的目标都是一样的,即找到在第一行已知的情况下,所有元素的press(在本例中press就是拨动次数)
#------2022年3月28日------#