C++dfs

如果学过数据结构,或者看过一些其他的资料,你会发现很多书上都会把栈和其他数据结构一起讲解,为什么我们会把栈放到深度优先搜索呢。到目前为止,我们还没有提及到深度优先搜索。没关系,你先好好理解栈和递归,如果没有理解栈和递归,是很难理解深度优先搜索的。这一节,我们将更深入的来看看递归和栈的关系。


为了理解递归和栈的关系,我们现在必须简单的关注一些计算机的底层知识。


在操作系统上运行的程序,其占用内存根据不同的用途被分成不同的区域。


代码区:这个区域存储着被装入执行的二进制机器,处理器会到这个区域取指并执行。
数据区:用于存储全局变量等。
堆区:进程可以在堆区动态地请求一定大小的内存,并在用完之后归还给堆区。动态分配和回收是堆区的特点。
栈区:用于动态地存储函数之间的关系,以保证被调用函数在返回时恢复到母函数中继续执行。
这里我们关注的是栈区。
当程序调用某个函数时,会将传入这个函数的参数依次压入一个栈中,就是我们上面提到的栈区,称为系统栈。同时在函数内部定义的局部变量也会压入系统栈。离开调用的函数的时候,会在栈顶弹出函数参数和局部变量。当递归调用某一函数的时候,每递归调用一次的时候,都会把参数和局部变量压入系统栈,这样直到到达最小面一层递归的边界条件的时候,使用的栈空间到达最大值,如果递归调用的层数太多,导致栈空间被占满而出现程序运行错误,这种错误就是我们常说的栈溢出,俗称爆栈。我们在写递归的是时候,一定要考虑到递归层数的问题,避免栈溢出。


现在我们知道递归调用和栈的关系以后,就提供给我们一种将递归改写成循环的思路——栈模拟递归。具体的可以等你对递归有了深入了解以后在去学习。如果你能理解递归和栈的关系,对你深入理解递归是很有帮助的。如果你不能理解递归和栈的关系也没关系,你可以继续往后面学习,等你理解了深度优先搜索以后再回来看这节。
深度优先搜索(depth-first-search)简称 dfs,应该算是应用得最广泛的搜索算法,也是竞赛中经常考察的一个难点。dfs 按照深度优先的方式搜索,通俗的说就是一条路走到黑。dfs 是一种穷举的手段,实际上就是把所有的可行方案列举出来,不断去试探,直到找到问题的解,其过程是对每一个可能的分支路径深入到不能再深入为止,而且每个节点只能访问一次。


dfs 是一种搜索算法,dfs 一般的实现方法是借助递归。




1
void dfs(int deep) {
2
    if (到达边界) {
3
      // 做一些处理后返回
4
    } else {
5
        for(所有可能的选择) {
6
            dfs(deep + 1);
7
        }
8
    }
9
}
上面是 dfs 的一般的写法。
dfs 和递归的区别是,dfs 是一种算法,注重的是思想,而递归是编程语言的一种写法。我们通过递归的写法来实现 dfs。 在之前我们讲到的递归每个节点最多只有 2 个分支,而在接下来的 dfs 中,每个节点可以扩展出很多个分支。下面我们通过一个实际问题来理解 dfs 到底干了什么。


相信大家都玩过走迷宫。用 2 维数组来表示一个迷宫




1
S##.
2
....
3
###T
'S'表示起点,'T'表示终点,'#'表示墙壁,'.'表示平地。你需要从'S'出发走到'T',每次只能上下左右走动,并且不能走出地图,也不能走进墙壁,每个点只能通过一次。现在要求你求出有多少种走的方案。
我们尝试用 dfs 来求解这个问题。先找到起点,每个点按照左,下,右,上的顺序尝试,从起点'S'开始,走到下一个点以后,把这个点再当做起点'S'继续按照顺序尝试,如果某个点上下左右都尝试走过了以后,便把起点回到走到这个点的点。继续尝试其他方向。直到所有点都尝试走了上下左右。好比你自己去走这个迷宫,你也要一个方向一个方向的尝试着走,如果这条路不行,就回头,尝试下一条路,现在由程序来完成这个过程。




