回溯法数独与N阶可达问题

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了回溯法数独与N阶可达问题相关的知识,希望对你有一定的参考价值。

  回溯法是剪了枝的穷举,这是字面上的说法,不太好理解,不如讲解实例来的酸爽,于是引出了N阶可达问题:

  有N个国家,每个国家有若干城市,小明要从中国(任意一个城市)出发,遍历所有国家(假设这个遍历顺序已经定了),最终到达美利坚(任意一个城市)。而城市之间有可能不可达,只有小明尝试过才知道(就是后面的check()函数),求满足要求的一条路径?

技术分享

  从上面的表述中我们已经嗅到了浓浓的穷举屌丝气质——遍历所有组合,但是我们的回溯思想总是基于这样一个简单的事实:如果当前选择导致你走进了死胡同,那么这个选择一定是错误的,同时基于这个错误的后续所有的选择都是错误而无意义的(剪枝)。道理的前半句表明我们要及时回溯,而后半句指出了这样做的优点是剪枝。比如小明要遍历中国---日本---美国,小明选择从中国武汉出发,这个选择是正确的还是错误的尚不明确,但是小明经过许多个check()之后发现,没有从武汉到日本任意一个城市的可达线路,这说明选择从武汉出发这个决定是错误的,应该回溯,重新选择一个中国的起点城市,小明在不知不觉中已经排除了形如(中国武汉市)---(日本XX市)---(美国XX市)的诸多组合,这就是所谓的剪了枝的穷举。

  数独问题也是典型的N阶可达问题,下面以一个挖去64个洞的数独为例来具体说明,每个洞有1~9共9种可能性,一共要填64个洞,并且每填写好一个洞对后续的步骤会产生影响。

技术分享

  假设我们是从左到右,从上到下依次填写数字,那么此数独问题可以表达为如下的64阶可达问题:

技术分享

  根据回溯思想,采用递归函数(因为每层的情况是一样的,请读者思考如果不一样该如何编程)依次处理编号为0~80共计81个格子,可写出如下的求解代码:

/*************************************************************** 
程序作者:yin
创建日期:2016-2-26 
程序说明:该程序读取同目录下sudoku.txt文件的特定的数独格式,然后
采用回溯法求解,当找到一解后立即返回结束搜索。程序的部分代码参考
自互联网。
************************************************************** */
#include<stdio.h>
#include <ctime>
int result=0; //结果数
int try_times=0;
 int sudoku[9][9];
void solver(int sudoku[9][9],int n);
void read_sudoku()
{
	FILE *fp=fopen("sudoku.txt","r+");
	
		for(int i=0;i<9;i++)
		for(int j=0;j<9;j++)
		{
			char temp=fgetc(fp);
			 if(temp!=‘\n‘)		sudoku[i][j]=temp-48;
			 else			sudoku[i][j]=fgetc(fp)-48;
		} 
	fclose(fp);
}
void show_sudoku(int a[9][9])
{
	printf(" -------------------\n");	
	for(int i=0;i<9;i++)
		{
			printf(" | ");
			for(int j=0;j<9;j++)
			{
				printf("%d",a[i][j]);
				if(j==2 || j==5 || j==8)printf(" | ");
			}
			
			printf("\n");
			if(i==2 || i==5 || i==8)printf(" -------------------\n");	
		}
}


//判断是否可以将第i行、第j列的数设为k
bool check(int sudoku[9][9],int i,int j,int k)
{
 int m,n;
 //判断行
 for(n=0;n<9;n++)
 {
  if(sudoku[i][n] == k)
   return false;
 }
 //判断列
 for(m=0;m<9;m++)
 {
  if(sudoku[m][j] == k)
   return false;
 }
 //判断所在小九宫格
 int t1=(i/3)*3,t2=(j/3)*3;
 for(m=t1;m<t1+3;m++)
 {
  for(n=t2;n<t2+3;n++)
  {
   if(sudoku[m][n] == k)
    return false;
  }
 }
 //可行,返回true
 return true;
}
//数独求解函数
void solver(int sudoku[9][9],int n)
{if(result==1) return;
 int temp[9][9];
 int i,j;
 for(i=0;i<9;i++)
  for(j=0;j<9;j++)
   temp[i][j]=sudoku[i][j];

 i=n/9; j=n%9; //求出第n个数的行数和列数
 //若可以后移,就后移一个格子,若不能程序结束 
 if(sudoku[i][j] != 0)
 {
  if(n == 80)//递归退出点 
	{
		result++;
		 printf(" 数独的解为:\a\n");
		show_sudoku(temp);
	}
  else    solver(temp,n+1); 
 } 
 else    //空各格子 
 {
  for(int k=1;k<=9;k++)
  {
   bool flag=check(temp,i,j,k);
   if(flag) //第i行、第j列可以是k
   {
 	//------------------------------------
 	try_times++;	
	//------------------------------------	  
    temp[i][j]=k; //设为k
    
    //若可以后移,就后移一个格子,若不能程序结束 
    if(n == 80) //递归退出点
	{
		result++;
		 printf(" 数独的解为:\a\n");
		show_sudoku(temp);
	}
    else		solver(temp,n+1);

    temp[i][j]=0; //回溯擦除这个错误 
   }
  }
  //退层点
 }

}
int main()
{
 time_t start,end; 
 start=clock();
 
	//读显数独题目
	read_sudoku();
	show_sudoku(sudoku);
	//按照一定的顺序,一个一个地处理格子
	solver(sudoku,0); 
	  if(result==0)	printf("此数独无解!\a");

 end=clock(); 
printf("------------------------------------------------------------------------\n");
printf("total run time :%10dms\n",end-start);
printf("        共尝试:%10ld次\n",try_times);
printf("------------------------------------------------------------------------\n");
 getchar(); 
 return 0;
}

  下面我们用号称世界最难的数独题来测试一下程序,首先在程序同目录下建立sudoku.txt的文件,然后输入以下内容:

800000000
003600000
070090200
050007000
000045700
000100030
001000068
008500010
090000400

  保存然后运行程序,得到如下结果。程序大约运行了60ms,在进行了49584次尝试之后找到了数独的解。

技术分享

  再来一发,号称专杀暴力破解的数独题试一下:

技术分享

  哇,好奇怪,世界最难数独都能在百毫秒内求解,为什么这个数独题居然花了大约37秒?莫非此数独真的有专杀暴力破解的神秘力量?欲知其中原理,且听下回分解。

 

以上是关于回溯法数独与N阶可达问题的主要内容,如果未能解决你的问题,请参考以下文章

为啥我使用回溯解决数独的 JAVA 代码没有给出任何解决方案? [关闭]

数独回溯算法

python中数独的回溯算法

数独代码(回溯)

数独求解器回溯算法不起作用

Javascript递归回溯数独求解器