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

我们在0x03节中使用递归实现的指数型、排列型和组合型枚举,其实就是深搜的三种最简单的形式。与之相关的 子集和问题、全排列问题、N皇后问题 等都是可以用深搜求解的经典NPC问题。

下节我们将进一步系统低讨论各类剪枝技巧。

1、AcWing 165. 小猫爬山

题意 :

  • 索道上的缆车最大承重量为 W,而 N 只小猫的重量分别是 C1、C2……CN。
  • 当然,每辆缆车上的小猫的重量之和不能超过 W。
  • 每租用一辆缆车,翰翰和达达就要付 1 美元,所以他们想知道,最少需要付多少美元才能把这 N 只小猫都运送下山?
  • 1≤N≤18,
  • 1≤Ci≤W≤108

思路 :

  • 可以使用深度优先搜索算法解决本题。在搜索的过程中,我们可以尝试依次把每一只小猫分配到一辆已经租用的缆车上,或者新租一辆缆车安置这只小猫。于是,我们实时关心的状态有:已经运送的小猫有多少只,已经租用的缆车有多少辆,每辆缆车上当前搭载的小猫重量之和。
  • 编写函数 d f s ( n o w , c n t ) dfs(now,cnt) dfs(now,cnt)处理第 now 只小猫的分载过程前now - 1只小猫已经装载),并且目前已经租用了 cnt 辆缆车。对于已经租用的这cnt辆缆车的当前搭载辆,我们使用一个全局数组 cab[] 来记录
  • now,cnt和cab数组共同标识着问题状态空间所类比的“图”中的一个”节点“。在这个节点“上,我们至多有 cnt + 1 个可能的分支
    1、尝试把第now只小猫分配到已经租用的第i ( 1 ≤ i ≤ c n t 1 \leq i \leq cnt 1icnt) 辆缆车上。如果第i辆缆车还装得下,我们就在 cab[i] 中累加 C n o w C_{now} Cnow,然后递归 d f s ( n o w + 1 , c n t ) dfs(now+1,cnt) dfs(now+1,cnt)
    2、尝试新租一辆缆车来安置这只小猫,也就是令cab[cnt + 1] = C_now,然后递归 d f s ( n o w + 1 , c n t + 1 ) dfs(now+1,cnt+1) dfs(now+1,cnt+1)
  • now = N + 1时,说明搜索到了递归边界,此时就可以用cnt更新答案
  • 为了让搜索过程更加高效,我们可以加入一个很显然的优化:如果在搜索的任何时刻发现cnt已经大于或等于已经搜到的答案,那么当前分支就可以立即回溯了。另外,重量较大的小猫显然比重量较轻的小猫更“难“运送,我们还可以在整个搜索前把小猫按照重量递减排序,优先搜索重量较大的小猫,减少搜索树“分支”的数量(搜索顺序很重要:大部分情况下,我们应该优先搜索分支数量比较少节点:因为质量大的小猫能够选择的缆车数量少)。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 20;

int n, m;
int c[N];
int mi = 18 * 1e8 + 1;
int cab[N];

void dfs(int now, int cur) {
    if (now == n + 1) {
        mi = min(mi, cur);
        return ;
    }
    if (cur >= mi) return ;
    for (int i = 1; i <= cur; ++ i) {
        if (cab[i] + c[now] <= m) {
            cab[i] += c[now];
            dfs(now + 1, cur);
            cab[i] -= c[now];
        }
    }
    cab[cur + 1] += c[now];
    dfs(now + 1, cur + 1);
    cab[cur + 1] -= c[now];
}

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; ++ i) scanf("%d", &c[i]);
    sort(c + 1, c + n + 1);
    reverse(c + 1, c + n + 1);
    dfs(1, 0);
    printf("%d", mi);
}

2、AcWing 166. 数独

题意 :

  • 数独 是一种传统益智游戏,你需要把一个 9×9 的数独补充完整,使得数独中每、每、每个 3×3 的九宫格内数字 1∼9 均恰好出现一次。
  • 请编写一个程序填写数独。
  • 每个测试用例占一行,包含 81 个字符,代表数独的 81 个格内数据(顺序总体由上到下,同行由左到右)。
  • 每个字符都是一个数字(1−9)或一个 .(表示尚未填充)。
  • 您可以假设输入中的每个谜题都只有一个解决方案

