题目描述
思路解析
一句话答案:LeetCode 935 骑士拨号器:骑士在电话键盘走「日」字,用计数 DP 的 dp[step][d] 记末位是 d 的号码数,按键盘邻接表逐步累加取模,答案是最后一行求和,时间 O(n),空间 O(1)(滚动数组)。
骑士拨号器到底在数多少个号码
国际象棋的骑士(也就是「马」)站在电话键盘上,每步按「日」字跳——直走两格再垂直拐一格。键盘排着 0 到 9,左下右下角的 * 和 # 不能落。骑士从任意数字起步、共跳 n-1 步拼成长度 n 的号码,问能拨多少个不同号码,对 1e9+7 取模(保留余数防溢出)。
n=1 哪都不跳、各数字算一个是 10;n=2 是 20,n=3 是 46。要数的是号码个数。
把所有号码都跳一遍,为什么行不通
每步在当前数字的邻居里挑一个落脚,一路走到底就是一个号码,可每多跳一步分叉翻几倍,n 稍大路径数就指数爆炸、逐条枚举跑不完。好在只问个数、不看具体号码。既然子问题反复撞见,算一次存起来复用——这就是动态规划(DP,落在某数字、还剩几步的方案数与怎么跳来的无关,算一次存 dp[step][d] 复用)。
邻接表怎么来,dp[step][d] 又存什么
先搭地基:把每个数字「一步能跳到谁」列出来,就是邻接表(记录谁与谁相邻的表)。比如 1 只能到 6、8,4 能到 0、3、9,0 只能到 4、6;而中间的 5 一跳就出键盘、没有邻居,是孤岛。这张表还对称:1 能到 6,6 也跳回 1。
再定义 dp[step][d]:已按 step+1 个键、末位停在 d 的号码个数(跳了 step 步落在 d 的号码数)。step 从 0 数起,dp[0][d] 是长度 1。答案是最后一行各 d 的 dp 值相加,因为末位可任取。
落在 d 的方案,为什么把来源全加起来
填表就靠一条转移:要让末位停在 d,上一步必在能一步跳到 d 的数字上,把这些来源在上一行的方案数全加起来就是 dp[step][d],即能跳到 d 的所有 prev 的 dp[step-1][prev] 之和。各来源不重不漏,每加一次对 1e9+7 取模防溢出。
拿 n=3 把三行表亲手填出来
第 0 步是长度 1 的号码,骑士哪都不跳、停哪都算一个,dp[0] 十格全是 1,求和 10,正是 n=1 的答案。
跳第 1 步逐格看来源:末位 0 从 4、6 来是 2,末位 4 由 0、3、9 来是 3,末位 5 谁都跳不到是 0。整行 [2,2,2,2,3,0,3,2,2,2],求和 20,对上 n=2。
再跳第 2 步只改取上一行 step1 的值:末位 0 来自 4、6 是 3+3=6,末位 1 来自 6、8 是 5,末位 4 来自 0、3、9 是 6。整行 [6,5,4,5,6,0,6,5,4,5],求和 46,与示例对上。
n=1 还跳了一步,答案为什么凭空翻倍
最容易踩的坑是循环次数:dp 初始化全 1 时号码已有 1 个数字,故只需再跳 n-1 步。n=1 一步都不该跳、答案是 dp[0] 求和的 10;多跑 1 步就把 10 错算成 20,正好翻倍。取模也别漏,否则大 n 撑爆整型。
复杂度上,数字只有 10 个、邻居数固定,每步常数工作,跳 n-1 步共 O(n)(大 O 记号,描述规模变大时操作数怎么涨)。每行只依赖上一行,用两个长 10 的数组轮换(滚动数组,只留最近几行覆盖旧值),空间 O(1)。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
先看清这块电话键盘:这是电话键盘:数字 0~9 排成这样。左下角和右下角是 * 和 #(这里画成「·」),骑士不能落在上面。
骑士怎么走:象棋里马走「日」:往一个方向直走两格、再垂直拐一格。在键盘上,这决定了从某个数字一步能跳到哪几个数字——下面一个个看。
从 1 出发能跳到哪:骑士站在 1(橙色)。走「日」字,它只能跳到 6 和 8(橙黄高亮)。所以从 1 出发,下一个数字只有这两种选择。
从 4 出发能跳到哪:换到 4,它能跳到 0、3、9 三个。位置不同,邻居数量也不同——这张「能跳到谁」的表是这题的地基。
从 6 出发能跳到哪:6 能跳到 0、1、7。注意 1 能跳到 6、6 也能跳回 1——这种「跳跃关系」是双向对称的。
从 0 出发能跳到哪:底下的 0 只能跳到 4 和 6。0 位置偏,所以邻居少。
特殊的 5 · 哪都跳不了:中间的 5 很特殊:骑士从 5 走「日」字会跳出键盘,所以它没有任何邻居。一旦跳出,5 就只能当「起点的第一个数字」,之后再也回不来。
把跳跃关系记成一张表:把每个数字「一步能跳到谁」全列出来,就是这张邻接表。它对称:a 在 b 的列表里,b 也在 a 的列表里。有了它,剩下就是数路径。
先想最直白的办法:最直白:从 10 个起点分别出发,每步在邻居里挑一个跳,把所有可能的路径都走一遍。n 大了路径会指数爆炸,太慢。
关键观察 · 数量可以累加:题目只问有多少个号码,不要具体号码。而「落在某数字、还要再跳几步」的方案数是固定的,与历史无关——这正是动态规划能省掉重复计算的根。
定义 DP 状态:我们换个角度:dp[step][d] 表示已经按了 step+1 个键、最后一个键是 d 的号码有多少个。求出最后一行全部加起来就是答案。
转移方程:要落在 d,上一步必然站在某个「能跳到 d」的数字 prev 上。把这些 prev 的方案数全加起来,就是这一步落在 d 的方案数。下面用 n=3 把表填出来。
建表 · 行=步,列=末位数字:这是要填的 DP 表:列是末位数字 0~9,行是跳的步数。step0 是号码长度 1,step2 是长度 3。一行填完再填下一行。
地基 · step0 全填 1:第 0 步(号码长度 1):骑士哪都不跳,停在任一数字都算一个号码,所以 dp[0][d] 全是 1。这一行加起来 = 10,正是 n=1 的答案。
填 step1 · 落在 0:要让末位是 0,上一步得站在能跳到 0 的数字上:4 或 6。把它们 step0 的值加起来 1+1=2,填入 dp[1][0]。
填 step1 · 落在 1:末位是 1:上一步在 6 或 8,加起来 = 2。dp[1][0] 已填好(灰)。一格一格往右推。
填 step1 · 落在 4(邻居更多):末位是 4:能跳到 4 的有三个数字 0、3、9,所以 dp[1][4] = 3。邻居多的格子,方案数就更大。2、3 也已填好(灰)。
填 step1 · 落在 5:末位是 5:没有任何数字能跳到 5(它是孤岛),所以 dp[1][5] = 0。一旦跳了步,就再也停不到 5 了。
填完 step1 · 整行:把 6、7、8、9 也照样填好,step1 这行是 [2,2,2,2,3,0,3,2,2,2]。整行加起来 = 20,正好是 n=2 的答案。
填 step2 · 落在 0:进入第 2 步。规则不变——末位 0 来自 4 和 6,但这次取的是 step1 的值:3 + 3 = 6。每一步都建立在上一步之上。
填 step2 · 落在 1:末位 1:来自 step1 的 6(=3) 和 8(=2),加起来 = 5。dp[2][0] 已落定(灰)。
填 step2 · 落在 4:末位 4:来自 step1 的 0、3、9(都是 2),加起来 = 6。2、3 也按同法填好了(灰)。
填 step2 · 落在 6:末位 6:来自 step1 的 0、1、7(都是 2),加起来 = 6。中间的 5 仍是 0(灰),因为没人能跳到它。继续把剩下几格填完。
填完 step2 · 整行:把 step2 这行全部填完:[6,5,4,5,6,0,6,5,4,5]。每个格子 = 长度 3、末位是该数字的号码个数。还差最后一步——把它们加起来。
求和 · 得到答案:n=3 的答案 = step2 整行之和 = 6+5+4+5+6+0+6+5+4+5 = 46,和示例完全对上。最后一行求和就是最终答案,因为号码末位可以是任意数字。
滚动优化 · 只留两行:每行只依赖紧挨着的上一行,更早的行用完就丢。所以空间能从「n×10」压到「两个长度 10 的数组」——这就是滚动数组优化。
本质金句:凡是「求有多少种走法 / 多少条路径」的题,别傻乎乎一条条走。按「当前在哪、还剩几步」分组,把方案数累加,是这类计数 DP 的通用思路。
易错实演 · 多跳一步会怎样:如果 n=1 还多跳了一步,就把答案从正确的 10(step0 之和)错算成 20。所以循环次数必须是 n-1,n=1 时一步都不跳。
边界三连:n=1 是最容易写错的边界——很多人循环跳了 1 步导致答案翻倍。n 很大时只要每步取模就稳,时间仍是线性。
面试追问:把「DFS 为何超时、DP 如何省、n 极大时矩阵快速幂」这条线讲清楚,能体现你对计数 DP 与优化阶梯的完整理解。
参考代码
class Solution: def knightDialer(self, n): MOD = 10 ** 9 + 7 moves = [[4, 6], [6, 8], [7, 9], [4, 8], [0, 3, 9], [], [0, 1, 7], [2, 6], [1, 3], [2, 4]] dp = [1] * 10 # step0: 长度1 都是1 for _ in range(n - 1): # 再跳 n-1 步 ndp = [0] * 10 for d in range(10): for nb in moves[d]: # d 能跳到 nb ndp[nb] = (ndp[nb] + dp[d]) % MOD dp = ndp return sum(dp) % MOD # 最后一行求和复杂度
- 时间:O(n),跳 n-1 步,每步处理 10 个数字 + 各自固定几个邻居,常数工作
- 空间:O(1),邻接表固定 10 个数字,dp 只用两个长度 10 的数组滚动
易错点
面试追问把动画讲成自己的话
追问为什么用 DP 而不是直接 DFS 枚举所有号码?
追问如果 n 大到 1e9 还能更快吗?
追问为什么必须取模、还要用 long?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
最长湍流子数组
LeetCode 978 · 中等 · 沿着 动态规划套路 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题