Android Unlock Patterns

Updated on 13 May, 2025
Android Unlock Patterns header image

Problem Statement

Android devices feature a unique security mechanism on their lock screens, utilizing a 3 x 3 grid of dots. Users can set an unlock pattern by connecting these dots in specific sequences. Each sequence forms a series of line segments linking consecutive dots. However, not all sequences are valid. A sequence is considered valid if it adheres to the following criteria:

  • Every dot in the sequence is unique.
  • If a line segment between two consecutive dots crosses through the center of a different dot, that intersected dot must have been included earlier in the sequence. This rule ensures that no dot is "jumped" over without being selected first.

Instances where a pattern is deemed valid or invalid are illustrated with examples in the problem statement, where specific sequences are analyzed. The task is to calculate the number of unique and valid unlock patterns given constraints on the number of dots used in the pattern, bounded by the integers m (minimum) and n (maximum). Additionally, the uniqueness of a pattern is determined by the presence of a distinct dot or differing orders of dots in comparison to other sequences.

Examples

Example 1

Input:

m = 1, n = 1

Output:

9

Example 2

Input:

m = 1, n = 2

Output:

65

Constraints

  • 1 <= m, n <= 9

Approach and Intuition

The challenge revolves around counting all potential unique sequences that meet the criteria outlined, from patterns with m dots to ones with n dots. Here’s a guided approach to understanding and solving this:

  1. Start by considering the potential moves from each dot, respecting the adjacency rules. Each dot has certain natural neighbors (direct adjacents) and potential leap-over neighbors (dots that require a middle-dot to have been used).

  2. A recursive (or backtracking) approach can be effective here. Define a function that attempts to extend each pattern from each starting dot, checking every possible continuation at each step, provided it meets one of two conditions:

    • The next dot has not been used yet.
    • Jumping to the next dot is allowed, meaning the middle dot has been used in the pattern already.
  3. Use an array to keep track of which dots have been used in the current recursive path to quickly check whether a move is valid.

  4. Begin the recursion with each of the 9 dots, using a depth-first search strategy. For each dot, attempt to move to every possible subsequent dot.

  5. Count all distinct valid sequences that fall within the required length specified by m and n.

  6. Consider optimizations:

    • Symmetry can be exploited because certain dots are symmetrically identical in their moves (corners with corners, edges with edges, center with itself).
    • Cache intermediate results during recursion to avoid duplicative work, especially when working from a symmetrically identical starting point.
  7. Continue to validate each potential sequence against the two criteria specified for a valid pattern as each new dot is attempted in the sequence.

This method will inherently count each unique valid pattern by exploring every possible combination of moves that meet the unlock criteria, adhering to constraints on sequence length (from minimum m to maximum n).

Solutions

  • C++
  • Java
  • Python
cpp
class Solution {
public:
    int countUnlockPatterns(int minLen, int maxLen) {
        int skips[10][10] = {0};
        // Defining skips based on the keypad layout and allowed combinations
        skips[1][3] = skips[3][1] = 2;
        skips[4][6] = skips[6][4] = 5;
        skips[7][9] = skips[9][7] = 8;
        skips[1][7] = skips[7][1] = 4;
        skips[2][8] = skips[8][2] = 5;
        skips[3][9] = skips[9][3] = 6;
        skips[1][9] = skips[9][1] = skips[3][7] = skips[7][3] = 5;

        int used = 0;
        int patterns = 0;
        int memo[10][1 << 10] = {0};
        memset(memo, -1, sizeof(memo));

        // Utilizing symmetry in pattern counting
        patterns += computePatterns(1, 1, minLen, maxLen, skips, used, memo) * 4;
        patterns += computePatterns(2, 1, minLen, maxLen, skips, used, memo) * 4;
        patterns += computePatterns(5, 1, minLen, maxLen, skips, used, memo);

        return patterns;
    }

private:
    int computePatterns(int start, int len, int minLen, int maxLen, int skips[10][10],
                        int used, int memo[10][1 << 10]) {
        // Check if current pattern length exceeds max length
        if (len > maxLen) return 0;
        if (memo[start][used] != -1) return memo[start][used];

        int countValid = 0;
        if (len >= minLen) {
            countValid++;
        }

        used |= 1 << (start - 1);  // Mark the number as used

        for (int next = 1; next <= 9; next++) {
            int via = skips[start][next];
            // Valid next step: not used and no skip or skip is already used
            if (!(used & (1 << (next - 1))) && (via == 0 || (used & (1 << (via - 1))))) {
                countValid += computePatterns(next, len + 1, minLen, maxLen, skips, used, memo);
            }
        }

        used &= ~(1 << (start - 1));  // Unmark the number
        return memo[start][used] = countValid;
    }
};