1
// 对坐标为(x, y)的点进行搜索
2
void dfs(int x, int y) {
3
    if (x,y) 是终点 {
4
        方案数增加
5
        return;
6
    }
7
    标记(x, y)已经访问
8
    for (x, y) 能到达的格子(tx, ty) {
9
        if (tx, ty) 没有访问 {
10
            dfs(tx, ty);
11
        }
12
    }
13
    取消(x, y)访问标记
14
}
上面给出了代码的框架。
前面用到的 dfs 算法都是比较形象的,很容易想象搜索过程,接下来我们看看一些需要抽象成 dfs 的问题。实际上,我们所遇到的大多数问题都是需要抽象成 dfs 的形式才能应用 dfs 进行搜索的。看下面一个问题。


给出 n 个整数,要求从里面选出 k个整数,使的选出来的数的和为 S。


前面我们已经学习过利用 2 进制状态枚举的方法来做这题,回顾一下 2 进制枚举的思想,实际上是在枚举每一个数选或者不选,对应的状态有 2^n
​​  种。利用类似的思想,我们也可以用 dfs 来抽象这样的枚举过程。在第一层 dfs 的时候,我们可以选择是否加上第一个数,如果加上第一个数,和值加上第一个数,dfs 进入到下一层,否则 dfs 直接进入到下一层,在第二层,对第二个数做同样的处理,dfs 的过程中记录已经选取的数的个数,如果已经选取了 k 个数,判断和值是否是 S。对于每一层,我们都有 2 个选择,选和不选,不同的选择,都会使得搜索进入 2个完全不同的分支进行搜索。
图片显示的是我们设计的 dfs 的搜索树,搜索树中的每个节点代表一个状态。具体的,我通过记录当前的 S和 k 值来表示我们搜索到的状态,初始状态是 S=0,k=0 。对于不同的决策,状态会在搜索树上的对应分支进行跳转。所以状态实际上是搜索树上面的节点。
从 n个不同元素中任取 m个元素,按照一定的顺序排列起来,叫做从 n个不同元素中取出 m个元素的一个排列。当 m=n 时所有的排列情况叫全排列。所谓全排列就是对 n个元素按照任意顺序排起来。根据高中组合数学的知识,我们知道对于 n个元素的全排列的个数为 n!。


比如 1, 2,3 生成全排列是




1
123
2
132
3
213
4
231
5
312
6
321
全排列是一种很好的枚举方法,很多场合下都能用到全排列算法。我们可以用 dfs 来实现全排列枚举。算法的思想是递归的枚举每个位置上放的元素,枚举到最后一个位置的时候,就得到了一个排列。




1
void permutation(int a[], int k, int n) {
2
    if (k == n) {
3
        //  对排列做相应的处理后返回
4
        return;
5
    }
6
    for (int i = k; i < n; ++i) {
7
        swap(a[k], a[i]);  // 交换a[k] 和 a[i],让 a[i] 成为排列中的第k个
8
        permutation(a, k + 1, n);
9
        swap(a[k], a[i]); // 这个分支搜索完成,还原刚才的交换。
10
    }
11
}
上面是全排列枚举的一般写法,生成了数组 a的全排列。
关于全排列,使用 C++ 的同学有一个福利,在 <algorithm> 库里面有一个生成全排列的next_permutation函数,可以直接调用。调用方法如下




1
#include <algorithm>
2
#include <stdio.h>
3
int main() {
4
    int a[] = {1, 2, 3};
5
    do {
6
        printf("%d %d %d\n", a[0], a[1], a[2]);
7
    } while (std::next_permutation(a, a + 3));
8
    return 0;
9
}
上面的程序对应的输出是




1
1 2 3
2
1 3 2
3
2 1 3
4
2 3 1
5
3 1 2
6
3 2 1
next_permutation 函数是生成当前排列的下一个排列,如果当前排列是按照字典序大小比较的最后一个排列,返回 false,否则返回 true。比如把上面程序中的初始 a 数组改成 {2, 1, 3},输出对应是




1
2 1 3
2
2 3 1
3
3 1 2
4
3 2 1
一般情况下竞赛里面全排列最多能枚举到有 10 个元素的排列,元素再多时间上就承受不了了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值