DFS与BFS 的爱恨情仇

前言

算法的过渡点,也就是从这里开始真正的代码强度开始展现,DFS和BFS必须掌握,才能有进一步发展的空间

  1. DFS,深度遍历,也就是一口气走到头不撞南墙不回头,同时它很讲究递归,也就是兜兜转转终是你的小tip,疯狂安利。它更像是栈,递归搜索树。
  2. BFS,广度遍历,是很稳重的少年,可以理解为层层遍历,用队列进行存放,可以理解为头出,将下一步的情况再排进队伍里,很对的点与边,攻城略地,走地图最短路径,走迷宫之类。

下面进入模板图解

排列数字

按字典序输出所有排列方案,每个方案占一行。
在这里插入图片描述

#include<bits/stdc++.h>
using namespace std;
const int N=8;
int n,q[N];
bool str[N];
void dfs(int v){
    if(v==n){
        for(int i=0;i<n;i++) printf("%d ",q[i]);
        puts("");
        return ;
    }
    for(int i=1;i<=n;i++){
        if(!str[i]){
            str[i]=1;
            q[v]=i;
            dfs(v+1);
            str[i]=0;
        }
    }
    
}
int main(){
    scanf("%d ",&n);
    dfs(0);
    return 0;
}

在这里详细推解一下模板

void dfs(int u){    
    if (u == n)    // 一个排列填充完成{
        for (int i = 0; i < n; i ++) printf("%d ",path[i]);  // 注意格式 别忘了每两个数字间用空格隔开
        puts("");  // 相当于输出一个回车
        return;
    }
    for (int i = 1; i <= n; i ++){
        if (!state[i])
        {
            path[u] = i;       // 把 i 填入数字排列的位置上
            state[i] = true;   // 表示该数字用过了 不能再用
            dfs(u + 1);        // 这个位置的数填好 递归到右面一个位置
            state[i] = false;  // 恢复现场 该数字后续可用
        }
    }
    // for 循环全部结束了 dfs(u)才全部完成 回溯
    return;        // 可写可不写
}

皇后问题

最常见的皇后问题,有八皇后也有好几个皇后,这是一个后宫佳丽三千的题
在这里插入图片描述

第一种解法,暴力解法
先讲原始方法,好理解一点,DFS按每个元素枚举 时间复杂度O(2的n2次幂)因为每个位置都有两种情况,总共有 n2 个位置先看代码主要是dfs 。

#include <iostream>
using namespace std;
const int N = 20;//对角线元素 2n-1 取20防止越界 
int n;
char g[N][N]; //存储图 
bool row[N],col[N], dg[N], udg[N]; //udg 副对角线 /
//英语单词 column 列   diagonal 主角线 \ 
//row 行

void dfs (int x,int y,int s) {  //xy为坐标 (x,y) s为 n皇后放置个数

    if (y == n) { //当x走到行末尾的时候  
        y = 0;    //转到下一行的第一个
        x++;
    }

    if (x == n) { //走到最后一行 且n皇后都放好的时候
        if (s == n) { // 如果找到方案的话
        for (int i = 0; i < n; i ++) {
            puts(g[i]);//puts输出二维数组 输出每一行如何就会自动换行 
            }//puts遍历字符串这个语法不懂看下
        puts("");
        }
        return; //返回调用函数进行执行
    }

    dfs(x, y + 1, s);//不放皇后  并且访问右节点

    // 判断皇后能否放在这格
    if (!row[x] && !col[y] && !dg[x + y] && !udg[x - y + n]) {
        g[x][y] = 'Q';//放皇后 然后把
        row[x] = col[y] = dg[x + y] = udg[x - y + n] = true;

        dfs(x , y + 1, s + 1);//放置皇后,找下一层的

        //回溯的时候 记得恢复现场 ↓ 
        row[x] = col[y] = dg[x + y] = udg[x - y + n] = false; 
        g[x][y] = '.';
    }
}

第二种解法
dg[x + y] = udg[x - y + n]的理解
dg[x + y] = udg[x - y + n] 有的兄弟可能不理解其中x+y到底是什么意思?

