程序设计与算法(六)_深度优先搜索

乾坤未定,你我皆是黑马

例题1:在图上寻找路径

在图上如何寻找从1到8的路径

运气最好:1->2->4->8

运气稍差:1->2->4->5->6->8

运气坏:1-3->7->9=>7->A=>7=>3->5->6->8(双线箭头表示回退)

不连通的图,无法从节点1走到节点8

完整的尝试过程如下:

1->2->4->3->7=>3=>4=>2->9=>2=>1

结论:不存在从1到8的路径

得出这个结论之前,一定会把从1出发能走到的点全部都走过。

深度优先搜索(Depth-First-Search)DFS:

从起点出发,走过的点要做标记,发现有没走过的点,就随意挑一个往前走,走不了就回退。

C++:

int main(){
    将所有的点都标记为新点;
    起点=1;
    终点=8;
    cout<<Dfs(起点);
}

判断从V出发能否走到终点
bool Dfs(V){
	if(V是终点)
		return true;
	else(v为旧点)//旧点:已经走过的点
		return false;
	将v标记为旧点;
	对和v相邻的每个节点U{
		if (Dfs(U)==true)
			return true;
	}
	return false;
}

图的表示方法——邻接矩阵

用一个二维数组G存放图,G(i,j)表示节点i和节点j之间的边的情况(如有无边,边方向,权值大小等)。

图的表示方法——邻接表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5CsGl7Ul-1587027965341)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20200413133645849.png)]

每个节点V对应一个一维数组,里面存放从V连出去的边,边的嘻嘻包括另一个顶点,还可能包含边权值等信息。

1:2 3
2:1 4 9 3
3:1 4 7 2
4:2 3
5:6 8
6:5 8
7:3
8:5 6
9:2

遍历复杂度:O(n+3) n:节点数目,e:边数目

例题2:城堡问题(百炼2815)

右图是一个城堡的地形图,请你计算一共有多少房间,最大的房间有多大,城堡被分成m*n(m<=50,n<=50)个方块,每个方块可以有0-4面墙。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K57DrPvt-1587028030277)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20200413133645849.png)]
输入:

第一行是两个整数:分别是南北向、东西向的方块数。

在接下来的输入行里,每个方块用一个数字来表示,用一个数字来表示方块周围的墙,1:西墙;2:北墙;4:东墙;8:南墙。每个方块用代表其周围墙的数字之和表示。城堡的内墙被计算两次,方块(1,1)的南墙同时也是方块(2,1)的北墙。输入的数据保证城堡至少有两个房间。

输出:

城堡的房间数、城堡中最大房间所包含的方块数。

样例输入:

4 7

11 6 11 6 3 10 6

7 9 6 13 5 15 5

1 10 12 7 13 7 5

13 11 10 8 10 12 13

样例输出:

5 7

解题思路:

把方块看做节点,相邻两个方块之间如果没有墙,则在方块之间连一条边,这样城堡就能转换成一个图。

求房间个数,实际上就是求图中有多少个极大连通子图。

一个连通子图,往里头加任意一个图里的其他点,就会不连通,那个这个连通子图就是极大连通子图。(如:8,5,6)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oRaRWxZ7-1587027965351)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20200413143836235.png)]

#include <iostream>
#include <stack>
#include <cstring>
using namespace std;

int R,C;
int rooms[60][60];//存放方块
int color[60][60];//方块是否染色的标记
int maxRoomArea=0,roomNum=0;
int roomArea;//正在探索的那个房间的面积

void Dfs(i,k){
    if (color[i][k])
        return;
    ++roomArea;
    color[i][k]=roomNum;
    if (rooms[i][k] & 1 ==0) Dfs(i,k-1);
    if (rooms[i][k] & 2 ==0) Dfs(i-1,k);
    if (rooms[i][k] & 4 ==0) Dfs(i,k+1);
    if (rooms[i][k] & 8 ==0) Dfs(i+1,k);
                
}

int main(){
	cin>>R>>C;
    for(int i=1;i<=R;i++)
        for(int j=1;j<=C;j++)
            cin>>rooms[i][j];
    memset(color,0,sizeof(color));//所有的方块都没有走过
    for (int i=1;i<=R;i++)
        for(int j=1;j<=C;j++){
            if (!color[i][j]){
                ++roomNum;roomArea=0;
                Dfs(i,k);
                maxRoomArea=max(roomArea,maxRoomArea);
            }
        }
     cout<<roomNum<<maxRoomArea<<endl;
}

例题3:踩方格(百炼4982)

有个方格矩阵,矩阵边界在无穷远处。有如下假设:

  • 只能移动一格
  • 走过的格子立即塌陷无法再走第二次
  • 只能向北、东、西三个方向走;

请问:如果允许在方格矩阵上走n步(n<=20),共有多少种不同的方案。

思路:

