Facebook Pixel

753. Cracking the Safe

Problem Description

You have a safe that's protected by a password. The password consists of n digits, where each digit can be any value from 0 to k-1.

The safe has an interesting way of checking passwords. Every time you type a digit, it checks the most recent n digits that were entered to see if they match the password.

For example, if the correct password is "345" (where n=3) and you type "012345":

  • After typing 0: checks "0" (only 1 digit so far) - incorrect
  • After typing 1: checks "01" (only 2 digits so far) - incorrect
  • After typing 2: checks "012" (now we have 3 digits) - incorrect
  • After typing 3: checks "123" (the last 3 digits) - incorrect
  • After typing 4: checks "234" (the last 3 digits) - incorrect
  • After typing 5: checks "345" (the last 3 digits) - correct! The safe opens.

Your task is to find the shortest possible string that, when entered, will guarantee to unlock the safe at some point. This string should contain every possible n-digit combination that can be formed using digits from 0 to k-1.

The solution uses an Eulerian circuit approach by constructing a directed graph where:

  • Each node represents a string of length n-1 using digits 0 to k-1
  • Each edge from node u to node v is labeled with a digit c (from 0 to k-1)
  • The edge represents that appending digit c to node u and taking the last n-1 characters gives you node v
  • Each edge essentially represents one possible n-digit password

Since each node has exactly k incoming and k outgoing edges, an Eulerian circuit exists in this graph. Traversing this circuit gives us the minimum-length string that contains all possible passwords.

Flowchart Walkthrough

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • Yes: This problem can be modeled as a graph where each node represents a string of length n-1, and edges represent adding a digit to form an n-length password.

Is it a tree?

  • No: The graph structure is not a tree - it has cycles. Each node has multiple incoming and outgoing edges (specifically k of each), forming a complex directed graph with cycles.

Is the problem related to directed acyclic graphs?

  • No: The graph contains cycles. We're looking for an Eulerian circuit that visits every edge exactly once, which inherently involves cycles.

Is the problem related to shortest paths?

  • No: We're not finding the shortest path between two specific nodes. Instead, we need to traverse every edge in the graph exactly once (Eulerian circuit).

Does the problem involve connectivity?

  • No: We're not checking if nodes are connected or finding connected components. We need to traverse all edges systematically.

Does the problem have small constraints?

  • Yes: While the theoretical number of passwords k^n can grow large, the problem typically has manageable constraints that allow for exhaustive exploration.

DFS/backtracking?

  • Yes: We use DFS to traverse the graph and find the Eulerian circuit. The backtracking nature helps us explore all edges systematically while keeping track of visited edges.

Conclusion: The flowchart leads us to use DFS/backtracking. The solution implements DFS to traverse a graph where we visit every edge (representing each possible n-digit password) exactly once, building the minimum-length string that contains all possible passwords.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is recognizing that we need a string containing every possible n-digit password as a substring. If we think naively, we might concatenate all k^n possible passwords, giving us a string of length n Γ— k^n. But we can do much better by overlapping passwords.

Consider passwords "123" and "234" - they share the substring "23". If we're clever about ordering, we can write "1234" which contains both passwords in just 4 characters instead of 6. This overlap property is crucial.

To systematically find the shortest string with all overlaps, we can model this as a graph problem. Think of each (n-1)-digit string as a node. For example, with n=3, the nodes would be "00", "01", "02", ..., "12", "23", etc.

An edge from node u to node v exists if we can append a digit to u and get v by taking the last (n-1) digits. For instance, from node "12", we can go to node "23" by appending "3" (giving us "123", whose last 2 digits are "23"). This edge represents the password "123".

Now here's the beautiful part: each node has exactly k outgoing edges (one for each digit we can append) and exactly k incoming edges. This means our graph has an Eulerian circuit - a path that visits every edge exactly once and returns to the starting point.

Finding an Eulerian circuit gives us the minimum-length string because:

  1. Each edge represents a unique n-digit password
  2. Traversing each edge once means including each password exactly once
  3. The overlapping is maximized because consecutive edges share (n-1) digits by construction

