A clear explanation of Android Unlock Patterns using backtracking, a jump table, and symmetry optimization.
Problem Restatement
We have a 3 x 3 Android lock screen.
The dots are numbered like this:
1 2 3
4 5 6
7 8 9Given two integers m and n, count how many valid unlock patterns have length at least m and at most n.
A pattern is a sequence of distinct dots.
A move from one dot to another is valid if either:
- The line does not pass through another dot.
- The line passes through another dot, but that middle dot has already been used.
For example, the move 1 -> 3 passes through 2.
So 1 -> 3 is invalid unless 2 was already selected.
The move 1 -> 9 passes through 5.
So 1 -> 9 is invalid unless 5 was already selected.
The order matters. Pattern 1 -> 2 -> 3 and pattern 3 -> 2 -> 1 are different patterns.
Input and Output
| Item | Meaning |
|---|---|
| Input | Two integers m and n |
| Output | Number of valid unlock patterns |
| Constraint | 1 <= m <= n <= 9 |
| Main rule | Dots cannot repeat |
| Special rule | A jump over a dot requires that dot to be used first |
Example function shape:
def numberOfPatterns(m: int, n: int) -> int:
...Examples
For:
m = 1
n = 1Every single dot is a valid pattern.
There are 9 dots, so the answer is:
9For:
m = 1
n = 2All length 1 patterns are valid.
For length 2, most pairs are valid, but some direct jumps are invalid.
Invalid length 2 moves include:
1 -> 3
3 -> 1
1 -> 7
7 -> 1
3 -> 9
9 -> 3
7 -> 9
9 -> 7
1 -> 9
9 -> 1
3 -> 7
7 -> 3
2 -> 8
8 -> 2
4 -> 6
6 -> 4Each of these crosses an unused middle dot.
So the answer for m = 1, n = 2 is:
65First Thought: Brute Force
A direct way is to generate every possible sequence of dots.
Since there are only 9 dots, the total number of sequences is limited.
For each sequence, we could check whether every move is valid.
This works, but it separates generation and validation. A cleaner method is to build only valid patterns from the start.
That leads to backtracking.
Key Insight
At any point, the next dot is valid if:
the dot has not been usedand either:
the move does not cross a middle dotor:
the crossed middle dot has already been usedWe can encode the crossing rule in a table.
Let jump[a][b] mean:
the dot that must already be used before moving from a to bIf jump[a][b] = 0, then moving from a to b does not require any middle dot.
Important required middle dots:
| Move | Required dot |
|---|---|
1 <-> 3 | 2 |
1 <-> 7 | 4 |
3 <-> 9 | 6 |
7 <-> 9 | 8 |
1 <-> 9 | 5 |
3 <-> 7 | 5 |
2 <-> 8 | 5 |
4 <-> 6 | 5 |
All other moves either have no middle dot or pass through no numbered dot.
Algorithm
Use DFS.
The DFS state contains:
| State | Meaning |
|---|---|
cur | Current dot |
length | Current pattern length |
used | Which dots are already in the pattern |
At each DFS call:
- Mark
curas used. - If
length >= m, count the current pattern. - If
length == n, stop extending. - Try every next dot from
1to9. - A next dot is valid if it is unused and its required middle dot is either
0or already used. - Recurse.
- Unmark
curbefore returning.
We can also use symmetry.
The four corner dots are equivalent:
1, 3, 7, 9The four edge dots are equivalent:
2, 4, 6, 8The center dot is unique:
5So instead of starting DFS from all 9 dots, we compute:
DFS from 1 * 4
DFS from 2 * 4
DFS from 5 * 1Correctness
The DFS only adds unused dots, so every counted pattern has distinct dots.
For each possible move from cur to next_dot, the algorithm checks the jump table.
If jump[cur][next_dot] == 0, the move has no required middle dot, so it is valid.
If jump[cur][next_dot] != 0, the move crosses a middle dot. The algorithm allows the move only when that middle dot has already been used. This matches the Android unlock rule exactly.
The DFS explores every valid continuation from each valid prefix. Since every valid pattern can be built one dot at a time while satisfying the same move rule, the DFS eventually reaches and counts every valid pattern whose length is between m and n.
The symmetry multiplication is valid because rotating or reflecting the grid maps corners to corners and edges to edges without changing the validity rules. Therefore all corner starts have the same count, and all edge starts have the same count.
Complexity
| Metric | Value | Why |
|---|---|---|
| Time | O(9!) upper bound | DFS explores permutations of at most 9 dots |
| Space | O(9) | Recursion depth and used array are bounded by 9 |
The practical runtime is very small because the grid has only 9 dots.
Implementation
class Solution:
def numberOfPatterns(self, m: int, n: int) -> int:
jump = [[0] * 10 for _ in range(10)]
jump[1][3] = jump[3][1] = 2
jump[1][7] = jump[7][1] = 4
jump[3][9] = jump[9][3] = 6
jump[7][9] = jump[9][7] = 8
jump[1][9] = jump[9][1] = 5
jump[3][7] = jump[7][3] = 5
jump[2][8] = jump[8][2] = 5
jump[4][6] = jump[6][4] = 5
used = [False] * 10
def dfs(cur: int, length: int) -> int:
used[cur] = True
count = 1 if length >= m else 0
if length < n:
for nxt in range(1, 10):
required = jump[cur][nxt]
if used[nxt]:
continue
if required == 0 or used[required]:
count += dfs(nxt, length + 1)
used[cur] = False
return count
total = 0
total += dfs(1, 1) * 4
total += dfs(2, 1) * 4
total += dfs(5, 1)
return totalCode Explanation
We first build the jump table.
jump = [[0] * 10 for _ in range(10)]The table uses indices 1 through 9, so size 10 keeps the indexing simple.
For example:
jump[1][3] = jump[3][1] = 2This means moving between 1 and 3 requires dot 2 to be used first.
The DFS marks the current dot:
used[cur] = TrueThen it counts the current pattern if its length is already within the allowed range:
count = 1 if length >= m else 0This is important because every prefix with length between m and n is a valid pattern by itself.
Then we try every possible next dot:
for nxt in range(1, 10):We skip used dots:
if used[nxt]:
continueThen we check the jump rule:
required = jump[cur][nxt]
if required == 0 or used[required]:
count += dfs(nxt, length + 1)If no middle dot is required, the move is valid.
If a middle dot is required, the move is valid only when that dot was already used.
Finally, we backtrack:
used[cur] = FalseThis restores the state so other branches can use cur later.
The final answer uses symmetry:
total += dfs(1, 1) * 4
total += dfs(2, 1) * 4
total += dfs(5, 1)Dot 1 represents all four corners.
Dot 2 represents all four edge centers.
Dot 5 represents the center.
Testing
def run_tests():
s = Solution()
assert s.numberOfPatterns(1, 1) == 9
assert s.numberOfPatterns(1, 2) == 65
assert s.numberOfPatterns(2, 2) == 56
assert s.numberOfPatterns(3, 3) == 320
assert s.numberOfPatterns(1, 9) == 389497
print("all tests passed")
run_tests()Test meaning:
| Test | Why |
|---|---|
m = 1, n = 1 | Every single dot is valid |
m = 1, n = 2 | Checks short patterns and invalid jumps |
m = 2, n = 2 | Counts only length 2 patterns |
m = 3, n = 3 | Known exact count for length 3 |
m = 1, n = 9 | Full range stress test |