以反对角线举例,x-y其实就是该点在对角线上的截距 由中学知识可知对角线方程为y = x + b,其中b表示截距也就是b = x - y(数组下标里面的东西),如果在不同行,但再同一对角线,经过方程计算得到的截距都是一样的,不懂就拿纸自己写一下,+n是为了防止负数产生, 因为数组下标是不可能为负数的,因为每个数都+n,他们映射到结果是一样的,不信你就换个比n大的数试试。
这个用的是y=ax+b的函数解决的,N=20的原因是,最多可由8皇后,所以结合对于y轴的截距,(8+1)*2就行,如果设计到更大的境界,直接0x3f

#include<bits/stdc++.h>
using namespace std;
const int N=20;
bool col[N],dg[N],udg[N];
int q[N][N];
bool 

void dfs(int v){
    if(v==n){
        for(int i=0;i<n;i++){
            for(int j=0;j<n;ij++){
                printf("c",q[i][j]);
            }
            cout<<endl;
        }
        cout<<endl;
        return ;
    }
    for(int i=0;i<n;i++){
        if(!col[i]&&)
    }
}
int main(){
    scanf("%d",&n);
    for(int i=0;i<n;i++)printf("%c",".");
    dfs(1);
    return 0;
}

BFS走迷宫

比如算法书上经常见的老鼠
给定一个 n×m 的二维整数数组,用来表示一个迷宫,数组中只包含 0 或 1,其中 0 表示可以走的路,1 表示不可通过的墙壁。

最初,有一个人位于左上角 (1,1) 处,已知该人每次可以向上、下、左、右任意一个方向移动一个位置。

请问,该人从左上角移动至右下角 (n,m) 处,至少需要移动多少次。

数据保证 (1,1) 处和 (n,m) 处的数字为 0,且一定至少存在一条通路。

输入格式
第一行包含两个整数 n 和 m。

接下来 n 行,每行包含 m 个整数(0 或 1),表示完整的二维数组迷宫。

输出格式
输出一个整数,表示从左上角移动至右下角的最少移动次数。

由于任意两个相邻的点的距离权值都为1, 所以d[][]存储的是其他点到源点的最短距离,也即用BFS求出的是最少移动次数
bfs 的模板解析,也就是队列,设置为空,然后依次进入,比较容易和题目中设定的数据情况相配合进行一些数据操作和修改,比较全面,重点解析一下模板,之后使用的这样的框架

int bfs() {
    queue<PII> q;   //定义一个坐标队列
    q.push({0,0});  //源点进队

    memset(d, -1, sizeof d);    //初始化各个点到源点的距离为-1
    d[0][0] = 0;                //源点到自己的距离为0

    int dx[4] = {0,1,0,-1}, dy[4] = {1,0,-1,0};     //向四个方向扩展的坐标数组(个人按照[上右下左]的顺序)

    while(!q.empty()) {
        auto t = q.front();     //取队头元素
        q.pop();                //队头元素出队

        for(int i = 0; i < 4; i++) {    //分别向四个方向扩展
            int x = t.first + dx[i], y = t.second + dy[i];  //扩展后的坐标
            //首先(x,y)不能越界, 然后g[x][y] == 0说明可以走(g[x][y] == 1说明是障碍物)
            //最后是只更新未被访问的点到源点的距离 (要求d[x][y] == -1)
            if(x >= 0 && x < n && y >= 0 && y < m && g[x][y] == 0 && d[x][y] == -1) {
                d[x][y] = d[t.first][t.second] + 1; //更新未被访问的点到源点的距离
                q.push({x,y});                      //(x,y)进队
            }
        }
    }
    return d[n-1][m-1];     //返回右下角元素到源点的距离
}

题目的解析代码