We use DFS to find this Eulerian circuit. Starting from any node (typically "0...0"), we explore all outgoing edges, marking them as visited. The path we take, when we record the digits on the edges, gives us our answer. We append the starting node's digits at the end to complete the circuit representation as a string.

Learn more about Depth-First Search and Graph patterns.

Solution Approach

The implementation uses DFS to find an Eulerian circuit in the graph we conceptualized. Let's walk through the key components:

Graph Representation: Instead of explicitly building the graph, we use an implicit representation. Each node is represented as an integer (the decimal value of the (n-1)-digit string). For example, with n=3, the node "12" is represented as the integer 12.

Key Variables:

  • mod = 10^(n-1): This helps us extract the last (n-1) digits when we append a new digit
  • vis: A set to track which edges (passwords) we've already used
  • ans: A list to collect the digits we traverse

DFS Algorithm:

def dfs(u):
    for x in range(k):
        e = u * 10 + x
        if e not in vis:
            vis.add(e)
            v = e % mod
            dfs(v)
            ans.append(str(x))

The DFS works as follows:

  1. From current node u, we try all k possible digits (x from 0 to k-1)
  2. e = u * 10 + x creates the edge identifier (the full n-digit password). For example, if u=12 and x=3, then e=123
  3. We check if this edge/password has been visited
  4. If not visited, we mark it as visited and calculate the next node: v = e % mod. This extracts the last (n-1) digits. For e=123 and mod=100, we get v=23
  5. We recursively call DFS on the new node v
  6. Important: We append the digit x to our answer AFTER the recursive call. This is because we're building the Eulerian circuit in reverse order (post-order traversal)

Final Assembly:

dfs(0)
ans.append("0" * (n - 1))
return "".join(ans)

We start the DFS from node 0 (representing "00...0" with n-1 zeros). After the DFS completes, we append n-1 zeros to represent the starting node itself. Since we collected digits in reverse order during DFS, joining them gives us the correct Eulerian circuit.

Why This Works: The algorithm ensures we visit each edge (password) exactly once through the vis set. The post-order collection of digits ensures that when we reverse the path (implicitly done by collecting in post-order), we get a valid sequence where each consecutive n digits form a unique password that appears in our graph.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small example with n=2 and k=2 (binary digits: 0 and 1).

Step 1: Identify all possible passwords With n=2 and k=2, we have 2Β² = 4 possible passwords: "00", "01", "10", "11"

Step 2: Construct the graph conceptually

  • Nodes represent all 1-digit strings: "0" and "1"
  • Edges represent 2-digit passwords:
    • From node "0":
      • Append 0 β†’ password "00" β†’ leads to node "0" (last 1 digit of "00")
      • Append 1 β†’ password "01" β†’ leads to node "1" (last 1 digit of "01")
    • From node "1":
      • Append 0 β†’ password "10" β†’ leads to node "0" (last 1 digit of "10")
      • Append 1 β†’ password "11" β†’ leads to node "1" (last 1 digit of "11")

Step 3: Execute DFS to find Eulerian circuit Starting from node 0:

Initial: u=0, vis={}, ans=[]

DFS(0):
  Try x=0: e=00, not visited
    Add 00 to vis
    Next node v = 00 % 10 = 0
    DFS(0):
      Try x=0: e=00, already visited, skip
      Try x=1: e=01, not visited
        Add 01 to vis
        Next node v = 01 % 10 = 1
        DFS(1):
          Try x=0: e=10, not visited
            Add 10 to vis
            Next node v = 10 % 10 = 0
            DFS(0):
              Try x=0: e=00, already visited, skip
              Try x=1: e=01, already visited, skip
              Return
            Append "0" to ans β†’ ans=["0"]
          Try x=1: e=11, not visited
            Add 11 to vis
            Next node v = 11 % 10 = 1
            DFS(1):
              Try x=0: e=10, already visited, skip
              Try x=1: e=11, already visited, skip
              Return
            Append "1" to ans β†’ ans=["0","1"]
          Return
        Append "1" to ans β†’ ans=["0","1","1"]
      Return
    Append "0" to ans β†’ ans=["0","1","1","0"]
  Try x=1: e=01, already visited, skip
  Return

