算法竞赛进阶指南0x22 深度优先搜索

        深度优先搜索 (DFS,Depth First Search),顾名思义,就是按照深度优先的顺序对"问题状态空间"进行搜索的算法。在第 0x00 章中,我们多次把一个问题的求解看作对问题状态空间的遍历与映射。从本章起,我们可以进一步把"问题空间"类比为一张图,其中的状态类比为节点,状态之间的联系与可达性就用图中的边来表示,那么使用深度优先搜索算法求解问题,就相当于在一张图上进行深度优先遍历。

        读者可以发现,深度优先搜索与"递归"和"栈"密切相关。我们倾向于认为"递归"是与递推相对的一种单纯的遍历方法,除了搜索之外,还有很多算法都可以用递归实现。而"深搜"是一类包括遍历形式、状态记录与检索、剪枝优化等算法整体设计的统称。

        在研究深度优先搜索算法之前,我们先来定义该过程产生的"搜索树"结构。在对图进行深度优先遍历处于点 x 时,对于某一些边(x,y),y 是一个尚未访问过的节点,程序从 x 成功进入了更深层的对 y 的递归;对于另外的一些边(x,y),y 已经被访问过,从而程序继续考虑其他分支。我们称所有点 (问题空间中的状态) 与成功发生递归的边 (访问两个状态之间的移动) 构成的树为一颗"搜索树"。整个深搜算法就是基于该搜索树完成的,为了避免重复访问,我们对状态进行记录和检索;为了使程序更加高效,我们提前剪除搜索树上不可能是答案的子树和分支,不去进行遍历。

例题:小猫爬山 CH2201

        Freda和rainbow饲养了 N 只小猫,这天,小猫们要去爬山。经历了千辛万苦,小猫们终于爬上了山顶,但是疲倦的它们再也不想徒步走下山了(呜咕>_<)。
Freda和rainbow只好花钱让它们坐索道下山。索道上的缆车最大承重量为 W ,而 N 只小猫的重量分别是 C1、C2、、、Cn。当然,每辆缆车上的小猫的重量之和不能超过 W 。每租用一辆缆车,Freda和rainbow就要付1美元,所以他们想知道,最少需要付多少美元才能把这N 只小猫都运送下山?

        可以使用深度优先搜索算法解决本题。在搜索的过程中,我们可以尝试依次把每一只小猫分配到一辆已经租用的缆车上,或者新组一辆缆车安置这只小猫。于是,我们实时关心的状态有:已经运送的小猫有多少只,已经租用的缆车有多少辆,每辆缆车上当前搭载的小猫重量之和。

        编写函数 DFS(now,cnt) 处理第 now 只小猫的分配过程 (前 now - 1只已经装载),并且目前已经租用了 cnt 辆缆车。对于已经租用的这 cnt 辆缆车的当前搭载辆,我们使用一个全局数组 cab[ ] 来记录。

        now,cnt 和 cab 数组共同标识着问题状态空间所类比的 "图" 中的一个 "节点"。在这个 "节点" 上,我们至多有 cnt + 1个可能的分支:

        1.尝试把第 now 只小猫分配到已经租用的第 i (1 <= i <= cnt) 辆缆车上。如果第 i 辆缆车还装的下,我们就在 cab[ i ]中累加 C_now,然后递归 DFS(now + 1,cnt)。

        2.尝试新租一辆缆车来安置这只小猫,也就是令 cab[cnt + 1] = C_now,然后递归 DFS(now + 1,cnt + 1)。

        当 now = N + 1时,说明搜索到了递归边界,此时就可以用 cnt 更新答案。

        为了让搜索过程更加高效,我们可以加入一个很显然的优化:如果在搜索的任何时刻发现 cnt 已经大于等于已经搜索到的答案,那么当前分支就可以立即回溯了。另外,重量较大的小猫显然比重量较轻的小猫更难运送,我们还可以在整个搜索前把小猫按重量递减排序,优先搜索重量较大的小猫,减少搜索树 "分支" 的数量。

参考代码:

#include <bits/stdc++.h>
#define i64 long long

inline i64 read() {
    bool sym = false; i64 res = 0; char ch = getchar();
    while (!isdigit(ch)) sym |= (ch == '-'), ch = getchar();
    while (isdigit(ch)) res = (res << 3) + (res << 1) + (ch ^ 48), ch = getchar();
    return sym ? -res : res;
}

i64 C[20], cab[20], n, w;
int ans;

void DFS(int now, int cnt) {
    if (cnt >= ans) {
        return;
    }
    if (now == n + 1) {
        ans = std::min(ans, cnt);
        return;
    }
    for (int i = 1; i <= cnt; i++) {
        //分配到已租用缆车
        if (cab[i] + C[now] <= w) {
            //能装下
            cab[i] += C[now];
            DFS(now + 1, cnt);
            cab[i] -= C[now];
            //还原现场
        }
    }
    cab[cnt + 1] = C[now];
    DFS(now + 1, cnt + 1);
    cab[cnt + 1] = 0;
}

int main() {
    n = read(), w = read();
    for (int i = 1; i <= n; i++) {
        C[i] = read();
    }
    std::sort(C + 1, C + 1 + n);
    std::reverse(C + 1, C + 1 + n);
    ans = n;
    //最多用 n 辆缆车
    DFS(1, 0);
    printf("%d\n", ans);
    return 0;
}

POJ3074 - Sudoku

         数独问题的搜索框架非常简单, 我们关心的 "状态" 就是数独的每个位置上填了什么数。在每个状态下,我们找出一个还没有填的位置,检查有哪些合法的数字可以填。这些合法的数字就构成该状态向下继续递归的 "分支"。

        搜索边界分为两种:一是如果所有位置都被填满,就找到了一个解;二是发现某个位置没有能填的合法数字,说明当前分支搜索失败,应该回溯去尝试其他分支。

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值