This C++ solution implements a method to count the number of valid unlock patterns on a classic Android 3x3 keypad, given constraints on pattern length. The function countUnlockPatterns determines the count between a minimum and maximum pattern length.

  • The solution uses a 2D array skips to specify intermediary numbers that must be passed when moving between two non-adjacent keys.
  • Utilizes a recursive helper function computePatterns to explore and count all valid patterns starting from each possible key.
  • Employs memoization, stored in memo, to avoid recomputing results for previously encountered states.
  • Takes advantage of the symmetry in a 3x3 grid, multiplying the results from starting points that are symmetrically equivalent, thus reducing computation.
  • computePatterns is designed to handle the backtracking through each possible unlock pattern, checking if moving to the next key is valid (not used or if a required 'skip' key is already used), and recursively counts all valid patterns from that step.

Enhancements such as utilizing bit manipulation (used) for efficiently marking the uses of keys and ensuring efficient pattern validation, demonstrate an optimized approach. The multiplication of counts for symmetric start points significantly reduces the calculations requisite for finding the total number of patterns.

Overall, this solution provides an efficient and optimized strategy using recursion, memoization, and grid symmetry to solve the problem of counting valid unlock patterns given certain conditions.

java
class UnlockPatterns {

    public int possiblePatterns(int minLen, int maxLen) {
        int[][] skipOver = new int[10][10];

        skipOver[1][3] = skipOver[3][1] = 2;
        skipOver[4][6] = skipOver[6][4] = 5;
        skipOver[7][9] = skipOver[9][7] = 8;
        skipOver[1][7] = skipOver[7][1] = 4;
        skipOver[2][8] = skipOver[8][2] = 5;
        skipOver[3][9] = skipOver[9][3] = 6;
        skipOver[1][9] = skipOver[9][1] = skipOver[3][7] = skipOver[7][3] = 5;

        int visited = 0;
        int patternCount = 0;
        Integer[][] memo = new Integer[10][1 << 10];

        patternCount +=
        calculatePatterns(1, 1, minLen, maxLen, skipOver, visited, memo) * 4;
        patternCount +=
        calculatePatterns(2, 1, minLen, maxLen, skipOver, visited, memo) * 4;
        patternCount +=
        calculatePatterns(5, 1, minLen, maxLen, skipOver, visited, memo);

        return patternCount;
    }

    private int calculatePatterns(
        int startNumber,
        int len,
        int minLen,
        int maxLen,
        int[][] skipOver,
        int visited,
        Integer[][] memo
    ) {
        if (len > maxLen) return 0;

        if (memo[startNumber][visited] != null) return memo[startNumber][visited];

        int countValid = 0;
        if (len >= minLen) {
            countValid++;
        }

        visited |= 1 << startNumber;

        for (int next = 1; next <= 9; next++) {
            int skip = skipOver[startNumber][next];
            if ((visited & (1 << next)) == 0 && (skip == 0 || (visited & (1 << skip)) != 0)) {
                countValid += calculatePatterns(next, len + 1, minLen, maxLen, skipOver, visited, memo);
            }
        }

        visited &= ~(1 << startNumber);

        return memo[startNumber][visited] = countValid;
    }
}

In the provided Java code for the Android Unlock Patterns problem, the objective is to calculate the number of valid patterns that can be formed on an Android unlock screen given constraints on the minimum and maximum length of the patterns.

Key Components of the Solution:

  • skipOver Array: Initializes a 2D array to handle skipped numbers in between moves. If a move from one number to another number directly crosses over a third number, that third number must have been visited prior for the move to be valid. For instance, moving from 1 to 3 requires that 2 has been visited.

  • possiblePatterns Method: Calculates the total number of valid patterns. It initializes variables to keep track of visited numbers and counts valid patterns. It accommodates symmetry in the keypad, multiplying the number of patterns from certain numbers to save computation. The method uses recursion through the helper function calculatePatterns.

  • calculatePatterns Recursive Helper Function: Determines the number of valid patterns starting from a specified number considering the visited numbers and current length of the pattern. It checks if the move is valid using the skipOver array. If valid, it recursively searches for more patterns by moving to the next number. The function utilizes memoization (memo array) to store previously computed results for specific conditions to enhance efficiency.

Working of the Recursion:

  1. Base cases check for pattern overflow and memoized results to return saved computations.
  2. Validity check for each pattern is performed. If the current length is within the needed bounds, it is counted.
  3. The function explores further patterns by marking the current number as visited and attempting moves to all other numbers, recursively increasing the length of the pattern.
  4. The state is backtracked (i.e., the current number is marked as unvisited) before the function returns to ensure correct pattern counting in subsequent recursions.