Step 4: Build final string

  • After DFS: ans = ["0","1","1","0"]
  • Append starting node (n-1 = 1 zero): ans = ["0","1","1","0","0"]
  • Join to get final string: "01100"

Step 5: Verify the result Let's check that "01100" contains all passwords:

  • Position 0-1: "01" βœ“
  • Position 1-2: "11" βœ“
  • Position 2-3: "10" βœ“
  • Position 3-4: "00" βœ“

All 4 possible passwords are present! The string length is 5, which is optimal (we need at least n + k^n - 1 = 2 + 4 - 1 = 5 characters to contain all 4 two-digit passwords with maximum overlap).

Solution Implementation

1class Solution:
2    def crackSafe(self, n: int, k: int) -> str:
3        """
4        Find the minimum string that contains all possible n-digit passwords using digits 0 to k-1.
5        Uses Hierholzer's algorithm to find an Eulerian path in a de Bruijn graph.
6      
7        Args:
8            n: Length of each password
9            k: Number of available digits (0 to k-1)
10      
11        Returns:
12            A string containing all possible n-digit passwords as substrings
13        """
14        def dfs(current_node: int) -> None:
15            """
16            Depth-first search to traverse all edges in the graph exactly once.
17          
18            Args:
19                current_node: Current (n-1)-digit node in the de Bruijn graph
20            """
21            # Try all possible digits to append
22            for digit in range(k):
23                # Create an edge by appending the digit to current node
24                # Edge represents an n-digit combination
25                edge = current_node * 10 + digit
26              
27                # Check if this edge has been visited
28                if edge not in visited_edges:
29                    visited_edges.add(edge)
30                  
31                    # Calculate the next node by removing the leftmost digit
32                    # This creates an (n-1)-digit node for the next iteration
33                    next_node = edge % modulo
34                  
35                    # Recursively visit the next node
36                    dfs(next_node)
37                  
38                    # Append the digit to result after visiting all edges from next_node
39                    # (Hierholzer's algorithm: build path in reverse)
40                    result.append(str(digit))
41      
42        # Calculate modulo for extracting last (n-1) digits
43        modulo = 10 ** (n - 1)
44      
45        # Set to track visited edges (n-digit combinations)
46        visited_edges = set()
47      
48        # List to store the result digits in reverse order
49        result = []
50      
51        # Start DFS from node 0 (representing "00...0" with n-1 zeros)
52        dfs(0)
53      
54        # Append the initial (n-1) zeros for the starting node
55        result.append("0" * (n - 1))
56      
57        # Join the result list to form the final string
58        return "".join(result)
59
1class Solution {
2    // Set to track visited edges in the graph
3    private Set<Integer> visitedEdges = new HashSet<>();
4    // StringBuilder to construct the result string
5    private StringBuilder result = new StringBuilder();
6    // Modulo value for computing next node (10^(n-1))
7    private int modValue;
8
9    /**
10     * Generates the shortest string that contains all possible n-digit passwords
11     * using digits 0 to k-1 (De Bruijn sequence)
12     * 
13     * @param n length of each password
14     * @param k range of digits (0 to k-1)
15     * @return the De Bruijn sequence containing all possible passwords
16     */
17    public String crackSafe(int n, int k) {
18        // Calculate modulo value for transitioning between nodes
19        modValue = (int) Math.pow(10, n - 1);
20      
21        // Start DFS from node 0
22        dfs(0, k);
23      
24        // Append initial (n-1) zeros to complete the sequence
25        result.append("0".repeat(n - 1));
26      
27        return result.toString();
28    }
29
30    /**
31     * Performs depth-first search using Hierholzer's algorithm to find Eulerian path
32     * 
33     * @param currentNode current node in the graph (represents last n-1 digits)
34     * @param k range of digits to try (0 to k-1)
35     */
36    private void dfs(int currentNode, int k) {
37        // Try each possible digit from 0 to k-1
38        for (int digit = 0; digit < k; ++digit) {
39            // Create edge by appending digit to current node
40            int edge = currentNode * 10 + digit;
41          
42            // Check if this edge has been visited
43            if (visitedEdges.add(edge)) {
44                // Calculate next node by removing the leftmost digit
45                int nextNode = edge % modValue;
46              
47                // Recursively explore from the next node
48                dfs(nextNode, k);
49              
50                // Append the digit to result after exploring (post-order)
51                result.append(digit);
52            }
53        }
54    }
55}
56
1class Solution {
2public:
3    string crackSafe(int n, int k) {
4        // Set to store visited edges (passwords) in the graph
5        unordered_set<int> visitedEdges;
6      
7        // Modulo value to get the last (n-1) digits of a number
8        // Used to compute the next node in the graph
9        int modValue = pow(10, n - 1);
10      
11        // Result string to build the de Bruijn sequence
12        string result;
13      
14        // DFS function to traverse the graph using Hierholzer's algorithm
15        // currentNode represents the current (n-1) digit state
16        function<void(int)> dfs = [&](int currentNode) {
17            // Try appending each possible digit (0 to k-1)
18            for (int digit = 0; digit < k; ++digit) {
19                // Form the edge by appending the digit to current node
20                // This represents a complete n-digit password
21                int edge = currentNode * 10 + digit;
22              
23                // Check if this edge (password) has been visited
24                if (!visitedEdges.count(edge)) {
25                    // Mark the edge as visited
26                    visitedEdges.insert(edge);
27                  
28                    // Move to the next node by removing the first digit
29                    // and keeping the last (n-1) digits
30                    int nextNode = edge % modValue;
31                    dfs(nextNode);
32                  
33                    // Append the digit to result after returning from DFS
34                    // (Post-order traversal for Eulerian path)
35                    result += (digit + '0');
36                }
37            }
38        };
39      
40        // Start DFS from node 0 (representing n-1 zeros)
41        dfs(0);
42      
43        // Add the initial (n-1) zeros to complete the de Bruijn sequence
44        result += string(n - 1, '0');
45      
46        return result;
47    }
48};
49
1/**
2 * Finds the shortest string that contains every possible combination of n digits using k different digits (0 to k-1).
3 * This is solved using Hierholzer's algorithm for finding Eulerian path in a De Bruijn graph.
4 * 
5 * @param n - Length of each combination
6 * @param k - Number of different digits to use (0 to k-1)
7 * @returns The shortest string containing all possible n-digit combinations
8 */
9function crackSafe(n: number, k: number): string {
10    // Calculate the modulo value for extracting the last (n-1) digits
11    // This represents the number of possible (n-1)-digit prefixes/suffixes
12    const modValue: number = Math.pow(10, n - 1);
13  
14    // Set to track visited edges in the graph
15    // Each edge represents a transition from one (n-1)-digit state to another
16    const visitedEdges: Set<number> = new Set<number>();
17  
18    // Array to store the result characters in reverse order
19    const resultChars: string[] = [];
20  
21    /**
22     * Depth-first search to traverse all edges in the De Bruijn graph.
23     * Each node represents an (n-1)-digit state, and edges represent adding a digit.
24     * 
25     * @param currentNode - Current (n-1)-digit state represented as a number
26     */
27    function traverseGraph(currentNode: number): void {
28        // Try adding each possible digit (0 to k-1) to the current state
29        for (let digit = 0; digit < k; digit++) {
30            // Create edge identifier by appending digit to current node
31            // Edge format: [currentNode][digit]
32            const edgeId: number = currentNode * 10 + digit;
33          
34            // Check if this edge has been visited
35            if (!visitedEdges.has(edgeId)) {
36                // Mark edge as visited
37                visitedEdges.add(edgeId);
38              
39                // Calculate next node by removing the first digit and keeping last (n-1) digits
40                const nextNode: number = edgeId % modValue;
41              
42                // Recursively traverse from the next node
43                traverseGraph(nextNode);
44              
45                // Add the digit to result (building string in reverse)
46                resultChars.push(digit.toString());
47            }
48        }
49    }
50  
51    // Start DFS from node 0 (representing "00...0" with n-1 zeros)
52    traverseGraph(0);
53  
54    // Add the initial (n-1) zeros to complete the string
55    // These represent the starting state of the first combination
56    resultChars.push('0'.repeat(n - 1));
57  
58    // Join all characters to form the final result
59    return resultChars.join('');
60}
61

