蓝桥杯决赛之方块填数


【编程题】(满分33分)

    “数独”是当下炙手可热的智力游戏。一般认为它的起源是“拉丁方块”,是大数学家欧拉于1783年发明的。

    如图[1.jpg]所示:6×6的小格被分为6个部分(图中用不同的颜色区分),每个部分含有6个小格(以下也称为分组)。
    
    开始的时候,某些小格中已经填写了字母(ABCDEF之一)。需要在所有剩下的小格中补填字母。

    全部填好后,必须满足如下约束:

    1. 所填字母只允许是A,B,C,D,E,F 中的某一个。

    2. 每行的6个小格中,所填写的字母不能重复。

    3. 每列的6个小格中,所填写的字母不能重复。

    4. 每个分组(参见图中不同颜色表示)包含的6个小格中,所填写的字母不能重复。

    为了表示上的方便,我们用下面的6阶方阵来表示图[1.jpg]对应的分组情况(组号为0~5):
000011
022013
221113
243333
244455
445555
 
    用下面的数据表示其已有字母的填写情况:
02C
03B
05A
20D
35E
53F

    很明显,第一列表示行号,第二列表示列号,第三列表示填写的字母。行号、列号都从0开始计算。

    一种可行的填写方案(此题刚好答案唯一)为:

E F C B D A
A C E D F B
D A B E C F
F B D C A E
B D F A E C
C E A F B D

    你的任务是:编写程序,对一般的拉丁方块问题求解,如果多解,要求找到所有解。

【输入、输出格式要求】

    用户首先输入6行数据,表示拉丁方块的分组情况。

    接着用户输入一个整数n (n<36), 表示接下来的数据行数

    接着输入n行数据,每行表示一个预先填写的字母。

    程序则输出所有可能的解(各个解间的顺序不重要)。

    每个解占用7行。

    即,先输出一个整数,表示该解的序号(从1开始),接着输出一个6×6的字母方阵,表示该解。

    解的字母之间用空格分开。

    如果找不到任何满足条件的解,则输出“无解”

    例如:用户输入:
000011
022013
221113
243333
244455
445555
6
02C
03B
05A
20D
35E
53F

    则程序输出:
1
E F C B D A
A C E D F B
D A B E C F
F B D C A E
B D F A E C
C E A F B D

   再如,用户输入:
001111
002113
022243
022443
544433
555553
7
04B
05A
13D
14C
24E
50C
51A
    则程序输出:
1
D C E F B A
E F A D C B
A B F C E D
B E D A F C
F D C B A E
C A B E D F
2
D C E F B A
E F A D C B
A D F B E C
B E C A F D
F B D C A E
C A B E D F
3
D C F E B A
A E B D C F
F D A C E B
B F E A D C
E B C F A D
C A D B F E
4
D C F E B A
B E A D C F
A D C F E B
F B E A D C
E F B C A D
C A D B F E
5
D C F E B A
E F A D C B
A B C F E D
B E D A F C
F D B C A E
C A E B D F
6
D C F E B A
E F A D C B
A B D F E C
B E C A F D
F D B C A E
C A E B D F
7
D C F E B A
E F A D C B
A D B F E C
B E C A F D
F B D C A E
C A E B D F
8
D C F E B A
F E A D C B
A D B C E F
B F E A D C
E B C F A D
C A D B F E
9
D C F E B A
F E A D C B
A F C B E D
B D E A F C
E B D C A F
C A B F D E

【注意】

    请仔细调试!您的程序只有能运行出正确结果的时候才有机会得分!
    
    在评卷时使用的输入数据与试卷中给出的实例数据可能是不同的。

    请把所有函数写在同一个文件中,调试好后,拷贝到【考生文件夹】下对应题号的“解答.txt”中即可。
    
    相关的工程文件不要拷入。
    
    源代码中不能使用诸如绘图、Win32API、中断调用、硬件操作或与操作系统相关的API。
    
    允许使用STL类库,但不能使用MFC或ATL等非ANSI c++标准的类库。

    例如,不能使用CString类型(属于MFC类库);例如,不能使用randomize, random函数(不属于ANSI C++标准)



分析:

  本题思路很简单,就是dfs暴搜,但是如果只是单纯的暴搜,在最后的时候再进行每行每列和每组的重复性判断,那么程序的效率会变得非常低,这时候就需要进行剪枝。对于剪枝,也是有很多方法的,首先dfs是通过对6*6的矩阵从左到右,从上到下的对每个方格依次放入’A’~’F’六个字母,这样的话,我们是先行遍历,所以每一行结束可以进行行的重复性判断,然后最后在6*6个方格全部放入字母后,进行列和组的重复性判断。但是这个剪枝方法,还是比较低效的。

  继续优化的话,我们使用三个数组usedlie,usedhang,usedzu来分别存放每列,行,组中已经使用的字母,具体例如usedlie[i][j]表示第i列的第j个字母(即’A’+j)已经使用了,这样我们就可以在每次对方格中的数据进行赋值时,都进行行,列,组的非重复性选择。这种方法,可以大大提高程序的效率。

  

  此外,本题还有几个小技巧:

