回溯法、数独与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秒?莫非此数独真的有专杀暴力破解的神秘力量?欲知其中原理,且听下回分解。

时间: 2024-08-11 01:34:23

回溯法、数独与N阶可达问题(一)的相关文章

数独1--暴力回溯法(时间超)

数独1--暴力回溯法(时间超) 一.心得 可用暴力搜索法(找唯一数单元格)和Dancing Links算法求解 先回顾之前的三篇文章 "算法实践--数独的基本解法",介绍求解数独的基本的暴力搜索法 "跳跃的舞者,舞蹈链(Dancing Links)算法--求解精确覆盖问题",网友huangfeidian介绍的求解数独的舞蹈链(Dancing Links)算法,这篇文章是介绍舞蹈链(Dancing Links)算法的. "算法实践--舞蹈链(Dancing

回溯法求解数独算法(C语言)

没有对输入的待解数独进行一般性验证(同一行.一列以及同一个小九宫格都不能出现重复数字) 算法利用回溯的思想: 从第一个空白处开始,找到其候选解(排除同行.同列以及同一小九宫格的所有出现过的数字,剩下未出现的数字都是候选解)的第一个值填入数独. 对第二个空白执行第一步(前面所填入的数字对此空白处有影响). 当出现某个空白的候选解个数为0时,就开始回溯,找到第一个候选解多于一个的,将其在使用的候选解设为不可取(本程序取值为-1),找到其下一个候选解,继续上面的步骤! 直到所有空白处填满,运算完成,输

python常用算法(7)——动态规划,回溯法

引言:从斐波那契数列看动态规划 斐波那契数列:Fn = Fn-1 + Fn-2    ( n = 1,2     fib(1) = fib(2) = 1) 练习:使用递归和非递归的方法来求解斐波那契数列的第 n 项 代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 # _*_coding:utf-8_*_ def fibnacci(n):     if n == 1

五大常用算法之四:回溯法

(转自:http://www.cnblogs.com/steven_oyj/archive/2010/05/22/1741376.html) 1.概念 回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径. 回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标.但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”. 许

回溯法 -数据结构与算法

1.回溯法算法思想: 定义: 回溯法(探索与回溯法)是一种选优搜索法,按选优条件向前搜索,以达到目标.但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”. 1.回溯法适用:有许多问题,当需要找出它的解集(全部解)或者要求回答什么解是满足某些约束条件的最优解时,往往要使用回溯法. 2.有组织的穷举式搜索:回溯法的基本做法是搜索或者有的组织穷尽搜索.它能避免搜索所有的可能性.即避免不必要的搜索.这种方

回溯法求迷宫问题

回溯法(探索与回溯法)是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标.但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”.本文使用回溯法求解迷宫问题迷宫问题,有一个m行n列的矩阵代表迷宫,1代表此路不通,0代表此路通.指定入口和出口,判断是否能从入口进,从出口走出.此程序只判断从路口到出口是否能走通,找到的路不一定是最短路(最短路的程序在下一篇中使用BFS算法给出),注意:从入口到

算法入门经典-第七章 例题7-4-1 拓展 n皇后问题 回溯法

实际上回溯法有暴力破解的意思在里面,解决一个问题,一路走到底,路无法通,返回寻找另   一条路. 回溯法可以解决很多的问题,如:N皇后问题和迷宫问题. 一.概念 回溯算法实际类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现不满足条件的时候,就回溯返回,尝试别的路径. 百度解释:回溯法(探索与回溯法)是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标.但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯

基于回溯法寻找哈密顿回路

回溯法是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标.但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”. 在包含问题的所有解的解空间树中,按照深度优先搜索的策略,从根结点出发深度探索解空间树.当探索到某一结点时,要先判断该结点是否包含问题的解,如果包含,就从该结点出发继续探索下去,如果该结点不包含问题的解,则逐层向其祖先结点回溯(其实回溯法就是对隐式图的深度优先搜索算法). 若用

算法思想之回溯法

一.概念 回溯:当把问题分成若干步骤并递归求解时,如果当期步骤没有合法选择,则函数将返回上一级递归调用,这种现象称为回溯. 回溯算法应用范围:只要把待求解问题分成不太多的步骤,每个步骤又只有不太多的选择,即可以考虑用回溯法. 回溯算法实际上是一个递归枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径. 回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标.但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不