搜索与图论-DFS

一、DFS

  • DFS 的关键点是递归和回溯。

1. DFS 简介

  • DFS(Depth First Search)是深度优先遍历,是图论当中一种非常重要的算法,生产上广泛用于拓扑排序,寻路(走迷宫),搜索引擎,爬虫等。
  • 定义是,不断地沿着顶点的深度方向遍历。顶点的深度方向是指它的邻接点方向。
  • 主要用于解决是否存在一个我们所需要的结果。因为 DFS 会首先把一种可能的情况尝试到底。才会回溯去尝试下一种情况,只要找到一种情况,就可以返回了。
  • DFS 问题一般分为两类:
  • (1) 定义的 DFS :对图的连通性进行测试,典型的问题:迷宫连通性测试、图的条件搜索等。
  • (2) 广义的 DFS–DFS 思路的应用: DFS 搜索顺序+规则问题、穷举结果寻求最优解/符合条件解等等,由于其穷举答案的本质,又被称为爆搜。

2. DFS 的实现步骤

  • (1) 从顶点出发。
  • (2) 访问顶点,也就是根节点。
  • (3) 依次从顶点的未被访问的邻接点出发,进行深度优先遍历;直至和顶点有路径相通的顶点都被访问。
  • (4) 若此时尚有顶点未被访问,则从一个未被访问的顶点出发,重新进行深度优先遍历,直到所有顶点均被访问过为止。

3. DFS 实际演示

在这里插入图片描述

  • (1) 首从根节点 1 开始遍历,它相邻的节点有 2,3,4,先遍历节点 2,再遍历 2 的子节点 5,然后再遍历 5 的子节点 9。

在这里插入图片描述

  • (2) 上图中一条路已经走到底了( 9是叶子节点,再无可遍历的节点 ),此时就从 9 回退到上一个节点 5,看下节点 5 是否还有除 9 以外的节点,没有继续回退到 2,2 也没有除 5 以外的节点,回退到 1,1 有除 2 以外的节点 3,所以从节点 3 开始进行深度优先遍历,如下。

在这里插入图片描述

  • (3) 同理从 10 开始往上回溯到 6, 6 没有除 10 以外的子节点,再往上回溯,发现 3 有除 6 以外的子点 7,所以此时会遍历 7。

在这里插入图片描述

  • (4) 从 7 往上回溯到 3, 1,发现 1 还有节点 4 未遍历,所以此时沿着 4, 8 进行遍历,这样就遍历完成了。
  • (5) 完整的节点的遍历顺序如下(节点上的的蓝色数字代表遍历的顺序)。

在这里插入图片描述

二、DFS 例题——排列数字

题目描述

给定一个整数 n,将数字 1∼n 排成一排,将会有很多种排列方法。
现在,请你按照字典序将所有的排列方法输出。

输入格式

共一行,包含一个整数 n。

输出格式

按字典序输出所有排列方案,每个方案占一行。

数据范围

1 ≤ n ≤ 7

输入样例

3

font size=5> 输出样例

1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1

具体实现

1. 样例演示

  • 题目当中要求的是将数字 1∼n 排成一排,枚举每一种可能发送的情况。
  • 最开始的时候,三个位置都是空的 _ _ _ 。
  • 首先,填写第一个空位,第一个空位可以填 1 _ _ 。
  • 填好第一个空位,填第二个空位,第二个空位可以填 2,填写后为:1 2 _ 。
  • 填好第二个空位,填第三个空位,第三个空位可以填 3,填写后为: 1 2 3 。
  • 此时,空位填完,无法继续填数,因此,这是第一种方案,输出该方案,然后进行回溯。
  • 退到了状态:1 2 _ 。剩余第三个空位没有填数。第三个空位上除了填过的 3 ,没有其他数字可以填。
  • 因此再往后退一步,退到了状态:1 _ _ 。第二个空位上除了填过的 2,还可以填 3 。第二个空位上填写 3,填写后为:1 3 _ 。
  • 填好第二个空位,填第三个空位,第三个空位可以填 2,填写后为:1 3 2 。
  • 这时候,空位填完,无法继续填数,所以这是第二种方案,输出该方案,然后进行回溯。
  • 然后往后退一步,退到了状态:1 3 _ 。剩余第三个空位没有填数。第三个空位上除了填过的 2,没有其他数字可以填。
  • 因此再往后退一步,退到了状态:1 _ _。第二个空位上除了填过的 2,3,没有其他数字可以填。
  • 因此再往后退一步,退到了状态:_ _ _。第一个空位上除了填过的 1,还可以填 2 和 3 。
  • 2 和 3 的填写方法跟 1 的填写方法相同,在此不做过多叙述,大家可以进行类比思考。
  • 整体流程如下图所示。

在这里插入图片描述

2. 实现思路

  • 详见样例演示。

3. 代码注解

  • st[N] 数组表示数字是否被用过,用 bool 类型对其进行表示,当其为 true 时表示被使用过,当其为 false 时,表示其没有被使用。
  • path[N] 数组保存排列。当排列的长度为 n 时,表明所有位置均已填充,为一种方案,对其进行输出。
  • dfs(i) 表示的含义是在 path[i] 处填写数字,然后递归的在下一个位置填写数字。
  • 回溯是指第 i 个位置填写某个数字的所有情况都遍历后, 第 i 个位置填写下一个数字。
  • 在完成以某一个数字为第一位置所有情况的输出时,要进行回溯,然后要注意将当下情况恢复为原始情况,即 state[N] 变为 false ,尚未使用。在此处,path[i] 不需要进行恢复原样,因为 path[i] 会被覆盖。

4. 实现代码

