AcWing 166. 数独(dfs剪枝,二进制优化)

数独是一种传统益智游戏,你需要把一个 9×9 的数独补充完整,使得图中每行、每列、每个 3×3 的九宫格内数字 1∼9 均恰好出现一次。

请编写一个程序填写数独。

输入格式
输入包含多组测试用例。

每个测试用例占一行,包含 81 个字符,代表数独的 81 个格内数据(顺序总体由上到下,同行由左到右)。

每个字符都是一个数字(1−9)或一个 .(表示尚未填充)。

您可以假设输入中的每个谜题都只有一个解决方案。

文件结尾处为包含单词 end 的单行,表示输入结束。

输出格式
每个测试用例,输出一行数据,代表填充完全后的数独。

输入样例:
4…8.5.3…7…2…6…8.4…1…6.3.7.5…2…1.4…
…52…8.4…3…9…5.1…6…2…7…3…6…1…7.4…3.
end

输出样例:
417369825632158947958724316825437169791586432346912758289643571573291684164875293
416837529982465371735129468571298643293746185864351297647913852359682714128574936

一、思路

每次挑选一个空白的格子,枚举这个位置应该填几(1~9,但可能不是所有数字都可以填,还需要满足某些性质)

在当前空格中我们要在1~9之中选择一些符合要求的数字,依次枚举这个格子选择可选的数字,每枚举完一个数后递归到下一层 。

假设

我们已经把当前空白格填上了数字2,下一步递归时,和之前的步骤相类似,即随便选择一个还未填好的位置,
用同样的方式枚举这个位置可以填写哪些数字。

选定之后再递归地枚举下一个数,直到把所有的空白位置全部填满为止,就找到了一组合法的解。

如果过程当中有一个格子什么数都填写不了,即备选集合为空集的话,则方案不合法,直接return即可。

二、搜索顺序总结

①每次随意挑选选定的格子
②枚举该格子选哪个数
③dfs

三、优化

如果直接按照上面这种最朴素的方式来写,很不幸会超时,题目给的数据较强,需要优化加速搜索过程。

我们优化的是前面步骤①和②。

优化方式一般有:
①优化搜索顺序(优先搜索决策少的点)
②排除冗余信息(如果可以证明从某一个分支往下走的话和前面搜索过的分支是等价的,就可以没必要搜了)
④最优性剪枝(如果往下搜的话,发现最优解比当前答案要大,则直接return)
⑤记忆化搜索

对于搜索顺序① 我们可以使用“优化搜索顺序”,每次选择可选的数字最少的空白格(决策较少)

对于搜索顺序② 我们可以先存一下每行,每列,每个九宫格当前可以填的数字有哪些

如row[i]表示第i行当前可以用的数字有哪些(初始时1~9均可以用)。
col[i]表示第i列当前可以用的数字有哪些(初始时1~9均可以用),
cell[i][j]表示每一个九宫格可以用哪些数字(初始时1~9均可以用)

每次我们判断(x,y)能用哪些数时,其实就是看这一行,这一列,该格所在的九宫格可以选哪些数,并取交集

这里想到用位运算来优化(两个数 相与 对应 两个集合 求交集),每一个row[i],col[j],cell[i][j]存的都是一个整数
(范围是0~2^9-1,因此其二进制表示里每个数有9位,不是0就是1,如果是1的话,表示这个数可以使用,0则反之)

判断(x,y)这个格子可以填写那些数字(交集)的公式:row[i]&col[y]&cell[x/3][y/3]

根据上面公式可以快速算出来,当前位置可以选择那些数字。

算完之后得到一个9位的二进制数,哪些位是1则表示可选,0则不可选。
也就是说我们要遍历一下这个求出来的二进制数交集的每一位1,因此可以用到lowbit运算(O(1)的时间返回x最后一位1)

对于每一位1,我们枚举当前空白格选上这个数字,将状态进行更新(主要是更新row,col,cell三个数组)。

更新之后,递归到下一层,如找到方案则直接返回,如果没找到,则将现场恢复,进行下一层递归

四、代码细节分析

①我们需要额外数组ones来帮助我们进行优化,这里对应我们对搜索顺序①的优化(每次选择可选的数字最少的空白格)。

