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.
n
and k
.n
.
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.
The optimal solution uses the concept of a de Bruijn sequence. Here's a step-by-step breakdown:
k
and length n
is a cyclic sequence in which every possible substring of length n
appears exactly once.n-1
characters to the end, ensuring all substrings are present in a linear string.n-1
.0
to k-1
to the node, resulting in a new node (shift left and add digit).n-1
zeros (e.g., "00...0").This approach ensures we generate the shortest possible string containing all combinations.
Let's walk through the problem with n = 2
and k = 2
(digits: 0 and 1, combinations of length 2).
This demonstrates how the algorithm systematically covers all combinations with minimal overlap.
O(k^n \times n)
(generate all combinations, each of length n)O(k^n \times n)
(store all combinations in a long string)O(k^n)
(each combination is visited once)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
.
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.
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('');
};