文章目录
前言
今天要讲的是一种方法论,即思维链刷题,你可以通过刻意练习,通过一步一步的暗示,来达到解题的目标。
正如大模型能够在COT(思维链)的加持下能够一步一步的得到最终的答案,我们在解决LeetCode的编程题时又何尝不是按照这种方式呢?不同的是,我们并没有人给我这种思维链般的“提示”,来指引我们一步一步的通过思考下一步应该做什么,来得到最终的代码。我们只能依靠大脑,不断地提示自己,遵照某一个思维链来解题。
那么关键来了,有些人在解题的时候,没有思路,或者思路混乱,这都是思维链没有成型导致的。因此只要学会某类提醒的思维链我们也能像大模型那样通过自我提示,一步一步的得到最终的代码。
那么怎么能够习得这些思维链呢?通过分析参考答案,你要做的不是记住参考答案,记住每个函数名称或者逻辑递归过程,那样做跟背书没俩样。你要做的是,抽区出导向问题答案的思维链,这就是常说的解题思路,掌握解题思路是比背住参考答案强上许多的。
后续,我会在不同类型的题目上,抽象出一些思维链出来,希望对大家在刷题或者理解参考答案时起到一点帮助。
233. 数字 1 的个数
题目描述
给定一个整数 n
,计算所有小于等于 n
的非负整数中数字 1
出现的个数。
示例 1:
输入:n = 13 输出:6
示例 2:
输入:n = 0 输出:0
提示:
0 <= n <= 109
思维链
- 首先,理解题目要求我们计算所有小于等于n的非负整数中数字1出现的个数。这是一个数位动态规划(数位DP)问题。
- 思考数位DP的基本思路:通常是将数字拆分为单独的位,然后从高位到低位或从低位到高位逐位考虑。
- 考虑如何定义状态。在这个问题中,我们可以定义状态为当前处理到的位置、当前已经出现1的个数以及是否受到了前面位数的限制。
- 确定状态转移方程。对于每一位,我们需要考虑填入0-9中的每一个数字时,对1的个数的影响。
- 考虑如何处理最高位的限制。当我们在处理某一位时,如果这一位是受到前面位数的限制的,那么这一位能够取的最大值就是原数字这一位的值。
- 实现记忆化搜索。由于数位DP的子问题很多是重复的,我们可以使用记忆化来避免重复计算。
- 思考如何初始化和调用记忆化搜索函数。我们需要从最高位开始,逐位向下搜索,并记录答案。
- 最后,返回从最高位开始的搜索结果作为答案。
知识点
- 数位动态规划(数位DP)
- 记忆化搜索
- 动态规划状态转移
技巧提示
- 理解数位DP的原理和实现方式。
- 学会使用记忆化来优化搜索过程。
- 注意处理数位上的限制条件。
参考答案
class Solution:
def countDigitOne(self, n: int) -> int:
@cache
def dfs(pos, cnt, limit):
if pos <= 0:
return cnt
up = a[pos] if limit else 9
ans = 0
for i in range(up + 1):
ans += dfs(pos - 1, cnt + (i == 1), limit and i == up)
return ans
a = [0] * 12
l = 1
while n:
a[l] = n % 10
n //= 10
l += 1
return dfs(l, 0, True)
600. 不含连续 1 的非负整数
题目描述
给定一个正整数 n
,请你统计在 [0, n]
范围的非负整数中,有多少个整数的二进制表示中不存在 连续的 1 。
示例 1:
输入: n = 5 输出: 5 解释: 下面列出范围在 [0, 5] 的非负整数与其对应的二进制表示: 0 : 0 1 : 1 2 : 10 3 : 11 4 : 100 5 : 101 其中,只有整数 3 违反规则(有两个连续的 1 ),其他 5 个满足规则。
示例 2:
输入: n = 1 输出: 2
示例 3:
输入: n = 2 输出: 3
提示:
1 <= n <= 109
思维链
- 首先,理解题目要求我们统计在给定范围内,二进制表示中不存在连续1的整数数量。这是一个典型的数位动态规划(DP)问题。
- 思考如何将整数转换为二进制表示,并且如何处理二进制位上的限制条件。这里可以使用位运算来实现。
- 考虑设计一个递归函数,用于深度优先搜索(DFS),并且需要记忆化以避免重复计算。这个函数将负责计算在当前限制条件下,有多少种合法的数字。
- 确定递归函数的参数。通常需要包括当前处理的位置(pos)、前一位的数字(pre)以及是否受到上限(limit)的约束。
- 确定递归的终止条件。当处理到最后一位时,应该返回1,因为此时已经构成了一个合法的数字。
- 确定递归的过程。对于每一位,我们可以选择填0或1,但需要根据前一位的数字和是否受到上限的约束来决定是否可以填1。
- 考虑如何处理连续1的限制。如果前一位是1,那么当前位不能填1,否则会违反题目的要求。
- 考虑如何处理上限的约束。如果当前位是受到上限约束的,那么只能填写与上限相同的数字或更小的数字。
- 实现记忆化。可以使用装饰器来缓存递归函数的结果,避免重复计算。
- 最后,从最高位开始调用递归函数,并返回结果作为答案。
知识点
- 数位动态规划
- 位运算
- 深度优先搜索
- 记忆化搜索
技巧提示
- 理解数位DP的基本概念和处理方法。
- 掌握位运算的基本操作,如位与(&)和位移(>>)。
- 学会使用记忆化来优化递归函数的性能。
- 注意处理边界条件和特殊情况,如最高位的处理和连续1的限制。
参考答案
class Solution:
def findIntegers(self, n: int) -> int:
@cache
def dfs(pos, pre, limit):
if pos <= 0:
return 1
up = a[pos] if limit else 1
ans = 0
for i in range(up + 1):
if pre == 1 and i == 1:
continue
ans += dfs(pos - 1, i, limit and i == up)
return ans
a = [0] * 33
l = 0
while n:
l += 1
a[l] = n & 1
n >>= 1
return dfs(l, 0, True)
其他的贴心妙招
最后,如果你经常使用Anki作为辅助记忆的软件的话,你可以将问题和思维链做成卡片,经常复习,达到训练的目的,久而久之,遇到类似问题,就能够有些思路了。
类似这样