算法竞赛进阶指南 搜索 0x24 迭代加深

迭代加深

深度优先搜索每次选定一个分支,不断深入,直至到达递归边界才回溯。这种策略大有一定的缺点。试想以下情况:搜索树每个节点的分支数目非常多,并且问题的答案在某个较浅的节点上。如果深搜在一开始选错了分支,就很可能在不包含答案的深层子树上浪费许多时间。

此时,我们可以从小到大限制搜索的深度,如果在当前深度限制下搜不到答案,就把深度限制增加,重新进行一次搜索,这就是 迭代加深 思想。所谓“迭代”,就是以上一次的结果为基础,重复执行以逼近答案的意思。

虽然该过程在深度限制为d时,会重复搜索第1~d - 1层的节点,但是当搜索树节点分支数目较多时,随着层数的深入,每层节点数会呈指数级增长,这点重复搜索与深层子树的规模相比,实在是小巫见大巫了。

总而言之,当搜索树规模随着层次的深入增长很快,并且我们能够确保答案在一个较浅的节点时,就可以采用迭代加深的深度优先搜索算法来解决问题中。读者可以进行大致的估算,有些题目描述甚至会包含“如果10步以内搜不到结果就算无解”的字样。

1、AcWing 170. 加成序列

题意 :

  • 满足如下条件的序列 X(序列中元素被标号为 1、2、3…m)被称为“加成序列”:
    X[1]=1
    X[m]=n
    X[1]<X[2]<…<X[m−1]<X[m]
    对于每个 k(2≤k≤m)都存在两个整数 i 和 j (1≤i,j≤k−1,i 和 j 可相等),使得 X[k]=X[i]+X[j]。
  • 你的任务是:给定一个整数 n,找出符合上述条件的长度 m 最小的“加成序列”。
  • 如果有多个满足要求的答案,只需要找出任意一个可行解。
  • 1≤n≤100

思路 :

  • 搜索框架:依次搜索序列中的每个位置k,枚举i和j作为分支,把x[i]和x[j]的和填到x[k]上,然后依次递归填写下一个位置。
  • 加上以下剪枝:
    1、优化搜索顺序
    为了让序列中的数尽快逼近n,在枚举i和j时从大到小枚举
    2、排除等效冗余
    对于不同的i和j,x[i] + x[j]可能是相等的。我们可以在枚举时用一个bool数组对x[i] + x[j]进行判重,避免重复搜索某一个和。
  • 经过观察分析可以发现,m的值(序列长度)不会太大( ≤ 10 \leq 10 10)(log100差不多7),而每次枚举两个数的和,分支很多。所以我们采用迭代加深的搜索方式,从1开始限制搜索深度,若搜索失败就增加深度限制重新搜索,直到找到一组解时即可输出答案。
#include <iostream>
using namespace std;
const int N = 10, M = 210;

int n;
int path[N];

bool dfs(int u, int k) {
    if (u == k) return path[u - 1] == n;
    
    bool st[M] = {0};
    for (int i = u - 1; i >= 0; -- i) {
        for (int j = i; j >= 0; -- j) {
            int s = path[i] + path[j];
            if (st[s] || s > n || s <= path[u - 1]) continue;
            st[s] = true;
            path[u] = s;
            if (dfs(u + 1, k)) return true;
        }
    }
    return false;
}

int main() {
    path[0] = 1;
    while (cin >> n, n) {
        int k = 1;
        while (!dfs(1, k)) ++ k;
        for (int i = 0; i < k; ++ i) cout << path[i] << ' ';
        cout << endl;
    }
}

双向搜索

除了迭代加深之外,双向搜索也可以避免在深层子树上浪费时间。在一些题目中,问题不但具有“初态”,还具有明确的“终态”,并且从初态开始搜索与从终态开始逆向搜索产生的搜索树都能覆盖整个状态空间,在这种情况下,就可以采用 双向搜索——从初态和终态出发各搜索一半状态,产生两颗深度减半的搜索树,在中间交汇,组合成最终的答案。

