2.2.3 DFS之迭代加深、双向DFS、IDA*

迭代加深

定义个层数上限max_depth,一层一层来搜索, 防止进到无底洞

在这里插入图片描述

迭代加深与bfs的区别

宽搜:会将当前层所有节点加入到队列,空间复杂度:指数级别
迭代加深: 只会沿着某条路径搜索,时间复杂度O(n)

迭代加深适合的问题

某些状态层数比较深,但是答案在比较浅的位置,可以保证程序进入到死胡同,提升效率。
后面还有IDA*算法配合迭代加深算法,这是bfs办不到的

效率问题

一层一层,会重复的搜索效率是否会变差。
比如刚开始搜1层, 第2次搜1,2层, 第k次搜1,2, …k层。中间会有重复的层数搜索。

答案:效率不会变差
10层2叉树
前9层总共节点数
2 0 + 2 1 + . . . + 2 9 = 2 10 − 1 2^0 + 2^1 + ... + 2^9 = 2^{10} -1 20+21+...+29=2101
所有节点连起来,最差情况也才等于第10层节点数,如果是多叉树,那前面的层数相比于下一层而言可以忽略不计。

在这里插入图片描述

AcWing 170. 加成序列

分析

可以大致的估算下n的序列范围
1, 2, 8 ,16, 32, 64, 128.
可以发现构造128,只需要长度为8
可以发现最大深度非常大, 1,2, 3, 4,… 99, 100.
但答案由于是最短序列,可能在比较浅的层数

剪枝

优化搜索顺序

依次考虑每个位置的数, 第1个填1, 第2个位置填2,依次往后填数

剪枝1:优先枚举较大的数

更快的到达n,层数较少

剪枝2:排除等效冗余

前面的数字可以用两次,比如前面有5个数, 那么总共有 C 5 2 + 5 C^2_5 + 5 C52+5种选法。
可能存在前两个数和相等情况. 1, 2, 3, 4. 1 + 4 = 2 + 3. 这样是一种等效冗余
可以递归中开个bool枚举每个数是否被枚举过,如果枚举过就跳过
在这里插入图片描述

代码

xnuohz:这题不需要恢复现场是不是因为可能有不同的path[i] + path[j]得到的值相同,只要其中一个搜不到答案,其他的组合也没必要搜了
yxc:对滴,这里和恢复现场无关,只是个剪枝,避免重复搜索。

#include <iostream>
using namespace std;
const int N = 110;
int path[N];

int n;

bool dfs(int u, int depth){
    if (u > depth) return false;
    
    if (path[u - 1] == n) return true;
    
    bool st[N] = {0};
    
    for (int i = u - 1; i >= 0; i -- )
        for (int j = i; j >= 0; j -- ){
            int s = path[i] + path[j];
            if (s > n || s <= path[u - 1] || st[s]) continue;
            st[s] = true; // st数组不是当前状态的一部分
            // st只是保证当前层搜索中不搜出重复的节点
            path[u] = s; // 因为同一层下一个状态u会被其他状态覆盖掉,因此
            // path[u]不需要恢复现场
            if (dfs(u + 1, depth)) return true;
        }
        
    return false;
}

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

双向DFS

在这里插入图片描述
可以剔除橙色部分区域,提高搜索效率

AcWing 171. 送礼物

分析

看起来像背包问题,背包问题的时间复杂度O(NV), 题目中 V ≤ 2 31 − 1 V \leq 2^{31 - 1} V2311, 显然会超时
n比较小46,可以爆搜
依次枚举每个物品选/不选。时间复杂度 O ( 2 46 ) O(2^{46}) O(246),也会超时.
选择双向dfs, O ( 2 23 ) O(2^{23}) O(223) 8 ∗ 1 0 6 8 * 10^6 8106不会超时

先将前面一半物品能够凑出来的重量搜索一遍(预处理),然后再搜后面一半物品,再搜后面一半物品的时候,可以查表预处理的结果
在这里插入图片描述

在这里插入图片描述

代码

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 46;

typedef long long LL;

