当前位置: 首页 > news >正文

【算法】dfs转dp的通用方式

说实话,dp一学就会,一做就懵的一个很重要的原因就是所谓递推不总是一件合乎思维的事。爬楼梯的题我们想象说dp[i]是爬后i级楼梯的种数,这样递推公式,数据结构,划分方法纷纷水到渠成,那像这样的题呢?

https://leetcode.cn/problems/student-attendance-record-ii/description/

可以用字符串表示一个学生的出勤记录,其中的每个字符用来标记当天的出勤情况(缺勤、迟到、到场)。记录中只含下面三种字符:
‘A’:Absent,缺勤
‘L’:Late,迟到
‘P’:Present,到场
如果学生能够 同时 满足下面两个条件,则可以获得出勤奖励:
按 总出勤 计,学生缺勤(‘A’)严格 少于两天。
学生 不会 存在 连续 3 天或 连续 3 天以上的迟到(‘L’)记录。
给你一个整数 n ,表示出勤记录的长度(次数)。请你返回记录长度为 n 时,可能获得出勤奖励的记录情况 数量 。答案可能很大,所以返回对 109 + 7 取余 的结果。

我们或许可以很快的识别到这个大问题中包含着重复的子问题,那么,这是个几维的动态规划?它的每一个维度代表什么?状态转移方程怎么写?

这时候就体现了dfs转dp的重要。正着想是符合人类的思维的,递归转递推才是正路,直接从递推开始,太困难了。

先看看这个dfs怎么写。假设我们现在已经有一段确定的序列,当然这段序列还没有到n长,那么自然有三种将它延长的方式,加’A’、加’L’和加’P’。对于A,如果序列中已经有一个A了,那么再加A就不合适了,是0,否则继续;对于L,如果序列的后两位刚好都是L,那就组成了连续三个L,是不行的,返回0,否则继续;对于P,没什么触发禁忌的事,直接都继续

因此可以提炼出两个需要在dfs中用到的变量,已有序列中A的个数,已有序列末尾有连续几个L。手动再加一个dfs进行到第几层的深度变量,用于让这个递归停止

const dfs = (Acnt, tailLcnt, deep) => {if (deep == n) return 1;const chooseA = Acnt === 1 ? 0 : dfs(Acnt + 1, 0, deep + 1);const chooseL = tailLcnt === 2 ? dfs(Acnt, tailLcnt + 1, deep + 1);const chooseP = dfs(Acnt, 0, deep + 1);return chooseA + chooseL + chooseP;
}dfs(0, 0, 0)

我总结的 dfs 转 dp 的方法有四个步骤:

  1. deep 变量反扣,作为 dp 的第一维度
  2. 其他变量保留,作为 dp 的其他维度
  3. dfs 返回表达式迁移为状态转移式
  4. dfs 的调用迁移为 dp 的某项

也就是说,本题的 dp 是三维 dp,dp[i][Acnt][tailLcnt],状态转移如下:

const dpfun = () => {// init 0 相关的省略for (let i = 1; i <= n; i++) {for (let Acnt = 0; Acnt < 2; Acnt++) {for (let tailLcnt = 0; tailLcnt < 3; tailLcnt++) {const chooseA = Acnt === 1 ? 0 : dp[i - 1][Acnt + 1][0];const chooseL = tailLcnt === 2 ? dp[i - 1][Acnt][tailLcnt + 1];const chooseP = dp[i - 1][Acnt][0];dp[i][Acnt][tailLcnt] = chooseA + chooseL + chooseP;}}}return dp[n][0][0];
}

deep 变量反扣很容易理解,正常来说 dfs 是一个问题规模逐渐缩小的一个过程,这个过程中 deep 从小到大,dp 要想反过来,当然要让规模逐渐增大,deep 就要从大到小,作为 dp 数组从最后一行转移到第一行就不大方便了,因此要将 deep 反扣,n -> 0 转为 0 -> n

dfs 的调用迁移为 dp 的某项,在原本的 dfs 代码中,返回的是 dfs(0, 0, 0),对应的 Acnt == 0, tailLcnt === 0, deep == 0,迁移后则是 i == n,Acnt == 0, tailLcnt === 0,即 dp[n][0][0]

这样问题就迎刃而解了