Time and Space Complexity

The time complexity is O(k^n), and the space complexity is O(k^n).

Time Complexity Analysis:

  • The algorithm performs a depth-first search (DFS) on a De Bruijn graph where each node represents a string of length n-1 with k possible characters
  • The total number of edges in this graph equals the number of possible strings of length n, which is k^n
  • Each edge is visited exactly once due to the vis set tracking visited edges
  • Each edge visit involves constant time operations (set lookup, set insertion, modulo operation, string append)
  • Therefore, the time complexity is O(k^n)

Space Complexity Analysis:

  • The vis set stores all visited edges, which can contain up to k^n elements (all possible n-length strings)
  • The ans list stores the result string, which has length k^n + (n-1) in the worst case
  • The recursion stack depth is at most k^n in the worst case when traversing the entire graph
  • The dominant factor is O(k^n) for both the visited set and the recursion stack
  • Therefore, the space complexity is O(k^n)

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrect Edge Representation for k > 10

The most critical pitfall in this solution is that it assumes base-10 representation (u * 10 + x) which only works correctly when k ≀ 10. When k > 10, digits can be two-digit numbers (like 10, 11, etc.), and the edge calculation breaks down.

Problem Example:

  • If n=2, k=12, and we're at node representing "11", trying to append digit "10":
  • Current code: edge = 11 * 10 + 10 = 120
  • This incorrectly represents the sequence "11" + "10" β†’ "1110" as 120
  • The modulo operation 120 % 10 = 0 gives wrong next node

