1. 宽度优先搜索BFS
从搜索的起点开始,不断地优先访问当前节点的邻居。即首先访问起点,然后依次访问起点尚未访问的邻居结点,再按照访问起点邻居节点的先后顺序依次访问它们的邻居,直至找到解或遍历整个解空间。常用于搜索最优值的问题。
步骤如下:
- 状态:需要确定所求解问题中的状态。通过状态的扩展,遍历所有状态,从中寻找所需要的答案。
- 状态扩展方式:再BFS中,需要尽可能扩展状态,并对先扩展得到的状态先进行下一次状态的扩展。
- 有效状态:对有些状态,无需再次扩展,而是直接舍弃。因为由分析问题可知,目标状态不可能由它们扩展得到。
- 队列:使用队列来实现先得到的状态优先扩展,所以每次取对头元素进行扩展。
- 标记:为了判断哪些状态有效而哪些无效。
- 有效状态数:问题中的有效状态数与时间复杂度同数量级,所以搜索之前要估算是否能接受。
- 最优:BFS常用于求解最优值,因为其搜索到的状态总是按照某个关键字递增。所以适用于“最少,最短,最优”等关键字的问题。
例题:农夫与🐄处于同一直线(可视为坐标轴)上,农夫的位置为N(0≤N≤100000),🐄的位置为K(0≤K≤100000)。农夫有两种移动方式:一分钟内从X走到X-1或X+1;一分钟内从X瞬移到2X。假设🐄不动,问农夫几分钟能追到🐄?
输入:每行两个数,分别为N和K
输出:时间
样例输入:5 17
样例输出:4
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int MAXN=100001;
struct Status//定义状态为(n,t)二元组
{
int n;//某个位置的坐标
int t;//到达这个位置需要的时间
Status(int n,int t):n(n),t(t) {}
};
bool visit[MAXN];//访问队列
int BFS(int n,int k)
{
queue<Status> Q;
Q.push(Status(n,0));//初始状态入队
visit[n]=true;//标记
while(!Q.empty())
{
Status current=Q.front();//访问对头元素
Q.pop();
if(current.n==k)//查找成功
return current.t;
//查找失败
for(int i=0;i<3;++i)//有三种状态可供转入
{
Status next(current.n,current.t+1);//定义一个新状态
if(i==0)//右走
next.n+=1;
else if(i==1)//左走
next.n-=1;
else if(i==2)//瞬移
next.n*=2;
//判断新状态是否合法
if(next.n<0||next.n>=MAXN||visit[next.n])
continue;
Q.push(next);//遍历过的状态入队
visit[next.n]=true;//标记
}
}
}
int main()
{
int n,k;
while(cin>>n>>k)
{
memset(visit,false,sizeof(visit));
cout<<BFS(n,k)<<endl;
}
return 0;
}
2. 深度优先搜索DFS
在搜索过程中,首先访问起点,然后访问它的邻居,再访问该节点的邻居(而非起点的其他邻居),重复直至找到解或当前结点已无未访问的邻居结点为止。之后回溯上一个结点,访问它的另一个邻居结点。
在DFS中,获得一个状态后,立即扩展这个状态,保证先得到的状态后扩展。思想类似堆栈,但是用递归来实现。常用DFS来判断问题是否有解。
剪枝:DFS中通过放弃对某些不可能产生结果的子集的搜索,达到提高效率的目的。
例题1:pxq大小的长方形国际象棋棋盘(小于8x8)上有一个骑士,他想按照日字规则走遍每一个格子。骑士可以在任意格子上开始或结束旅行。请找一条符合的路径。
输入:第一行是样例个数,之后每一行是棋盘长宽p,q;
输出:路径
注:国际象棋的棋盘行用数字表示,列用字母表示,日字走法如下
/*
样例输入:
3
1 1
2 3
4 3
样例输出:
Scenario #1:
A1
Scenario #2:
impossible
Scenario #3:
A1B3C1A2B4C2A3B1C3A4B2C4
*/
#include<iostream>
#include<cstdio>
#include<string>
#include<cstring>
using namespace std;
const int MAXN=30;
int p,q;
bool visit[MAXN][MAXN];
int direction[8][2]={//8个方向
{-1,-2},{1,-2},{-2,-1},{2,-1},{-2,1},{2,1},{-1,2},{1,2}
};
bool DFS(int x,int y,int step,string ans)//状态为(x,y,step)三元组,表示到(x,y)所需步数
{
if(step==p*q)//遍历成功
{
cout<<ans<<endl;//输出路径结点字符串
return true;
}
else
{
for(int i=0;i<8;++i)//按日字理论上有8个方向
{
//遍历邻居结点,得到新状态坐标
int nx=x+direction[i][0];
int ny=y+direction[i][1];
char col=ny+'A';//用于ans字符串后缀更新
char row=nx+'1';
//非法状态
if(nx>=p||nx<0||ny>=q||ny<0||visit[nx][ny])
continue;
//标记
visit[nx][ny]=true;
//递归
if(DFS(nx,ny,step+1,ans+col+row))
return true;
visit[nx][ny]=false;//取消标记
}
}
return false;
}
int main()
{
int n;
cin>>n;
int caseNumber=0;
while(n--)
{
cin>>p>>q;
memset(visit,false,sizeof(visit));//初始化
cout<<"Scenario #"<<++caseNumber<<" :"<<endl;
visit[0][0]=true;
if(!DFS(0,0,1,"A1"))//因为可以在任一格开始,所以可以默认从左下角开始
cout<<"impossible"<<endl;//遍历失败
}
return 0;
}
例题2:棋盘游戏
#include<iostream>
#include<cstdio>
#include<cstring>
#include<climits>
using namespace std;
const int MAXN=6;
int a[MAXN][MAXN];//棋盘
bool visit[MAXN][MAXN];//访问
int sx,sy,ex,ey;//起点,终点
int answer=INT_MAX;//整体最小代价初始化为无穷大
int direction[4][2]={
{0,-1},{0,1},{-1,0},{1,0}//上下左右
};
void DFS(int x,int y,int cost,int state)//cost为当前代价
{
if(visit[x][y])
return;
if(x==ex&&y==ey)//找到终点
{
answer=min(answer,cost);//更新最小代价
return;
}
if(cost>answer)
return;
visit[x][y]=true;
for(int i=0;i<4;++i)//上下左右四个邻居
{
int nx=x+direction[i][0],ny=y+direction[i][1];
if(x<0||y<0||x>=MAXN||y>=MAXN)
continue;
int add=a[nx][ny]*state;
int nstate=add%4+1;
DFS(nx,ny,cost+add,nstate);
}
visit[x][y]=false;//回溯
return;
}
int main()
{
memset(visit,false,sizeof(visit));
for(int i=0;i<6;++i)
{
for(int j=0;j<6;++j)
{
cin>>a[i][j];
}
}
cin>>sx>>sy>>ex>>ey;
DFS(sx,sy,0,1);
cout<<answer<<endl;
return 0;
}
例题3:输出1~n的全排列(n≤9)
#include<iostream>
#include<cstdio>
using namespace std;
const int MAXN=10;
int p[MAXN]={0};
bool visit[MAXN]={false};
int n;//对1~n全排列
void DFS(int x)//x为参与全排列的个数
{
if(x==n+1)//找到目标状态
{
for(int i=1;i<=n;i++)//输出当前排列方式
{
cout<<p[i]<<" ";
}
cout<<endl;
return;
}
for(int i=1;i<=n;i++)//状态扩展
{
if(visit[i]!=true)//状态合法
{
p[x]=i; //修改
visit[i]=true; //标记
DFS(x+1); //递归
visit[i]=false; //还原标记(回溯)
}
}
}
int main()
{
while(cin>>n)
{
DFS(1);
}
return 0;
}
例题4:组合数输出
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int MAXN=22;
int p[MAXN];
bool visit[MAXN]={false};
int n,r;
void DFS(int index)
{
if(index==r+1)
{
for(int i=1;i<=r;i++)
cout<<p[i]<<" ";
cout<<endl;
return;
}
for(int i=p[index-1];i<=n;i++)
{
if(visit[i]!=true)
{
p[index]=i;
visit[i]=true;
DFS(index+1);
visit[i]=false;
}
}
return ;
}
int main()
{
while(cin>>n>>r)
{
memset(visit,false,sizeof(visit));
p[0]=1;
DFS(1);
}
return 0;
}
3.回溯法
回溯法(backtracking)是优先搜索的一种特殊情况,又称为试探法,常用于需要记录节点状态的深度优先搜索。通常来说,排列、组合、选择类问题使用回溯法比较方便。顾名思义,回溯法的核心是回溯。
原理:在搜索到某一节点的时候,如果我们发现目前的节点(及其子节点)并不是需求目标时,我们回退到原来的节点继续搜索,并且把在目前节点修改的状态还原。
优点:可以始终只对图的总状态进行修改,而非每次遍历时新建一个图来储存状态。
过程:与普通的深度优先搜索一样,都有[修改当前节点状态]→[递归子节点] 的步骤,只是多了回溯的步骤,变成了[修改当前节点状态]→[递归子节点]→[回改当前节点状态]。
两个小诀窍:一是按引用传状态,二是所有的状态修改在递归完成后回改。回溯法修改一般有两种情况,一种是修改最后一位输出,比如排列组合;一种是修改访问标记,比如矩阵里搜字符串。
例题:
单词搜索:不同于排列组合问题,本题采用的并不是修改输出方式,而是修改访问标记。在我们对任意位置进行深度优先搜索时,我们先标记当前位置为已访问,以避免重复遍历(如防止向右搜索后又向左返回);在所有的可能都搜索完成后,再回改当前位置为未访问,防止干扰其它位置搜索到当前位置。使用回溯法,我们可以只对一个二维的访问矩阵进行修改,而不用把每次的搜索状态作为一个新对象传入递归函数中。