Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

753. Cracking the Safe - Leetcode Solution

Problem Description

The "Cracking the Safe" problem asks you to generate the shortest possible string that contains every possible combination of a password lock. The lock has n digits, and each digit can be any value from 0 to k-1. Each combination is a string of length n using these digits. The goal is to find a string such that every possible combination of length n appears as a substring at least once. You must return one valid solution (there may be several), and each combination should appear exactly once as a substring.

  • Input: Two integers, n and k.
  • Output: A string representing the shortest sequence that "cracks the safe".
  • Constraints: Each combination must appear exactly once as a substring of length n.

Thought Process

At first glance, the problem seems like a brute-force search: generate every possible combination of length n using digits 0 to k-1 and concatenate them. However, this would result in a very long string, much longer than necessary, as there would be a lot of overlap between combinations.

To optimize, we need to find a way to overlap the combinations as much as possible. This is similar to finding a path through all possible combinations where each new combination shares a prefix or suffix with the previous one. This concept is related to de Bruijn sequences, which are known to contain every possible substring of length n over a given alphabet exactly once.

The challenge is then to construct such a sequence efficiently. Instead of brute-force, we try to systematically build the string so that every combination appears exactly once, leveraging overlaps.

Solution Approach

The optimal solution uses the concept of a de Bruijn sequence. Here's a step-by-step breakdown:

  1. Understanding de Bruijn Sequences:
    • A de Bruijn sequence for alphabet size k and length n is a cyclic sequence in which every possible substring of length n appears exactly once.
    • For this problem, we can "unwrap" (linearize) the cyclic sequence by appending the first n-1 characters to the end, ensuring all substrings are present in a linear string.
  2. Graph Representation:
    • Imagine each node as a string of length n-1.
    • Each edge corresponds to appending a digit 0 to k-1 to the node, resulting in a new node (shift left and add digit).
  3. Hierholzer's Algorithm for Eulerian Path:
    • We can perform a depth-first search (DFS) to traverse every edge exactly once, which corresponds to visiting every possible combination.
    • We track visited edges (combinations) to avoid repeats.
    • Each time we traverse an edge, we append the digit to our result.
  4. Implementation Details:
    • Start with a node of n-1 zeros (e.g., "00...0").
    • Recursively try all possible digits for the next position.
    • After visiting all edges from a node, append the digit to the result.
    • Finally, add the starting node to the result to complete the sequence.

This approach ensures we generate the shortest possible string containing all combinations.

Example Walkthrough

Let's walk through the problem with n = 2 and k = 2 (digits: 0 and 1, combinations of length 2).

  • All possible combinations: "00", "01", "10", "11".
  • We start with "0".
  • Try appending "0": get "00" (first combination).
  • Try appending "1": get "01" (second combination).
  • Now, from "1", try appending "0": get "10" (third combination).
  • From "0", try appending "1": get "11" (fourth combination).
  • After traversing all, the sequence will be "00110".
  • Check substrings of length 2: "00", "01", "11", "10" — all combinations are present.

This demonstrates how the algorithm systematically covers all combinations with minimal overlap.

Time and Space Complexity

  • Brute-force Approach:
    • Time Complexity: O(k^n \times n) (generate all combinations, each of length n)
    • Space Complexity: O(k^n \times n) (store all combinations in a long string)
  • Optimized (de Bruijn Sequence) Approach:
    • Time Complexity: O(k^n) (each combination is visited once)
    • Space Complexity: O(k^n) (to track visited combinations and build the result string)

The optimized approach is exponentially faster and more memory-efficient than brute-force, especially for larger values of n and k.

Summary

The "Cracking the Safe" problem is elegantly solved using the concept of de Bruijn sequences, which efficiently overlap combinations to minimize the sequence length. By modeling the problem as traversing all edges in a directed graph (using DFS and Hierholzer's algorithm), we ensure every possible password combination appears exactly once. This approach is both optimal and elegant, making it a classic example of applying graph theory to real-world combinatorial problems.

Code Implementation

class Solution:
    def crackSafe(self, n: int, k: int) -> str:
        seen = set()
        res = []
        start = '0' * (n - 1)
        
        def dfs(node):
            for x in map(str, range(k)):
                nei = node + x
                if nei not in seen:
                    seen.add(nei)
                    dfs(nei[1:])
                    res.append(x)
        dfs(start)
        return start + ''.join(res[::-1])
      
class Solution {
public:
    string crackSafe(int n, int k) {
        unordered_set<string> seen;
        string res, start(n - 1, '0');
        function<void(string)> dfs = [&](string node) {
            for (int i = 0; i < k; ++i) {
                string nei = node + to_string(i);
                if (!seen.count(nei)) {
                    seen.insert(nei);
                    dfs(nei.substr(1));
                    res += to_string(i);
                }
            }
        };
        dfs(start);
        return start + string(res.rbegin(), res.rend());
    }
};
      
class Solution {
    public String crackSafe(int n, int k) {
        Set<String> seen = new HashSet<>();
        StringBuilder res = new StringBuilder();
        StringBuilder start = new StringBuilder();
        for (int i = 0; i < n - 1; i++) start.append('0');
        dfs(start.toString(), seen, res, k, n);
        res.append(start);
        return res.toString();
    }
    
    private void dfs(String node, Set<String> seen, StringBuilder res, int k, int n) {
        for (int i = 0; i < k; i++) {
            String nei = node + i;
            if (!seen.contains(nei)) {
                seen.add(nei);
                dfs(nei.substring(1), seen, res, k, n);
                res.append(i);
            }
        }
    }
}
      
var crackSafe = function(n, k) {
    const seen = new Set();
    let res = [];
    const start = '0'.repeat(n - 1);
    function dfs(node) {
        for (let i = 0; i < k; i++) {
            const nei = node + i;
            if (!seen.has(nei)) {
                seen.add(nei);
                dfs(nei.slice(1));
                res.push(i);
            }
        }
    }
    dfs(start);
    return start + res.reverse().join('');
};