今天主要专项练习了深度优先搜索(dfs),终于还算是理解了这个算法的原理,今天总算没有虚度,下面就谈一谈我对这个算法的肤浅的理解吧:
深度优先算法其实就是回溯+搜索。它的基本思想是:为了求得问题的解,先选择某一种可能的情况向前探索,在探索过程中,一旦发现这一步的选择是错误的,就退回一步返回上一层次,然后重新选择继续向下探索,如此反复,直至得解或证明无解。
那么什么是回溯呢?回溯法是在用来求问题的所有解时,要回溯到根,且根结点的所有子树都已被搜索遍才结束。而回溯法在用来求问题的任一解时,只要搜索到问题的一个解就可以结束。这种以深度优先的方式系统地搜索问题的解的算法称为回溯法,它适用于解一些组合数较大的问题。回溯主要用递归来实现,在递归构造中,生成和检查过程可以有机结合起来,从而减少不必要的枚举。
接下来附上我今天专项练习的代码,我都写上了详细的注释,如果这几道例题搞懂了,那么这个知识点也就掌握了。
eg1.n皇后问题(以6皇后为样例输入)
#include<iostream>
#include<cmath>
using namespace std;
void print(int*a,int n)
{
for(int i=1;i<=n;i++)
cout<<"("<<i<<","<<a[i]<<")"<<" ";
cout<<endl;
}
bool judge(int*a,int i)//与前i-1行比较,判断第i行的当前位置是否合法
{
for(int k=1;k<i;k++)
if(a[k]==a[i]||fabs(i-k)==fabs(a[i]-a[k]))
return 0;
return 1;
}
void queen(int*a,int i,int n)/* 虽然只用了一个一维数组,但是其中已经保存了足够的信息。
因为每一行只能放一个皇后,所以一维数组的第i个位置存放的
是在第i行的哪一列(第j列)上放置了皇后。这个递归函数
每次处理一行,直到第n行(下标从1开始)。*/
{
for(int j=1;j<=n;j++)
{
a[i]=j;
if(judge(a,i)) /* 这个递归程序的结束条件是第n行放置完毕,
所以,当递归函数从调用queen(a, i + 1, n)返回时,
就是回到了第i行,继续搜索合适的位置。当第i + 1行的
所有位置都不能满足的时候,上面的调用就会返回,也就
是进行了所谓的回溯。这个回溯不需要显式的恢复以前的
调用环境,因为所需要的信息没有被破坏。*/
{
if(i==n)
print(a,n);
else
queen(a,i+1,n);//下一行放置皇后
}
}
}
int main()
{
int n;
cin>>n;
int *a=new int[n+1];
queen(a,1,n);
return 0;
}
输入6
输出结果:
一共有四种情况,每行代表一种情况。
eg2.跳马问题(骑士周游问题的变式)
问题简述:要求在8*8中国象棋格子,任意位置放一个马,如何不重复地把格子走完
#include<iostream>//8*8棋盘
#include<iomanip>
using namespace std;
int u[8]={1,2,1,2,-1,-1,-2,-2};//8个方向上x,y增量
int v[8]={2,1,-2,-1,2,-2,-1,1};
int a[100][100]={0},num=0;//a[][]记每一步走在棋盘的哪一格
bool b[100][100]={0};//记棋盘的每一格有没有被走过
int search(int,int,int);//以每一格为阶段,在每一步中试遍8个方向
int print();//打印方案
int main()
{
a[1][1]=1;b[1][1]=1;//从(1,1)开始走
search(1,1,2);//从(1,1)开始,搜第2步该怎样走
cout<<num<<endl;//输出总方案数
return 0;
}
int search(int i,int j,int n)
{
int k,x,y;
if(n>64)//打印最大规模统计方案
{
print();
return 0;
}
for(k=0;k<=7;k++)//遍历8个方向
{
x=i+u[k];y=j+v[k];
if(x<=8&&x>=1&&y<=8&&y>=1&&(!b[x][y]))
{
b[x][y]=1;
a[x][y]=n;
search(x,y,n+1);//从(x,y)去搜下一步该如何走
b[x][y]=0;
a[x][y]=0;
}
}
}
int print()
{
num++;
if(num<=8)
{
for(int k=1;k<=8;k++)
{for(int kk=1;kk<=8;kk++)
cout<<setw(8)<<a[k][kk];
cout<<endl;
}
cout<<endl;
}
}
......
eg3.素数环
问题简述: 将从1到n这20个整数围成一个圆环,若其中任意2个相邻的数字相加,结果均为素数,那么这个环就成为素数环。
#include<iostream>
#include<cmath>
using namespace std;
bool b[21]={0};
int total=0,a[21]={0};
int search(int);//回溯过程
int print();//打印结果
bool pd(int,int);//判断素数
int main()
{
search(1);
}
int search(int t)
{
int i;
for(i=1;i<=20;i++)
if(pd(a[t-1],i)&&(!b[i]))//满足条件
{
a[t]=i;//保存结果
b[i]=1;//保存结果
if(t==20)
{if(pd(a[20],a[1]))
print();}//如果到达目的地输出解
else search(t+1);/*
回溯就是让计算机自动的去搜索,碰到符合的情况就结束或者保存起来,
在一条路径上走到尽头也不能找出解,就回到上一级的岔路口,选择一条以前没有走过的路继续探测,直到找到解或者走完所有路径为止。
就这一点,回溯和所谓的DFS(深度优先搜索)是一样的。那现在的关键是,怎么实现搜索呢?
回溯既然一般使用递归来实现,那个这个递归调用该如何来写呢?
我现在的理解就是,进行回溯搜索都会有一系列的步骤,每一步都会进行一些查找。
而每一步的情况除了输入会不一样之外,其他的情况都是一致的。
这就刚好满足了递归调用的需求。通过把递归结束的条件设置到搜索的最后一步,就可以借用递归的特性来回溯了。
因为合法的递归调用总是要回到它的上一层调用的,
那么在回溯搜索中,回到上一层调用就是回到了前一个步骤。
当在当前步骤找不到一种符合条件情况时,那么后续情况也就不用考虑了,
所以就让递归调用返回上一层(也就是回到前一个步骤)找一个以前没有尝试过的情况继续进行。
当然有时候为了能够正常的进行继续搜索,需要恢复以前的调用环境。*/
b[i]=0;
}
}
int print()
{
total++;
if(total<=20) //限定打印前20个结果
{cout<<"<"<<total<<">";
for(int j=1;j<=20;j++)
cout<<a[j]<<" ";
cout<<endl;}
}
bool pd(int x,int y)
{
int k=2,i=x+y;
while(k<=sqrt(i)&&i%k!=0) k++;
if(k>sqrt(i)) return 1;
else return 0;
}
输出结果:
eg4.马的遍历
问题简述:
中国象棋半张棋盘如图所示。马自左下角往右上角跳。今规定只许往右跳,不许往左跳。比如图4(a)中所示为一种跳行路线,并将所经路线打印出
#include<iostream>
using namespace std;
int a[100][3];//a[m][1]记录第m步起始横坐标,a[m][2]记录第m步起始纵坐标
int t;//记录路径总数
int y[4]={1,2,-1,-2};
int x[4]={2,1,2,1};//四种移动规则,x代表横坐标,y代表纵坐标
int search(int);//搜索
int print(int);//打印
int main()
{
a[1][1]=0;a[1][2]=0;//第一步从(0,0)开始,a[1][1]表示第一步起始横坐标,a[1][2]表示第一步起始纵坐标
search(2);
return 0;
}
int search(int i)
{
for(int j=0;j<4;j++)//向4个方向跳
if(a[i-1][1]+x[j]>=0&&a[i-1][1]+x[j]<=8&&a[i-1][2]+y[j]>=0&&a[i-1][2]+y[j]<=4)//判断马是否越界
{
a[i][1]=a[i-1][1]+x[j];
a[i][2]=a[i-1][2]+y[j];//保存当前马的位置
if(a[i][1]==8&&a[i][2]==4)
print(i);
else search(i+1);//搜索下一步
}
}
int print(int ii)
{
t++;
cout<<t<<": ";
for(int i=1;i<ii;i++)
cout<<"("<<a[i][1]<<","<<a[i][2]<<")"<<"-->";
cout<<"(4,8)";
cout<<endl;
}
输出结果: