回溯法:
回溯的意义是在递归直到可解的最小问题后,逐步返回原问题的过程,而这里所说的回溯算法实际上是一个类似枚举的搜索尝试方法,它的主题思想是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。
回溯算法师尝试搜索算法中最基本的一种算法,其采用了一种“走不通就掉头”的思想,作为控制结构。在使用回溯算法解决问题中每向前走一步都有很多路径需要选择,但当没有决策信息或决策信息不充分时,只好尝试某一路径向下走,直至走到一定程度后得知此路不通时,再回溯到上一步尝试其他路径;当然在尝试成功时,则问题得解而算法结束。
回溯法典型例题-八皇后问题:
要在8*8的国际象棋棋盘中放八个皇后,使任意两个皇后都不能互相吃掉。规则:皇后能吃掉同一行、同一列、同一对角线的任意棋子。下图为一种方案,求所有的解。
第一种解决方法就是枚举法,这种方法较为简单,共有8^8个状态,但实际搜索当中并不会搜索那么多种状态,因为前面已经说明,回溯法采用的是“走不通就掉头”的策略,所以当我们枚举时候加一些相应的约束条件,可使算法的效率大大提高。
那么怎么确定约束条件呢?
约束条件有三个:
(1)不在同一列,即:xi !=xj(xi ,xj 分别代表第 i ,j 个皇后分别位于第 xi , xj 列);
(2)不在同一主对角线:xi – i != xj – j;
(3)不在同一副对角线:xi + i ! = xj + j;
(2)和(3)又可以合并为一个“不在同一对角线上”的约束条件,表示为:
abs( xi – xj ) != abs( i – j ) abs()代表取绝对值。
故得到算法如下
算法一:
#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
int check(int a[ ],int n)
{
int i;
for(i=1; i<=n-1; i++)
if(abs(a[i]-a[n])==abs(i-n)||(a[i]==a[n]))
return 0;
return 1;
}
int main()
{
int a[9];
for(a[1]=1; a[1]<=8; a[1]++)
{
for(a[2]=1; a[2]<=8; a[2]++)
{
if(check(a,2)==0)
continue;
for(a[3]=1; a[3]<=8; a[3]++)
{
if(check(a,3)==0)
continue;
for(a[4]=1; a[4]<=8; a[4]++)
{
if(check(a,4)==0)
continue;
for(a[5]=1; a[5]<=8; a[5]++)
{
if(check(a,5)==0)
continue;
for(a[6]=1; a[6]<=8; a[6]++)
{
if(check(a,6)==0)
continue;
for(a[7]=1; a[7]<=8; a[7]++)
{
if(check(a,7)==0)
continue;
for(a[8]=1; a[8]<=8; a[8]++)
{
if(check(a,8)==0)
continue;
else
{
for(int i=1; i<=8; i++)
printf("%d ",a[i]);
printf("\n");
}
}
}
}
}
}
}
}
}
}
枚举算法虽然有很好的可读性,也能从中体会到回溯的思想,但它只能解决“皇后个数为常量”的问题,却不能解决任意的n皇后问题,下面就是典型的非递归回溯算法:
算法二:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
using namespace std;
int a[30],n;
bool check(int k)
{
for(int i=1; i<k; i++)
if(abs(a[i]-a[k])==abs(i-k)||a[i]==a[k])
return false;
return true;
}
void output()
{
for(int i=1; i<=n; i++)
cout<<a[i]<<" ";
cout<<endl;
}
void backdate(int n)
{
int k=1;
a[1]=0;
while(k>0)
{
a[k]++;
while(a[k]<=n&&check(k)==false) //为第k个皇后搜索位置
a[k]++;
if(a[k]<=n)
{
if(k==n) //找到一组解
output();
else
a[++k]=0;//下一个皇后要从头开始搜索
}
else
k--;//回溯
}
}
int main()
{
cin>>n;
backdate(n);
}
递归回溯算法:
我们用b,d,c数组分别来记录棋盘上的n个列,2n-1个主对角线,2n-1个副对角线的占用情况。
说明:
用i,j分别 表示皇后所在的行列(或者说i号皇后在j列),同一主对角线上的行列下标的差一样,若用表达式 i-j 编号,则范围是 -n+1 ~ n+1,所以用i-j+n表示主对角线编号,范围就是1~ 2n-1。同样地,副对角线上行列下标的和一样,用表达式i+j编号,则范围2~2n。
算法三:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
using namespace std;
int a[20],b[20],c[20],d[20];
int n,t,k;
void output()
{
t++;
cout<<t<<":";
for(int k=1;k<=n;k++)
cout<<" "<<a[k];
cout<<endl;
}
void tryy(int i)
{
for(int j=1;j<=n;j++)
{
if(b[j]==0&&c[i+j]==0&&d[i-j+n]==0)
{
a[i]=j; //摆放皇后
b[j]=1; //占领第j列
c[i+j]=1; //主对角线
d[i-j+n]=1; //副对角线
if(i<n)
tryy(i+1); //搜索
else //找到一组解
output();
b[j]=0;
c[i+j]=0;
d[i-j+n]=0; //重新设置为0
}
}
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
b[i]=0;
c[i]=0;
c[n+i]=0;
d[i]=0;
}
tryy(1);
}