标签:
回溯法是剪了枝的穷举,这是字面上的说法,不太好理解,不如讲解实例来的酸爽,于是引出了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秒?莫非此数独真的有专杀暴力破解的神秘力量?欲知其中原理,且听下回分解。
标签:
原文地址:http://www.cnblogs.com/huaxiaforming/p/5221982.html