Depth First Search 即 DFS,意为深度优先搜索,是所有的搜索手段之一。它是从某个状态开始,不断进行状态转移,直到不能转移后,向后回退,一直到遍历完所有的状态。
作为搜索算法的一种,DFS 主要是用于解决 NP
完全问题。但是,深度优先搜索算法的时间复杂度较高,深度优先搜索是 O(n!) 的阶乘级算法,它的效率非常低,在数据规模变大时,此算法就难以解决当前的问题了。
所以搜索算法使用于状态节点较小规模的问题。
DFS 是一种非常重要的回溯算法,它是通过递归设计转移状态,再加上边界判断,与结果检查,构成的基本搜索框架。
DFS 最重要的就是设计回溯,所谓回溯就是还原现场,保证在执行另一分支的时候能够确保所有的改变只受当前状态的影响,所以在一条路走不通时就要修改。特殊的修改可以达到特殊的回溯效果,回溯时剪枝,回溯时调整路线,都是可以的。
步骤:
-
确定该题目的状态(包括边界)
-
找到状态转移方式
-
找到问题的出口,计数或者某个状态
-
设计搜索
int check(参数)
{
if(满足条件)
return 1;
return 0;
}
bool pd(参数){
相应操作
}
void dfs(int step)
{
判断边界pd()
{
不在边界内,即回溯
}
尝试每一种可能
{
满足check条件
标记
继续下一步dfs(step+1)
恢复初始状态(回溯的时候要用到)
}
}
DFS在网格图(棋盘)问题的最短路径搜索问题
点个赞吧谢谢ヽ( ̄ω ̄( ̄ω ̄〃)ゝ
算法
不断改变上下左右的顺序为基础
+
全局回溯(主动后退,搜索到所有路径)
+
单路回退(被动后退,一条路径的探索)=所有路径长度的集合
思路
是否到达目标位置?
是->记录最短路径,回溯其它路径(主动后退)
否->四向扩展,找到下一步可以到达的位置是否有下一步?
是->步数加一,传入下一方位点,dfs到此点
否->回退,继续上一层dfs的剩余方位
反复,直到找到目标位置
工具
- 初始值为INT_MAX的最短路径minpath
- 方位数组: 如果我们的四个方向约定为右下左上(顺时针),则我们先右走,走不通则回退一步下走,下走不了则左走,左走不了则上走,都走不了则回退一步,继续上一个点的下一个方位点,然后以此点重新开始以右为顺时针的扩展
- 访问数组:回溯时沿路取反(主被动后退)
模拟
我称单条路径的试探性后退叫回退,搜索到了路径然后找另外一条路径时的后退称为回溯
0010
0000
0010
0100
0001
起点11,终点43
11->右12,12->右13走不通,回退到12
12->下22,22->右23,23->右24,24->右25越界,回退到24
24->下34,34->右35越界,回退到34
34->下44,44->右45越界,回退到44
44->下54越界,回退到44
44->左43,到达终点,结束本条路径
但本条路径7步不确定是否为最短路径
搜索到一条路径,然后回溯,并沿途设置为未访问
回溯到24时,24->上14(左边被标记为已访问的,所以顺时针的左失败了,到了上)
14的上下左右都不可以,回退
回退到22,22->下32,32->右下失败
32->左31,31->下41,...,41->下51,51->右52,52->右53,53->上43
又找到一条路径,步数为9,回溯,并沿路依次设置为未访问
回溯到 22时,22->左,...,22->左21,21->下31...,...,53->上43
找到一条路径为9
回溯到11,11下走...
测试样例
5 4
0 0 1 0
0 0 0 0
0 0 1 0
0 1 0 0
0 0 0 1
1 1 4 3
答案为7
附注解的代码
#include<bits/stdc++.h>
using namespace std;
int minpath=INT_MAX;
int n,m;//棋盘大小
int sx,sy,ex,ey;//起终点
int chessboard[100][100];//棋盘
int vis[100][100];//访问数组:0未访问,1访问
int dx[4]={1,-1,0,0},dy[4]={0,0,-1,1}; //方向数组,以右开始的顺时针
void dfs(int x,int y,int step){//传入一个扩展点及其步数
if(x==ex&&y==ey){//找到了
if(step<minpath)minpath=step;//到达终点,更新最短路长
return;//回溯,找另外一条路径
}
//该点dfs扩展下去
for(int d=0;d<=3;++d){
int arx=x+dx[d],ary=y+dy[d];
//边界值:arx(0,n],ary(0,m]因为棋盘数组舍弃0行列,故行列>0,且不超过nm
if(chessboard[arx][ary]==0&&vis[arx][ary]==0&&arx<=n&&arx>0&&ary>0&&ary<=m){
vis[arx][ary]=1;
dfs(arx,ary,step+1);//step是值传递,不用减回来,回溯即返回上一层时,这个step+1失效
vis[arx][ary]=0;//回溯
}
}
return;
}
int main(){
ios::sync_with_stdio(0);cin.tie(nullptr);cout.tie(nullptr);
cin>>n>>m;//输入地图大小-行列
for(int i=1;i<=n;++i)for(int j=1;j<=m;++j)cin>>chessboard[i][j];//摆放棋盘
cin>>sx>>sy>>ex>>ey;//起终点xy坐标
vis[sx][sy]=1;//起点已访问
dfs(sx,sy,0);
cout<<"搜索到该点花费步数: "<<minpath<<endl;
return 0;
}
纯享版
#include<bits/stdc++.h>
using namespace std;
int minpath=INT_MAX;
int n,m;//棋盘大小
int sx,sy,ex,ey;//起终点
int chessboard[100][100];//棋盘
int vis[100][100];//访问数组:0未访问,1访问
int dx[4]={1,-1,0,0},dy[4]={0,0,-1,1};//方向数组,以右开始的顺时针
void dfs(int x,int y,int step){
if(x==ex&&y==ey){
if(step<minpath)minpath=step;
return;
}
for(int d=0;d<=3;++d){
int arx=x+dx[d],ary=y+dy[d];//扩展点around x,y
if(chessboard[arx][ary]==0&&vis[arx][ary]==0&&arx<=n&&arx>0&&ary>0&&ary<=m){
vis[arx][ary]=1;
dfs(arx,ary,step+1);
vis[arx][ary]=0;//回溯
}
}
return;
}
int main(){
ios::sync_with_stdio(0);cin.tie(nullptr);cout.tie(nullptr);
cin>>n>>m;
for(int i=1;i<=n;++i)for(int j=1;j<=m;++j)cin>>chessboard[i][j];
cin>>sx>>sy>>ex>>ey;
//dfs
vis[sx][sy]=1;
dfs(sx,sy,0);
cout<<"最短路径长: "<<minpath<<endl;
return 0;
}
例题
状态搜索代表:N皇后问题
用一个N×N 的矩阵来表示棋盘,但是我们不需要定义这样的数组,只要心中有 N×N 的棋盘即可。
-
算法开始:
当前行设为第一行,当前列设为第一列,从第一行第一列开始搜索,即只能让皇后从第一行放到第 n 行。
这样在每次判断是否满足情况时我们不用去判断是否皇后在相同行。
我们只用判断之前的 1到a−1 个皇后的位置和当前第 a 个皇后的位置是否属于同一列或者斜线,判断是否同一列。
-
判断边界:
在当前行,当前列的位置上判断是否满足条件(即保证经过这一点的行,列与斜线上都没有两个皇后),若不满足,跳到第 55 步,即不符合边界条件。
首先说一下,什么叫不符合边界条件,不只是跳出了搜索范围,剪枝也可以从这里开始,比如这里不满足条件,向下继续搜索也不会再有结果。
这可以理解为超出边界的剪枝,我们的边界只得可能存在解的范围,这里已经超出有解的范围,必然要被踢出。
判断条件:
我们用数组 x[a]=i 来表示第 a 个皇后的位置在第 a 行第 i 列,我们不用考虑是否在同一行的问题你,我们只用判断之前的 1到 a−1 个皇后的位置和当前第 a 个皇后的位置是否属于同一列或者斜线。
判断是否属于同一列: 就判断 x[a] 是否等于 x[i]; 判断是否属于同一斜线:等同于判断行之差是否等于列之差也,即 abs(x[k]−x[i])=abs(k−i)。
-
搜索过程:
调用
Check
函数。如果 边界条件,就继续调用放下一个皇后的位置
-
Check
函数:如果当搜索到第N+1 行的时候,即代表前N 行已经搜索完了,所以这个时候正好求出了一个解,记录加一。
-
在当前位置上不满足条件的情形,进行回溯。
#include <iostream>
#include <cstdio>
using namespace std;
int x[15] = {0};
int sum,n;
int PD(int k)
{
for(int i=1; i<k; i++)
{
if(abs(k-i)==abs(x[k]-x[i]))
return 0;
else if (x[k]==x[i])
return 0;
//即判断是否符合条件来放,i表示皇后所在的行数,x[i]表示所在的列数,
//所以前面那个条件用来判断两个皇后是否在对角线上,后面用来判断是否在同一列上。
//行数不需要判断,因为他们本身的i就代表的是行数
}
return 1;
}
bool check(int a)
{
if(a>n)
sum++;
else
return 0;
return 1;
}
void DFS(int a)
{
if(check(a))
return ;
else
for(int i=1; i<=n; i++)
{
x[a]=i;
//第a个皇后放的列数
if(PD(a))
//判断是否能放这步
DFS(a+1);
//能的话进行下一个皇后的放置
else continue ;
//不能就下一列
}
}
int main()
{
cin>>n;
//表示几个皇后
DFS(1);
//每次都从第一个皇后开始
cout<<sum<<endl;
return 0;
}
图的路径搜索代表: 路径之谜
小明冒充 X 星球的骑士,进入了一个奇怪的城堡。城堡里边什么都没有,只有方形石头铺成的地面。假设城堡地面是 n×n 个方格。如下图所示。
按习俗,骑士要从西北角走到东南角。可以横向或纵向移动,但不能斜着走,也不能跳跃。每走到一个新方格,就要向正北方和正西方各射一箭。(城堡的西墙和北墙内各有 n 个靶子)同一个方格只允许经过一次。但不必走完所有的方格。如果只给出靶子上箭的数目,你能推断出骑士的行走路线吗?有时是可以的,比如上图中的例子。
本题的要求就是已知箭靶数字,求骑士的行走路径(测试数据保证路径唯一)
输入:
第一行一个整数 N (0≤N≤20),表示地面有 N×N 个方格。
第二行 N 个整数,空格分开,表示北边的箭靶上的数字(自西向东)
第三行 N 个整数,空格分开,表示西边的箭靶上的数字(自北向南)
输出:
输出一行若干个整数,表示骑士路径。
为了方便表示,我们约定每个小格子用一个数字代表,从西北角开始编号: 0,1,2,3 ⋯⋯
输入输出样例:
输入
4
2 4 3 4
4 3 3 3
比如,上图中的方块编号为:
箭靶 | 2 | 4 | 3 | 4 |
---|---|---|---|---|
4 | 0 | 1 | 2 | 3 |
3 | 4 | 5 | 6 | 7 |
3 | 8 | 9 | 10 | 11 |
3 | 12 | 13 | 14 | 15 |
输出
0 4 5 1 2 3 7 11 10 9 13 14 15
运行限制:
最大运行时间:1s
最大运行内存: 128M
这里用一个 N×N 的矩阵来表示城堡的位置,横向、纵向标号 1−N。
我们采用逆推法,既然原题目是走到哪里射一支箭,那我们就走到那里之后拔一支箭,如果最后得到所有的靶子上都没有箭了,由于题目的路径唯一,那就证明我们找到了题目所要求的路径。
-
算法开始:
当前行设为第一行,当前列设为第一列,从第一行第一列开始搜索。
然后从左上角初始位置,按照题目意思进行寻路。
-
判断边界:
在当前行,当前列的位置上判断是否满足条件,若不满足,跳到第 55 步,即不符合边界条件。 判断条件如下:
- flag[x][y]==1 标记数组已经被标记,已被走过,不能再走,超出边界
- x<1 从左侧走出方格。
- x>n 从右侧走出方格。
- y<1 从上侧走出方格。
- y>n 从下侧走出方格。
- col[x]<=0 没走到右下角,箭用完了。
- rol[y]<=0 没走到右下角,箭用完了
-
搜索过程:
调用
Check
函数。 如果边界条件满足,就继续调用搜索,找到下一步的位置 -
check(参数):
如果当搜索到x=n,y=n 时,且靶子上的箭都没了,按就找到了答案。
按照题目输出即可。
-
在当前位置上不满足条件的情形,进行回溯,并还原现场
#include <bits/stdc++.h>
using namespace std;
struct PII
{
int first;
int second;
};
const int N = 30;
int rol[N];
int col[N];
int n;//格子数 长宽从1到n
bool flag[N][N]; //用来标记是否走过
vector<PII> res;
//---------图的路径搜索常用方向移动表示-------
int dx[4]= {0,1,-1,0};
int dy[4]= {1,0,0,-1};
// 两两组合形成上下左右四个方向
// 1------------------> x
// |
// |
// |
// |
// |
// |
// |
// ↓
// y
// dx[0]=0 dy[0]=1 那么代表向下的方向
// dx[1]=1 dy[1]=0 那么代表向右的方向
// dx[2]=-1 dy[0]=0 那么代表向左的方向
// dx[3]=0 dy[1]=-1 那么代表向上的方向
//--------------------------------------------
bool check(int x, int y) //判断走过的路径的箭靶数是否与目标相同
{
if(x==n && y==n)
{
for(int i=1; i<=n; i++)
{
if(col[i]!=0)
{
return false;
}
//如果箭靶上的数目不为0,根据逆推,我们通过当前路径得不到箭靶上的结果
}
for(int i=1; i<=n; i++)
{
if(rol[i]!=0)
{
return false;
}
//如果箭靶上的数目不为0,根据逆推,我们通过当前路径得不到箭靶上的结果
}
for(int i=0; i<res.size(); i++)
{
int x=res[i].first;
//x 轴坐标
int y=res[i].second;
//y 轴坐标
int sum=n*(x-1)+y-1 ;
// 通过计算的到为题目要求的坐标系
cout <<sum<< " ";
}
cout << endl;
return false;
// 成功终止
}
return true; //继续搜索
//关于终止还是继续我们交给判定即可
}
bool pd(int x2,int y2) //边界判断
{
if(flag[x2][y2]==1)
return 0;
//已被走过,不能再走,超出边界
else if(x2<1)
return 0;
//从左侧走出方格
else if(x2>n)
return 0;
//从右侧走出方格
else if(y2<1)
return 0;
//从上侧走出方格
else if(y2>n)
return 0;
//从下侧走出方格
else if(col[x2]<=0)
return 0;
//没走到右下角,箭用完了
else if(rol[y2]<=0)
return 0;
//没走到右下角,箭用完了
else return 1;
//符合边界条件,可以继续执行搜索
}
void dfs(int x,int y)
{
if(!check(x,y))
{
return ;
//包含不符合规则的地方,回溯,用于剪枝
}
else
{
for(int i=0; i<4; i++)
{
int xt=dx[i]+x;
int yt=dy[i]+y;
if(!pd(xt,yt))
{
continue ;
//不符合要求继续换方向搜索
}
else
{
//因为要进行位置转移,我们给它起个名字,叫作案现场
//比如向下移动
flag[xt][yt]=true;
col[xt]--;
rol[yt]--;
res.push_back({xt,yt});
dfs(xt,yt);
//搜索回溯后,因为没有找到正确答案,所以要回复作案现场,返回到搜索之前
res.pop_back();
flag[xt][yt]=false;
col[xt]++;
rol[yt]++;
}
}
}
}
int main()
{
cin >> n;
for(int i=1; i<=n; i++)
cin >> rol[i];
for(int i=1; i<=n; i++)
cin >> col[i];
flag[1][1]=true;
col[1]--;
rol[1]--;
res.push_back({1,1});
dfs(1,1);
return 0;
}
最大数字
给定一个正整数 N 。你可以对 N 的任意一位数字执行任意次以下 22 种操 作:
-
将该位数字加 1 。如果该位数字已经是 9 , 加 1 之后变成 0 。
-
将该位数字减 1 。如果该位数字已经是 0 , 减 1 之后变成 9 。
你现在总共可以执行 1 号操作不超过 A 次, 2 号操作不超过 B 次。 请问你最大可以将 N 变成多少?
解题思路:
看上去 N 的范围貌似很大,达到了 1e17
的范围,但其实我们最多只需要考虑这最多 17
位数,所以可以想到爆搜得到答案。
一个数的大小是从左到右依次判断,所以我们从最左边开始枚举,我们无需关注后面的数,要利用自己的 1
号操作和 2
号操作 保证当前这个数位的数一定要尽可能最大
然后分别考虑两种操作,首先两种操作不可能混用,因为它们是抵消的效果,所以要么对这个数全使用 1
操作,要么 2
操作。假设某个数位的值为 x
,首先考虑 1
号操作,使用后可以让该数位变大,出于贪心考虑,我们想让它变成 9
,那么需要进行 9-x
次 1
号操作,当然可能此时 1
号操作并不足以让我们将 x
变成 9
,但我们还是使用剩余的全部的次数将其变大,所以每次考虑 1
号操作应该使用的操作数 t
应该为 t=min(n,9-x)
,此时 x
将变为 x+t
,然后进行下一位的判断。
其次我们考虑 2
号操作,这个的判断比较简单,它是让某个值减小,唯一能让某个数变大的机会就是将其减到 0
后再减就会变成 9
。那么这样操作需要的次数就是 x+1
,如果操作次数不够,那我们宁愿不使用,因为这只会让这个数位变得更小。
在深搜 dfs
的过程中,参数记录遍历到第几个数位以及此时累计的和,当搜索完所有数位后,将此时的和与答案进行一个取 max
,最后的值则为答案。
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
char c[20];
LL ans=0;
//n:1号操作剩余次数 m:2号操作剩余次数
int n,m;
void dfs(int i,LL v){
int x=c[i]-'0';
if(c[i]){
//应该使用的操作次数
int t=min(n,9-x);
n-=t;
dfs(i+1,v*10+x+t);
//回溯
n+=t;
//考虑操作2是否能够使用
if(m>x){
m-=x+1;
dfs(i+1,v*10+9);
//回溯
m+=x+1;
}
}else{
//答案取max
ans=max(ans,v);
}
}
int main()
{
scanf("%s%d%d",c,&n,&m);
dfs(0,0);
printf("%lld\n",ans);
return 0;
}
点个赞吧谢谢ヽ( ̄ω ̄( ̄ω ̄〃)ゝ