深度优先搜索dfs算法刷题笔记【蓝桥杯】

  • 其实网上已经有不少dfs的算法笔记,但我之所以还再写一篇,主要是因为我目前见到的笔记,都有些太偏向理论了。
  • 对于基础薄弱的或是没有基础的人(like me),有点不合适,因为看了,也不能说自己会了。
  • 所以这篇主要是实践(题目)出发

理论

  1. 为了求得问题的解,先选择某一种可能情况向下继续递归
  2. 在这个过程中,当发现原来的选择是错误的,就退回一步重新选择,继续向下探索
  3. 反复进行这个操作,直到出现结果、无解或者是遍历完毕

实践

走出迷宫

题目描述

小明现在在玩一个游戏,游戏来到了教学关卡,迷宫是一个N*M的矩阵。
小明的起点在地图中用“S”来表示,终点用“E”来表示,障碍物用“#”来表示,空地用“.”来表示。
障碍物不能通过。小明如果现在在点(x,y)处,那么下一步只能走到相邻的四个格子中的某一个:(x+1,y),(x-1,y),(x,y+1),(x,y-1);
小明想要知道,现在他能否从起点走到终点。

输入描述

本题包含多组数据。
每组数据先输入两个数字N,M
接下来N行,每行M个字符,表示地图的状态。
数据范围:
2<=N,M<=500
保证有一个起点S,同时保证有一个终点E.

输出描述

每组数据输出一行,如果小明能够从起点走到终点,那么输出Yes,否则输出No

示例一
输入

3 3
S..
..E
...
3 3
S##
###
##E

输出

Yes
No

解析

  • 这道题是一道非常标准的题,很好地帮助你理解深度优先搜索
  • 算法思想:从S开始,往右进行dfs(不一定要右,可以根据自己需要),右边的是 . 符合条件,所以进入右边的这个点 . 然后从这个点开始继续dfs,下一个点如果不符合条件就回退到这个点
  • 具体的放在代码里说

题解

#include
using namespace std;
char maze[510][510];
bool vis[510][510];
int dir[4][2]={{1,0},{0,1},{-1,0},{0,-1}};
int N,M;
bool in(int n,int m){
    return 0>N>>M){
        memset(vis,0,sizeof(vis));
        getchar();
        for(int i=1;i<=N;++i){
            for(int j=1;j<=M;++j){
                scanf("%c",&maze[i][j]);
                if(maze[i][j]=='S') y=i,x=j;
            }
            getchar();
        }
        if(dfs(y,x)) printf("Yes\n");
        else printf("No\n");
    }
}

N皇后问题

题目描述

给出一个n×n的国际象棋棋盘,你需要在棋盘中摆放n个皇后,使得任意两个皇后之间不能互相攻击。具体来说,不能存在两个皇后位于同一行、同一列,或者同一对角线。请问共有多少种摆放方式满足条件

输入描述

一行,一个整数n(1≤n≤12),表示棋盘的大小。

输出描述

输出一行一个整数,表示总共有多少种摆放皇后的方案,使得它们两两不能互相攻击。

示例一
输入

4

输出

2

解析

  • 这道题是dfs+回溯(因为如果出现皇后不难放的情况需要回退到上一步),有点像 走出迷宫 的变式(只不过限制不能搜索的条件变化了一些)
  • 设计dfs算法:
    1. 设置return条件,当摆了n个皇后时,就要将方案数+1,并且返回。或者是当搜索的行数超过给出的n行
    1. 进行向下搜索的设计:不能与之前的皇后出现同行、同列或者同对角线,所以可以将之前的皇后用遍历的方式放在各个位置,目前的皇后设置判断不与之前的皇后出现限制。给之前的皇后的位置标记。如果没破坏限制,就将目前的皇后放在相应位置,拿下一个皇后,继续向下搜索

题解

