学习方法与心得:动态规划在算法中的应用(拨号器)
在学习算法时,动态规划(DP)是一个非常重要的工具,特别是面对状态转移问题时。在这道“骑士跳跃电话号码”问题中,我们利用动态规划来处理多种可能的跳跃路径,并通过逐步积累结果来求解最终的答案。这不仅是对算法思想的深入理解,也是对动态规划技巧的实际应用。
题目描述
小F正在使用一个骑士跳跃方式的拨号器。这个拨号器是一个类似电话键盘的 3x4 矩阵,每个数字对应一个单元格,骑士只能站在蓝色数字单元格上进行跳跃(数字 1 到 9 和 0)。骑士的移动方式和国际象棋中的马相同:它可以垂直移动两个单元格并水平移动一个单元格,或水平移动两个单元格并垂直移动一个单元格,形成 "L" 形。
123
456
789
*0#
给定一个整数 n,你需要帮助小F计算骑士可以拨出的所有长度为 n 的不同电话号码的数量。骑士可以从任何数字开始,并在 n-1 次有效跳跃后得到一个有效号码。答案可能非常大,因此你需要返回对 10^9 + 7 取模的结果。
测试样例
样例1:
输入:
n = 1输出:10
样例2:
输入:
n = 2输出:20
样例3:
输入:
n = 3输出:46
样例4:
输入:
n = 4输出:104
题目分析与思路
本题的核心是模拟骑士(马)在数字键盘上的跳跃路径,每个数字都可以通过骑士跳跃规则跳到其他数字。我们需要通过动态规划来记录长度为 ( n ) 的所有可能电话号码,最终求出结果。
-
跳跃规则映射:首先需要明确骑士从每个数字跳到哪些数字。我们可以利用字典或映射数组来表示这些跳跃规则。
-
动态规划表设计:我们使用一个二维数组
dp[i][j]来表示长度为 ( i+1 ) 的电话号码,以数字j结尾的数量。动态规划的状态转移非常直观,每次根据当前数字的跳跃规则更新下一步的电话号码数量。 -
最终结果:通过遍历所有可能的终点,得到长度为 ( n ) 的电话号码数量。
完整代码如下
def solution(n: int) -> int:
MOD = 10**9 + 7
# 定义骑士可以跳跃的规则
jumps = {
1: [6, 8],
2: [7, 9],
3: [4, 8],
4: [3, 9, 0],
5: [], # 5 不能跳到任何其他数字
6: [1, 7, 0],
7: [2, 6],
8: [1, 3],
9: [2, 4],
0: [4, 6]
}
# 初始化 dp 数组
dp = [[0] * 10 for _ in range(n)]
# 初始状态:第 0 次跳跃时,每个数字都可以作为起点
for i in range(10):
dp[0][i] = 1
# 状态转移
for i in range(n - 1):
for j in range(10):
if dp[i][j] > 0:
for next_digit in jumps[j]:
dp[i + 1][next_digit] = (dp[i + 1][next_digit] + dp[i][j]) % MOD
# 计算最终结果
result = sum(dp[n - 1]) % MOD
return result
if __name__ == '__main__':
print(solution(1) == 10)
print(solution(2) == 20)
print(solution(3) == 46)
print(solution(4) == 104)
代码详解
1. 跳跃规则映射
jumps = {
1: [6, 8],
2: [7, 9],
3: [4, 8],
4: [3, 9, 0],
5: [], # 5 不能跳到任何其他数字
6: [1, 7, 0],
7: [2, 6],
8: [1, 3],
9: [2, 4],
0: [4, 6]
}
这个字典定义了每个数字的跳跃目标。例如,数字 1 可以跳跃到 6 和 8,而数字 5 没有有效的跳跃路径。
2. 动态规划数组
dp = [[0] * 10 for _ in range(n)]
我们初始化一个二维数组 dp,dp[i][j] 表示长度为 ( i+1 ) 的电话号码以数字 j 结尾的数量。最初,所有的值为 0。
3. 初始化状态
for i in range(10):
dp[0][i] = 1
长度为 1 的电话号码,每个数字都可以作为独立的电话号码。因此,我们初始化第一行,所有数字的数量为 1。
4. 状态转移
for i in range(n - 1):
for j in range(10):
if dp[i][j] > 0:
for next_digit in jumps[j]:
dp[i + 1][next_digit] = (dp[i + 1][next_digit] + dp[i][j]) % MOD
这里的状态转移是核心。对于每个长度为 ( i+1 ) 的电话号码,以数字 j 结尾,我们通过跳跃规则将结果转移到长度为 ( i+2 ) 的电话号码中。每次转移时,我们都考虑当前 dp[i][j] 的数量,并加到 dp[i + 1][next_digit] 中。
5. 最终结果
result = sum(dp[n - 1]) % MOD
最终,我们通过累加 dp[n-1] 中所有数字的值,得到长度为 ( n ) 的电话号码数量。
- 动态规划的关键是明确如何定义状态和状态转移关系,并通过递推逐步求解问题。
- 本题展示了如何结合数字跳跃规则来处理状态转移,通过动态规划高效地计算电话号码的数量。
关于本题的时间复杂度与优化
-
时间复杂度为 ( O(30n) ),因为每个
dp[i][j]的状态转移最多涉及到 3 个跳跃目标(数字j的跳跃集合的最大长度)。因此,总体时间复杂度为 ( O(30n) ),可以认为是 ( O(n) ) 级别,非常高效。 -
空间复杂度是 ( O(10n) ),即 ( O(n) ),这对于大多数问题规模来说是非常可接受的。
知识总结与学习建议
在这道题目中,几个关键点和技巧是:
1. 动态规划(Dynamic Programming, DP)
- 定义:动态规划是一种通过将问题分解为子问题来求解的算法设计方法,适用于具有重叠子问题和最优子结构性质的问题。
- 核心思想:通过保存子问题的解(通常是存储在数组或表格中),避免重复计算,从而提高效率。
在此题中的应用:
- 使用
dp[i][j]表示长度为 ( i+1 ) 的电话号码以数字j结尾的数量。 - 基于跳跃规则,状态从前一个数字的状态转移到当前数字的状态。
- 通过逐步积累结果,最终得到所求的电话号码数量。
2. 状态转移与递推关系
- 状态定义:我们定义
dp[i][j]为长度为 ( i+1 ) 的电话号码以数字j结尾的数量。 - 转移方程:通过当前数字的跳跃规则更新到下一状态。例如,从数字
j跳跃到其所有可能的目标数字,将dp[i][j]的数量加到dp[i+1][next_digit]中。 - 初始化:初始化
dp[0][i] = 1,表示长度为 1 的电话号码可以直接由任何数字开始。
关键点:
- 边界条件:初始化时,长度为 1 的电话号码可以是任意数字(0-9),因此
dp[0][i] = 1。 - 递推关系:通过跳跃规则,将上一状态的结果累加到当前状态。
3. 跳跃规则
- 本题中,跳跃规则是固定的,每个数字可以跳跃到其他特定数字。比如:
- 数字 1 可以跳跃到数字 6 和 8,数字 2 可以跳跃到数字 7 和 9,等等。
- 这要求我们在动态规划转移时,考虑每个数字的跳跃目标,逐步扩展电话号码的可能性。
4. 时间和空间复杂度
- 时间复杂度:由于每个
dp[i][j]的状态转移最多涉及到 3 个跳跃目标(每个数字的跳跃目标数量最多为 3),因此总体时间复杂度是 ( O(30n) ),可以认为是 ( O(n) ),适合处理较大规模的问题。 - 空间复杂度:使用一个二维数组
dp存储所有长度为 ( n ) 的电话号码的中间结果。空间复杂度为 ( O(10n) ),即 ( O(n) ),其中 10 是数字的个数。
5. 模运算(Modulo Operation)
- 目的:由于电话号码数量可能非常大,需要对结果进行模运算,避免溢出。
- 操作:在每次更新
dp[i + 1][next_digit]时,使用模运算:dp[i + 1][next_digit] = (dp[i + 1][next_digit] + dp[i][j]) % MOD,确保计算结果保持在一个合理的范围内。
6. 动态规划的优化
- 空间优化:本题的状态转移只依赖于前一行,因此可以用两个一维数组代替二维数组,节省空间。
- 技巧:如果题目允许,可以尝试将原本的二维数组
dp进一步优化为滚动数组(rolling array)结构,减少空间复杂度。
高效学习方法
在刷题时,可以结合以下方法提高学习效率:
- 逐步练习:先从简单的 DP 问题入手,逐渐增加题目的难度。理解每个题目的核心思想,尤其是如何设计状态转移方程和递推公式。
- 总结归纳:每次解决一个 DP 问题后,尝试总结出适用的模板或常见的状态转移模式,帮助自己在类似题目中快速应用。
- 错题重练:对于做错的题目,花时间理解错因,并通过回顾题目分析加深理解。可以利用错题本或在线平台的错题集进行针对性复习。
工具运用
结合豆包MarsCode AI 刷题的功能,可以帮助我们:
- 生成多样化的题目:通过AI生成不同难度的动态规划题目,并逐步提高难度,帮助我们在掌握基础后更好地挑战复杂问题。
- 错误反馈与优化:AI能够自动检测我们的错误并给出反馈,帮助我们发现漏洞或不合理的设计,从而加速学习进程。
- 与其他资源结合:通过与在线教程、博客、书籍等资源结合,进一步拓展对动态规划的理解,不仅限于题目本身。
这道题目是通过动态规划解决路径问题的经典例子,它能够帮助我们提高解题思路和技巧,特别是对于复杂的状态转移问题。在学习过程中,我们可以针对此类题进行重点研究。