题解 UVA1309 【Sudoku】

传送门

分析

暴搜+剪枝+位运算优化即可。

剪枝

如何剪枝呢?面对每一个状态,我们分别考虑每一个空格、行、列和十六宫格

  • 如果有一个位置哪个字母都不能填,那么就立刻回溯。

  • 如果有一个位置只能填 11 个字母,那么就立刻填这个字母。

判断完上述几种情况后,我们再选择能填的字母最少的位置,枚举填哪个字母即可。

位运算优化

对于每个位置,用一个 1616 位二进制数表示该位置能填的情况,每次递归前提前存个副本,方便回溯。

坑点

输入时用的组数 TT 一定要开 long long,不然你会被恶心的数据 WA\color{#E74C3C}\text{WA}

代码

#include<bits/stdc++.h>
using namespace std;
//数独模板********************************************
const int N=4;//数独有N*N行和N*N列 
char kong='-';//空位的字符 
char jiz='A';//基准,如'0'或'A' 
char s[N*N][N*N];
int cnt[1<<N*N],f[1<<N*N],num[N*N][N*N],n=0;
vector<int>e[1<<N*N];
//存储 
void Sudoku_work(int i,int j,int k)
{
	for(int t=0;t<N*N;t++)
	{
		//把同行同列都标记(第k位的1变0,0变1) 
		num[i][t]&=~(1<<k);//行 
		num[t][j]&=~(1<<k);//列 
	}
	int x=i/N*N,y=j/N*N;//求所属格子的左上角点 
	for(int ti=0;ti<N;ti++)
		for(int tj=0;tj<N;tj++)
			num[x+ti][y+tj]&=~(1<<k);//标记整个十六宫格
}
//搜索 
bool Sudoku_dfs(int ans)
{
	if(!ans)//填完了说明可行 
		return 1;
	int pre[N*N][N*N];//副本 
	memcpy(pre,num,sizeof(pre));//复制
	//剪枝1:遍历当前所有空格
	for(int i=0;i<N*N;i++)
		for(int j=0;j<N*N;j++)
			if(s[i][j]==kong)
			{
				if(!num[i][j])//若某个位置哪个字母都不能填,则回溯
					return 0;
				if(cnt[num[i][j]]==1)//若某个位置只能填一个字母,则马上填
				{
					s[i][j]=f[num[i][j]]+jiz;
					Sudoku_work(i,j,f[num[i][j]]);
					if(Sudoku_dfs(ans-1))
						return 1;
					s[i][j]=kong;
					memcpy(num,pre,sizeof(num));
					return 0;
				}
			}
	//剪枝2:考虑所有的行 
	for(int i=0;i<N*N;i++)
	{
		int w[N*N],o=0;
		memset(w,0,sizeof(w));
		for(int j=0;j<N*N;j++)
			if(s[i][j]==kong)
			{
				o|=num[i][j];
				for(unsigned int k=0;k<e[num[i][j]].size();k++)
					++w[e[num[i][j]][k]];
			}
			else
			{
				o|=(1<<(s[i][j]-jiz));
				w[f[1<<(s[i][j]-jiz)]]=-1;
			}
		if(o!=(1<<N*N)-1)//若该行每个字母都不能填,立即回溯 
			return 0;
		for(int k=0;k<N*N;k++)
			if(w[k]==1)//若只能填在该行某个位置上,立即填写 
				for(int j=0;j<N*N;j++)
					if(s[i][j]==kong&&((num[i][j]>>k)&1))
					{
						s[i][j]=k+jiz;
						Sudoku_work(i,j,k);
						if(Sudoku_dfs(ans-1))
							return 1;
						s[i][j]=kong;
						memcpy(num,pre,sizeof(num));
						return 0;
					}
	}
	//剪枝3:考虑所有的列 
	for(int j=0;j<N*N;j++)
	{
		int w[N*N],o=0;
		memset(w,0,sizeof(w));
		for(int i=0;i<N*N;i++)
			if(s[i][j]==kong)
			{
				o|=num[i][j];
				for(unsigned int k=0;k<e[num[i][j]].size();k++)
					++w[e[num[i][j]][k]];
			}
			else
			{
				o|=(1<<(s[i][j]-jiz));
				w[f[1<<(s[i][j]-jiz)]]=-1;
			}
		if(o!=(1<<N*N)-1)//若该列每个字母都不能填,立即回溯 
			return 0;
		for(int k=0;k<N*N;k++)
			if(w[k]==1)//若只能填在该列某个位置上,立即填写
				for(int i=0;i<N*N;i++)
					if(s[i][j]==kong&&((num[i][j]>>k)&1))
					{
						s[i][j]=k+jiz;
						Sudoku_work(i,j,k);
						if(Sudoku_dfs(ans-1))
							return 1;
						s[i][j]=kong;
						memcpy(num,pre,sizeof(num));
						return 0;
					}
	}
	//剪枝4:考虑所有的十六宫格 
	for(int i=0;i<N*N;i+=N)
		for(int j=0;j<N*N;j+=N)
		{
			int w[N*N],o=0;
			memset(w,0,sizeof(w));
			for(int p=0;p<N;p++)
				for(int q=0;q<N;q++)
					if(s[i+p][j+q]==kong)
					{
						o|=num[i+p][j+q];
						for(unsigned int k=0;k<e[num[i+p][j+q]].size();k++)
							++w[e[num[i+p][j+q]][k]];
					}
					else
					{
						o|=(1<<(s[i+p][j+q]-jiz));
						w[f[1<<(s[i+p][j+q]-jiz)]]=-1;
					}
			if(o!=(1<<N*N)-1)//若该十六宫格每个字母都不能填,立即回溯
				return 0;
			for(int k=0;k<N*N;k++)
				if(w[k]==1)//若只能填在该十六宫格某个位置上,立即填写
					for(int p=0;p<N;p++)
						for(int q=0;q<N;q++)
							if(s[i+p][j+q]==kong&&((num[i+p][j+q]>>k)&1))
							{
								s[i+p][j+q]=k+jiz;
								Sudoku_work(i+p,j+q,k);
								if(Sudoku_dfs(ans-1))
									return 1;
								s[i+p][j+q]=kong;
								memcpy(num,pre,sizeof(num));
								return 0;
							}
		}
	//选择可以填写的字母最少的位置,再枚举 
	int k=N*N+1,tx,ty;
	for(int i=0;i<N*N;i++)
		for(int j=0;j<N*N;j++)
			if(s[i][j]==kong&&cnt[num[i][j]]<k)
			{
				k=cnt[num[i][j]];
				tx=i;
				ty=j;
			}
	for(unsigned int i=0;i<e[num[tx][ty]].size();i++)
	{
		int tz=e[num[tx][ty]][i];
		Sudoku_work(tx,ty,tz);
		s[tx][ty]=tz+jiz;
		if(Sudoku_dfs(ans-1))
			return 1;
		s[tx][ty]=kong;
		memcpy(num,pre,sizeof(num));
	}
	return 0;
}
//得到cnt值 
int Sudoku_get_cnt(int i)
{
	int k=i&-i;//相当于k=lowbit(i)
	e[i].push_back(f[k]);
	for(unsigned int j=0;j<e[i-k].size();j++)
		e[i].push_back(e[i-k][j]);
	return cnt[i-k]+1;
}
//初始化 
void Sudoku_init() 
{
	memset(f,0,sizeof(f));
	for(int i=0;i<N*N;i++)//求2的幂对应的次方 
		f[1<<i]=i;//f[x]=log(x)
	cnt[0]=0;
	for(int i=1;i<(1<<N*N);i++)
		cnt[i]=Sudoku_get_cnt(i);
}
//输入 
void Sudoku_cin()
{
	for(int i=0;i<N*N;i++)
		cin>>s[i];
}
//输出 
void Sudoku_cout()
{
	for(int i=0;i<N*N;i++)
	{
		for(int j=0;j<N*N;j++)
			cout<<s[i][j];
		cout<<endl;
	}
}
//入口 
void Sudoku()//使用前记得Sudoku_init()和Sudoku_cin()
{
	for(int i=1;i<N*N;i++)
		cin>>s[i];
	for(int i=0;i<N*N;i++)//初始化每一位都是1 
		for(int j=0;j<N*N;j++)
			num[i][j]=(1<<N*N)-1;//第i行第j列的存储情况 
	int ans=0;//还差几个填完 
	for(int i=0;i<N*N;i++)
		for(int j=0;j<N*N;j++)
			if(s[i][j]!=kong)//非空则插入 
				Sudoku_work(i,j,s[i][j]-jiz);
			else
				ans++;
	Sudoku_dfs(ans);
}
//数独模板********************************************
int main()
{
	Sudoku_init();
	long long T=0;
	while(cin>>s[0])
	{
		if(T++)
			puts("");
		Sudoku();
		Sudoku_cout(); 
	} 
	return 0;
}

我不想用 Dancing Links,可能是因为我不会

  • 算法思维能力的训练远比学习几个能直接拿来解决问题的数据结构重要。——《算法竞赛进阶指南》

福利

双倍经验&三倍经验&四倍经验

posted @ 2021-03-20 21:31  luckydrawbox  阅读(8)  评论(0)    收藏  举报  来源