Overall, the approach efficiently handles the constraints imposed by the grid layout of the Android unlock screen by considering symmetry, utilizing backtracking for pattern generation, and improving performance with memoization. This Java solution provides an elegant and effective way to enumerate potential unlock patterns within given length boundaries.

python
class PatternCounter:
    def countLockPatterns(self, minLen: int, maxLen: int) -> int:
        jumps = [[0] * 10 for _ in range(10)]

        # Defining direct jump requirements between keys
        jumps[1][3] = jumps[3][1] = 2
        jumps[4][6] = jumps[6][4] = 5
        jumps[7][9] = jumps[9][7] = 8
        jumps[1][7] = jumps[7][1] = 4
        jumps[2][8] = jumps[8][2] = 5
        jumps[3][9] = jumps[9][3] = 6
        jumps[1][9] = jumps[9][1] = jumps[3][7] = jumps[7][3] = 5

        visited = 0
        pattern_count = 0
        memo = [[-1] * (1 << 10) for _ in range(10)]

        # Calculate patterns starting from corners (1, 3, 7, 9) considering symmetry
        pattern_count += (
            self._generatePatterns(
                startKey=1, length=1, minLen=minLen, maxLen=maxLen, jumps=jumps, visited=visited, memo=memo
            )
            * 4
        )

        # Calculate patterns starting from edges (2, 4, 6, 8) considering symmetry
        pattern_count += (
            self._generatePatterns(
                startKey=2, length=1, minLen=minLen, maxLen=maxLen, jumps=jumps, visited=visited, memo=memo
            )
            * 4
        )

        # Calculate patterns from the center key (5)
        pattern_count += self._generatePatterns(
            5, 1, minLen, maxLen, jumps, visited, memo
        )

        return pattern_count

    def _generatePatterns(
        self, startKey: int, length: int, minLen: int, maxLen: int, jumps: list, visited: int, memo: list,
    ) -> int:
        if length > maxLen:
            return 0

        if memo[startKey][visited] != -1:
            return memo[startKey][visited]

        patterns = 0
        if length >= minLen:
            patterns += 1

        visited = self._markVisited(visited, startKey)

        for nextKey in range(1, 10):
            intermediate = jumps[startKey][nextKey]
            if not self._isVisited(visited, nextKey) and (
                intermediate == 0 or self._isVisited(visited, intermediate)
            ):
                patterns += self._generatePatterns(
                    nextKey,
                    length + 1,
                    minLen,
                    maxLen,
                    jumps,
                    visited,
                    memo,
                )

        visited = self._unmarkVisited(visited, startKey)
        memo[startKey][visited] = patterns
        return patterns

    def _markVisited(self, bitmask: int, pos: int) -> int:
        return bitmask | (1 << (pos - 1))

    def _unmarkVisited(self, bitmask: int, pos: int) -> int:
        return bitmask ^ (1 << (pos - 1))

    def _isVisited(self, bitmask: int, pos: int) -> bool:
        return (bitmask >> (pos - 1)) & 1 == 1

Explore an efficient way to calculate the number of valid unlock patterns on an Android device, using Python. The solution achieves this by employing backtracking and memoization techniques, tailored to adhere to the constraints of minimum and maximum pattern lengths. Here is a streamlined explanation of the approach:

  • The PatternCounter class contains methods crucial for determining the count of valid patterns.
  • A matrix jumps captures the need to pass through an intermediate key to move between certain pairs of keys directly, a vital part of validating pattern moves.
  • Memoization is utilized via the memo array to store results of subproblems and avoid redundant calculations. This significantly boosts performance.
  • The function _generatePatterns recursively calculates the number of valid sequences that can be formed starting from a specified key. It critically checks if a move is valid, considering any required intermediate keys.
  • For keys connected directly without an intermediary (like 1 to 3 or 4 to 6), certain conditions are set in the jumps matrix.
  • Counting starts separately from corner keys, edge keys, and the center, applying symmetrical properties to minimize computations (since patterns from corners or edges will mirror other corners or edges respectively).
  • Pattern counting is initiated by calling _generatePatterns for each starting key type with initial pattern lengths and symmetry considerations.

Benefits of this implementation include:

  • Efficient calculation through the use of bitwise operations to track visited keys.
  • Optimization by considering symmetry and memoization to prevent recalculations of the state.
  • Versatility in handling varying minimum and maximum pattern lengths effectively.

This approach not only adheres strictly to the problem’s constraints but also ensures optimal performance for real-world usage in security mechanisms such as lock screens.

Comments

No comments yet.