如下图所示,左侧是直接进行一次搜索产生的搜索树,右侧是双向搜索的两颗搜索树,避免了层数过深时分支数量的大规模增长。
在这里插入图片描述

1、AcWing 171. 送礼物

题意 :

  • 翰翰一共准备了 N 个礼物,其中第 i 个礼物的重量是 G[i]。
  • 达达的力气很大,他一次可以搬动重量之和不超过 W 的任意多个物品。
  • 达达希望一次搬掉尽量重的一些物品,请你告诉达达在他的力气范围内一次性能搬动的最大重量是多少。
  • 1≤N≤46,
  • 1≤W,G[i]≤2^31−1

思路 :

  • 这道题目就是“子集和”问题的扩展——从给定的N个数中选择几个,使它们的和最接近W。了解“背包”问题的读者可能已经发现,这道题目也是一个“大体积”的背包问题。这类问题的直接解法就是进行“指数型”枚举——搜索每个礼物选还是不选,时间复杂度为O(2^N)。当然,若搜索过程中已选的礼物重量之和已经大于W,可以及时剪枝。
  • 此题 N ≤ 45 N \leq 45 N45, 2 45 2^{45} 245的复杂度过高,这时我们就可以利用双向搜索的思想,把礼物分成两半。
  • 首先,我们搜索出从前一半礼物中选出若干个,可能达到的0~W之间的所有重量值,存放在一个数组A中,并对数组A进行排序、去重。
  • 然后,我们进行第二次搜索,尝试从后一半礼物中选出一些。对于每个可能达到的重量值t,在第一部分得到的数组A中二分查找 ≤ W − t \leq W-t Wt的数值中最大的一个,用二者的和更新答案,
  • 这个算法的时间复杂度只有 O ( 2 N / 2 l o g 2 N / 2 ) = O ( N ∗ 2 N / 2 ) O(2^{N/2}log2^{N/2})=O(N*2^{N/2}) O(2N/2log2N/2)=O(N2N/2)了。我们还可以加入一些优化:
    1、优化搜索顺序
    把礼物按照重量降序排序后再分半、搜索
    2、选取适当的“折半划分点”
    因为第二次搜索需要再第一次搜索得到的数组中进行二分查找,效率相对较低,所以我们应该稍微增加第一次搜索的礼物数,减少第二次搜索的礼物数。经过本地随机数据的实验,我们发现取第1~N/2+2个礼物为“前一半”,取第N/2+3~N个礼物为“后一半”时,搜索的速度最快。
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 47, M = 1 << 24;

int n, m;
int g[N];
int weights[M], cnt;
int k, mx;

void dfs(int u, int s) {
    if (u == k) {
        weights[cnt ++ ] = s;
        return ;
    }
    if ((ll)s + g[u] <= m) dfs(u + 1, s + g[u]);
    dfs(u + 1, s);
}
void dfs2(int u, int s) {
    if (u == n) {
        int l = 0, r = cnt - 1;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if ((ll)weights[mid] + s <= m) l = mid;
            else r = mid - 1;
        }
        if ((ll)weights[l] + s <= m) mx = max(mx, weights[l] + s);
        return ;
    }
    if ((ll)s + g[u] <= m) dfs2(u + 1, s + g[u]);
    dfs2(u + 1, s);
}

int main() {
    cin >> m >> n;
    for (int i = 0; i < n; ++ i) cin >> g[i];
    sort(g, g + n);
    reverse(g, g + n);
    
    k = n / 2;
    dfs(0, 0);
    
    sort(weights, weights + cnt);
    int t = 1;
    for (int i = 1; i < cnt; ++ i) {
        if (weights[i] != weights[i - 1]) {
            weights[t ++ ] = weights[i];
        }
    }
    cnt = t;
    
    dfs2(k, 0);
    cout << mx;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值