int n, m, k;
int w[N];
int weights[1 << 25], cnt = 1; // 重量0也算,因此cnt中算了0, cnt = 1
int ans;

void dfs1(int u, int s){
    if (u == k){
        weights[cnt ++] = s;
        return ;
    }
    
    dfs1(u + 1, s); // 不选u
    if ((LL) s + w[u] <= m) dfs1(u + 1, s + w[u]);
}

void dfs2(int u, int s){
    if (u >= n){ // u >= n表示已经计算出后面所有数的总和的情况,包括选/不选
    // u >= n 而不是 u == n 是因为 当 n = 2的时候, k = n / 2 + 2,永远 > n,就会死递归。
        int l = 0, r = cnt - 1;
        while (l < r){
            int mid = l + r + 1 >> 1;
            if ((LL) s + weights[mid] <= m) l = mid;
            else r = mid - 1;
        }
        
        ans = max(ans, s + weights[l]);
        return ;
    }
    dfs2(u + 1, s);
    if ((LL) s + w[u] <= m) dfs2(u + 1, s + w[u]);
    
}

int main(){
    cin >> m >> n;
    for (int i = 0; i < n; i ++ ) cin >> w[i];
    
    sort(w, w + n);
    reverse(w, w + n);
    
    k = n / 2 + 2;
    dfs1(0, 0);
    
    sort(weights, weights + cnt);
    cnt = unique(weights, weights + cnt) - weights;
    
    dfs2(k, 0);
    
    cout << ans << endl;
    
    return 0;
}

IDA*

IDA* 一般配合迭代加深
搜索的时候一圈一圈的扩展,每次搜索会有max_depth上限
每次搜索的时候,会预估下最少需要多少步到达答案。
如果从
(当前点出发 + 预估步数(当前点到真实值的估价值) >= max_depth)
那么,递归可以提前退出
这里需要跟A*算法一样,需要保证估价函数 <= 真实值

AcWing 180. 排书

分析

在这里插入图片描述

可以枚举书的长度,当长度为i的时候
起点可以从1 ~ n - i + 1
因此长度为i的段有 n - i + 1种
会剩下 n - i 个数, 会有 n - i + 1个空档可以放,最前面那个空档是原来的位置,因此有n - i 个空档
长度为i的决策,总共有(n - i + 1) * (n - i) 种选择
再枚举一遍i, 可以计算总的选择方案

题目n 最大15
总的方案数 = 15 ∗ 14 + 14 ∗ 13 + 2 ∗ 1 15 * 14 + 14 * 13 + 2 * 1 1514+1413+21
注意: 将前面的书插入到后面跟后面的书插入到前面是同一种。因此还需要/ 2

1 ∗ 2 + 2 ∗ 3 + . . . + n ∗ ( n + 1 ) = n ∗ ( n + 1 ) ∗ ( n + 2 ) 3 1 * 2 + 2 * 3 + ... + n * (n + 1) = \frac{n * (n + 1) * (n + 2)}{3} 12+23+...+n(n+1)=3n(n+1)(n+2)
14 ∗ 15 ∗ 16 / 3 = . . . 14 * 15 * 16 / 3 = ... 141516/3=...
最后再/ 2 = 560

题目最多有4次选择, 56 0 4 560^4 5604必定超时,可以用双向宽搜/双向dfs可以过, 时间复杂度 56 0 2 560^2 5602
也可以IDA*做法

在这里插入图片描述

可以分析当前这个排列,最少需要多少步,可以变成排好序的。
考虑后继关系
1~ 2, 2 ~ 3, n - 1 ~ n
排好序n个数中,有n - 1个后继关系
对于图中截取的红色段移动到后面最多只会影响3个数的后继关系,可以统计下当前序列中有多少个后继关系是不正确的,记为tot.
那么最少需要 ⌈ t o t / 3 ⌉ = ⌊ t o t + 2 3 ⌋ \lceil tot / 3\rceil =\lfloor \frac{tot + 2}{3} \rfloor tot/3=3tot+2 注意:(左边上取整,右边是下取整,仔细看).

在这里插入图片描述

代码

#include <iostream>
#include <cstring>
using namespace std;

