3.1 搜索与图论 | DFS、BFS、拓扑排序

3.1 搜索与图论 | DFS、BFS、拓扑排序

这是我的一个算法网课学习记录,道阻且长,好好努力

3.1.1 DFS与BFS

DFS与BFS都可以对空间进行搜索,搜索的结构像一棵树一样,但是搜索的方式不同。

DFS十分“执着”,不撞南墙不回头;BFS比较“稳重”,层层递进。

一些比较:

数据结构空间特性
DFS栈stackO(h)空间有优势
BFS队列queueO(2h)最短路

3.1.1.1 深度优先搜索 DFS

暴力搜索

从搜索树的角度思考

例题1:ACWing 842. 排列数字

给定一个正整数n,生成1 ∼ n 的全排列,按照字典序输出。

#include <iostream>

using namespace std;

const int N = 10;

int n;
int path[N];
bool st[N];

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 (!st[i]) // 如果这个数没有被使用过
        {
            path[u] = i; // 存入路径节点
            st[i] = true; // 更新标记
            dfs(u + 1); // 进入下一层递归
            st[i] = false; // 回溯:恢复现场
        }
}

int main()
{
    cin >> n;

    dfs(0);

    return 0;
}

对于一些问题可以全部遍历一遍,在逐一判断是否符合要求;也可以在遍历的时候进行判断,从而减少一定的循环次数,也就是所谓的 剪枝 (剪枝有很多中方法)

例题2:ACWing 843. n-皇后问题

n-皇后问题是指将 n 个皇后放在 n∗n 的国际象棋棋盘上,使得皇后不能相互攻击到,即任意两个皇后都不能处于同一行、同一列或同一斜线上。

Ans_1:

#include <iostream>

using namespace std;

const int N = 10;

int n;
char g[N][N];
bool col[N], dg[N], udg[N]; // 分别用于判断是否在同一列、正对角线、反对角线

void dfs(int u)
{
    if (u == n)
    {
        for (int i = 0; i < n; i ++ ) puts(g[i]); // 逐行输出
        puts("");
        return;
    }

    for (int i = 0; i < n; i ++ ) // 循环,进行枚举
    {
        if (!col[i] && !dg[u + i] && !udg[n - u + i]) // 如果满足规则
        {
            g[u][i] = 'Q'; // 将皇后存入该点
            col[i] = dg[u + i] = udg[n - u + i] = true; // 更新标记
            dfs(u + 1); // 进入下一层递归
            col[i] = dg[u + i] = udg[n - u + i] = false; // 回溯:还原
            g[u][i] = '.'; // 覆盖皇后
        }
    }
}

int main() {
    cin >> n;
    // 初始化棋盘
    for (int i = 0; i < n; i++)  
        for (int j = 0; j < n; j++)
            g[i][j] = '.';

    dfs(0);

    return 0;
}

通过分析可以知道,每一行只会有一个皇后,再运用dfs

22行的代码if (!col[i] && !dg[u + i] && !udg[n - u + i])

由于在坐标系当中,在这里的情景下,直线的关系有两种y = x + by = - x + b,因此有b = x - yb = x + y,对于前者,由于数组的idx不能是负数,因此加上一个n,修正使之为正数。于是,在这里在判断正反对角线的时候,就有了udg[n - u + i]加上了一个n

Ans_2:更原始的做法

#include <iostream>

using namespace std;

const int N = 10;

int n;
int path[N];
bool st[N];

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 (!st[i]) // 如果这个数没有被使用过
        {
            path[u] = i; // 存入路径节点
            st[i] = true; // 更新标记
            dfs(u + 1); // 进入下一层递归
            st[i] = false; // 回溯:恢复现场
        }
}

int main()
{
    cin >> n;

    dfs(0);

    return 0;
}

3.1.1.2 宽度优先搜索 BFS

例题:ACWing 844. 走迷宫

给定一个n*m的二维整数数组,用来表示一个迷宫,数组中只包含0或1,其中0表示可以走的路,1表示不可通过的墙壁。

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

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

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

using namespace std;

typedef pair<int, int> PII;

const int N = 110;

int n, m;
int g[N][N]; // 存放地图
int d[N][N]; // 存放每个点到起点的距离
PII q[N * N]; // 手写的队列
PII Prev[N][N]; // 存放每个点是由其前一个点扩展出来的

int bfs()
{
    int hh = 0, tt = 0; // 队列 初始化
    q[0] = {0, 0};

    memset(d, -1, sizeof d); // 距离初始化为-1 表示没有走过
    
    d[0][0] = 0; // 表示起点走过了

    int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1}; // 声明方向向量

    while (hh <= tt) // 队列不空
    {
        auto t = q[hh ++ ]; // 取队头元素

        for (int i = 0; i < 4; i ++ ) // 枚举4个方向
        {
            int x = t.first + dx[i], y = t.second + dy[i]; // 
            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; // 更新到起点的距离
                Prev[x][y] = t;
                q[ ++ tt] = {x, y}; // 新坐标入队
            }
        }
    }

    int x = n - 1, y = m - 1;
    while (x || y) // x和y只要有一个不等于0 输出从终点到起点的路线坐标
    {
        cout << x << ' ' << y << endl;
        auto t = Prev[x][y];
        x = t.first, y = t.second;
    }

    return d[n - 1][m - 1]; // 输出右下角点距离起点的距离
}

