DFS 深度优先搜索

来源:互联网 发布:淘宝上的曼森吉他 编辑:程序博客网 时间:2024/05/01 12:28

DFS引入:

首先对于DFS第一次接触毫无疑问就是图的遍历了,利用DFS来遍历了图的所有顶点,DFS里面用到了递归,是一层一层深入到底直到那层不再有没访问过的点,才返回上一层,再继续把这一层所有的点访问一遍,用这样的方式来遍历。

下面是我写的一个例子:就5个顶点,输入4条边,通过DFS来输出所有的顶点

#include<iostream>using namespace std;int map[5][5];bool vis[5];void DFS(int i){int j;vis[i]=true;//把现在这个点设置为已访问cout<<"V"<<i<<endl;//把现在的这个点输出for(j=0;j<5;j++)//把跟这个点有边的未访问的点都访问了{if(map[i][j]==1 && !vis[j])//找到有边且没访问过的点,就深入一层DFS(j);}}void DFS_visit(){memset(vis,false,sizeof(vis));//初始化所有的顶点都没访问过int i;for(i=0;i<5;i++){if(!vis[i])//如果这个点没访问过就对这个点深度优先搜索DFS(i);}}int main(){int a,b;for(int i=0;i<4;i++){cin>>a>>b;map[a][b]=map[b][a]=1;}DFS_visit();}

八皇后问题:

虽然最初接触DFS是通过图的遍历,但是后来发现,DFS是没有模板的,它是根据不同的题目变化较大的,而且主要的难点在于递归函数的写法。


先来看看八皇后问题,这是DFS和回溯法的基础例题,八皇后问题:在8X8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。

DFS的递归思路就是一行一行地摆皇后,如果摆到第8行证明当前摆法没问题,如果摆到某一行的每一列都与之前的行数的皇后的位置冲突,证明当前的摆法是不可行,就回溯到上一层,进行上一层的下一种可能。

 

引用下别人的伪代码:

递归方法,先来看一下递归算法的伪代码

void trial(int row)

{

     //递归时候,我们从第0行开始,然后每次递归时候,都向下一行,一直到棋盘的最后一行

     //这时候就表示已经是正确的解了,所有进入该函数首先判断是否是正确的解

     if(row>N)//这里就表示row的行数已经去到最大行数了

    {

         //输出此时的棋盘

    }

    else

    {

        for(int i=0;i<N;++i)

        { 

             //不是解,这时候需要在本行的每一列开始试探放旗子,如果可以的话继续向下递归

             #修改记录冲突的数组

              if(放在这个位置不会冲突)

             {

                     trial(row+1);

             }

             放在这里冲突,那么修改回记录的数组,继续下一列(回溯法的特点)

        }

     }

}

 

