回溯法数独与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阶可达问题的主要内容,如果未能解决你的问题,请参考以下文章