回忆dfs
在上次听了搜索之后,我对搜索的印象也是更加的深刻了(也许吧)。。那么我就在这里重新回忆一下搜索,以来巩固(也许吧)
搜索呢,大致分为两种(也许是就分为两种),一种是dfs,一种是bfs。Dfs呢,十分稳定,暴力超时九个点!但是代码书写容易;bfs呢,效率是dfs的n倍,但是代码书写的难度是和效率成正比的!!所以说我现在也只能大致掌握dfs,而bfs却只能算入门。。
接下来我会给大家dfs。
Dfs:
dfs是搜索中较为重要的一种,其模板形式有很多:如走迷宫、八皇后、质数肋骨等多种使用dfs’的方法。可以说,dfs的作用域是十分广泛的,但也是十分超时的。
(接下来的基础内容来自学校教材)
dfs学习要经历三个阶段:1、递归 2、回溯(当然有些题不需要) 3、dfs
接下来三个内容逐个进行分析:
递归:一种程序结构,更多的考虑怎么构建递归式子和递归结构的实现,一般参数不会很多。
回溯:比递归高级,但是更多的考虑把修改过的值在用过之后重新改回来。需要更过的考虑递归的参数与局部变量、全局变量之间的关系。
dfs:需要掌握基本的dfs结构,需要更多地考虑搜索的顺序,如果优化。
那么搜索大致结构如下:
框架【一】
Int dfs(int k){
For (int i=1;i<=算符种数;i++)
IF (满足条件){
保存结果;
If (到达目的地) 输出解;
Else dfs(k+1);
恢复:保存结果之前的状态{回溯}
}
}
当然框架是死的,但题确实活的,所以我们还得依题而论。
框架【二】
Int dfs(int k){
If (到达目的地) 输出解;
Else
For (int i=1;i<=算符种数;i++)
IF (满足条件){
保存结果;
dfs(k+1);
恢复:保存结果之前的状态{回溯}
}
}
按照模板而言,我是比较喜欢框架二的,当然这也是按个人喜好定。
好,那么对于刚接触搜索的人而言。搜索一般就是“只有思路,没有代码”。我刚接触时当然也是这样,看着搜索一脸蒙逼。所以搜索想要学好也只能靠刷题,从之吸取经验,从而提高能力。那么接下来就放几道题目。
1、 迷宫
题目描述:有一个N*M的方格迷宫。其中有T个障碍,障碍不能通过。给定起始坐标和终点坐标,问一共有几种走法?(每次只能上下左右走)
题目分析:想做这道题目,不用搜索还是十分难的,因为当前状态是很难记录的,就算暴力也很难(虽然dfs就是暴力)。那么我们做这道题是就得有一个思路导图:
1、 到达,方案数累加一。
判断是否到达目的地
2、未到达,继续搜索四个方向,直至到达。
那么做这道题就得涉及到一个小知识点:坐标增量了。坐标增量我们一般用数组dx和dy表示,当然他们数组的上限就是方向种数,例如这道题就是4。
那么方向数组有什么用呢??当然就是记录方向的啦。dx和dy的每一个相同下标的存值就表示一个方向。
那么这听着自然是很蒙蔽的,接下来给图解释。
int dx[5]={-1,1,0,0};//自坐向右为上、下、右、左
int dy[5]={0,0,1,-1};//自坐向右为上、下、右、左
那么具体图例解释:
x-1,y(此坐标点为当前坐标点的上方,也就是题目中所说的上方向)(2,3) 此时x和y就同时加上了dx[1]和dy[1]. |
||||
x,y-1(此坐标点为当前坐标点的左方,也就是题目中说的左方向) (3,2) 此时x和y就同时加上了dx[4]和dy[4] |
x ,y(当前坐标)
(3,3) |
x,y+1(此坐标点为当前坐标点的右方,也就是题目中说的右方向) (3,4) 此时x和y就同时加上了dx[3]和dy[3] |
||
x+1,y(此坐标点为当前坐标点的下方,也就是题目中所说的下方向)(4,3) 此时x和y就同时加上了dx[2]和dy[2] |
||||
那么坐标增量就是根据横纵坐标系去理解,也是比较好懂的。
好,那么既然思路有了,坐标增量也知道了,具体代码如下:
#include<bits/stdc++.h>
using namespacestd;
intn,m,t,t1,h1,t2,h2,ans=0;
intdx[5]={-1,1,0,0};//
intdy[5]={0,0,1,-1};//如上,伟大的坐标增量
boola[501][501]={};//判断当前点有没有障碍,同时也避免了出界的可能
void dfs(intx,int y){
if (x==t2&&y==h2){//如果到达目的地
ans++;return;//方案数累加一。跳出循环
}
a[x][y]=0;//因为当前点已经走过,定为不可走
for (int i=0;i<=3;i++){//四个方向搜索
int xx=x+dx[i],yy=y+dy[i];//当前走过方向之后的下标
if (a[xx][yy]) dfs(xx,yy);//如果没障碍或没越界或没走过
}
a[x][y]=1;//回溯一步
}
int main(){
cin>>n>>m>>t;
for (int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
a[i][j]=1;//现将迷宫范围内赋初值为可走
cin>>t1>>h1>>t2>>h2;//起点与终点
for (int i=1;i<=t;i++){//障碍的输入
int x,y;
scanf("%d%d",&x,&y);
a[x][y]=0;//定为不可走
}
dfs(t1,h1);//从起点搜索
cout<<ans;//输出
return 0;
}
这题只是一个搜索的基本模板,没什么难度,得好好理解。
2、 细胞问题
题目简介:一矩形阵列由数字0到9组成,数字1到9代表细胞,细胞的定义为沿细胞数字上下左右还是细胞数字则为同一细胞,求给定矩形阵列的细胞个数。如阵列:
0234500067
1034560500
2045600671
0000000089
有4个细胞。
题目分析:那么这四个细胞还可能有点难找,具体如下:
0234500067
1034560500
2045600671
0000000089
每种颜色对应一种细胞
那么将样例解释一遍后题目就变得蛮清晰的了。还是原来的方法,从图中的某一个点开始,上下左右搜索,如果是数字,就继续搜索数字,再讲数字置零,视为已被搜索过。
那么我们就要思考一个问题:具体从哪个点开始呢?
很显然,是从第一个搜索到的非零数字开始。因为只要出现了一个非零数,就意味着至少有一个细胞,就以这个数字的位置为起点开始搜索,把和它有关的所有非零数(也就是细胞)置零。然后就在查找,找到一个非零数,继续搜索……
那么很显然这道题也需要用方向数组,那么方向数组上面已经解释过了,这里就不解释了。
这道题的输入用字符数组输入即可。
那么代码如下:
#include<bits/stdc++.h>
using
namespace std
;
char a
[201][201];
int ans
=0;
int dx
[4]={-1,1,0,0};
int dy
[4]={0,0,-1,1};//。。方向数组
void
dfs(int x
,int y
){
int xx
,yy
;
a
[x][y]='0';//将当前细胞置零
for
(int i
=0;i
<=3;i
++){//四个方向搜索
xx
=x
+dx
[i
];yy
=y
+dy
[i
];//当前下标
if
(a
[xx
][yy
]!='0')
dfs(xx
,yy
);//如果有个细胞,继续遍历
}
}
int
main(){
int n
,m
;
memset(a
,'0',sizeof(a
));//现将布尔数组全定为‘0’,避免了出界
scanf("%d%d",&n
,&m
);
for
(int i
=1;i
<=n
;i
++)
for
(int j
=1;j
<=m
;j
++)
cin
>>a
[i
][j
];//输入
for
(int i
=1;i
<=n
;i
++)
for
(int j
=1;j
<=m
;j
++)
if
(a
[i
][j
]!='0'){//判断,如果当前出现了细胞,就把和之有关的细胞都遍历一遍,在置零。
ans
++;//统计一个细胞
dfs(i
,j
);//开始搜索
}
cout
<<ans
;//输出
return
0;
}
3、 工作分配问题
题意简述:设有n件工作分配给n个人。将工作i分配给第j个人所需的费用为Cij。试设计一个算法,为每一个人都分配1件不同的工作,并使总费用达到最小。设计一个算法,对于给定的工作费用,计算最佳工作分配方案,使总费用达到最小。
输入样例:
输出样例:
3 9
10 2 3
2 3 4
3 4 5
方案数为第一项工作分配给第二个人;第二项工作分配给第一个人;第三项工作分配给第三个人。
题目分析:这道题的搜索形式就不同于前几题的了,前面几题都是以坐标增量的形式出现的,而这题却不同。这题主要的做法就是暴力,枚举每一项工作,在枚举每一项工作的每一个人,如果当前的人没有工作,那么就把工作分配给当前的人……当计算到最后一项工作时,就可以做个统计,取最小值。
那么具体代码如下:
#include<bits/stdc++.h>
using
namespace std
;
int n
;
int a
[21][21]={};//工作,表示第i想工作给第j个人的费用
int minn
=1000000000;//找最小值
bool f
[101]={};//判断当前人有没有工作
void
dfs(int t
,int ans
){
if
(t
>n
){//如果工作都被选完了
minn
=min(ans
,minn
);//取较小值
return;//退出这层
}
if
(ans
>minn
)return;//一个小剪枝,如果总费用已经大于当前最消费用,就不用看,直接退出这层递归
for
(int i
=1;i
<=n
;i
++)//n个人循环
if
(!f
[i
]){//表示当前人没有工作
f
[i
]=1;//标志为有工作
ans
+=a
[t
][i
];//总值累加
dfs(t
+1,ans
);//将下一个工作进行搜索
ans
-=a
[t
][i
];//回溯
f
[i
]=0;//回溯
}
}
int
main(){
scanf("%d",&n
);
for
(int i
=1;i
<=n
;i
++)
for
(int j
=1;j
<=n
;j
++)
scanf("%d",&a
[i
][j
]);//输入
dfs(1,0);//从第一个工作,零元是开始搜索
cout
<<minn
;//输出
return
0;
}
4、 质数肋骨
题意简述:有一个长度为n的肋骨。从右边开始切下肋骨,每次还剩下的肋骨上的数字都组成一个质数,,这样的数称为质数肋骨。例如n=4,质数肋骨如下:2333
2339//例:2339为质数,233为质数,23位质数,2为质数。
2393
2399
2939
3119
3137
3733
3739
3793
3797
5939
7193
7331
7333
7393
问所有长度为n的所有质数肋骨有哪些?
题目分析:首先,可以想到一种暴力手法:先用筛法求素数讲长为n以内的所有素数求出;在枚举所有长为n的数,一层层剥离、判断……虽然会超时,但是也可以给大家开一开脑洞。
暴力代码如下:
#include<bits/stdc++.h>
using
namespace std
;
bool a
[99999999]={};//最多8位
int n
,x
=1,y
=10,s
;
int
main(){
cin
>>n
;
for
(int i
=1;i
<=n
-1;i
++)//范围计算
x
*=10,y
*=10;
a
[1]=a
[0]=1;//1不是质数
for
(int i
=2;i
<=y
-1;i
++)
if
(!a
[i
])
for
(int j
=2;j
<=(y
-1)/i
;j
++)
a
[i
*j
]=1;//线性筛素数过程
for
(int i
=x
;i
<=y
-1;i
++){//在范围内循环判断
bool f
=1;//默认当前值为质数
s
=i
;//将i存给s进行剥离判断。
while
(s
>0){//一层层剥离
if
(a
[s
]){f
=0;break;}//只要不是质数就退出
s
/=10;
}
if
(f
) cout
<<i
<<endl
;//如果是个质数就输出
}
return
0;
}
贪心过样例!暴力出奇迹!
暴力方法打一通是不是很爽!!超时的是不是也很爽!!这个暴力算法只是一个开胃菜。接下来才是真正的分析:
如果直接枚举所有的n位数,在判断,肯定会超时的。
通过对问题的思考,我们发现:如果一个数要满足条件,最高位数字只能是2,3,5,7,因为是质数肋骨的基础的基础就是每一位(这不是指真的每一位,而是质数肋骨的条件)都得是质数。从第二位开始,我们填充的数字只能是奇数(偶数不用考虑)。然后没加一位数字,没判断一次,如果一旦不满足条件就退出。
本体就有了一点dfs的味道,更在乎搜索的策略与优化。
那么具体代码如下:
#include<bits/stdc++.h>
using
namespace std
;
int n
;
bool
f(int x
){//判断数字是否为质数,是则返回真,否则返回假。
if
(x
==1)return
0;//1不是质数
if
(x
>1&&x
<4)return
1;//在1到4范围内的2和3是质数
for
(int i
=2;i
<=sqrt(x
);i
++)//质数判断的范围,可以去记一记。
if
(x
%i
==0)return
0;//如果一旦能被一个数整除,那么就一定不是质数,返回假
return
1;//如果成功活到了这儿,就说明之前的循环没有一个数能够整除当前数,说明当前数就是一个质数。
}
void
dfs(int ans
,int len
){
if
(!f(ans
))
return;//如果是不是一个质数的话就退出递归
if
(len
==n
)
{//如果长度达到了n
cout
<<ans
<<endl
;//输出这个数,这里不用再次判断是否为质数,因为之前已经判断过了。
return;//推出递归
}
for
(int i
=1;i
<=9;i
=i
+2)
dfs(ans
*10+i
,len
+1);//同样,循环奇数还是凑
}
int
main(){
cin
>>n
;//输入位数
for
(int i
=1;i
<=9;i
++)//判断个位
if(f(i
))
dfs(i
,1);//如果各位是个质数,就说明有可能组成质数肋骨,就进行搜索。
return
0;
}
好,这就是较为经典的四题搜索(虽然其他经典的题目还有一大堆。。)。
小总结:这就是dfs,他就是由上面的基础的两个模板变化而来。但是学习dfs就不能太过死板,被那两个框架框住,因为有很多题目都不是这种形式的,都是自己做、自已创造。所以,学习dfs需要懂得灵活运用,举一反三(虽然我还没够到达那步。。)。不过最重要的还是多刷题!多刷题!多刷题!刷题是十分重要的。。