935. Knight Dialer - Explanation

Problem Link



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

  1. Create a jumps array where jumps[d] lists all digits reachable from digit d.
  2. For n == 1, return 10 since any single digit is valid.
  3. Define dfs(n, d) that returns the count of numbers with n remaining digits starting from digit d.
  4. Base case: if n == 0, return 1 (found one valid number).
  5. Sum up dfs(n - 1, next) for each next in jumps[d].
  6. Call dfs(n - 1, d) for each starting digit d from 0 to 9 and 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 res

Time & Space Complexity

  • Time complexity: O(3n)O(3 ^ n)
  • Space complexity: O(n)O(n) 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

  1. Create a 2D memoization array dp[digit][remaining_steps] initialized to -1.
  2. In dfs(n, d), check if dp[d][n] is already computed; if so, return it.
  3. Otherwise, compute the sum of paths from all reachable digits and store in dp[d][n].
  4. Apply modulo to prevent overflow.
  5. 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 res

Time & Space Complexity

  • Time complexity: O(n)O(n)
  • Space complexity: O(n)O(n)

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

  1. Initialize dp[d][0] = 1 for all digits (base case).
  2. For each step from 1 to n-1:
    • For each digit d, sum up dp[j][step-1] for all j in jumps[d].
  3. The answer is the sum of dp[d][n-1] for all digits d.
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)) % mod

Time & Space Complexity

  • Time complexity: O(n)O(n)
  • Space complexity: O(n)O(n)

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

  1. Initialize dp array with 1 for each digit.
  2. For each step:
    • Create a new nextDp array initialized to 0.
    • For each digit d, add dp[d] to nextDp[j] for each j in jumps[d].
    • Replace dp with nextDp.
  3. Sum all values in dp for 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 res

Time & Space Complexity

  • Time complexity: O(n)O(n)
  • Space complexity: O(1)O(1)

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

  1. Use 4 values: D (digit 5 area), A (corners 1,3,7,9), B (top/bottom 2,8), C (sides 4,6).
  2. Initialize based on initial counts: [1, 4, 2, 2] for [D, A, B, C].
  3. For each step, compute new values using transition rules derived from the jump patterns.
  4. 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) % mod

Time & Space Complexity

  • Time complexity: O(n)O(n)
  • Space complexity: O(1)O(1)

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

  1. Build a 10x10 transition matrix where mat[i][j] = 1 if a knight can jump from digit i to digit j.
  2. Use fast matrix exponentiation to compute mat^(n-1).
  3. 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 ans

Time & Space Complexity

  • Time complexity: O(logn)O(\log n)
  • Space complexity: O(1)O(1)

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.