#include<iostream>using namespace std;const int N=8;int iCount;int ColPos[N];//ColPos[i]=j;表示的就是第i行的皇后在第j列bool IsPosition(int row,int col)//判断row行col列的位置与之前的位置有没冲突{for(int i=0;i<row;i++){//皇后是一行行摆的,所以不会出现同行的冲突,主要看同列与同斜线的冲突,同斜线用斜率==1与斜率==-1来判断if(ColPos[i]==col)return false;if(ColPos[i]-col==i-row || ColPos[i]-col==row-i)return false;}return true;}void Queen(int row){if(row==N){iCount++;
for(int x=0;x<N;x++){for(int y=0;y<N;y++){if(ColPos[x]==y)cout<<"Q ";else cout<<"* ";}cout<<endl;}system("pause");}else {for(int col=0;col<N;col++){ColPos[row]=col;if(IsPosition(row,col))Queen(row+1);elseColPos[row]=-1;}}}int main(){for(int i=0;i<N;i++)ColPos[i]=-1;//每一行的皇后位置初始化为-1,代表都没真正把皇后放上棋盘Queen(0);cout<<iCount;}


 从这题可以得出一些递归的规律:首先有一个像ColPos这样用于保存之前状态的容器,递归过程是一个个处理(具体像八皇后的一行行地摆),每次处理要针对情况看下是否满足了条件,没满足条件就要去找可以满足现状的情况,先记录在那个保存状态的容器中,判断当前记录的情况能否满足现状若能就进行下一个处理,若不能就回溯(就是把之前先记录的状态改回去)。

 

全排列问题:

参照上面的思路,解决全排列问题,我们需要一个保存之前状态的容器,针对这题就是保存答案的容器了:char ans[100];递归过程也是一个个处理(从没用过的字符中选择一个字符放入ans中。

#include<iostream>using namespace std;char data[10],ans[10];bool flag[10];int len;void Prem(int n){if(n==len)//排列到第len个字符就意味着已经出现一个可行的排列了,输出{printf("%s\n",ans);return ;}else{for(int i=0;i<len;i++){//在data数组中找一个字符作为ans[n]的字符ans[n]=data[i];if(!flag[i]){flag[i]=true;Prem(n+1);flag[i]=false;//把第n+1位排列好或者甚至是已经输出了,回溯的过程就需要把flag[i]设回没用过的状态,循环其他可能}elseans[n]='\0';}return;}}int main(){scanf("%s",data);//输入要全排序的字符串len=strlen(data);//求出长度memset(flag,false,sizeof(flag));//初始化为全部字符都没用过memset(ans,0,sizeof(ans));//初始化ans为空,貌似不初始化这个也没问题Prem(0);//排列第0个位置的字符}


 HDU 3283

题意大概是给你一个由数字组成的字符串,就你输出全排列中,比它大的下一个排列组合数,例如123就输出132这样

#include<iostream>#include<cstdio>#include<algorithm>#include<string>using namespace std;char data[100],ans[100];bool flag[100],IsPrint;int len;string FirstString;int main(){int TotalCaseNum,i,j,k,CurCasenum;cin>>TotalCaseNum;while(TotalCaseNum--){cin>>CurCasenum;cin>>FirstString;len=FirstString.size();cout<<CurCasenum<<" ";if(next_permutation(FirstString.begin(),FirstString.end()))//next_permutation(first,end,cmp)没cmp默认为小于,直接可以求出所有全排列中排在它后面那个排列组合,如果已经最大了就返回false,否则返回true,first->end的位置就会变成新的排列{cout<<FirstString<<endl;}elsecout<<"BIGGEST"<<endl;}}


 组合:

我的想法与排列和相似,C(5,3)这样的例子,从5个数中取3个,那么我也像排列那样,3个数一个一个取放入ans数组中,但是与排列不同的是组合对于重复的数字是属于同一种的,例如123与321是同一个组合来的,怎样避免这样的情况出现呢,我们排列取数的时候是for(i=0;i<len;i++) ans[x]=data[i];是按顺序取的数,那么取i=2的时候,i=0与i=1的数肯定之前就取过了,避免不同排列当成不同组合就不能取第二个数取回之前取过的数,那么第二个数就必须在i+1到len之间取,这就是排列与组合之间不同的地方了。

#include<iostream>using namespace std;int ans[10],data[10]={1,2,3,4,5};void Com(int s,int e,int x){int i;if(x==3)//这里示范取3个数,所以当取数取到第3个数就应该输出了{for(i=0;i<3;i++)cout<<ans[i]<<" ";cout<<endl;}else{for(i=s;i<=e;i++){ans[x]=data[i];//取过的数都集中在i之前,所以i之后都没取过,自然不需要设vis[]数组标志哪些数访问过了Com(i+1,e,x+1);}}}int main(){Com(0,4,0);//参数分别是:待取数序列的开始端,结束端,取第几个数}


 整数拆分问题:

http://blog.chinaunix.net/uid-26548237-id-3503956.html这个讲得很清楚,特别是递归的做法。

HDU有条整数拆分的裸题:

HDU 1028

#include<iostream>using namespace std;long long vis[121][121];//这个数组用来vis[i][j]=solve(n,max)来记忆化搜索,不然会超时的int solve(int n,int max){if(n==1 || max==1)return 1;if(vis[n][max]>0)return vis[n][max];if(max>n){return solve(n,n);}if(max==n)return solve(n,n-1)+1;elsereturn solve(n,max-1)+solve(n-max,max);//f(n,m)=f(n,m-1)(不包含m)+f(n-m,m)(包含了m)}int main(){int n;int i,j;for(i=1;i<=120;i++){for(j=1;j<=120;j++){vis[i][j]=solve(i,j);}}while(cin>>n)cout<<vis[n][n]<<endl;}


 上面这些全是递归的基础题目,要理解那个一层层来的过程才会明白DFS的编法吧(我觉得),下面是一些DFS的例题:

HDU 1045

题意大概就是一个最多4*4的city,'.'代表空路,'X'代表墙壁,叫你在空路上建炮塔,炮塔能上下左右4个方向扫射,墙壁能挡住子弹,炮塔能打爆炮塔,所以同一排不能放2个炮塔,问题就是叫你输出一座城最多放几个炮塔。

思考过程:用map[4][4]来保存地图吧,空路=0,墙壁=2,那么有0的地方就能放炮塔,放了炮塔需要把相应位置变1,表示有子弹打到的地方,那么有1和2的地方就不能放炮塔了,用一个Max的全局变量来保存每种情况比较之后的最大值,也就是结果。但是编的时候就会发现在一种情况完成之后要递归回去上一层,这时就需要把该层所加炮塔拆除掉,同时出去这个炮塔的弹道,但是全部1不清楚那些是这个炮塔的弹道,所以想到了一个解决办法就是加的炮塔是第几个,它的弹道就用几号来表示,墙壁换成-1.

#include<iostream>#include<string>using namespace std;int map[4][4];int Max,n;void DFS(int CurFireBlockNum){int i,j,x,y;//一格一格地循环找为0的空地方炮塔for(i=0;i<n;i++){for(j=0;j<n;j++){if(map[i][j]==0){CurFireBlockNum++;//加一个炮塔,下面修改map的弹道for(x=j;x<n;x++){if(map[i][x]==0) map[i][x]=CurFireBlockNum;else if(map[i][x]==-1)break;}for(x=j;x>=0;x--){if(map[i][x]==0) map[i][x]=CurFireBlockNum;else if(map[i][x]==-1)break;}for(x=i;x<n;x++){if(map[x][j]==0) map[x][j]=CurFireBlockNum;else if(map[x][j]==-1)break;}for(x=i;x>=0;x--){if(map[x][j]==0) map[x][j]=CurFireBlockNum;else if(map[x][j]==-1)break;}DFS(CurFireBlockNum);//向下一层搜索,搜索完就应该把该炮塔拆掉,并修改弹道for(x=j;x<n;x++){if(map[i][x]==CurFireBlockNum) map[i][x]=0;else if(map[i][x]==-1)break;}for(x=j;x>=0;x--){if(map[i][x]==CurFireBlockNum) map[i][x]=0;else if(map[i][x]==-1)break;}for(x=i;x<n;x++){if(map[x][j]==CurFireBlockNum) map[x][j]=0;else if(map[x][j]==-1)break;}for(x=i;x>=0;x--){if(map[x][j]==CurFireBlockNum) map[x][j]=0;else if(map[x][j]==-1)break;}CurFireBlockNum--;}}}//如果Map上没空地了就意味一种情况诞生了,比较Maxif(CurFireBlockNum>Max)Max=CurFireBlockNum;}int main(){int i,j;string str;while(cin>>n && n){for(i=0;i<n;i++){cin>>str;for(j=0;j<str.size();j++){if(str[j]=='.')map[i][j]=0;else if(str[j]=='X')map[i][j]=-1;}}Max=0;DFS(0);cout<<Max<<endl;}}


 HDU 1241

题意大概:

3 5

* @ * @ *

*  * @  *  *

* @ * @ *    输入行数列数,@表示有油田的存在,把横,竖或者斜着相连的@可以看成一个整体,问输入的地图有多少个整体。

思路:把地图‘*’=0,'@'=200,设一个把地图便利一次的循环,如果遇到1就从这个点开始DFS,把和这个点连在一起的所有点全部赋num(num从1开始),DFS完了之后就num++,继续那个循环,知道循环结束,不再出现200的点,就是说所有小油田都被纳入了大整体中,因为题目讲明大整体数目最多不过100,所以我设了200。

#include<iostream>using namespace std;int map[101][101],m,n;int Max;int diri[]={1,1,1,-1,-1,-1,0,0};int dirj[]={0,1,-1,0,1,-1,1,-1};void DFS(int i,int j,int num){map[i][j]=num;//先把目前所在点赋numfor(int x=0;x<8;x++)//往8个方向深入{int nexti=i,nextj=j;nexti+=diri[x];nextj+=dirj[x];if(nexti<0 || nextj<0 || nexti>=m || nextj>=n || map[nexti][nextj]!=200)continue;DFS(nexti,nextj,num);//直到找到满足条件的相邻的油田就深入下一层,递归过程中不能改变num的值,因为递归过程中的点全部都是8个方向上相邻的一些点,这些点都应该是同一个num标记}}int Solve(){int i,j,num=0;for(i=0;i<m;i++){for(j=0;j<n;j++){if(map[i][j]==200){num++;DFS(i,j,num);//可能最难搞就是DFS这个,这里就是递归}}}return num;}int main(){int i,j;char ch;while(cin>>m>>n){if(m==0 && n==0)break;//读进map数据for(i=0;i<m;i++){for(j=0;j<n;j++){cin>>ch;if(ch=='*')map[i][j]=0;else if(ch=='@')map[i][j]=200;}}cout<<Solve()<<endl;}}


 HDU 1312 red and blcak

题意大概是:给出行列数目,输入一地图:'.'表示一个黑色点,'#'表示一个红色点,'@'表示人初始位置,初始位置也是'.'  人只能走黑色点,问一幅地图能走多少个黑色点

思路:map记录地图,0表示黑色点,1表示红色点,0走过之后边1表示不能再走,从起点向4个方向一层层递归下去,往下一层就增加一个黑色点,走都没路递归回去的时候不能回复走过的点,否则会计算重复。

#include<iostream>using namespace std;int row,col,map[21][21],sx,sy;int Max;int dirx[]={1,-1,0,0};int diry[]={0,0,-1,1};void DFS(int r,int c){int i,j;for(i=0;i<4;i++){int nextrow=r+dirx[i];int nextcol=c+diry[i];if(nextrow<0 || nextcol<0 || nextrow>=row || nextcol>=col || map[nextrow][nextcol]==1)continue;Max++;map[nextrow][nextcol]=1;DFS(nextrow,nextcol);}}int main(){int i,j;char ch;while(cin>>col>>row){if(col==0 && row==0)break;for(i=0;i<row;i++){for(j=0;j<col;j++){cin>>ch;if(ch=='.')map[i][j]=0;else if(ch=='#')map[i][j]=1;else if(ch=='@'){sx=i,sy=j;map[i][j]=1;}}}//读进数据Max=1;DFS(sx,sy);cout<<Max<<endl;}}


有很多题目都是这种给地图式的题,就是按照方向递归下去,但是会有一些在递归过程中出现条件很多的变态题目就不是很好做了,例如要计算你走到某个格子拐了几个弯,还有一些例如HDU那个胜利大逃亡(2)也是,要去了特定某个之后才能走某些格子,还加上计时,相当麻烦。但是除此之外,也有一些利用dfs来做的题目,这些题目不是地图式的题目,有点像引入递归过程的时候排列组合的题目,但是要更难,数据一大了就容易超时,需要想一些情况来剪枝,没深入到底就可以根据一些判断认为后面的没必要深入下去之类的条件。

HDU 1518 Square

题意大概是给你一些固定长度的棍子,叫你端连端的把这些棍子组成一个正方形。题目有点讲不清楚,题目没讲这些棍子是否要全部用完,例如给出的一个例子:

8 1 7 2 6 4 4 3 5     8根棍子,后面8个长度,用完8根棍子组成正方形:(1,7)(2,6)(4,4)(3,5)         不用完8根棍子组成正方形:(7)(1,6)(2,5)(4,3)  剩1根4的没用

由于这点令我WA了很多次,知道看了其他人的结题报告发现别人怎么全部都求和的来求正方形的边长我才恍然大悟,居然是要全部棍子都用上,还害我对正方形的边长问题纠结了N久。知道了这点,正方形的边长就是sum/4,那么每条边都应该小才等于边长才能全部棍子组成正方形,所以每条边都应该由1条或者多条棍子组成,而我们要做的就是递归这些组成了,知道出现了一种情况是有4条有棍子组成的长为sum/4的边为止。

先来看我一开始想到的代码:

#include<iostream>#include<algorithm>using namespace std;int sticks[30];bool vis[30];int StickNum,TestCaseNum;bool flag;void DFS(int bian,int Curlength,int num)//解释参数:bian是要组成正方形的边长,不能变的;num是现在是组成第几条边;Curlength是num条边的已有长度,因为1条边是由1根或多根棍组成,所以例如已经深入到决定了加入一根棍如num边,深入下一层的时候就要选一根棍子和已有长度加起来小于等于bian的根子继续组成Num边{if(num>=4)//如果4条边都组成完成就设个标志{flag=true;return;}if(flag)//如果遇到了4边完成的标志就不用继续深入,一直返回就是return;for(int i=StickNum;i>=0;i--)//从StickNum条棍子里选没用过的棍子来组成num边{if(!vis[i] && sticks[i]+Curlength<=bian){vis[i]=true;//用了第i条棍子,设个用过的标志,递归回去时应该设回没用过的标志if(sticks[i]+Curlength==bian)//加上第i条棍子刚好组成变成就开始深入第num+1条边了DFS(bian,0,num+1);else//还没刚好组成num边就修改下当前长度,继续深入组成num边DFS(bian,Curlength+sticks[i],num);vis[i]=false;}}}int main(){int i,j,sum;cin>>TestCaseNum;while(TestCaseNum--){memset(sticks,0,sizeof(sticks));memset(vis,false,sizeof(vis));sum=0;cin>>StickNum;for(i=0;i<StickNum;i++){cin>>sticks[i];sum+=sticks[i];}if(sum%4!=0){cout<<"no"<<endl;continue;}                  sort(sticks,sticks+StickNum);if(sticks[StickNum-1]>sum/4){cout<<"no"<<endl;continue;}flag=false;DFS(sum/4,0,0);if(flag)cout<<"yes"<<endl;elsecout<<"no"<<endl;}}


上面的代码测试了好一些例子都没问题,但是却会超时,看了其他人的报告才发现还要继续剪枝,看了代码可能更清楚,这里不知道怎么说好

#include<iostream>#include<algorithm>using namespace std;int sticks[30];bool vis[30];int StickNum,TestCaseNum;bool flag;//注意:sticks数组已按长度排序好void DFS(int bian,int Curlength,int pos,int num)//新增点在于pos这个参数,如果组成新边就应该从整个数组中找没用过的棍子,但是如果是组成中的边,就应该从比上次用过的棍子小的棍子中找,由于每根棍子都小于等于边长,数组又排好序,组成边的时候肯定就先找根尽量大的棍子组成边,接着从小于等于这根棍子的棍子堆里继续找下一根组成同条边的棍子{if(num>=4){flag=true;return;}if(flag)return;for(int i=pos;i>=0;i--){if(!vis[i] && sticks[i]+Curlength<=bian){vis[i]=true;if(sticks[i]+Curlength==bian)DFS(bian,0,StickNum-1,num+1);elseDFS(bian,Curlength+sticks[i],i-1,num);vis[i]=false;}}}int main(){int i,j,sum;cin>>TestCaseNum;while(TestCaseNum--){memset(sticks,0,sizeof(sticks));memset(vis,false,sizeof(vis));sum=0;cin>>StickNum;for(i=0;i<StickNum;i++){cin>>sticks[i];sum+=sticks[i];}if(sum%4!=0){cout<<"no"<<endl;continue;}sort(sticks,sticks+StickNum);if(sticks[StickNum-1]>sum/4){cout<<"no"<<endl;continue;}flag=false;DFS(sum/4,0,StickNum-1,0);if(flag)cout<<"yes"<<endl;elsecout<<"no"<<endl;}}


 

原创粉丝点击