#include<bits/stdc++.h>
using namespace std;
const int N=110;
pair<int,int> q[N*N];
int n,m;
int g[N][N];
int distance[N][N];
int bfs(){
    int head=0,tail=0;
    q[0]={0,0};
    memset(distance,-1,sizeof(distance));
    d[0][0]=0;
    d[x]={-1,0,1,0};
    d[y]={0,1,0,-1};
    while(head<=tail){
        pair<int,int> road=q[head++];
        for(int i=0;i<4;i++){
            int x=road.first+dx[i],y=road.second+dy[i];
            if(x>=0&&x<n&&y>=0&&y<m&&g[x][y]==0&&distance[x][y]==-1){
                distance[x][y]=distance[road.first][road.second]+1;
                q[++tail]={x,y};
            }
        }
    }
    return distance[n-1][m-1];
}

八数码

在这里插入图片描述
邻接链表
对于任意图 G = (V, E) 的某个节点 u
Adj[u] 表示节点 u 的邻接节点组成的一个链表
图的属性
对于任意图 G = (V, E) 的某个节点 u
u.d 表示从源节点 s 到节点 u 的距离
最短路径
对于任意图 G = (V, E) 的某两个节点 u, v
u 到 v 的最短路径为 u 到 v 的路径中边数最短的路径
最短路径距离为
δ(u,v)=min边数u→⋯→v
δ(u,v)=min边数⁡u→⋯→v
若 u 到 v 没有路径, 则 δ(u,v)=∞
add函数是邻接表的数据基础

void add(int a,int b){
    e[idx]=b;
    ne[idx]=h[a];
    h[a]=idx++;
}

算法以源节点 s 为中心, 以广度优先的策略向四周探索节点, 直到探索完所有节点为止

广度优先搜索算法的详细思路

算法将节点分为三类

未探索的节点, 用白色表示
已探索的节点, 用黑色表示
边界节点(其实就是加入到队列里的节点), 用灰色表示
算法的核心代码依次重复执行下列操作

  1. 从队列中取出节点 u
  2. 扫描 Adj[u]
  3. 对于扫描到的白色节点 v, v 设为灰色, v.d = u.d + 1 后加入队列
  4. 扫描到灰色或黑色节点就跳过, 不进行任何操作
  5. u 设为黑色
BFS(G, s)伪代码
    初始化
    s.color = 灰色
    s.d = 无穷大
    s.π = NIL

    for u in G.V - {s}
        u.color = 白色
        u.d = 无穷大
        u.π = NIL       // 一般用不到, 构建广度优先搜索树的时候会用到, 需要构建从 s 到某一结点的最短路径时也会用到

    Q = ф
    s 入队

    while Q != ф
        u = Q 出队元素

        for v in G.Adj[u]
            if v.color == 白色
                v.color = 灰色
                v.d = u.d + 1
                v.π = u
                v 入队

        u.color = 黑色

八数码的核心解题代码,BFS详解

vector<pair<int,int>> des = {{0,-1},{1,0},{0,1},{-1,0}};
int a[4] = {-1, 3, 1, -3};

// 节点的 v.d 和 v.π 属性
// 最好用 unordered_map 实现, 底层原理是哈希表, 查询操作的时间复杂度是 O(1)
unordered_map<string,int> d;
unordered_map<string,string> pi;

