Cracking the Safe
HardPrompt
There is a safe protected by a password. The password is a sequence of n digits where each digit can be in the range [0, k - 1].
The safe has a peculiar way of checking the password. When you enter in a sequence, it checks the most recent n digits that were entered each time you type a digit.
- For example, the correct password is
"345"and you enter in"012345":- After typing
0, the most recent3digits is"0", which is incorrect. - After typing
1, the most recent3digits is"01", which is incorrect. - After typing
2, the most recent3digits is"012", which is incorrect. - After typing
3, the most recent3digits is"123", which is incorrect. - After typing
4, the most recent3digits is"234", which is incorrect. - After typing
5, the most recent3digits is"345", which is correct and the safe unlocks.
- After typing
Return any string of minimum length that will unlock the safe at some point of entering it.
Example 1:
Input: n = 1, k = 2
Output: "10"
Explanation: The password is a single digit, so enter each digit. "01" would also unlock the safe.Example 2:
Input: n = 2, k = 2
Output: "01100"
Explanation: For each possible password:
- "00" is typed in starting from the 4th digit.
- "01" is typed in starting from the 1st digit.
- "10" is typed in starting from the 3rd digit.
- "11" is typed in starting from the 2nd digit.
Thus "01100" will unlock the safe. "10011", and "11001" would also unlock the safe.
Constraints:
1 <= n <= 41 <= k <= 101 <= kn <= 4096
Approaches
2 approaches with complexity analysis and trade-offs.
This approach involves building the result string incrementally using a backtracking algorithm. We start with an initial password (e.g., n zeros). Then, we try to extend this string one character at a time. At each step, we consider appending each possible digit. If appending a digit creates a new, unseen password, we make that move and continue the search recursively. If we hit a dead end or a path that doesn't lead to a full solution, we backtrack by undoing the last move. This process continues until we have generated a string that contains all k^n possible passwords.
Algorithm
- Use a
HashSet<String>to keep track of all the uniquen-digit passwords we have formed so far. - Use a
StringBuilderto construct our candidate superstring. - Start by initializing the
StringBuilderwithnzeros and adding this initial password to our set ofvisitedpasswords. - The core of the algorithm is a recursive
dfsfunction that returnstrueupon finding a complete solution. - In
dfs, first check if the size of ourvisitedset equalsk^n. If it does, we have found all passwords, and the search is complete, so we returntrue. - If not, take the last
n-1characters from ourStringBuilder. This forms the prefix for the next potential password. - Loop through all possible digits
d. Appenddto the prefix to form anewPassword. - If
newPasswordis not in ourvisitedset, it's a valid next move. AddnewPasswordto the set, appenddto ourStringBuilder, and make a recursive call todfs. - If the recursive call returns
true, it means a solution was found down that path, so we propagatetrueup the call stack. - If the recursive call returns
false, we must backtrack. We removenewPasswordfrom thevisitedset and delete the last characterdfrom ourStringBuilderto explore other possibilities. - Since a solution is always possible, this search is guaranteed to find a valid string.
Walkthrough
We use a HashSet<String> to keep track of all the unique n-digit passwords we have formed so far and a StringBuilder to construct our candidate superstring. We start by initializing the StringBuilder with n zeros and adding this initial password to our set of visited passwords. The core of the algorithm is a recursive dfs function that returns true upon finding a complete solution. In dfs, we first check if the size of our visited set equals k^n. If it does, we have found all passwords, and the search is complete, so we return true. If not, we take the last n-1 characters from our StringBuilder to form the prefix for the next potential password. We then loop through all possible digits d, appending d to the prefix to form a newPassword. If newPassword is not in our visited set, it's a valid next move. We add newPassword to the set, append d to our StringBuilder, and make a recursive call to dfs. If the recursive call returns true, it means a solution was found down that path, so we propagate true up the call stack. If the recursive call returns false, we must backtrack by removing newPassword from the visited set and deleting the last character d from our StringBuilder to explore other possibilities. Since a solution is always possible, this search is guaranteed to find a valid string.
class Solution { private int totalPasswords; private int n; private int k; private Set<String> visited; private StringBuilder result; public String crackSafe(int n, int k) { this.n = n; this.k = k; this.totalPasswords = (int) Math.pow(k, n); this.visited = new HashSet<>(); this.result = new StringBuilder(); for (int i = 0; i < n; i++) { result.append('0'); } visited.add(result.toString()); if (dfs()) { return result.toString(); } return ""; // Should not be reached } private boolean dfs() { if (visited.size() == totalPasswords) { return true; } String prefix = result.substring(result.length() - n + 1); for (int i = 0; i < k; i++) { String password = prefix + i; if (!visited.contains(password)) { visited.add(password); result.append(i); if (dfs()) { return true; } // Backtrack visited.remove(password); result.deleteCharAt(result.length() - 1); } } return false; }}Complexity
Time
`O(n * k^n)`. The search space is a graph with `k^n` edges. The DFS will traverse each edge once. At each step, creating the new password string and performing set operations takes `O(n)` time.
Space
`O(n * k^n)`. This is dominated by the `visited` set which stores `k^n` passwords of length `n`. The recursion stack can also go as deep as `k^n`.
Trade-offs
Pros
It's a direct and intuitive application of backtracking for this type of construction problem.
Cons
The repeated modification of the result
StringBuilder(append and delete) can be slightly less efficient than a single final construction.The deep recursion can potentially lead to a
StackOverflowErrorfor the maximum possiblek^n(4096), although it's often fine in practice.
This is a more elegant and standard approach that frames the problem in terms of graph theory. We can model the problem as finding an Eulerian path in a De Bruijn graph. The nodes of this graph are all possible prefixes of length n-1, and the directed edges represent appending a digit to form a password of length n. Since this graph is guaranteed to have an Eulerian path (a path that visits every edge exactly once), our goal is to find it. The sequence of digits appended along this path, when combined with the starting prefix, gives the shortest superstring. Hierholzer's algorithm, commonly implemented with a post-order DFS, is perfect for this task.
Algorithm
- Model the problem as a graph where nodes are prefixes of length
n-1and edges are thekpossible digits to append. - Use a
Set<String>to keep track of visited edges (which are the full passwords of lengthn). - Use a
StringBuilderto build the sequence of appended digits. - Implement a post-order DFS function
dfs(String node). - Inside
dfs(node), loop through all possible next digitsd. - For each
d, form the edge (password)p = node + d. - If
phas not been visited, mark it as visited and recursively calldfson the destination node, which isp.substring(1). - After the recursive call for a given
dreturns, appenddto the resultStringBuilder. This is the "post-order" step. - Start the process by calling
dfson an initial node, typicallyn-1zeros. - After the initial
dfscall completes, theStringBuilderwill contain thek^ndigits that form the path, but in reverse order. - The final string is the starting node concatenated with the reversed
StringBuilder.
Walkthrough
The algorithm explores the implicit graph of prefixes. A node is a string of n-1 digits. An edge from node u is formed by appending a digit d, creating a password u+d. The destination node of this edge is the last n-1 characters of the password, i.e., (u+d).substring(1). We use a Set<String> to keep track of visited edges (passwords) and a StringBuilder to build the final sequence of appended digits. The algorithm is a recursive DFS function, dfs(String node). Inside dfs(node), we loop through all possible next digits d. For each d, we form the edge (password) p = node + d. If p has not been visited, we mark it as visited and recursively call dfs on the destination node, which is p.substring(1). Crucially, after the recursive call for a given d returns, we append d to our result StringBuilder. This is the "post-order" step. It ensures that a digit is added to the result only after the entire path starting from its corresponding edge has been fully explored. We start the process by calling dfs on an initial node, typically n-1 zeros. After the initial dfs call completes, the StringBuilder will contain the k^n digits that form the path, but in reverse order of traversal. The final result is constructed by taking the starting node (the initial n-1 zeros), appending it to the StringBuilder, and then reversing the whole thing.
class Solution { private Set<String> visited; private StringBuilder result; private int k; public String crackSafe(int n, int k) { if (n == 1 && k == 1) return "0"; this.k = k; this.visited = new HashSet<>(); this.result = new StringBuilder(); StringBuilder startNodeBuilder = new StringBuilder(); for (int i = 0; i < n - 1; i++) { startNodeBuilder.append('0'); } String startNode = startNodeBuilder.toString(); dfs(startNode); result.append(startNode); return result.reverse().toString(); } private void dfs(String node) { for (int i = 0; i < k; i++) { String password = node + i; if (!visited.contains(password)) { visited.add(password); dfs(password.substring(1)); result.append(i); } } }}Complexity
Time
`O(n * k^n)`. The algorithm visits each of the `k^n` edges of the De Bruijn graph exactly once. The work done per edge involves string manipulations and set operations, taking `O(n)` time.
Space
`O(n * k^n)`. The space is dominated by the `visited` set, which stores `k^n` passwords of length `n`, and the recursion stack, which can reach a depth of `k^n`.
Trade-offs
Pros
It's an elegant and efficient algorithm based on a solid mathematical foundation (De Bruijn sequences).
The result string is constructed once at the end, which can be more efficient than repeated modifications.
Cons
The logic of post-order traversal and final string construction can be less intuitive than a direct backtracking approach.
Like the backtracking approach, it uses a recursive implementation that could face stack depth limits, although an iterative version using an explicit stack is also possible.
Solutions
Solution
class Solution {private Set<Integer> vis = new HashSet<>();private StringBuilder ans = new StringBuilder();private int mod;public String crackSafe(int n, int k) { mod = (int)Math.pow(10, n - 1); dfs(0, k); ans.append("0".repeat(n - 1)); return ans.toString(); }private void dfs(int u, int k) { for (int x = 0; x < k; ++x) { int e = u * 10 + x; if (vis.add(e)) { int v = e % mod; dfs(v, k); ans.append(x); } } }}class Solution { public: string crackSafe ( int n , int k ) { unordered_set < int > vis ; int mod = pow ( 10 , n - 1 ); string ans ; function < void ( int ) > dfs = [ & ]( int u ) { for ( int x = 0 ; x < k ; ++ x ) { int e = u * 10 + x ; if ( ! vis . count ( e )) { vis . insert ( e ); dfs ( e % mod ); ans += ( x + '0' ); } } }; dfs ( 0 ); ans += string ( n - 1 , '0' ); return ans ; } };class Solution: def crackSafe(self, n: int, k: int) -> str: 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)) mod = 10 ** (n - 1) vis = set() ans = [] dfs(0) ans . append("0" * (n - 1)) return "" . join(ans)Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.