思路 :

  • 数独问题的搜索框架非常简单,我们关心的“状态”就是数独的每个位置填了什么数。在每个状态下,我们找出一个还没有填的位置,检查有哪些合法的数字可以填。这些合法的数字就构成该状态向下继续递归的“分支”
  • 搜索边界分为两种:1、如果所有位置都被填满,就找到了一个解;2、如果发现某个位置没有能填的合法数字,说明当前分支搜索失败,应该回溯去尝试其他分支
  • Hint:在任意一个状态下,我们只需要找出1个位置,考虑该位置上填什么数(如上页图搜索树的根节点只有2个分支),不需要枚举所有的位置和可填的数字向下递归(因为其他位置在更深的层次会被搜索到),以免重叠、混淆“层次”和“分支”,造成重复遍历若干棵覆盖同一状态空间的搜索树。
  • 然而,数独问题的“搜索树”规模仍然很大,在搜索算法中,应该采取与人类类似的策略:在每个状态下,从所有未填的位置里选择“能填的合法数字”最少的位置(也就是说优先搜索分支最少的节点),考虑该位置上填什么数,作为搜索的分支,而不是任意找出1个位置。
  • 在搜索程序中,影响时间效率的因素除了搜索树的规模(影响算法的时间复杂度),还有在每个状态上记录、检索、更新的开销(影响程序运行的“常数”时间)。我们可以使用 位运算 来代替数组执行“对数独各个位置所填数组的记录”以及“可填性的检查与统计”。这就是我们所说的程序“常数优化”。具体地说:
    1、对于 每行、每列、每个九宫格,分别用一个 9位二进制(全局整数变量)保存哪些数字还可以填(1表示还可以填)
    2、对于每个位置,把它所在行、列、九宫格的 3个二进制数 做 位与**&** 运算,就可以得到该位置能填哪些数(&后如果为1说明在三个二进制数中均为1,说明在三个中都可以被填),用lowbit运算就可以把能填的数字取出
    3、当一个位置填上某个数后,把该位置所在的行、列、九宫格记录的二进制数的对应位改为0,即可更新当前状态;回溯时改回1即可还原现场
  • 通过本节的内容,读者可能已发现,一般搜索解法的基本框架并不难,但如何减小搜索树的规模并快速遍历搜索树却是一门高深的学问。在接下来的两节中,我们就终点研究所搜的优化。我们还会探讨16*16的数独问题,进一步提高求解数独的效率。
#include <iostream>
using namespace std;
const int N = 9;

char str[100];
int map[1 << N]; // map[i]表示log_2{i},常与lowbit搭配
int ones[1 << N]; // ones[i]表示二进制表示下为i的1的个数
int row[N], col[N], cell[3][3];

inline int lowbit(int x) {
    return x & -x;
}
void init() {
    for (int i = 0; i < N; ++ i) row[i] = col[i] = (1 << N) - 1;
    for (int i = 0; i < 3; ++ i) {
        for (int j = 0; j < 3; ++ j) {
            cell[i][j] = (1 << N) - 1;
        }
    }
}
inline int get(int x, int y) {
    return row[x] & col[y] & cell[x / 3][y / 3];
}
bool dfs(int cnt) {
    if (!cnt) return true;
    
    int minv = 10;
    int x, y;
    for (int i = 0; i < N; ++ i) {
        for (int j = 0; j < N; ++ j) {
            if (str[i * 9 + j] == '.') {
                int t = ones[get(i, j)];
                if (minv > t) {
                    minv = t;
                    x = i, y = j;
                }
            }
        }
    }
    for (int i = get(x, y); i; i -= lowbit(i)) { // 枚举(x, y)所有可选方案
        int t = map[lowbit(i)]; // 二进制表示为i,
        
        // 修改状态
        row[x] -= 1 << t;
        col[y] -= 1 << t;
        cell[x / 3][y / 3] -= 1 << t;
        str[x * 9 + y] = t + '1';
        
        if (dfs(cnt - 1)) return true;
        
        // 恢复现场
        row[x] += 1 << t;
        col[y] += 1 << t;
        cell[x / 3][y / 3] += 1 << t;
        str[x * 9 + y] = '.';
    }
    return false;
}

int main() {
    for (int i = 0; i < N; ++ i) map[1 << i] = i;
    // 数二进制下1的个数
    for (int i = 0; i < 1 << N; ++ i) {
        int s = 0;
        for (int j = i; j; j -= lowbit(j)) ++ s;
        ones[i] = s;
    }
    
    while (scanf("%s", str), str[0] != 'e') {
        
        init();
        
        int cnt = 0; // 尚未填的数量
        for (int i = 0, k = 0; i < N; ++ i) {
            for (int j = 0; j < N; ++ j, ++ k) { // k = i * 9 + j,k为字符串中对应位置
                if (str[k] != '.') {
                    int t = str[k] - '1';
                    row[i] -= 1 << t;
                    col[j] -= 1 << t;
                    cell[i / 3][j / 3] -= 1 << t; // 由行、列得到处于第几个九宫格
                }
                else ++ cnt;
            }
        }
        
        dfs(cnt);
        
        printf("%s\n", str);
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值