前言:深入学习了两天dfs算法相关的知识,根据自己的学习认识总结一下:
常见的dfs算法题有如下几类:
1:不需要回溯的dfs
A:特点:
a:判断 “是否存在” 的问题,遍历所有的点直接判断;
b:要求能到达的 “数量” 问题;
B:例题
典型的例题1——数量问题:红与黑_牛客题霸_牛客网 (nowcoder.com)
#include<stdio.h>
int m,n,i,j;
char a[30][30];
int dfs(int x,int y)
{
if(x>=m||x<0||y>=n||y<0)//边界
{
return 0;
}
else if(a[x][y]=='#')//限制条件
{
return 0;
}
else//正常搜索条件
{
a[x][y]='#';//标记不能回头
return 1+dfs(x-1,y)+dfs(x+1,y)+dfs(x,y-1)+dfs(x,y+1);//递归公式
}
}
int main()
{
while(scanf("%d%d",&m,&n)!=EOF)
{
for(i=0;i<m;i++)
{
scanf("%s",a[i]);
}
for(i=0;i<m;i++)
{
for(j=0;j<n;j++)
{
if(a[i][j]=='@')
{
printf("%d\n",dfs(i,j));
break;
}
}
}
}
}
例题2——是否存在的问题:
原题目描述
一天Extense在森林里探险的时候不小心走入了一个迷宫,迷宫可以看成是由 n∗n 的格点组成,每个格点只有2种状态,.和#,前者表示可以通行后者表示不能通行。
同时当Extense处在某个格点时,他只能移动到东南西北(或者说上下左右)四个方向之一的相邻格点上,Extense想要从点A走到点B,问在不走出迷宫的情况下能不能办到。
如果起点或者终点有一个不能通行(为#),则看成无法办到。
注意:A、B不一定是两个不同的点。
输入格式
第1行是测试数据的组数 k,后面跟着 k 组输入。
每组测试数据的第1行是一个正整数 n,表示迷宫的规模是 n∗n 的。
接下来是一个 n∗n 的矩阵,矩阵中的元素为.或者#。
再接下来一行是 4 个整数 ha,la,hb,lb,描述 A 处在第 ha 行, 第 la 列,B 处在第 hb 行, 第 lb 列。
注意到 ha,la,hb,lb全部是从 0 开始计数的。
输出格式
k行,每行输出对应一个输入。
能办到则输出“YES”,否则输出“NO”。
数据范围
1≤n≤100
输入样例:
2
3
.##
..#
#..
0 0 2 2
5
.....
###.#
..#..
###..
...#.
0 0 4 0
1
2
3
4
5
6
7
8
9
10
11
12
13
输出样例:
YES
NO
#include<stdio.h>
#include<string.h>
int k,n,e,b,c,d,ans;
char a[105][105]={0};
int st[105][105]={0};
int p[4]={-1,0,1,0},q[4]={0,1,0,-1};//偏移矩阵
void dfs(int x1,int y1,int x2,int y2)
{
if(x1==x2&&y1==y2)//出口
{
ans=1;
return;
}
for(int i=0;i<4;i++)//递归
{
int j=x1+p[i];
int k=y1+q[i];
if(j>=0&&j<n&&k>=0&&k<n&&a[j][k]=='.'&&st[j][k]==0)//这里其实就是所谓的剪枝,即事先判断去除不必要的路径
{
st[j][k]=1;//标记不能回头
dfs(j,k,x2,y2);
}
}
}
int main()
{
scanf("%d",&k);
for(int i=0;i<k;i++)
{
ans=0;
memset(a,0,sizeof(a));//重置矩阵
scanf("%d",&n);
for(int j=0;j<n;j++)
{
scanf("%s",a[j]);
}
scanf("%d%d%d%d",&e,&b,&c,&d);
if(a[e][b]=='#'||a[c][d]=='#')
{
printf("NO");
}
else
{
st[e][b]=1;
dfs(e,b,c,d);
if(ans==1)
{
printf("YES\n");
}
else
{
printf("NO\n");
}
}
}
}
注意:这个里面提到的偏移矩阵,实际上两两组合起来就是确定从当前位置下一步的走法(上下左右、东南西北),根据实际情况会有所变化,也有的人习惯直接用二维矩阵来表示,原理是一样的
2:需要回溯的dfs:
A:特点:
a:往往是求最优路径/方法;
b:基础的就是求方案数量。
B:例题
典型例题1——方案数量类型:
题目描述
马在中国象棋以日字形规则移动。
请编写一段程序,给定 n∗m 大小的棋盘,以及马的初始位置 (x,y),要求不能重复经过棋盘上的同一个点,计算马可以有多少途径遍历棋盘上的所有点。
输入格式
第一行为整数 T,表示测试数据组数。
每一组测试数据包含一行,为四个整数,分别为棋盘的大小以及初始位置坐标 n,m,x,y。
输出格式
每组测试数据包含一行,为一个整数,表示马能遍历棋盘的途径总数,若无法遍历棋盘上的所有点则输出 0。
数据范围
1≤T≤9,
1≤m,n≤9,
0≤x≤n−1,
0≤y≤m−1
输入样例:
1
5 4 0 0
1
2
输出样例:
32
#include<stdio.h>
#include<string.h>
int T,n,m,index;
char a[10][10];
int p[8]={-2,-1,2,1,-2,-1,2,1},q[8]={1,2,1,2,-1,-2,-1,-2};//偏移量
void dfs(int x,int y,int sum)
{
if(sum==n*m)
{
index++;
return;
}
a[x][y]='F';
for(int i=0;i<8;i++)
{
int j=x+p[i];
int k=y+q[i];
if(j>=0&&j<m&&k>=0&&k<n&&a[j][k]=='T')//剪枝
{
//printf("%d %d %d\n",j,k,sum);//测试语句,忽略
dfs(j,k,sum+1);
}
}
a[x][y]='T'; //回溯
}
int main()
{
scanf("%d",&T);
for(int i=0;i<T;i++)
{
index=0;
int x,y;
scanf("%d%d%d%d",&n,&m,&x,&y);
memset(a,'T',sizeof(a));
dfs(x,y,1);//起始时占有一个点位
printf("%d\n",index);
}
}
结合这些代码来总结一下回溯和剪枝就十分好理解了:
1:回溯用人话来说,就是在标记之后,又要消除标记,消除标记的递归过程,就是回溯。
2:剪枝用人话来讲,就是根据题目中或者隐藏的限制条件,来避免不必要的搜索,电脑性能好的甚至可以不用,但是做题由时间限制的话必须得考虑。
以上就是入门dfs的入门课,吃透了之后就基本能理解了,不然有些题连代码都看不懂,下面进行更复杂的应用
3:提升应用
#include <stdio.h>
int a[10][10];//存值
int b[10][10];//标记数组
int min;
int m,n,sum=0,index=0;
int dx[4]={0,1,0,-1};//偏移矩阵
int dy[4]={1,0,-1,0};
void f(int s,int i,int j,int bs)
{
if(s==sum/2&&min>bs)
min=bs;//判断是否更新答案
b[i][j]=0; //此时a[0][0] 只作连接用 所以格子和值不增加
for(int k=0;k<4;k++)
{
int x=dx[k]+i;
int y=dy[k]+j;//边界判断和剪枝
if(b[x][y]==1&&x>=0&&x<n&&x>=0&&x<m){
if(x==0&&y==0) f(s,x,y,bs);
else f(s+a[x][y],x,y,bs+1);
}
}
b[i][j]=1;//消除标记
}
int main()
{
int i,j;
scanf("%d%d",&m,&n);//输入宽、高
min=n*m;
for(i=0;i<n;i++)//注意坑点 先输入纵坐标的m 再输入的横坐标
for(j=0;j<m;j++)
{scanf("%d",&a[i][j]);//值
sum+=a[i][j]; b[i][j]=1;//初始化标记数组
}
if(sum%2==1)printf("0");//预判是否可以分两部分
else{ f(a[0][0],0,0,1);
if(index!=0) printf("%d\n",min);
else printf("0\n");
}
return 0;
}
#include<stdio.h>
int a[1001][1001]={0};//邻接矩阵
int c[1001]={0};//为0可以访问
int b[1001]={0};//站点访问 次数
int n,m,index=0;// 记录可行路径条数
void dfs(int x,int y)
{
if(x==y)
{
index++;//得到可行路径的条数
for(int i=1;i<=n;i++)//记录可行路径访问点的次数
{
if(c[i]==1)
{
b[i]++;
}
}
}
c[x]=1;
for(int i=1;i<=n;i++)
{
if(a[x][i]==1&&c[i]==0)
{
dfs(i,y);
}
}
c[x]=0;
}
int main()
{
int p,q,flog=0;
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)//得到目标邻接矩阵
{
int x,y;
scanf("%d%d",&x,&y);
a[x][y]=1;
a[y][x]=1;
}
scanf("%d%d",&p,&q);
dfs(p,q);
if(index==0)
{
printf("-1\n");
}
else
{
for(int i=1;i<=n;i++)
{
if(b[i]==index)//可行路径的条数和访问的次数相等,则表明该点位必需点
{
flog++;
}
}
printf("%d\n",flog-1);
}
}
总结:
在写dfs题的时候,往往是在矩阵的基础上进行的,有些矩阵里的值有实际作用的,就不能轻易的在原矩阵上做修改,而是需要通过标记矩阵来进行标记,根据实际情况,标记矩阵的形式和数量也会变化,至于偏移矩阵,根据移动的方式来看是否需要设置。(宏的工作)
而为了减少dfs的复杂度,根据题意往往是由很明显的不符合项,可以在dfs之前就完成判断(主函数里的工作)
dfs与剪枝和和回溯往往会被同时涉及到,如果不明白递归原理,则可以记一记这个回溯模板(dfs函数里的工作)
同时,dfs参数的模式定义比较个性化,主要包含:起点、终点、随递归变化的终止元素、固定的参考值等。(dfs函数的设置)
下面小结一下dfs基本应用模板:
#include<>
设置原矩阵;
设置标记矩阵,....;
设置偏移矩阵;
设置一些全局变量;
dfs()
{
if()//终止条件
{
终止时会进行到的操作
}
标记
for()
{
移动到下一步的操作;
if()//剪枝,根据限制条件设置
{
dfs();
}
}
消除标记//回溯
}
个人见解,希望有所帮助,不喜勿喷