Wiki OI 1174 靶形数独

7 篇文章 0 订阅
2 篇文章 0 订阅

题目链接:http://wikioi.com/problem/1174/

算法与思路:状态压缩 + 启发式搜索

想要看懂这篇题解需要有一定位运算的基础,初学者可以参考以下链接

http://www.matrix67.com/blog/archives/263

所谓启发式搜索,就是在状态空间中的搜索对每一个搜索的位置进行评估,得到最好的位置,

再从这个位置进行搜索直到目标。这样可以省略大量无谓的搜索路径,提高了效率。

在我们玩数独的时候,很平常的想法就是从数字缺少最少的那行或列开始填数,

这样受到的限制最多,可选取的数范围最小,而启发式搜索正是遵循这样的思路;

在搜索的过程中,需要判断所填的数字是否在其所在的行、列和小九宫格中出现过,如果不进行标记,

那么每次搜索都需要遍历行、列、小九宫格来得到合法的数字范围,时间代价太大;

而将行,列,小九宫格的状态转换成一个九位的二进制数,则可以很好的解决这一问题。

详见注释。

代码实现:

#include<stdio.h>
#include<math.h>
int row_fill[10] = {}, row_status[10] = {}, col_status[10] = {};
int m_sudo[5][5] = {}, row_lack[10] = {};
int ans = -1, s_order[10], map[10][10];
void score()   //计算分数,更新ans
{
    int sum = 0, i, j;
    for(i = 1; i < 5; i++)
    {
	  	for(j = i; j < 11 - i; j++)
        	sum += (map[i][j] + map[10 - i][j]) * (5 + i);
       	for(j = i + 1; j < 10 - i; j++)
        	sum += (map[j][i] + map[j][10 - i]) * (5 + i);
    }
    sum += map[5][5] * 10;   
    if(sum > ans) 
	    ans = sum;
}
void dfs(int k)  	//搜索部分,k不表示搜索第k行,而是第s_order[k]行
{
	if(k==10) 
		score(); 	// k=10表示九行都搞定了,开始算分
    else
    {
    	int x, y, j, pos, p, i = s_order[k]; 	//i表示第i行,j表示第j列
      	x = 511 - row_fill[i];					//511(2)=111,111,111(2),故此行的意思是将第i行缺位取出来
                	   							//此时x中1表示缺位,y与x同
      	y = x & -x;    							//y是取出本行第一个缺位,在这一次搜索里就搜索这个缺位
      	row_fill[i] |= y;     					//下一次搜索时,这一位已填,故把缺位补上
      	j = (int)log2(y) + 1; 					//j就是y用二进制表示1所在的位数,即j列
      	pos = 511 - (row_status[i] | col_status[j] | m_sudo[(i-1)/3][(j-1)/3]); //这一步是取出可以填哪些数
      	while(pos > 0)
      	{
			p = pos & -pos;  				//取出可以填的一个数
       		pos -= p;       				//去掉已填的数
       		map[i][j] = (int)log2(p) + 1; 	//填入map中
       		row_status[i] |= p;    			//修改row_status,col_status,m_sudo,这个数已用过,'|'写成'+'也行
       		col_status[j] |= p;
       		m_sudo[(i-1)/3][(j-1)/3] |= p;
       		if(x == y) 
				dfs(k+1);					//若x = y,则这一行只有一个空,即现在已填的空,故搜索k+1
       		else 
			   	dfs(k); 					//若x不等于y,则这一行还有空没填,继续搜索这一行
       		row_status[i] -= p; 			//搜索完,还原row_status,col_status,m_sudo
       		col_status[j] -= p;
       		m_sudo[(i - 1) / 3][(j - 1) / 3] -= p;
       }
	   row_fill[i] -= y;  					//s搜索完,还原row_fill[i]
    }
}
int main()
{
	int i, j, used_n;
    for(i = 1; i < 10; i++)
		for(j = 1; j < 10; j++)
		{
			scanf("%d", &map[i][j]);        	//读入数独,数组map记的是数独。
         	if(map[i][j] > 0)
           	{
			   	row_fill[i] |= 1 << (j - 1);    //数组row_fill记的是第i行填数情况
                       							//row_fill[i]写成二进制,第j位为0,表示map[i][j]=0,未填数,
										  		//同理第j位为1,表示map[i][j]>0,已填数
				used_n = 1 << (map[i][j] - 1);  //used_n写成二进制,第k位为1,表示数字k已用过
            	if(((row_status[i] & used_n) != 0) || ((col_status[j] & used_n) != 0) || 
				((m_sudo[(i - 1) / 3][(j - 1)/3] & used_n) != 0))
            	{
					printf("-1\n");
					return 0;
				}							//判断某一行,列,九宫格是否有同一数字出现两次
            	row_status[i] |= used_n;  	//数组row_status记录第i行数字使用情况
                       						//row_status[i]写成二进制,第j位为0,表示i行,j没用过
                            				//同理第j位为1,表示i行,j用过
            	col_status[j] |= used_n;    //数组col_status记录第j列数字使用情况,意义同row_status
            	m_sudo[(i-1)/3][(j-1)/3] |= used_n; 
								//数组m_sudo记录某一小九宫格数字用的情况,意义同row_status
            }        			//九个小九宫格分别是   
								//m_sudo[0][0], m_sudo[0][1], m_sudo[0][2]
								//m_sudo[1][0], m_sudo[1][1], m_sudo[1][2]
         						//m_sudo[2][0], m_sudo[2][1], m_sudo[2][2]
          	else
				row_lack[i]++;			 //数组row_lack记的是某一行缺数的个数
		} 							    
    for(i = 1; i < 10; i++) 
		s_order[i] = i;         		//数组s_order记录搜索各行的顺序 
    for(i = 1; i < 9; i++)              //按各行空缺数的个数将s_order从小到大排序
    	for(j = i + 1; j < 10; j++)     //使得一会搜的时候,先搜缺数少的行,这就是启发式搜索 
       		if(row_lack[s_order[i]] > row_lack[s_order[j]])
         	{
				s_order[i] ^= s_order[j];         //swap位运算版
          		s_order[j] ^= s_order[i];
          		s_order[i] ^= s_order[j];
			}
    for(i = 1; row_lack[s_order[i]] == 0; i++);    //考虑到某一行缺数可能为0,故先找到缺数的行
    	dfs(i);
    printf("%d\n",ans);
    return 0;
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值