int main()
{
    cin >> n >> m;

    for (int i = 0; i < n; i ++ )
        for (int j = 0; j < m; j ++ )
            cin >> g[i][j];

    cout << bfs() << endl;

    return 0;
}

3.1.1.3 树与图的存储

树是一种特殊的图 (无环连通图)。

图分为两种,有向图(边是有方向的)与无向图(边是无方向的,即a–>b, a<–b)。

  • 有向图
    • 邻接矩阵
    • 邻接表:有n个点,就开n个单链表,用于存储该点可以到达哪些点。(类比拉链法)

3.1.1.4 树与图的深度优先遍历

例题:ACWing 846. 树的重心

给定一颗树,树中包含n个结点(编号1~n)和n-1条无向边。

请你找到树的重心,并输出将重心删除后,剩余各个连通块中点数的最大值。

重心定义:重心是指树中的一个结点,如果将这个点删除后,剩余各个连通块中点数的最大值最小,那么这个节点被称为树的重心。

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100010, M = N * 2; // M是双向边 *2
int n, m;
int h[N], e[M], ne[M], idx;
bool st[N];
int ans = N;

void add(int a, int b) // 将b添加到a节点指向的数的链表中
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

// 返回以u为根的子树中点的数量
int dfs(int u)
{
    st[u] = true; // 标记一下u点已经被搜过了

    int sum = 1, res = 0;
    for (int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (!st[j]) // 如果没有被标记
        {
            int s = dfs(j);
            res = max(res, s);
            sum += s;
        }
    }
    res = max(res, n - sum); // 删去后该节点后,剩余各个连通块中点数的最大值

    ans = min(ans, res);
    return sum;
}

int main()
{
    cin >> n;
    memset(h, -1, sizeof h); // 初始化h数组元素为-1

    for (int i = 0; i < n - 1; i ++ )
    {
        int a, b;
        cin >> a >> b;
        add(a, b), add(a, b); // 双向标记
    }

    dfs(1);

    cout << ans << endl;

    return 0;
}

3.1.1.5 树与图的宽度优先遍历

重边是指有两条完全相同的边,自环是指自己指向自己。

例题:ACWing 847. 图中点的层次

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环。

所有边的长度都是1,点的编号为 1∼n。

请你求出 1 号点到 n 号点的最短距离,如果从 1 号点无法走到 n 号点,输出 − 1。

#include <cstring>
#include <iostream>

using namespace std;

const int N = 100010;

int n, m;
int h[N], e[N], ne[N], idx; // 邻接表
int d[N], q[N]; // 数组模拟队列 d是距离 q是队列

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

int bfs()
{
    int hh = 0, tt = 0;
    q[0] = 1;

    memset(d, -1, sizeof d); // 初始化距离为-1

    d[1] = 0;

    while (hh <= tt)
    {
        int t = q[hh ++ ]; // 取队头

        for (int i = h[t]; i != -1; i = ne[i]) // 对每一层的节点进行遍历
        {
            int j = e[i];
            if (d[j] == -1) // 如果没有被搜索到
            {
                d[j] = d[t] + 1; // 当前节点到根节点的距离+1
                q[ ++ tt] = j; // 插入到队列当中
            }
        }
    }

    return d[n]; // 返回n点到根节点的距离
}

int main()
{
    cin >> n >> m;

    memset(h, -1, sizeof h);

    for (int i = 0; i < m; i ++ )
    {
        int a, b;
         cin >> a >> b;
         add(a, b);
    }

    cout << bfs() << endl;

    return 0;
}

3.1.2 树与图的遍历拓扑排序

图的拓扑序必须要满足以下两点:

  1. 每个顶点只出现一次。
  2. 对于图中的任何一条边,起点必须在终点之前。

有向图才有拓扑序列。

一个有向无环图一定存在一个拓扑序列。

一个有向无环图一定至少存在一个入度为0的点。

一个有向无环图的拓扑序列不一定唯一。

例题:ACWing 848. 有向图的拓扑序列

给定一个n个点m条边的有向图,点的编号是1到n,图中可能存在重边和自环。输出其任意一个拓扑序列。如果不存在则返回-1 。

using namespace std;

const int N = 100010;

int n, m;
int h[N], e[N], ne[N], idx;
int q[N], d[N]; // q表示队列 d表示入度

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

bool toposort()
{
    int hh = 0, tt = -1;

    for (int i = 1; i <= n; i ++ )
        if(!d[i]) // 如果入度为0 进队
            q[ ++ tt] = i;

    while (hh <= tt)
    {
        int t = q[hh ++ ];

        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            d[j] -- ; // 删掉前面的一个点 入度-1
            if (d[j] == 0) q[ ++ tt] = j;
        }
    }

    return tt == n - 1;
}

int main()
{
    cin >> n >> m;

    memset(h, -1, sizeof h);

    for (int i = 0; i < m; i ++ )
    {
        int a, b;
        cin >> a >> b;
        add(a, b);
        d[b] ++ ;
    }

    if (toposort())
    {
        for (int i = 0; i < n; i ++ ) printf("%d ", q[i]);
        puts("");
    }
    else puts("-1");

    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值