递归:从(i,j)出发,走n步的方案数,等于以下三项之和:

从(i+1,j)出发,走n-1步的方案数。前提:(i+1,j)还没走过

从(i,j+1)出发,走n-1步的方案数。前提:(i,j+1)还没走过

从(i,j-1)出发,走n-1步的方案数。前提:(i,j-1)还没走过

C++:

#include <iostream>
#include <cstring>
using namespace std;
int visited[30][50];
int ways(int i,int j,int n){
    if(n==0) 
        return 1;//就是一步都不用走,就是不走,就是一种走法
    visited[i][j]=1;
    int num=0;
    if (!visited[i][j-1])
        num+=ways(i,j-1,n-1);
    if (!visited[i][j+1])
        num+=ways(i,j+1,n-1);
    if (!visited[i+1][j])
        num+=ways(i+1,j,n-1);
    visited[i][j]=0; //关键
    return num;  
}

int main(){
    int n;
    cin>>n;
    memset(visited,0,sizeof(visited));
    cout<<ways(0,25,n)<<endl;//从(0,25)走n步的方案数
    return 0;
}

例题:4:寻路问题

N个城市,标号1到N。城市之间有R条单向道路。

每条道路连接两个城市,有长度和过路费两个属性。

小安只有K块钱,想从城市1走到城市N。问最短需要走多长的路。如果到不了,输出-1

2<=N<=100

0<=K<=10000

0<=R<=10000

每条路的长度L,1<=L<=100

每条路的过路费T,0<=T<=100

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3iZmH3FG-1587027965355)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20200415131804348.png)]

#include <iostream>
#include <vector>
#include <cstring>
using namespace std;
int K,N,R;

struct Road{
    int d,L,T; //终点,长度,费用
};

vector< vector< Road> > G(110);

int minLen;
int totalLen;
int totalCost;
int visited[110];

void dfs(int s){
    if (s==N){
        minLen=min(minLen,totalLen);
        return ;
    }
    for(int i=0;i<G[s].size();i++){
        Road r=G[s][i];//对起点为s的边进行遍历
        if(totalCost+r.t>K)
            continue;
        if (!visited[r.d]){
            totalLen+=r.L;
            totalCost+=r.t;
            visited[r.d]=1;
            dfs(r.d);
            visited[r.d]=0;
            totalLen-=r.L;
            totalCost-=r.t;
        }
    }
}

int main(){
    cin>>K>>N>>R;
    for (int i=0;i<R;i++){
        int s;//起点
        Road r;
        cin>>s>>r.d>>r.L>>r.t;
        if ( s!= r.d){
            G[s].push_back(r);
        }
    }
    memset(visited,0,sizeof(visited));
    totalLen=0;
    minLen=1<<30; //一个非常大的数
    totalCost=0;
    visited[1]=1; //走过第一个点了
    dfs(1);
    if (minLen<(1<<30)){
        cout<<minLen<<endl;
    }
    else{
        cout<<"-1"<<endl;
    }
    return 0;
}

上述运行:时间超时

最优剪枝:

  • 如果当前已经找到的最优路径为L,那么在继续搜索的过程中,总长度大于L的走法,就可以直接放弃,不用走到底了。
#include <iostream>
#include <vector>
#include <cstring>
using namespace std;
int K,N,R;

struct Road{
    int d,L,T; //终点,长度,费用
};

vector< vector< Road> > G(110);

int minLen;
int totalLen;
int totalCost;
int visited[110];

void dfs(int s){
    if (s==N){
        minLen=min(minLen,totalLen);
        return ;
    }
    for(int i=0;i<G[s].size();i++){
        Road r=G[s][i];//对起点为s的边进行遍历
        if(totalCost+r.t>K)
            continue;
        if (!visited[r.d]){
            //最优剪枝
            if (totalLen+r.L>minLen)
                continue;
            totalLen+=r.L;
            totalCost+=r.t;
            visited[r.d]=1;
            dfs(r.d);
            visited[r.d]=0;
            totalLen-=r.L;
            totalCost-=r.t;
        }
    }
}

int main(){
    cin>>K>>N>>R;
    for (int i=0;i<R;i++){
        int s;//起点
        Road r;
        cin>>s>>r.d>>r.L>>r.t;
        if ( s!= r.d){
            G[s].push_back(r);
        }
    }
    memset(visited,0,sizeof(visited));
    totalLen=0;
    minLen=1<<30; //一个非常大的数
    totalCost=0;
    visited[1]=1; //走过第一个点了
    dfs(1);
    if (minLen<(1<<30)){
        cout<<minLen<<endl;
    }
    else{
        cout<<"-1"<<endl;
    }
    return 0;
}

上述程序,提交到OJ上还是超时:

修改思路:

用mid(k,m)表示:走到城市k时总过路费为m的条件下,最优路径的长度。若在后续的搜索时,再次走到k时,如果总路费恰好是m,且此时的路径长度已经超过mid(k,m)就不必再走下去了:

#include <iostream>
#include <vector>
#include <cstring>
using namespace std;
int K,N,R;

struct Road{
    int d,L,T; //终点,长度,费用
};

vector< vector< Road> > G(110);

int minLen;
int totalLen;
int totalCost;
int visited[110];
int minL[110][10010];

void dfs(int s){
    if (s==N){
        minLen=min(minLen,totalLen);
        return ;
    }
    for(int i=0;i<G[s].size();i++){
        Road r=G[s][i];//对起点为s的边进行遍历
        if(totalCost+r.t>K)
            continue;
        if (!visited[r.d]){
            //最优剪枝
            if (totalLen+r.L>minLen)
                continue;
            if (totalLen+r.L>=minL[r.d][totalCost+r.t])
                continue;
            minL[r.d][totalCost+r.t]=totalLen+r.L;
            totalLen+=r.L;
            totalCost+=r.t;
            visited[r.d]=1;
            dfs(r.d);
            visited[r.d]=0;
            totalLen-=r.L;
            totalCost-=r.t;
        }
    }
}

int main(){
    cin>>K>>N>>R;
    for (int i=0;i<R;i++){
        int s;//起点
        Road r;
        cin>>s>>r.d>>r.L>>r.t;
        if ( s!= r.d){
            G[s].push_back(r);
        }
    }
    memset(visited,0,sizeof(visited));
    totalLen=0;
    minLen=1<<30; //一个非常大的数
    totalCost=0;
    visited[1]=1; //走过第一个点了
    for (int i=0;i<110;i++)
        for (int j=0;j<10010;j++)
            minL[i][j]=1<<30;
    dfs(1);
    if (minLen<(1<<30)){
        cout<<minLen<<endl;
    }
    else{
        cout<<"-1"<<endl;
    }
    return 0;
}

例题5:切蛋糕

要制作一个体积是Nπ的M层生日蛋糕,每层都是一个圆柱体。

从下网上,高度和半径逐层递减。由于要在蛋糕上抹奶油,为尽可能节约经费,我们希望蛋糕外表面的面积Q最小。

令Q=Sπ

编程时给出的N和M,找出蛋糕的制作方案,使得S最小

解题思路:

  • 枚举每一层可能的高度和半径
  • 如何确定搜索范围?底层蛋糕的最大可能半径和最大可能高度
  • 搜索顺序,哪些地方体现搜索顺序?从底层向上
  • 如何剪枝?
    • 剪枝1:搭建过程中发现已经建好的面积已经超过目前求的的最优表面积,或者预见到完成后的面积一定会超过目前最优表面积,则停止搭建
    • 搭建过程中预见到再往上搭,高度无法安排或者半径无法安排。
    • 搭建过程中发现还没搭的那些层的体积一定会超过还缺的体积,则停止搭建。
    • 搭建过程中发现还没搭的那些层的体积,最大也到不了还缺的体积,则停止搭建。
#include <iostream>
#include <vector>
#include <cstring>
#include <cmath>
using namespace std;
int N,M;
int minArea=1<<30;//最优表面积
int area=0;
int minv[30],mins[30];

void Dfs(int v,int n,int r,int h){
    //n层蛋糕凑体积v,最底层半径不超过r,高度不超过h
    if (n==0){ //如果层为0
        if (v) //如果体积还剩,则不成功,返回
            return ;
        else{//n=0,v=0,成功
            minArea=min(minArea,area);
            return;
        }
    }
    if (v<=0) 
        return;
    //枚举第n层的高度和半径,第n层的半径和高度至少是n;
    for( int rr=r;rr>=n;--rr){
        if (n==M)//底面积
            area=rr*rr;//第一次向area添加值
    	for (int hh=h;hh>=n;hh--){       
            area+=2*rr*hh; //每加一层就添加一个侧面积
            
            //约束条件
            if (area>minArea)
                continue;
            else
                if (minArea-area<minv[n])
                    continue;
            v_=v-rr*rr*hh;
            if (v_<minv[n-1])
                continue;
            
            Dfs(v-rr*rr*hh,n-1,rr-1,hh-1);
            area-=2*rr*hh;
        }        
    }       
}

int main(){
    cin>>N>>M;
    int maxR=(int)sqrt(N);
    int maxH=M;
    minv[0]=0;
    mins[0]=0;
    //设定每一层的最小面积和最小体积
    for (int i=1;i<=M;i++){
        minv[i]=min[i-1]+i*i*i;
        mins[i]=min[i-1]+2*i*i;
    }
    
    Dfs(int N,M,maxR,maxH);
    if (minArea==1<<30)
        cout<<0<<endl;
    else
        cout<<minArea<<endl;
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值