int BFS(string st)
{
    queue<string> q;            // 队列 Q

    string ed = "12345678x";    // 设置搜索的终止节点 ed

    // 源节点 st 初始化
    d[st] = 0;      // 设置 st.d = 0
    pi[st] = "";    // 设置 st.π = NIL
    q.push(st);     // st 入队


    // 只要队列不空, 就执行 while 循环
    while(!q.empty())
    {
        // 出队操作
        string s = q.front();
        q.pop();

        // 判断是否到达终止节点
        if(s == ed)
            return d[ed];   // 输出 ed.d

        // 临时保存一下 s 的属性
        // 用 distance 保存 s.d
        // 用 parent 保存 s
        int distance = d[s];
        string parent(s);

        // 用 index 存放 'x' 在 s 中的下标
        // 用 x 和 y 存在 'x' 在 3*3 矩阵中的坐标
        int index = s.find('x');
        int x = index / 3, y = index - 3 * x;

        // 遍历 Adj[s], 看看是否有未探索的节点
        for(int i = 0; i < 4; i++)
        {
            // 计算邻接节点中 'x' 的坐标
            int nx = x + des[i].first;
            int ny = y + des[i].second;

            if(nx >= 0 && nx <=2 && ny >= 0 && ny <= 2 )    // 判断坐标是否有效
            {
                swap(s[index],s[index+a[i]]);   // 生成邻接节点, 此时的 s 是原来 s 的邻接节点
                if(!d.count(s))                 // 判断邻接节点是否探索过
                {                               // 如果未探索过
                    d[s] = distance + 1;        // 设置 v.d = u.d + 1
                    pi[s] = parent;             // 设置 v.π = u
                    q.push(s);                  // v 入队
                }
                swap(s[index],s[index+a[i]]) ;  // 恢复 s 为原来的 s
            }
        }
    }
    return -1;  // 如果探索完所有节点都没有找到 ed 节点, 就返回 -1
}

题解全码

#include<iostream>
#include<unordered_map>
#include<queue>

using namespace std;

queue<string> q;//队列记录待分析的结点
unordered_map<string, int> d;//无需映射记录到达各个结点的最短步数
int dx[4] = {0, 0, -1, 1}, dy[4] = {-1, 1, 0, 0};

const string ed = "12345678x";

int bfs(string state) {
    q.push(state);
    d[state] = 0;

    while (q.size() > 0) {
        auto t = q.front();
        q.pop();

        int idx = t.find('x');
        int a = idx / 3, b = idx % 3;
        int distance = d[t];

        if (t == ed)
            return d[t];

        for (int i = 0; i < 4; i++) {
            int x = a + dx[i], y = b + dy[i];//进行移动时需要二维化
            swap(t[x * 3 + y], t[idx]);

            if (x >= 0 && x < 3 && y >= 0 && y < 3 && !d[t]) {
                d[t] = distance + 1;
                q.push(t);
            }

            swap(t[x * 3 + y], t[idx]);

        }
    }

    return -1;
}

int main() {
    string state;
    char c;

    for (int i = 0; i < 9; i++) {//二维->一维
        cin >> c;
        state += c;
    }

    cout << bfs(state);
}

树与图的深度优先遍历 树的重心

这道题涉及到DFS和之前学到的find 搜索树,
在这里插入图片描述
加入一个图解
在这里插入图片描述

using namespace std;

const int N = 1e5 + 10; //数据范围是10的5次方
const int M = 2 * N; //以有向图的格式存储无向图,所以每个节点至多对应2n-2条边

int h[N]; //邻接表存储树,有n个节点,所以需要n个队列头节点
int e[M]; //存储元素
int ne[M]; //存储列表的next值
int idx; //单链表指针
int n; //题目所给的输入,n个节点
int ans = N; //表示重心的所有的子树中,最大的子树的结点数目

bool st[N]; //记录节点是否被访问过,访问过则标记为true

//a所对应的单链表中插入b  a作为根 
void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

// dfs 框架
/*
void dfs(int u){
    st[u]=true; // 标记一下,记录为已经被搜索过了,下面进行搜索过程
    for(int i=h[u];i!=-1;i=ne[i]){
        int j=e[i];
        if(!st[j]) {
            dfs(j);
        }
    }
}
*/

//返回以u为根的子树中节点的个数,包括u节点
int dfs(int u) {
    int res = 0; //存储 删掉某个节点之后,最大的连通子图节点数
    st[u] = true; //标记访问过u节点
    int sum = 1; //存储 以u为根的树 的节点数, 包括u,如图中的4号节点

    //访问u的每个子节点
    for (int i = h[u]; i != -1; i = ne[i]) {
        int j = e[i];
        //因为每个节点的编号都是不一样的,所以 用编号为下标 来标记是否被访问过
        if (!st[j]) {
            int s = dfs(j);  // u节点的单棵子树节点数 如图中的size值
            res = max(res, s); // 记录最大联通子图的节点数
            sum += s; //以j为根的树 的节点数
        }
    }

    //n-sum 如图中的n-size值,不包括根节点4;
    res = max(res, n - sum); // 选择u节点为重心,最大的 连通子图节点数
    ans = min(res, ans); //遍历过的假设重心中,最小的最大联通子图的 节点数
    return sum;
}

