问题描述
小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
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] }
代码呈现:
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) 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)
代码解释
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)
解题思路
-
问题理解:
- 骑士在拨号器上跳跃,每次跳跃形成一个 "L" 形。
- 需要计算骑士可以拨出的所有长度为
n的不同电话号码的数量。
-
数据结构选择:
- 使用字典
jumps来存储每个数字可以跳跃到的其他数字。 - 使用二维数组
dp来记录每个数字在每个跳跃次数下的可能路径数。
- 使用字典
-
算法步骤:
- 初始化:对于第 0 次跳跃,每个数字都可以作为起点,因此
dp[0][i] = 1。 - 状态转移:
- 对于每个跳跃次数
i和每个数字j,如果dp[i][j] > 0,则遍历j可以跳跃到的所有数字next_digit。 - 更新
dp[i + 1][next_digit],表示从j跳到next_digit的路径数。
- 对于每个跳跃次数
- 最终结果:计算
dp[n - 1]中所有数字的路径数之和,并对10^9 + 7取模。
- 初始化:对于第 0 次跳跃,每个数字都可以作为起点,因此
知识运用
-
动态规划 (Dynamic Programming):
- 使用
dp数组来存储中间结果,避免重复计算。 - 通过状态转移方程
dp[i + 1][next_digit] = (dp[i + 1][next_digit] + dp[i][j]) % MOD来更新路径数。
- 使用
-
字典 (Dictionary):
- 使用字典
jumps来存储每个数字可以跳跃到的其他数字,便于快速查找。
- 使用字典
-
列表 (List):
- 使用二维列表
dp来存储每个数字在每个跳跃次数下的可能路径数。
- 使用二维列表
-
取模运算:
- 在每次更新路径数时,使用
% MOD来防止数值溢出,并确保结果在合理范围内。
- 在每次更新路径数时,使用
-
循环和条件判断:
- 使用嵌套循环来遍历所有可能的状态,并使用条件判断来处理特殊情况(如数字 5 不能跳到任何其他数字)。 学习此代码后,我们可以总结出以下几个关键知识点和编程技巧:
知识点总结
动态规划(Dynamic Programming, DP):
动态规划是一种解决复杂问题的方法,它将问题分解为更简单的子问题,并存储这些子问题的解以避免重复计算。 在本例中,dp数组用于存储骑士经过不同次数跳跃后到达每个数字的路径数。
模运算(Modular Arithmetic):
当处理可能产生大数的运算时,模运算用于保持结果在可接受的范围内。 在此代码中,MOD = 10**9 + 7用于对结果进行模运算,以防止整数溢出。
字典(Dictionary)用于表示映射关系:
字典是Python中的一种数据结构,用于存储键值对。 在此代码中,字典jumps用于表示骑士可以从一个数字跳跃到哪些其他数字。
二维数组(Two-Dimensional Array):
二维数组是数组的数组,用于存储二维数据。 在此代码中,dp是一个二维数组,其中dp[i][j]表示骑士经过i次跳跃后到达数字j的路径数。
嵌套循环(Nested Loops):
嵌套循环是在一个循环内部再嵌套一个或多个循环。 在此代码中,嵌套循环用于遍历每次跳跃和每个可能的当前数字,以及更新下一次跳跃后的状态。
条件语句(Conditional Statements):
条件语句用于根据条件执行不同的代码块。 在此代码中,if dp[i][j] > 0:用于检查当前状态是否有效,从而决定是否更新下一次跳跃后的状态。
函数定义和调用(Function Definition and Call):
函数是组织代码的一种方式,它将代码封装为一个可重用的单元。 在此代码中,solution函数定义了计算骑士跳跃路径数的逻辑,并在主程序中调用以测试不同的输入值。
主程序(Main Program):
主程序是程序的入口点,它执行程序的主要逻辑。 在此代码中,主程序包含对solution函数的调用和结果的打印输出。
编程技巧总结
代码注释:
注释用于解释代码的功能和逻辑,使代码更易于理解和维护。 在此代码中,注释用于解释变量、字典和循环的作用。
变量命名:
变量名应具有描述性,以反映其存储的数据或执行的功能。 在此代码中,变量名如n、MOD、jumps、dp、i、j、next_digit和result都清晰地表达了它们的用途。
代码结构:
代码应具有良好的结构,以便易于阅读和理解。 在此代码中,代码被组织为函数定义、字典初始化、DP数组初始化、状态转移和结果计算等清晰的块。
测试:
编写测试用例是验证代码正确性的重要步骤。 在此代码中,主程序包含对solution函数的调用和与预期结果的比较,以测试代码的正确性。
通过学习此代码,可以加深对动态规划、模运算、字典和二维数组等知识点的理解,并掌握一些有用的编程技巧。这些知识点和技巧对于解决类似的问题和编写高效的代码非常有帮助。通过这些知识点和思路,代码能够有效地计算出骑士可以拨出的所有长度为 n 的不同电话号码的数量。