#include
using namespace std;
int n,ans=0,num=0;
bool a=true;
int column[15];
void dfs(int row){          //放进来row行数的参数
    if(num==n){
        ++ans;
        return;
    }
    if(row==n+1) return;        //return条件
    for(int i=1;i<=n;++i){       //遍历每一列
        for(int j=1;j>n;
    dfs(1);
    cout<

数独挑战

题目描述

数独是一种填数字游戏,英文名叫 Sudoku,起源于瑞士,上世纪 70 年代由美国一家数学逻辑游戏杂志首先发表,名为 Number Place,后在日本流行,1984 年将 Sudoku 命名为数独,即 “独立的数字” 的缩写,意思是 “在每一格只有一个数字”。
2004 年,曾任中国香港高等法院法官的高乐德 (Wayne Gould) 把这款游戏带到英国,成为英国流行的数学智力拼图游戏。
深度优先搜索dfs算法刷题笔记【蓝桥杯】_第1张图片
玩家需要根据9×9盘面上的已知数字,推理出所有剩余位置的数字,并满足每一行、每一列、每一个粗线九宫格内的数字包含有 1-9 的数字,且不重复。
现在给你一个数独,请你解答出来。每个数独保证有且只有一个解。

输入描述

输入仅一组数据,共 9 行 9 列,表示初始数独(其中 0 表示数独中的空位)

输出描述

输出共 9 行 9 列,表示数独的解。
注意⾏末没有空格。

示例一
输入

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

输出

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

思路

  • 也是经典深搜题,然后注意一下行、列、粗线九宫格
  • 从输入 0 的地方开始,向下搜;而不是从第一个地方开始搜
  • 剩下的题解里说的

题解

#include
using namespace std;
int sudoku[10][10],cnt=0;
vector>v;                  //存放数组内为 0 的坐标
bool out=false;
bool check(int x,int y){
    for(int i=1;i<=9;++i){
        if(sudoku[i][y]==sudoku[x][y]&&i!=x) return false;        //确定列没有相同的
        if(sudoku[x][i]==sudoku[x][y]&&i!=y) return false;        //确定行没有相同的
    }
    int tx=(x-1)/3*3,ty=(y-1)/3*3;
    for(int i=tx+1;i<=tx+3;++i){
        for(int j=ty+1;j<=ty+3;++j){
            if(i==x&&j==y) continue;                                       
            if(sudoku[i][j]==sudoku[x][y]) return false;                 //确定粗线九宫格没有相同的
        }
    }
    return true;                                                                    //如果前面的都没有返回false,那么就在这返回true
}
void dfs(int dep){
    if(dep==cnt){        //先确定最终return的条件,如果所有的 0 都填完了,就return,并且设立out为true使后面退出递归
        out=true;
        return;
    }
    int x=v[dep].first,y=v[dep].second;
    for(int i=1;i<=9;++i){
        sudoku[x][y]=i;                                     //后面的都是经典深搜了,没什么好说的
        if(!check(x,y)){
            sudoku[x][y]=0;
            continue;
        }
        dfs(dep+1);
        if(out) return;
        sudoku[x][y]=0;
    }
}
int main(){
    for(int i=1;i<=9;++i){
        for(int j=1;j<=9;++j){
            scanf("%d",&sudoku[i][j]);
            if(!sudoku[i][j]) ++cnt,v.push_back({i,j});
        }
    }
    dfs(0);
    for(int i=1;i<=9;i++){
        for(int j=1;j<=9;j++) printf("%d ",sudoku[i][j]);              //这里和题目要求的不一样,算是一个小漏洞
        printf("\n");
    }
}

幸运数字Ⅱ

题目描述

定义一个数字为幸运数字当且仅当它的所有数位都是4或者7。
比如说,47、744、4都是幸运数字而5、17、467都不是。
定义next(x)为大于等于x的第一个幸运数字。给定l,r,请求出next(l) + next(l + 1) + … + next(r - 1) + next(r)

输入描述

两个整数l和r (1 <= l <= r <= 1000,000,000)。

输出描述

一个数字表示答案

示例一
输入

2 7

输出

33

示例二
输入

7 7

输出

7

首先描述一下,我一开始写的想法:对 l--r 上的每一个数用一遍搜索,然后搜索到一个 “幸运数字” ,然后再剪枝(没写上去)。但是开销是:需要对 l--r 及 r 后面一定数量的数字,统统进行check的遍历(当数字位数长的时候,遍历数字string的开销大)

原先的解法

#include
using namespace std;
int l,r,ans=0,flag;
bool check(int num){
    stringstream s;
    s<<num;
    string n=s.str();
    for(int i=0;i<n.size();++i){
        if(n[i]!=4||n[i]!=7) return false;
    }
    return true;
}
void next(int num){
    if(check(num)){
        ans+=num;
        return;
    } 
    int i=num;
    while(++i){
        
    }
}
int main(){
    scanf("%d%d",&l,&r);
    next(l);
    printf("%d",ans);
}

观看了他人的解法,发现基本都是一个思路

思路

  • 将0到最大值的数中所有的 “幸运数字” 找出来(通过n*10,降低时间开销到9位数)
  • sort一下 “幸运数字” 的数组,将其按从小到大排序,然后辅助后面的剪枝策略

题解

#include
using namespace std;
typedef long long ll;
ll a[1001000];
ll cnt =0;
void dfs(ll n){
    if(n>=44444444444) return;
    a[cnt++]=n*10+4;
    a[cnt++]=n*10+7;
    dfs(n*10+4),dfs(n*10+7);
}
int main(){
    ll l,r,pos=0,ans=0;
    cin>>l>>r;
    dfs(0);
    sort(a,a+cnt);
    for(ll i=l;i<=r;i++){
        while(a[pos]<i) pos++;
        ans+=a[pos];
    }
    cout<<ans;
}

[NOIP2017]奶酪

题目描述

深度优先搜索dfs算法刷题笔记【蓝桥杯】_第2张图片

输入描述

深度优先搜索dfs算法刷题笔记【蓝桥杯】_第3张图片

输出描述

在这里插入图片描述

示例一
输入

3
2 4 1
0 0 1
0 0 3
2 5 1
0 0 1
0 0 4
2 5 2
0 0 2
2 0 4

输出

Yes
No
Yes

说明

深度优先搜索dfs算法刷题笔记【蓝桥杯】_第4张图片

备注

深度优先搜索dfs算法刷题笔记【蓝桥杯】_第5张图片

思路

  • 这道题可以用并查集做,但鉴于本章是dfs的算法刷题笔记,故采用的是深搜的方法
  • 其实也是经典题了,没太多想说的,感觉自己做了那么多,好像真的掌握了
  • 不用回溯(自己思考一下为什么吧)

题解

#include
using namespace std;
typedef long long ll;
int n,h;
ll r;
bool flag,vis[1005];
class Hole{
    public:
      ll x,y,z;
};
Hole hole[1005];
bool check(Hole cur,Hole next){
    return pow(cur.x-next.x,2)+pow(cur.y-next.y,2)+pow(cur.z-next.z,2)<=4*r*r;
}
void dfs(int dep){
    if(hole[dep].z+r>=h){
        flag=true;
        return;
    }
    for(int i=1;i<=n;++i){
        if(!vis[i]&&check(hole[dep],hole[i])) vis[i]=1,dfs(i);
    }
}
int main(){
    int T,sign;
    scanf("%d",&T);
    while(T--){
        flag=false;
        memset(vis,0,sizeof(vis));
        scanf("%d%d%lld",&n,&h,&r);
        for(int i=1;i<=n;++i) scanf("%lld%lld%lld",&hole[i].x,&hole[i].y,&hole[i].z);
        for(int i=1;i<=n;++i) if(hole[i].z<=r) dfs(i);
        if(flag) printf("Yes\n");
        else printf("No\n");
    }
}

Lake Counting

题目描述

Due to recent rains, water has pooled in various places in Farmer John’s field, which is represented by a rectangle of N x M (1 <= N <= 100; 1 <= M <= 100) squares. Each square contains either water (‘W’) or dry land (‘.’). Farmer John would like to figure out how many ponds have formed in his field. A pond is a connected set of squares with water in them, where a square is considered adjacent to all eight of its neighbors.
Given a diagram of Farmer John’s field, determine how many ponds he has.

输入描述

Line 1: Two space-separated integers: N and M * Lines 2…N+1: M characters per line representing one row of Farmer John’s field. Each character is either ‘W’ or ‘.’. The characters do not have spaces between them.

输出描述

Line 1: The number of ponds in Farmer John’s field.

示例一
输入

10 12
W…WW.
.WWW…WWW
…WW…WW.
…WW.
…W…
…W…W…
.W.W…WW.
W.W.W…W.
.W.W…W.
…W…W.

输出

3

思路

  • 这道题也不过是打表题了,没什么好说的
  • 但之所以放上来,是因为这道题一遍过(已经刷题刷得渐入佳境)

题解

# include
using namespace std;
bool vis[105][105];
char water_square[105][105];
int ans=0,n,m,dir[8][2]={{0,1},{1,0},{1,1},{-1,0},{0,-1},{-1,-1},{1,-1},{-1,1}};
void dfs(int x,int y){
    for(int i=0;i<8;++i){
        int xx=x+dir[i][0],yy=y+dir[i][1];
        if(!vis[xx][yy]&&water_square[xx][yy]=='W'&&xx>0&&xx<=m&&yy>0&&yy<=n) {
            vis[xx][yy]=1,dfs(xx,yy);
        }
    }
}
int main(){
    memset(vis,0,sizeof(vis));
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;++i){
        getchar();
        for(int j=1;j<=m;++j) scanf("%c",&water_square[j][i]);
    }
    for(int i=1;i<=n;++i){
        for(int j=1;j<=m;++j){
            if(!vis[j][i]&&water_square[j][i]=='W') vis[j][i]=1,dfs(j,i),ans++;
        }
    }
    printf("%d",ans);
}

下面的题是含有剪枝策略的深搜题

[NOI1999]生日蛋糕

题目描述

深度优先搜索dfs算法刷题笔记【蓝桥杯】_第6张图片

输入描述

有两行,第一行为N(N≤10000),表示待制作的蛋糕的体积为Nπ;第二行为M(M≤20),表示蛋糕的层数为M。

输出描述

仅一行,是一个正整数S(若无解则S=0)。

示例一
输入

100
2

输出

68

备注

附:圆柱公式
体积V=πR2H
侧面积A’=2πRH
底面积A=πR2

思路

  • 看到题目,首先可能会想到贪心(因为题目中提到了 ,但是很明显,每一步会影响到后面的操作,所以摒弃)
  • 当然,不难想到用dfs算法,并且一定要剪枝(因为普通深搜,光想想时间复杂度都极高)
  • 接着确定剪枝条件(这道题说真的,还是挺考察数学知识的,在理解数学关系的一些地方卡了很久):
    1. 在中途dfs中计算出的 面积>已知最小面积
    2. 在中途dfs中计算出的 体积>题目给出的体积
    3. 在中途dfs中 估算出的最小体积>已知最小体积 别问我怎么想到这3个,一遍一遍地TLE,慢慢迭代出来的
  • 设计dfs算法:
    1. 首先确定return条件,如果搜到最高层时并且体积=题目给出的体积就取一下最小体积并return
    2. 开始遍历:我的题解是从蛋糕下往上搜。先从半径开始(可能从高开始也行),半径的范围需要确定最小为层数,最大为该层的下方那层的半径-1,,高度范围高度最小为该层层数,最大应该由给出的体积,已知体积和假设的最小体积估算
    3. 大体上就没了,具体细节在代码里解释

题解

#include
using namespace std;
int mins[25],minv[25],n,m,ans=INT_MAX;         //mins和minv存放估计的最小面积和体积(因为题目中说明了半径和高度为整数,并且由上往下递增,所以可以估计最小)

void dfs(int dep,int r,int h,int s,int v){   //传入的dep是要计算的那层层数,而r,h是上一层的半径和高度
    if(dep==0){
        if(v==n) ans=min(ans,s);
        return;
    }
    int max_height=h;
    if(s+mins[dep-1]>=ans) return;
    if(v+minv[dep-1]>n) return;
    if(2*(n-v)/r+s>=ans) return;
    for(int i=r-1;i>=dep;--i){
        if(dep==m) s=i*i;
        max_height=min(h-1,(n-minv[dep-1])/i/i);  //这里是确定高度的上界,n-minv[]……得到的是由估算最小体积计算出的高度。因为h-1是当前层数不可逾越的最大高度,而n-minv……同样也是不可逾越的最大高度,所以取两个中较小那个
        for(int j=max_height;j>=dep;--j) dfs(dep-1,i,j,s+2*i*j,v+i*i*j);
    }
}
int main(){
    scanf("%d%d",&n,&m);
    memset(mins,0,sizeof(mins));
    memset(minv,0,sizeof(minv));
    for(int i=1;i<=m;++i) mins[i]=mins[i-1]+2*i*i,minv[i]=minv[i-1]+i*i*i;
    dfs(m,n,n,0,0);
    if(ans==INT_MAX) printf("-1");
    else printf("%d",ans);
}

这道题comprehensively来说有难度(主要是在数学方面上的细节,剪枝算法方面没难度)

小木棍

乔治有一些同样长的小木棍,他把这些木棍随意砍成几段,直到每段的长都不超过50。现在,他想把小木棍拼接成原来的样子,但是却忘记了自己开始时有多少根木棍和它们的长度。给出每段小木棍的长度,编程帮他找出原始木棍的最小可能长度。

输入描述

第一行为一个单独的整数N表示砍过以后的小木棍的总数。第二行为N个用空格隔开的正整数,表示N根小木棍的长度

输出描述

输出仅一行,表示要求的原始木棍的最小可能长度

示例一
输入

9
5 2 1 5 2 1 5 2 1

输出

6

备注

1≤N≤60

思路

  • 问题问到了最小,但是却没用到贪心策略。我的看法是:1. 题目中有一个 可能,2. 用木棍组合时,势必会影响到后面的操作(因为前面已经拿了一些木棍,所以已经被拿的木棍不能在后面再考虑了)
  • 首先根据题意知道一定是要用剪枝策略,不然必超时,所以确定剪枝策略
  1. 确定的单根最短木棍长度一定要可以被总长度整除
  2. 从单根最小的长度开始深搜,直到总长为止(其实,这里也是我一开始做题的时候的盲区,不知道一开始的深搜该从长度的枚举开始,还认为是先提出木棍组合的长度然后进行深搜,但是这个方法自然不行。因为深搜的模板应该是 循环进入,逐次深搜,dfs()里再dfs()
  3. 如果已经成立了,则设置return
  4. 一根木棍无法匹配时,与它相同长度的木棍也无法匹配
  5. 要回溯,因为用一根木棍放到已有的木棍组合里不匹配时,要拿出来,为后面的木棍组合匹配

题解

#include
using namespace std;
int N,stick[65],total=0,m,len;
bool vis[65],check=false;
void dfs(int which,int current,int rest){           //which——现在是第几根原长木棍,current是刚刚被纳入木棍组合的被砍木棍,rest——此单根木棍还需多长才能达到原长
    if(which==m){
        check=true;
        return;
    }
    if(rest==0) {
        int i=1;
        while(!vis[i])++i;      //找到一根最大长度能用的木棍(以最大长度为基准)
        vis[i]=0;
        dfs(which+1,i,len-stick[i]);
        vis[i]=1;
    }else{
        for(int i=current+1;i<=N;++i){
            if(vis[i]&&stick[i]<=rest){
                vis[i]=0;
                dfs(which,i,rest-stick[i]);
                vis[i]=1;
                if(check) return;
                while(stick[i]==stick[i+1])++i;
            } 
        }
    }
}
int main(){
    scanf("%d",&N);
    for(int i=1;i<=N;++i) scanf("%d",&stick[i]),total+=stick[i];
    sort(stick+1,stick+1+N,greater());
    memset(vis,1,sizeof(vis));
    vis[1]=0;
    for(len=stick[1];len<=total;++len){
        if(total%len!=0) continue;
        m=total/len;                           //m为推理出的原长木棍的数量
        dfs(1,1,len-stick[1]);
        if(check) {
            printf("%d",len);
            return 0;
        }
    }
}

你可能感兴趣的:(算法刷题笔记,深度优先,算法,蓝桥杯)