int main() {
    memset(h, -1, sizeof h); //初始化h数组 -1表示尾节点
    cin >> n; //表示树的结点数

    // 题目接下来会输入,n-1行数据,
    // 树中是不存在环的,对于有n个节点的树,必定是n-1条边
    for (int i = 0; i < n - 1; i++) {
        int a, b;
        cin >> a >> b;
        add(a, b), add(b, a); //无向图
    }

    dfs(1); //可以任意选定一个节点开始 u<=n

    cout << ans << endl;

    return 0;
}

树与图的深度优先遍历

在这里插入图片描述
有向图才会涉及到重边和闭环问题,也就是最短路径问题中需要注意的踩雷点,计算好出度和入度
本题最大的难点就在于如何进行层次遍历,我们来看这样一个例子,有这样一幅图,我已经将其转换成了邻接表进行存储。(不知道如何转换成邻接表存储的同学,建议先划到下方,4. 可能有帮助的前置习题,在给出的链接中复习一下如何用邻接表存储图)
在这里插入图片描述
虽然放在了bfs算法里,但是他本质是dijkstra算法,用queue的形式表现出来有一种BFS的逻辑,也可以用根优化等等,或者理解成topsort走到头也可以。

有向图的拓扑

划重点,只有有向图也有拓扑,也正是有了方向,才解决了闭环
在这里插入图片描述
解题思路

  1. 首先记录各个点的入度

  2. 然后将入度为 0 的点放入队列

  3. 将队列里的点依次出队列,然后找出所有出队列这个点发出的边,删除边,同事边的另一侧的点的入度 -1。

  4. 如果所有点都进过队列,则可以拓扑排序,输出所有顶点。否则输出-1,代表不可以进行拓扑排序。

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int e[N], ne[N], idx;//邻接矩阵存储图
int h[N];
int q[N], hh = 0, tt = -1;//队列保存入度为0的点,也就是能够输出的点,
int n, m;//保存图的点数和边数
int d[N];保存各个点的入度

void add(int a, int b){
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

void topsort(){
    for(int i = 1; i <= n; i++){//遍历一遍顶点的入度。
        if(d[i] == 0)//如果入度为 0, 则可以入队列
            q[++tt] = i;
    }
    while(tt >= hh){//循环处理队列中点的
        int a = q[hh++];
        for(int i = h[a]; i != -1; i = ne[i]){//循环删除 a 发出的边
            int b = e[i];//a 有一条边指向b
            d[b]--;//删除边后,b的入度减1
            if(d[b] == 0)//如果b的入度减为 0,则 b 可以输出,入队列
                q[++tt] = b;
        }
    }
    if(tt == n - 1){//如果队列中的点的个数与图中点的个数相同,则可以进行拓扑排序
        for(int i = 0; i < n; i++){//队列中保存了所有入度为0的点,依次输出
            cout << q[i] << " ";
        }
    }
    else//如果队列中的点的个数与图中点的个数不相同,则可以进行拓扑排序
        cout << -1;//输出-1,代表错误
}


int main(){
    cin >> n >> m;//保存点的个数和边的个数
    memset(h, -1, sizeof h);//初始化邻接矩阵
    while (m -- ){//依次读入边
        int a, b;
        cin >> a >> b;
        d[b]++;//顶点b的入度+1
        add(a, b);//添加到邻接矩阵
    }
    topsort();//进行拓扑排序
    return 0;
}

这个就是很直观的拓扑排序

福利板子

数据结构与算法比赛常用的套题板子,做了汇总,主要是DFS BFS 各种花式树与图
自己上传的资源,如果有打比赛需要的可以下载一下
https://download.csdn.net/download/qq_43621422/85174186

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

磊哥哥讲算法

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值