原因
由于二进制数中有多少个1我们就有多少种选择,所以我们需要求一下某一个数的二进制表示有多少个1(每次运算需要logn),我们可以优化一下,将所有答案打一个表,之后直接查即可(避免重复运算)。

注意:由于每个数的范围均是2~2^9-1,因此ones数组要开在1< (2^9),ones[i]即i的二进制表示有多少个1(有点哈希的思想)

②我们还需要有一个map数组,大小和ones数组一样 在lowbit运算时,lowbit(x)返回的是x最后一位1,比如101000返回的是1000,实际上是8,但我们想知道的并不是8,而是8对应的第几位是1,其实想要的是4(第4位是1),因此我们要存一下:1->(返回)1,10->2,100->3,1000->4,这些信息就开一个map数组来存。

五、注释代码

#include

using namespace std;

const int N = 9,M = 1<<N;
char str[100];
int row[N],col[N],cell[3][3];
int ones[M],Map[M];

void init()
{
	for(int i=0;i<N;++i) row[i]=col[i]=M-1;
	for(int i=0;i<3;++i)
	{
		for(int j=0;j<3;++j)
		{
			cell[i][j]=M-1;
		}
	}
}

inline int lowbit(int x)
{
	return x&(-x);
}

//还需要写一个函数 作用:求(x,y)位置的备选集合是什么(求交集)
inline int get(int x,int y)//求可选方案交集
{
	return row[x]&col[y]&cell[x/3][y/3];
}

//dfs是一个返回布尔类型的函数,如果可以找到答案则返回true,反之返回false
bool dfs(int cnt)
{
	if(!cnt) return true;
	
	//找出可选方案数最少的空白格
	int minv = 10;
	int x,y;
	for(int i=0;i<N;++i)
	{
		for(int j=0;j<N;++j)
		{
		    int tmp = ones[get(i,j)];
		    
			if(str[i*N+j]=='.'&&minv>tmp)
			{
				minv = tmp,x = i,y = j;
			}
		}
	}
	
	for(int i=get(x,y);i;i-=lowbit(i))//枚举(x,y)这个格子的备选方案:
	{
		int t = Map[lowbit(i)];
		//修改状态
		row[x]-=1<<t;
		col[y]-=1<<t;
		cell[x/3][y/3]-=1<<t;
		str[x*N+y]='1'+t;
		if(dfs(cnt-1)) return true;
		//恢复现场
		row[x]+=1<<t;
		col[y]+=1<<t;
		cell[x/3][y/3]+=1<<t;
		str[x*N+y]='.';
	}
	
	return false;
}

int main()
{
    //打表
	for(int i=0;i<N;++i) Map[1<<i]=i;//Map含义见细节分析
	for(int i=0;i<M;++i)
	{
		int s = 0;
		int j = i;
		for(; j; j-=lowbit(j))
		{
			++s;
		}
		ones[i]=s;//i的二进制表示中有s个1
	}
	
	while(cin>>str,str[0]!='e')
	{
		init();
		
		int cnt=0;
		int r = 0;//输入的字符串每一个字符所在的行数
		for(int i=0;str[i];++i)
		{
			int c = i%N;//输入的字符串每一个字符所在的列数
			if(!c&&i) ++r;
			if(str[i]=='.') ++cnt;//如果是'.' 则需要填数,用cnt记录一下一共要填多少个格子
			else
//如果不是'.' 说明已经有数字了,先映射到0~8,之后,第i行我们就不能放t这个数字了,将row[i]的第t位1
//置为0,同理,第j列也不能放t了,将col[i]的第t位1置为0......
			{
				int t = str[i]-'1';
				row[r]-=1<<t;
				col[c]-=1<<t;
				cell[r/3][c/3]-=1<<t;
			}
		}
		dfs(cnt);//之后dfs(cnt),我们要把cnt个没填过的格子填满
		cout<<str<<endl;//填满后将答案输出
	}
	
	return 0;
}

六、补充

inline关键字:编译器自动将inline内联函数展开,放到我们调用的位置,可以节省调用函数的时间。
注意递归函数不能加inline。

你可能感兴趣的:(搜索,深搜,深度优先,剪枝,算法)