[dfs] aw180. 排书(IDA*+dfs深入理解+思维+好题)

1. 题目来源

链接:180. 排书

相关链接:

2. 题目解析

IDA* 就是 基于迭代加深的 A * 算法

在 迭代加深 dfs 的基础上加上 A* 算法的剪枝就很就是 IDA*。即,在 dfs 过程中,针对每个节点都用估价函数估计它距离答案至少需要 dfs 多少层,如果当前层数加上估价层数大于了迭代加深设定的层数的话,很明显当前分支及以后的分支都是无法找到答案的,所以就可以直接剪枝了。

要求针对节点的估价距离一定要小于等于真实距离。

手写笔记,乱!
在这里插入图片描述


针对本题。

首先的首先,确定搜索顺序:枚举所有可选长度的连续区间,再枚举这些区间能放到的所有位置,就能枚举到所有方案。

估价函数设计:

  • 定义序列中每个数的后缀不匹配的情况。
  • 后缀匹配指的是当前序列位置上的数的后一个数刚好比当前位置的数大 1。
  • 终点序列是 123456… ,它们的后缀都是匹配的。
  • 针对当前节点状态,统计不匹配的后缀数量。一次的状态更新最好可以修复三个后缀数量,假设当前状态有 t 个后缀不匹配的位置,那么每次最多修复 3 个,至少需要修复 ⌈ t 3 ⌉ \lceil \frac t 3 \rceil 3t 次。即 dfs 至少需要再向下递归 ⌈ t 3 ⌉ \lceil \frac t 3 \rceil 3t 层才有可能搜到正确答案。
  • 可以以此配合迭代加深 depth 设置,来进行剪枝。

每个状态都是一组数,用数组存储。每次的状态更新、恢复现场都可以借鉴八数码问题的 backup 备份设计。

在此,同理可开一个 backup[N] 数组作为 局部变量 来存每个状态的当前层情况,dfs 向下递归时就直接在当前状态的数组中进行状态更新。状态需要回溯时,就用 backup 中备份的状态进行回溯即可。

但是显然这样做,每个节点都需要开一个局部变量数组来进行状态备份…空间复杂度就是 O ( n ) O(n) O(n) 的了。

我们需要理解

  • dfs 过程中栈内不会出现同层的节点。一条 dfs 路径一定是随着深度递增的。
  • 在回溯时,只需要当前路径中上面各层节点的状态,跟已经遍历过的节点、没遍历的节点、根本扯不上关系的节点都无关。
  • 所以,我们只需要开一个全局数组w[5][N] 用来存 dfs 每一层节点的状态,针对每条路径,只会存每层的一个节点的状态。
  • w[5][N] 数组中的状态就是反复覆盖使用,但能保证正确的回溯时的恢复现场。
  • 这样就将空间复杂度从 O ( n ) O(n) O(n) 优化到 O ( l o g n ) O(logn) O(logn) 了。 是一个非常棒的优化!
  • 但实际上,这两者在时间复杂度上是一样的。

具体看手写笔记吧,写的很详细:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


时间复杂度: O ( 56 0 4 ) O(560^4) O(5604),实际上使用 IDA* 后状态很少

空间复杂度: O ( 5 n ) O(5n) O(5n)


全局变量:

#include <iostream>
#include <cstdio>
#include <cstring> 
#include <algorithm>

using namespace std;

const int N = 15;

int n;
int p[N];           // 临时存当前层的状态
int w[5][N];        // 备份,记录当前状态,用于回溯时的状态恢复,直接拷贝恢复为之前原状态即可

// 统计后缀不匹配的个数,返回距离有序的最小操作步数,有序时返回 0
int f() {
    int res = 0;
    for (int i = 1; i < n; i ++ )
        if (p[i] != p[i - 1] + 1)
            res ++ ;
    
    return (res + 2) / 3;                   // 上取整,得到当前状态距离最终状态的最小距离
}

