一、深度优先搜索(DFS)
深度优先搜索是一种用于遍历或搜索树或图的算法。在深度优先搜索中,从起始顶点开始,沿着路径直到不能再继续为止,然后回溯并探索下一个分支。
深度优先搜索研究的是有没有一条路径能从A到B的问题,它不考虑这个过程的长与短,只考虑有没有。
1、递归回溯算法框架
基本思想
为了求得问题的解,先选择某一种可能情况向前探索,在探索过程中,一旦发现原来的选择是错误的,就退回一步重新选择,然后继续向前探索,如此反复执行,直到得到解或证明无解。
框架
int search(int t)
{
for(i=1;i<=算符种数;i++)
{
if(满足条件)
{
保存结果;
if(到目的地) 输出解;
else search(t+1);
恢复:保存结果之前的状态(回溯)
}
}
}
int search(int t)
{
if(到目的地) 输出解;
else
{
for(i=1;i<=算符种数;i++)
if(满足条件)
{
保存结果;
search(t+1);
恢复:保存结果之前的状态(回溯)
}
}
}
2、算法应用
【例题1】
素数环:从1到20,将这20个数摆成一个环,要求相邻的两个数的和是一个素数。
#include <bits/stdc++.h>
using namespace std;
int a[21],b[21],total;
bool pd(int x,int y) //判断素数
{
int i=x+y;
for(int j=2;j*j<=i;j++)
{
if(i%j==0)
return false;
}
return true;
}
void print()
{
total++; //total没有定义的话默认初始值为0
cout<<"<方案"<<total<<">";
for(int j=1;j<=20;j++) cout<<a[j]<<" ";
cout<<endl;
}
void search(int t) //t代表第t个位置
{
int i;
for(i=1;i<=20;i++) //每个位置有20种可能
{
if(pd(a[t-1],i)&&b[i]==0) //b[i]==0表示i可用
{
a[t]=i; //将结果保存到数组a[]
b[i]=1; //标记结果已被占用
if(t==20) //第20个要跟第1个进行判断
{
if(pd(a[20],a[1])) print();
}
else search(t+1); //没到第20个就往下走
b[i]=0; //都不满足就回溯
}
}
}
int main()
{
search(1); //从第1个开始搜索
cout<<total<<endl;
return 0;
}
【例题2】
设有n个整数集合{1,2,3...n},从中任意取出r个数进行排列(r<n),试列出所有的排列。
#include<bits/stdc++.h>
using namespace std;
int n,r,a[10001],b[10001],num;
void print()
{
num++;
for(int i=1;i<=r;i++)
{
cout<<" "<<a[i];
}
cout<<endl;
}
void search(int t)
{
int i;
for(i=1;i<=n;i++)
{
if(b[i]==0)
{
a[t]=i;
b[i]=1;
if(t==r) print();
else search(t+1);
b[i]=0;
}
}
}
int main()
{
cout<<"请输入两个数n和r:";
cin>>n>>r;
search(1);
cout<<"number="<<num<<endl;
return 0;
}
【例题3】
任何一个大于1的自然数n,总可以拆分成若干个小于n的自然数之和。当n=7,共有14种拆分方法:
#include<bits/stdc++.h>
using namespace std;
int a[10001]={1},n,total;
int print(int t)
{
cout<<n<<"=";
for(int i=1;i<=t-1;i++)
{
cout<<a[i]<<"+";
}
cout<<a[t]<<endl;
total++;
}
int search(int s,int t)
{
int i;
for(i=a[t-1];i<=s;i++)
{
if(i<n)
{
a[t]=i;
s=s-i;
if(s==0) print(t);
else search(s,t+1);
s=s+i;
}
}
}
int main()
{
cin>>n;
search(n,1);
cout<<"total="<<total<<endl;
return 0;
}
【例题4】
八皇后问题:要在国际象棋棋盘中放八个皇后,使得任意两个皇后都不能互相吃(皇后只能吃同一行、同一列、同一对角线的任意棋子)。
#include <bits/stdc++.h>
using namespace std;
int a[9],b[9],c[17],d[17],sum=0;
void print()
{
int i;
sum++;
cout<<"sum="<<sum<<endl;
for(i=1;i<=8;i++)
{
cout<<" "<<a[i];
}
cout<<endl;
}
void search(int i) //i表示行
{
int j; //j表示列
for(j=1;j<=8;j++)
{
if((b[j]==0)&&(c[i+j]==0)&&(d[i-j+7]==0)) //右上-左下的对角线行与列加起来是定值 ,左上-右下的对角线行与列相减是定值
{
a[i]=j; //保存这一行的皇后放在了哪一列
b[j]=1; //列被占用
c[i+j]=1; //右上-左下的对角线被占用
d[i-j+7]=1; //左上-右下的对角线被占用
if(i==8) print();
else search(i+1); //继续放下一行的皇后
b[j]=0; //列释放
c[i+j]=0; //对角线释放
d[i-j+7]=0;
}
}
}
int main()
{
search(1);
return 0;
}
二、广度优先搜索(BFS)
广度优先搜索算法(又称宽度优先搜索)是最简便的图的搜索算法之一,这一算法也是很多重要的图的算法的原型。
1、基本思想
广度优先算法的核心思想是,从初始节点开始,应用算符生成第一层节点,检查目标节点是否在这些后继节点中,若没有,再用产生式规则将所有第一层的节点注意扩展,得到第二层节点,并逐一检查第二层节点中是否含有目标节点。若没有,再用算符逐一扩展第二层的所有节点,如此一次扩展,检查下去,直到发现目标节点为止。即:
- 从图中的某一顶点V0开始,先访问V0;
- 访问所有与V0相邻接的节点V1,V2,...,Vt;
- 依次访问与V1,V2,...,Vt相邻接的所有未曾访问过的节点;
- 循此以往,直至所有的顶点都被访问过为止。
广度优先搜索是一种分层的查找过程,每向前走一步可能访问一批顶点,不像深度优先搜索那样有往回退的情况,因此它不是一个递归的算法。
2、算法应用
【例题1】
解救A同学:迷宫由n行m列的单元格组成(n和m都小于等于50),每个单元格要么是空地,要么是障碍物。你的任务是找到一条从迷宫的起点通往A同学所在位置的最短路径,最终需要几步。注意障碍物是不能走的,当然也不能走到迷宫之外。
输入与输出:
分析:我们用一个二维数组来存储这个迷宫,最开始的时候我们在迷宫(1,1)处,可以往右或者往下走。“一层一层”扩展的方法来找到A同学。扩展时每发现一个点就将这个点加入到队列中,直至走到A同学的位置(p,q)时为止。
方法一:广搜+队列
#include<iostream>
#include<cstdio>
#include<queue>
using namespace std;
struct Node //经过每一个节点的时候都要记录步数,所以用结构体来实现
{
int x,y,step; //step代表走到(x,y)所需最小步数
};
int a[101][101]; //数组存储迷宫的原始数据
bool book[101][101]; //布尔类型表示迷宫是否被访问过了
int n,m,start_x,start_y,end_x,end_y;
bool flag=false; //先假定不能到达A同学的位置
int np[4][2]={{0,1},{1,0},{0,-1},{-1,0}}; //代表方向,向下右上左走
queue<Node> que; //初始化一个队列,队列里是Node节点
void bfs()
{
Node startnode; //存储当前坐标
startnode.x=start_x;
startnode.y=start_y;
startnode.step=0;
book[start_x][start_y]=true; //当前坐标已经被访问过了
que.push(startnode); //存储到队列
while(!que.empty())
{
for(int i=0;i<4;i++) //四个方向都走一走
{
int nx=que.front().x+np[i][0];
int ny=que.front().y+np[i][1];
//判断是否在迷宫内、有没有障碍物、有没有被走过
if(nx>=1&&nx<=n&&ny>=1&&ny<=m&&a[nx][ny]==0&&!book[nx][ny])
{
book[nx][ny]=true; //代表新节点被走过了
Node newnode;
newnode.x=nx;newnode.y=ny;newnode.step=que.front().step+1; //存储新节点
que.push(newnode); //新节点加入队列
}
if(nx==end_x&&ny==end_y) //走到目的地了
{
flag=true; //可以走到A同学
return;
}
}
que.pop(); //否则移出队列
}
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
cin>>a[i][j];
}
}
cin>>start_x>>start_y>>end_x>>end_y;
bfs();
if(flag) cout<<que.back().step; //能走到A同学输出队列
else cout<<-1; //不能走到A同学
return 0;
}
方法二:深搜+递归
#include<bits/stdc++.h>
using namespace std;
int a[101][101]; //数组存储迷宫的原始数据
bool book[101][101]; //布尔类型表示迷宫是否被访问过了
int n,m,start_x,start_y,end_x,end_y,step=0,mins=100;
bool flag=false; //先假定不能到达A同学的位置
int np[4][2]={{0,1},{1,0},{0,-1},{-1,0}}; //代表方向,向下右上左走
void search(int i,int j)
{
for(int k=0;k<4;k++)
{
int nx=i+np[k][0],ny=j+np[k][1];
if(nx>=1&&nx<=n&&ny>=1&&ny<=m&&a[nx][ny]==0&&!book[nx][ny])
{
step++;
book[nx][ny]=1;
if(nx==end_x&&ny==end_y)
{
flag=true;
if(step<mins) mins=step;
}
else search(nx,ny);
step--;
book[nx][ny]=0;
}
}
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
cin>>a[i][j];
}
}
cin>>start_x>>start_y>>end_x>>end_y;
search(1,1);
cout<<mins<<endl;
return 0;
}
方法三:深搜+栈
#include <iostream>
#include <cstdio>
#include <stack>
using namespace std;
struct Node{
int x,y,step;
};
int a[101][101];
bool book[101][101];
int n,m,start_x,start_y,end_x,end_y,minx=100;
bool flag=false;
int np[4][2]={{0,1},{1,0},{0,-1},{-1,0}};
stack<Node> stk; //栈
void dfs(); //深搜+栈
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
cin>>a[i][j];
}
}
cin>>start_x>>start_y>>end_x>>end_y;
dfs();
cout<<minx;
return 0;
}
void dfs()
{
Node startnode; //新节点
startnode.x=start_x;
startnode.y=start_y;
startnode.step=0;
book[start_x][start_y]=true;
stk.push(startnode); //压入栈中
while(!stk.empty()) //如果栈不为空就循环
{
if(stk.top().x==end_x&&stk.top().y==end_y) //到达目的地
{
if(stk.top().step<minx)
{
minx=stk.top().step; //最小步数
}
while(stk.size()>1) stk.pop(); //出栈,从头再找
break;
}
else //没到终点
{
int d=0;
for(int i=0;i<4;i++) //四个方向都走
{
int nx=stk.top().x+np[i][0];
int ny=stk.top().y+np[i][1];
if(nx>=1&&nx<=n&&ny>=1&&ny<=m&&a[nx][ny]==0&&!book[nx][ny])
{
d++; //如果d一次都没有增加过,那就是走到死胡同了
book[nx][ny]=1;
Node newnode; //新节点
newnode.x=nx;newnode.y=ny;newnode.step=stk.top().step+1;
stk.push(newnode); //入栈
break; //深搜,一条路走到底,所以不会再去找相邻的节点了,跳出循环
}
}
if(d==0)
{
stk.pop(); //走到死胡同了,踢出栈,从下一个节点继续搜索
}
}
}
}
【例题2】
交通图:如下图表示的是从城市A到城市H的交通图。从图中可以看出,从城市A到城市H要经过若干个城市。相邻城市之间的距离为1,现要找出一条经过城市最少的一条路线。(H-F-A)
算法分析:用邻接矩阵来表示,0表示能走,1表示不能走。
方法:广搜+队列
#include<iostream>
#include<queue> //用到队列
using namespace std;
int map[9][9]={
{0,0,0,0,0,0,0,0,0},
{0,1,0,0,0,1,0,1,1},
{0,0,1,1,1,1,0,1,1},
{0,0,1,1,0,0,1,1,1},
{0,0,1,0,1,1,1,0,1},
{0,1,1,0,1,1,1,0,0},
{0,0,0,1,1,1,1,1,0},
{0,1,1,1,0,0,1,1,0},
{0,1,1,1,1,0,0,0,1}
};
bool book[101]; //标识某个城市是否走过
int pre[101]; //表示当前城市的前一个城市
queue<int> que;
void bfs();
int main()
{
bfs();
int k=8; //八个城市
cout<<char(k+64); //输出一个H
while(pre[k])
{
cout<<"-"<<char(pre[k]+64); //整型要转变成字符型
k=pre[k];
}
return 0;
}
void bfs()
{
que.push(1); //第一个城市入队
book[1]=true; //第一个城市被访问过了
while(!que.empty())
{
int cur=que.front(); //队列最前面的节点
for(int i=1;i<=8;i++) //找跟队列最前面的节点相连的节点
{
if(map[cur][i]==0&&!book[i]) //相连,并且没被访问过
{
book[i]=true; //标识为访问过了
que.push(i); //入列
pre[i]=cur; //pre存储前一个城市
}
}
que.pop(); //走到死胡同了就pop出来
}
}