Solution: Use base-k arithmetic instead of base-10:

def crackSafe(self, n: int, k: int) -> str:
    def dfs(current_node: int) -> None:
        for digit in range(k):
            # Use base-k representation
            edge = current_node * k + digit  # Changed from * 10
          
            if edge not in visited_edges:
                visited_edges.add(edge)
                next_node = edge % modulo
                dfs(next_node)
                result.append(str(digit))
  
    # Use k^(n-1) instead of 10^(n-1)
    modulo = k ** (n - 1)  # Changed from 10 ** (n - 1)
    visited_edges = set()
    result = []
  
    dfs(0)
    result.append("0" * (n - 1))
    return "".join(result)

2. Stack Overflow for Large Inputs

The recursive DFS can cause stack overflow when k^n is large (many edges to traverse).

Solution: Use iterative DFS with explicit stack:

def crackSafe(self, n: int, k: int) -> str:
    modulo = k ** (n - 1)
    visited_edges = set()
    result = []
  
    # Iterative DFS using stack
    stack = [0]
    # Track next digit to try for each node
    next_digit = {i: 0 for i in range(modulo)}
  
    while stack:
        u = stack[-1]
        if next_digit[u] < k:
            digit = next_digit[u]
            next_digit[u] += 1
            edge = u * k + digit
            if edge not in visited_edges:
                visited_edges.add(edge)
                v = edge % modulo
                stack.append(v)
        else:
            # All edges from u have been explored
            if len(stack) > 1:  # Not the initial node
                # Calculate which digit led to this node
                prev = stack[-2]
                for d in range(k):
                    if (prev * k + d) % modulo == u:
                        result.append(str(d))
                        break
            stack.pop()
  
    result.append("0" * (n - 1))
    return "".join(result)

3. Edge Case: n = 1

When n = 1, the graph degenerates to a single node with k self-loops. The current approach still works but is unnecessarily complex.

Solution: Add special handling for n = 1:

def crackSafe(self, n: int, k: int) -> str:
    if n == 1:
        # Simply concatenate all digits from 0 to k-1
        return "".join(str(i) for i in range(k))
  
    # Rest of the original algorithm...

4. Missing Validation for k = 1

When k = 1, there's only one possible digit (0), so the only possible password is "000...0" (n zeros).

Solution: Add special case handling:

def crackSafe(self, n: int, k: int) -> str:
    if k == 1:
        return "0" * n
  
    # Rest of the algorithm...
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of these properties could exist for a graph but not a tree?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More