有些读者可能会去思考,那么这里的dp[i][Acnt][tailLcnt]是什么?dfs是从左往右的在左串的最后加字符,那么dp反过来理应是从右往左在右串的开头加字符,然而dp数组的状态转移却显然不是这个意思,仍然是在这个子串的后面加’A’‘L’‘P’,和dfs一个逻辑,这不是很怪异吗?

和dfs是一个逻辑就对了!我们本来dp的转移逻辑就是从dfs迁移来的(见步骤三)。dp的内涵不在于将dfs的思路彻底逆转,而在于在dfs的过程中不重复计算相同的部分,将算过的子问题的答案存起来作为数组,在计算的时候可以直接拿到数据而非递归进去进行计算。我们不必纠结对于每个状态,dp数组的那一项具体代表什么意思,因为经过了转换(尤其还有出于方便目的对于deep的反扣),这种对每一项的解释已经很难表达了,我们只需要抓住在这个过程中,算法的内核没有发生变化,初始的状态没有发生变化,要求解的状态也是同一个状态即可

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • Python 办公自动化 处理 Excel 数据 【1】推荐
  • 设计模式实战:即时通讯应用的设计与实现
  • 主成分分析SPSS步骤+Matlab程序
  • OLAP引擎之Druid
  • 洛谷 CF295D Greg and Caves
  • Java数组的应用场景
  • 音频剪辑软件哪个好用?五大音频剪辑软件分享
  • Chrome快捷键提高效率
  • Vue 3 + Pinia 实现网页刷新功能
  • 在js中判断对象是空对象的几种方法
  • MySQL库表的基本操作
  • uniapp 页面跳转传参:父页面监听子页面传过来的数据
  • linux下串口通信相关知识
  • 避免CSRF攻击的方案
  • 数据炼金术:用Python爬虫精炼信息
  • 自己简单写的 事件订阅机制
  • 「前端早读君006」移动开发必备:那些玩转H5的小技巧
  • 【跃迁之路】【735天】程序员高效学习方法论探索系列(实验阶段492-2019.2.25)...
  • Java Agent 学习笔记
  • Javascript弹出层-初探
  • js数组之filter
  • magento2项目上线注意事项
  • Next.js之基础概念(二)
  • Nodejs和JavaWeb协助开发
  • PAT A1092
  • 分布式熔断降级平台aegis
  • 看完九篇字体系列的文章,你还觉得我是在说字体?
  • 利用阿里云 OSS 搭建私有 Docker 仓库
  • 如何设计一个比特币钱包服务
  • 设计模式(12)迭代器模式(讲解+应用)
  • 阿里云服务器购买完整流程
  • ​什么是bug?bug的源头在哪里?
  • ​总结MySQL 的一些知识点:MySQL 选择数据库​
  • #git 撤消对文件的更改
  • (2.2w字)前端单元测试之Jest详解篇
  • (3)llvm ir转换过程
  • (AngularJS)Angular 控制器之间通信初探
  • (C#)if (this == null)?你在逗我,this 怎么可能为 null!用 IL 编译和反编译看穿一切
  • (cos^2 X)的定积分,求积分 ∫sin^2(x) dx
  • (Demo分享)利用原生JavaScript-随机数-实现做一个烟花案例
  • (Java入门)学生管理系统
  • (PWM呼吸灯)合泰开发板HT66F2390-----点灯大师
  • (大众金融)SQL server面试题(1)-总销售量最少的3个型号的车及其总销售量
  • (考研湖科大教书匠计算机网络)第一章概述-第五节1:计算机网络体系结构之分层思想和举例
  • (生成器)yield与(迭代器)generator
  • (转)chrome浏览器收藏夹(书签)的导出与导入
  • (转)创业家杂志:UCWEB天使第一步
  • .NET COER+CONSUL微服务项目在CENTOS环境下的部署实践
  • .net core webapi 大文件上传到wwwroot文件夹
  • .NET 反射的使用
  • .NET 服务 ServiceController
  • .NET 中 GetProcess 相关方法的性能
  • .pyc文件是什么?
  • //usr/lib/libgdal.so.20:对‘sqlite3_column_table_name’未定义的引用
  • /bin、/sbin、/usr/bin、/usr/sbin