bool dfs(int u, int depth) {
    if (u + f() > depth) return false;      // 启发式剪枝
    if (!f()) return true;                  // 当估价函数返回 0,等价于排好序了

    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 ++ ) {         // 枚举区间防止位置,放到 k 的后面
                memcpy(w[u], p, sizeof p);              // 备份当前状态
                
                // 状态更新,需要将 len 这段放到 k 的后面
                // [r+1, k] 需要放到 [l, r] 的前面,类比归并排序
                int y = l;
                for (int x = r + 1; x <= k; x ++ , y ++ ) p[y] = w[u][x];
                for (int x = l; x <= r; x ++ , y ++ ) p[y] = w[u][x];

                if (dfs(u + 1, depth)) return true;
                
                /*
                 * 打印当前层的正确状态,可以看到是怎么进行 5 步内更改的 
                 * 递归,是按状态倒序打印的,且最终有序时,会在 if (!f()) return true; 直接 return ; 
                 * 不会打印有序的状态
                if (dfs(u + 1, depth)) {
                    for (int i = 0; i < n; i ++ ) cout << w[u][i] << ' ';
                        cout << endl;
                    return true;
                }
                */

                memcpy(p, w[u], sizeof p);          // 恢复现场
            }
        }

    return false;
}

int main() {
    int T;
    scanf("%d", &T);

    while (T -- ) {
        scanf("%d", &n);
        for (int i = 0; i < n; i ++ ) scanf("%d", &p[i]);   // 初始状态

        int depth = 0;
        while (depth < 5 && !dfs(0, depth)) depth ++ ;      // 层数小于 5 且没搜到答案,则迭代加深继续搜

        if (depth >= 5) puts("5 or more");
        else printf("%d\n", depth);         
    }

    return 0; 
}

局部变量:

#include <iostream>
#include <cstdio>
#include <cstring> 
#include <algorithm>

using namespace std;

const int N = 15;

int n;
int p[N];           // 临时存当前层的状态

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

bool dfs(int u, int depth) {
    if (u + f() > depth) return false;    
    if (!f()) return true;                 

    int w[N];		// 针对每个节点,都开一个空间,用来存储原状态,用于回溯
    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, p, sizeof p);             
                int y = l;
                for (int x = r + 1; x <= k; x ++ , y ++ ) p[y] = w[x];
                for (int x = l; x <= r; x ++ , y ++ ) p[y] = w[x];
                if (dfs(u + 1, depth)) return true;
                memcpy(p, w, sizeof p);      
            }
        }

    return false;
}

int main() {
    int T;
    scanf("%d", &T);

    while (T -- ) {
        scanf("%d", &n);
        for (int i = 0; i < n; i ++ ) scanf("%d", &p[i]);   

        int depth = 0;
        while (depth < 5 && !dfs(0, depth)) depth ++ ;     

        if (depth >= 5) puts("5 or more");
        else printf("%d\n", depth);         
    }

    return 0; 
}
内容: 这个合成医疗保健数据集的创建是为了作为数据科学、机器学习和数据分析爱好者的宝贵资源。 灵感: 医疗保健数据通常很敏感,并受隐私法规的约束,因此难以访问以进行学习和实验。为了解决这一差距,我利用 Python 的 Faker 库生成了一个数据集,该数据集反映了医疗保健记录中常见的结构和属性。通过提供这些合成数据,我希望促进医疗保健分析领域的创新、学习和知识共享。 表格信息: 每列都提供有关患者、其入院情况和提供的医疗保健服务的特定信息,使此数据集适用于医疗保健领域的各种数据分析和建模任务。以下是数据集中每一列的简要说明 - 名字:此列表示与医疗保健记录关联的患者的姓名。 年龄:患者入院时的年龄,以年表示。 性:指示患者的性别,“男性”或“女性”。 血型:患者的血型,可以是常见的血型之一(例如,“A+”、“O-”等)。 医疗状况:此列指定了与患者相关的主要医疗状况或诊断,例如“糖尿病”、“高血压”、“哮喘”等。 入学日期:患者入住医疗机构的日期。 医生:在患者入院期间负责护理的医生的姓名。 医院:标识患者收治的医疗机构或医院。 保险提供商:此列指示患者的保险提供商,可以是多个选项之一,包括“Aetna”、“Blue Cross”、“Cigna”、“UnitedHealthcare”和“Medicare”。 账单金额:患者在入院期间为他们的医疗保健服务开具的账单金额。这表示为浮点数。 房间号:患者入院期间入住的房间号。 入场类型:指定入院类型,可以是“紧急”、“选择性”或“紧急”,以反映入院的情况。 出院日期:患者从医疗机构出院的日期,基于入院日期和实际范围内的随机天数。 药物:确定患者在入院期间开具或服用的药物。例子包括“阿司匹林”、“布洛芬”、“青霉素”、“扑热息痛”和“立普妥”。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ypuyu

如果帮助到你,可以请作者喝水~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值