1.我们并不是直接将字母’A’~’F’放入方格,而是放入数字0~5,因为对于数字的操作显然比操作字符简单一些,最后在输出的时候,只需要 数字+’A’即可。

2.对于组的存放,并不像行列一样直接通过方格的位置横纵座标就能获得,我们在输入的时候,就将属于第i组的方格[x][y]经过矩阵展开后,将结果放在zu[i]中,也就是说zu[i][j]=x*6+y表示座标为[x][y]的方格数组第i组,且是其第j个元素。(但是这个技巧是在上述提到的最后进行组重复性判断下使用,便于快速找到每组的方格,而在使用usedzu进行重复性判断的时候,该数组就不需要了)

3.对于初始化已经放入字母的方格,我们可以使用already数组,already[i][j]=1就表示指定的方格中已经放入了字母,这样,在进行dfs遍历到该方格时,直接跳过遍历下一个方格即可。


以下即为源码:

#include<iostream>
#include<string.h>
using namespace std;
//int zu[6][6];                    //zu[i][j]表示第i组的第j个位置的下标转换结果 
//int pos[6]={0};                  //pos[i]表示下一个第i组的位置放在zu[i]中的位置,即zu[i][pos[i]]
//char test[7][7]={"EFCBDA","ACEDFB","DABECF","FBDCAE","BDFAEC","CEAFBD"};
int res[6][6];                   //res表示0~5数字形式的结果 
bool already[6][6];              //already表示指定位置的元素已经被赋值 
int count=0;                      //count表示已找到的结果数 
bool usedlie[6][6];               //used[i][j]表示下标为i的列是否已经使用了数字j 
bool usedhang[6][6];              //used[i][j]表示下标为i的行是否已经使用了数字j 
bool usedzu[6][6];
int zuadj[6][6];                 //zuadj表示指定位置的元素所在的组编号 
void print()
{
	cout<<++count<<endl;
	if(count%10==0) char c=cin.get();
	for(int i=0;i<6;i++)
	{
		for(int j=0;j<6;j++)
		{
		  cout<<char(res[i][j]+'A')<<" ";
		}
		cout<<endl;
	}
	cout<<endl;
}
void dfs(int x,int y)
{
	if(x==6 && y==0)   // 方格填数结束 ,而在之前通过 usedlie,usedhang,usedzu已经判断过行列组的元素重复性,这里直接输出即可 
	{
	    print();
	    return;                  
	}                        
	
	int nextx=x;
	int nexty=y+1;
	if(nexty>5)
	{
		nextx++;
		nexty=0;
	}
	 
	 if(already[x][y]==true) 
	 {
	 	dfs(nextx,nexty);
	 }
	 else{
	 	for(int i=0;i<6;i++)
	    {
	    	if(usedlie[y][i]==false && usedhang[x][i]==false && usedzu[zuadj[x][y]][i]==false)
	    	{
	    		 res[x][y]=i;
	    		 usedlie[y][i]=true;
	    		 usedhang[x][i]=true;
	    		 usedzu[zuadj[x][y]][i]=true;
	 	         dfs(nextx,nexty);
	 	         usedlie[y][i]=false;
	 	         usedhang[x][i]=false;
	 	         usedzu[zuadj[x][y]][i]=false;
	    	}
	    }
	 }
}
int main()
{
	for(int i=0;i<6;i++)
	{
		string str="";
		cin>>str;
		for(int j=0;j<6;j++)
		{
		   int temp=str[j]-'0';
		   zuadj[i][j]=temp;
		   //zu[temp][pos[temp]++]=i*6+j;
		}
	}
	
	int N;
	cin>>N;
	for(int i=0;i<N;i++)
	{
		string str="";
		cin>>str;
		int x=str[0]-'0';
		int y=str[1]-'0';
		int value=str[2]-'A';
		res[x][y]=value;
		
		already[x][y]=true;
		usedlie[y][value]=true;
		usedhang[x][value]=true;
		usedzu[zuadj[x][y]][value]=true;
	}
	dfs(0,0);
	return 0;
} 

PS:虽然拿到这题就有了思路-dfs加剪枝,但是代码实现起来却着实浪费了我不少时间,尤其是在剪枝的优化时,代码也是不断调试了好长时间。但是,说实话,这样不断调试,一点点优化,最后有结果的感觉,真是棒棒哦~代码也是写的酣畅淋漓~

点赞