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 digits0
tok-1
- Each edge from node
u
to nodev
is labeled with a digitc
(from0
tok-1
) - The edge represents that appending digit
c
to nodeu
and taking the lastn-1
characters gives you nodev
- 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 ann
-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.
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:
- Each edge represents a unique
n
-digit password - Traversing each edge once means including each password exactly once
- 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 digitvis
: A set to track which edges (passwords) we've already usedans
: 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:
- From current node
u
, we try allk
possible digits (x
from0
tok-1
) e = u * 10 + x
creates the edge identifier (the fulln
-digit password). For example, ifu=12
andx=3
, thene=123
- We check if this edge/password has been visited
- If not visited, we mark it as visited and calculate the next node:
v = e % mod
. This extracts the last(n-1)
digits. Fore=123
andmod=100
, we getv=23
- We recursively call DFS on the new node
v
- 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 EvaluatorExample 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")
- Append
- 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")
- Append
- From node
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
withk
possible characters - The total number of edges in this graph equals the number of possible strings of length
n
, which isk^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 tok^n
elements (all possible n-length strings) - The
ans
list stores the result string, which has lengthk^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...
Which of these properties could exist for a graph but not a tree?
Recommended Readings
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos graph svg Graph Fundamentals Tree with 0 cycle At this point you should be pretty familiar with trees A tree is a special kind of graph a connected acyclic cycle less graph A graph may contain cycle s and nodes could be disconnected A tree
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Donβt Miss This!