#include <bits/stdc++.h>
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 ++ )
        {
            cout << path[i] << ' ' ;
        }
        cout << endl;
        return;
    }
    
    for (int i = 1; i <= n; i ++)
    {
        if (!st[i])  //判断这个数是否被使用过
        {
            path[u] = i;
            st[i] = true;
            dfs(u + 1);
            //path[i]不需要进行恢复原样,因为path[i]都会被覆盖。
            st[i] = false;
        }
    }
}

int main()
{
    cin >> n;
    dfs(0);
    system("pause");
    return 0;
}

三、DFS 例题—— n-皇后问题(经典)

题目描述

n − 皇后问题是指将 n 个皇后放在 n×n 的国际象棋棋盘上,使得皇后不能相互攻击到,即任意两个皇后都不能处于同一行、同一列或同一斜线上。
在这里插入图片描述
现在给定整数 n,请你输出所有的满足条件的棋子摆法。

输入格式

共一行,包含整数 n。

输出格式

每个解决方案占 n 行,每行输出一个长度为 n 的字符串,用来表示完整的棋盘状态。
其中 . 表示某一个位置的方格状态为空,Q 表示某一个位置的方格上摆着皇后。
每个方案输出完成后,输出一个空行。
注意:行末不能有多余空格。
输出方案的顺序任意,只要不重复且没有遗漏即可。

数据范围

1 ≤ n ≤ 9

输入样例

4

输出样例

.Q…
…Q
Q…
…Q.

…Q.
Q…
…Q
.Q…

具体实现——按行进行枚举

1. 样例演示

  • 具体样例演示如下图:
    在这里插入图片描述

2. 实现思路

  • 首先,从第一行开始看,皇后可以放到哪一列。
  • 然后每一行依次进行枚举。
  • 注意:剪枝,就是提前判断,当前这个方案一定不合法时,直接将与这个方法相关的都完全排除掉。

3. 代码注解

在这里插入图片描述

  • 因为 y-x 可能会是负数,但我们的数组下标不能是负数,所以,在此我们加上偏移量 n ,保证数组下标均为正数。

4. 实现代码

#include <bits/stdc++.h>
using namespace std;

const int N = 20;

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);
    system("pause");
    return 0;
}

具体实现——按格子进行枚举

1. 实现思路

  • 对每个格子进行枚举,每个格子都有放或不放两种情况,只要我们枚举到最后一个格子,就可以知道共有几种布局方案了。
    在这里插入图片描述

2. 实现代码

#include <bits/stdc++.h>
using namespace std;

const int N = 20;

int n;
char g[N][N];
bool row[N], col[N], dg[N * 2], udg[N * 2];

void dfs(int x,int y,int s)//x,y 表示坐标,u表示当前有几个皇后
{
    if (s > n) 
    {
        return;
    }
    if (y == n)
    {
        y = 0;
        x ++ ;
    }
    if (x == n)
    {
        if (s == n)
        {
            for (int i = 0; i < n; i ++ ) 
            {
                puts(g[i]);
            }
            puts("");
        }
        return;
    }

//不放皇后
    g[x][y] = '.';
    dfs(x, y + 1, s);

//放皇后
    if (!row[x] && !col[y] && !dg[x + y] && !udg[x - y + n])
    {
        row[x] = col[y] = dg[x + y] = udg[x - y + n] = true;
        g[x][y] = 'Q';
        dfs(x, y + 1, s + 1);
        g[x][y] = '.';
        row[x] = col[y] = dg[x + y] = udg[x - y + n] = false;
    }
}

int main()
{
    cin >> n;
    for(int i = 0; i < n; i ++)
    {
        for(int j = 0;j < n; j ++)
        {
            g[i][j] = '.';
        }
    }
    dfs(0,0,0);
    system("pause");
    return 0;
}
  • 7
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
以下是使用并查集解决图论-桥问题的C++代码,不使用Tarjan算法: ```c++ #include <iostream> #include <vector> using namespace std; const int MAXN = 1e5 + 5; int n, m, cnt, ans; int fa[MAXN], low[MAXN], dfn[MAXN]; vector<int> vec[MAXN]; int find(int x) { if (fa[x] == x) return x; return fa[x] = find(fa[x]); } void Union(int u, int v) { int x = find(u); int y = find(v); if (x != y) fa[x] = y; } void dfs(int u, int pre) { low[u] = dfn[u] = ++cnt; for (int i = 0; i < vec[u].size(); i++) { int v = vec[u][i]; if (!dfn[v]) { dfs(v, u); low[u] = min(low[u], low[v]); if (low[v] > dfn[u]) { ans++; Union(u, v); } } else if (v != pre) { low[u] = min(low[u], dfn[v]); } } } int main() { cin >> n >> m; for (int i = 1; i <= n; i++) fa[i] = i; for (int i = 1; i <= m; i++) { int u, v; cin >> u >> v; vec[u].push_back(v); vec[v].push_back(u); } dfs(1, 0); cout << ans << endl; return 0; } ``` 该代码与使用Tarjan算法的代码相似,只是将Tarjan算法中的递归调用改为了深度优先搜索。在`dfs`函数中,`low[u]`表示u能够到达的最小dfn值,`dfn[u]`表示u的dfs序,`pre`表示u的父亲节点。当`v`是`u`的子节点时,如果`v`还没有被访问过,就递归调用`dfs`函数,并更新`low[u]`的值。如果`low[v] > dfn[u]`,则说明(u, v)是桥,合并u和v所在的集合。如果`v`已经访问过,并且v不是u的父亲节点,就更新`low[u]`的值。 最后输出ans即可,ans表示桥的数量。 注意:此代码实现并不是最优解,仅作为参考。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

虚心求知的熊

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

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

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

打赏作者

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

抵扣说明:

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

余额充值