935. Knight Dialer - Explanation
Prerequisites
Before attempting this problem, you should be comfortable with:
- Recursion - Breaking down problems into smaller subproblems with base cases
- Dynamic Programming - Memoization and tabulation techniques to avoid redundant computation
- Graph Traversal - Understanding state transitions as edges in an implicit graph
- Matrix Exponentiation - Using matrix multiplication to compute recurrences in O(log n) time (for optimal solution)
1. Recursion
Intuition
A knight on a phone dial pad can only jump to specific digits based on its L-shaped movement. For example, from digit 1, the knight can reach 6 or 8. We can precompute these valid jumps for each digit. To count all n-digit numbers, we try starting from each digit and recursively count how many paths of length n exist. This explores all possibilities but leads to massive redundant computation.
Algorithm
- Create a
jumpsarray wherejumps[d]lists all digits reachable from digitd. - For
n == 1, return10since any single digit is valid. - Define
dfs(n, d)that returns the count of numbers withnremaining digits starting from digitd. - Base case: if
n == 0, return1(found one valid number). - Sum up
dfs(n - 1, next)for eachnextinjumps[d]. - Call
dfs(n - 1, d)for each starting digitdfrom0to9and sum the results.
class Solution:
def knightDialer(self, n: int) -> int:
if n == 1:
return 10
mod = 1000000007
jumps = [
[4, 6], [6, 8], [7, 9], [4, 8], [0, 3, 9],
[], [0, 1, 7], [2, 6], [1, 3], [2, 4]
]
def dfs(n, d):
if n == 0:
return 1
res = 0
for j in jumps[d]:
res = (res + dfs(n - 1, j)) % mod
return res
res = 0
for d in range(10):
res = (res + dfs(n - 1, d)) % mod
return resTime & Space Complexity
- Time complexity:
- Space complexity: for recursion stack.
2. Dynamic Programming (Top-Down)
Intuition
The recursive solution recalculates the same subproblems many times. For instance, the count of paths from digit 4 with 5 remaining steps is computed repeatedly. By memoizing results in a 2D array indexed by (digit, remaining steps), we avoid redundant work. Each unique state is computed only once.
Algorithm
- Create a 2D memoization array
dp[digit][remaining_steps]initialized to-1. - In
dfs(n, d), check ifdp[d][n]is already computed; if so, return it. - Otherwise, compute the sum of paths from all reachable digits and store in
dp[d][n]. - Apply modulo to prevent overflow.
- Return the total count from all starting digits.
class Solution:
def knightDialer(self, n: int) -> int:
if n == 1:
return 10
mod = 1000000007
jumps = [
[4, 6], [6, 8], [7, 9], [4, 8], [0, 3, 9],
[], [0, 1, 7], [2, 6], [1, 3], [2, 4]
]
dp = [[-1] * (n + 1) for _ in range(10)]
def dfs(n, d):
if n == 0:
return 1
if dp[d][n] != -1:
return dp[d][n]
dp[d][n] = 0
for j in jumps[d]:
dp[d][n] = (dp[d][n] + dfs(n - 1, j)) % mod
return dp[d][n]
res = 0
for d in range(10):
res = (res + dfs(n - 1, d)) % mod
return resTime & Space Complexity
- Time complexity:
- Space complexity:
3. Dynamic Programming (Bottom-Up)
Intuition
Instead of recursing from the top, we can build up from the base case. Starting with step 0 (each digit has exactly 1 way to form a 1-digit number), we iteratively compute the counts for step 1, step 2, and so on up to step n-1. At each step, the count for a digit equals the sum of counts from all digits that can jump to it.
Algorithm
- Initialize
dp[d][0] = 1for all digits (base case). - For each step from
1ton-1:- For each digit
d, sum updp[j][step-1]for alljinjumps[d].
- For each digit
- The answer is the sum of
dp[d][n-1]for all digitsd.
class Solution:
def knightDialer(self, n: int) -> int:
if n == 1:
return 10
mod = 1000000007
jumps = [
[4, 6], [6, 8], [7, 9], [4, 8], [0, 3, 9],
[], [0, 1, 7], [2, 6], [1, 3], [2, 4]
]
dp = [[0] * (n + 1) for _ in range(10)]
for d in range(10):
dp[d][0] = 1
for step in range(1, n):
for d in range(10):
dp[d][step] = sum(dp[j][step - 1] for j in jumps[d]) % mod
return sum(dp[d][n - 1] for d in range(10)) % modTime & Space Complexity
- Time complexity:
- Space complexity:
4. Dynamic Programming (Space Optimized)
Intuition
Since each step only depends on the previous step, we don't need to store the entire DP table. We can use two 1D arrays (or swap between them) to track counts for the current and previous steps. This reduces space from O(10 * n) to O(10), which is effectively O(1).
Algorithm
- Initialize
dparray with1for each digit. - For each step:
- Create a new
nextDparray initialized to0. - For each digit
d, adddp[d]tonextDp[j]for eachjinjumps[d]. - Replace
dpwithnextDp.
- Create a new
- Sum all values in
dpfor the final answer.
class Solution:
def knightDialer(self, n: int) -> int:
if n == 1:
return 10
mod = 1000000007
jumps = [
[4, 6], [6, 8], [7, 9], [4, 8], [0, 3, 9],
[], [0, 1, 7], [2, 6], [1, 3], [2, 4]
]
dp = [1] * 10
for step in range(n - 1):
next_dp = [0] * 10
for d in range(10):
for j in jumps[d]:
next_dp[j] = (next_dp[j] + dp[d]) % mod
dp = next_dp
res = 0
for d in dp:
res = (res + d) % mod
return resTime & Space Complexity
- Time complexity:
- Space complexity:
5. Dynamic Programming (Optimal)
Intuition
We can exploit the symmetry in the phone dial pad. Due to the knight's movement pattern, several digits behave identically: digits 1, 3, 7, 9 form one group (corners), digits 2 and 8 form another (top and bottom), digits 4 and 6 another (left and right), and digit 0 is alone. By grouping these, we reduce the state space to just 4 values, leading to constant-time transitions per step.
Algorithm
- Use 4 values:
D(digit 5 area),A(corners1,3,7,9),B(top/bottom2,8),C(sides4,6). - Initialize based on initial counts:
[1, 4, 2, 2]for[D, A, B, C]. - For each step, compute new values using transition rules derived from the jump patterns.
- Sum all groups for the final answer.
class Solution:
def knightDialer(self, n: int) -> int:
if n == 1:
return 10
mod = 10**9 + 7
jumps = [1, 4, 2, 2] # [D, A, B, C]
for _ in range(n - 1):
tmp = [0] * 4
tmp[0] = jumps[3]
tmp[1] = 2 * jumps[2] + 2 * jumps[3]
tmp[2] = jumps[1]
tmp[3] = 2 * jumps[0] + jumps[1]
jumps = tmp
return sum(jumps) % modTime & Space Complexity
- Time complexity:
- Space complexity:
6. Matrix Exponentiation
Intuition
The transition from one step to the next can be represented as a matrix multiplication. If we encode the adjacency of knight jumps in a 10x10 matrix, then raising this matrix to the power n-1 gives us the count of paths of length n. Matrix exponentiation allows us to compute the result in O(log n) time, making this approach extremely efficient for very large n.
Algorithm
- Build a 10x10 transition matrix where
mat[i][j] = 1if a knight can jump from digitito digitj. - Use fast matrix exponentiation to compute
mat^(n-1). - Sum all entries in the result matrix to get the total count of
n-digit numbers.
class Matrix:
def __init__(self, size):
self.n = size
self.a = [[0] * size for _ in range(size)]
def __mul__(self, other):
n = self.n
MOD = 10**9 + 7
product = Matrix(n)
for i in range(n):
for j in range(n):
for k in range(n):
product.a[i][k] = (product.a[i][k] + self.a[i][j] * other.a[j][k]) % MOD
return product
def matpow(mat, n, size):
res = Matrix(size)
for i in range(size):
res.a[i][i] = 1 # Identity matrix
while n:
if n & 1:
res = res * mat
mat = mat * mat
n >>= 1
return res
class Solution:
def knightDialer(self, n: int) -> int:
if n == 1:
return 10
MOD = 10**9 + 7
jumps = [
[4, 6], [6, 8], [7, 9], [4, 8], [0, 3, 9],
[], [0, 1, 7], [2, 6], [1, 3], [2, 4]
]
mat = Matrix(10)
for i in range(10):
for j in jumps[i]:
mat.a[i][j] = 1
res = matpow(mat, n - 1, 10)
ans = sum(sum(res.a[i]) for i in range(10)) % MOD
return ansTime & Space Complexity
- Time complexity:
- Space complexity:
Common Pitfalls
Forgetting That Digit 5 Has No Valid Jumps
The knight cannot jump to any digit from position 5 on the phone dial pad. A common mistake is either forgetting to include digit 5 in the initial count (for n=1) or incorrectly assuming it contributes to longer sequences. For n >= 2, digit 5 effectively becomes a dead end and contributes nothing to the count.
Integer Overflow Without Modulo Operations
The number of valid phone numbers grows exponentially and quickly exceeds 32-bit integer limits. Failing to apply the modulo operation (10^9 + 7) after each addition leads to overflow. Apply modulo at every step where values are accumulated, not just at the final result.
Incorrect Jump Adjacency List
The knight's L-shaped movement on a phone dial creates a specific jump pattern that must be encoded correctly. Common mistakes include forgetting certain jumps (like 0 can reach 4 and 6) or adding invalid jumps (like 5 reaching any digit, or digits jumping to themselves). Double-check the adjacency list against the actual phone pad layout.