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

说实话,dp一学就会,一做就懵的一个很重要的原因就是所谓递推不总是一件合乎思维的事。爬楼梯的题我们想象说dpi是爬后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进行到第几层的深度变量,用于让这个递归停止

js 复制代码
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,dpiAcnttailLcnt,状态转移如下:

js 复制代码
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,即 dpn00

这样问题就迎刃而解了

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

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

相关推荐
Forever Nore14 小时前
LeetCode 4 寻找两个正序数组的中位数 - 二分
算法·leetcode
罗西的思考16 小时前
【OpenClaw具身硬件】MiniClaw 阅读笔记---(1)基础
人工智能·算法·机器学习
蛋先生DX17 小时前
大模型参数存储格式揭秘:BF不是男朋友
深度学习·算法·llm
爱跳舞的烤冷面17 小时前
自学嵌入式第22天(数据结构——哈希)
数据结构·算法·哈希算法
猎嘤一号18 小时前
博弈论(Game Theory)的理论、算法与工程
人工智能·算法·安全·博弈论
月光船幽幽18 小时前
影子模式下保护 logits 不被修改
人工智能·python·算法
马拉AI18 小时前
腾讯开源 Agent 记忆系统,AI“换对话就忘”的问题有了新解法(附安装使用教程)
人工智能·算法·开源·科研
地平线开发者19 小时前
征程6工具链模型X86推理方式说明
算法
地平线开发者20 小时前
【征程6】校准量化中HistogramObserver解析
算法
OuO-220 小时前
笔试强训 Day 34:ISBN 号码、kotori 和迷宫、矩阵最长递增路径
java·算法·矩阵