const int N = 15;
int w[5][N];
int q[N];
int n;

int f(){
    int tot = 0;
    for (int i = 0; i + 1 < n; i ++ )
        if (q[i + 1] != q[i] + 1) 
            tot ++;
            
    return (tot + 2) / 3;
}

bool dfs(int depth, int max_depth){
    if (depth + f() > max_depth) return false;
    if (f() == 0) return true;
    
    for (int len = 1; len <= n; len ++ )
        for (int l = 0; l + len - 1 < n; l ++ ){
            int r = l + len - 1;
            for (int k = r + 1; k < n; k ++ ){
                memcpy(w[depth], q, sizeof q);
                int x, y; // 位置移动见上图,先移动绿色r + 1 ~ k
                // 然后直接在绿色后面接原来数组的l ~ r那段
                for (x = r + 1, y = l; x <= k; x ++ , y ++ ) q[y] = w[depth][x];
                for (x = l; x <= r; x ++, y ++ ) q[y] = w[depth][x];
                if (dfs(depth + 1, max_depth)) return true;
                memcpy(q, w[depth], sizeof q);
            }
        }
    return false;
}

int main(){
    int T;
    cin >> T;
    while (T -- ){
        cin >> n;
        for (int i = 0; i < n; i ++ ) cin >> q[i];
        
        int depth = 0;
        while (depth < 5 && !dfs(0, depth)) depth ++;
        
        if (depth >= 5) puts("5 or more");
        else cout << depth << endl;
    }
    
    return 0;
}

AcWing 181. 回转游戏

分析

直接打表记录下 图片中每个数的位置。
由于每次操作会从外面移进来一个数,从里面移出一个数。
比如拉动题目中的“A”,只会往中心进来1个数。最少需要8 - (当前中心重复数最大的数量)可以使得中心全是同一个数
f() = 8 - (当前中心重复数最大的数量)
在这里插入图片描述

在这里插入图片描述

代码

#include <iostream>
#include <cstring>
using namespace std;
const int N = 24;

int op[8][7] = {
    {0, 2, 6, 11, 15, 20, 22},
    {1, 3, 8, 12, 17, 21, 23},
    {10, 9, 8, 7, 6, 5, 4},
    {19, 18, 17, 16, 15, 14, 13},
    {23, 21, 17, 12, 8, 3, 1},
    {22, 20, 15, 11, 6, 2, 0},
    {13, 14, 15, 16, 17, 18, 19},
    {4, 5, 6, 7, 8, 9, 10}
};
int opposite[8] = {5, 4, 7, 6, 1, 0, 3, 2};
int center[8] = {6, 7, 8, 11, 12, 15, 16, 17};

int q[N];
int path[100];

int f(){
    static int sum[4];
    
    memset(sum, 0, sizeof sum);
    
    for (int i = 0; i < 8; i ++ ) sum[q[center[i]]] ++;
    
    int maxv = 0;
    for (int i = 1; i <= 3; i ++ ) maxv = max(maxv, sum[i]);
    
    return 8 - maxv;
}

void operate(int x){
    int t = q[op[x][0]]; //找到q中op位置上第一个数
    for (int i = 0; i < 6; i ++ ) q[op[x][i]] = q[op[x][i + 1]];
    q[op[x][6]] = t;
    
}

bool dfs(int depth, int max_depth, int last){
    if (depth + f() > max_depth) return false;
    if (f() == 0) return true;
    
    for (int i = 0; i < 8; i ++ ){
        if (opposite[i] != last){
            operate(i);
            path[depth] = i;
            if (dfs(depth + 1, max_depth, i)) return true;
            operate(opposite[i]);
        }
    }
    return false;
}

int main(){
    while (cin >> q[0], q[0]){
        for (int i = 1; i < N; i ++ ) cin >> q[i];
        
        int depth = 0;
        while (!dfs(0, depth, -1)) depth ++;
        
        if (!depth) printf("No moves needed");
        else {
            for (int i = 0; i < depth; i ++ ) printf("%c", path[i] + 'A');
        }
        printf("\n%d\n", q[6]);
    }
    
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值