Leetcode 753. Cracking the Safe

Problem Explanation

The problem is asking to return any password of minimum length that can be guaranteed to open a box. The password should be a sequence of n digits where each digit can be one of the first k digits which are 0, 1, ..., k-1. For instance, if the original password is "345", it can be opened by typing "012345" because the correct password matches the suffix of the entered password.

For example: for a correct password "23" and k=2 to unlock the safe, the digit sequence could be "012300". Here, when the sequence is entered from left to right the correct password "23" appears at the point where "23" is entered, and similarly, the correct password "00" appears when at the start - which also unlocks the safe.

Solution Approach

This solution takes a depth first search approach. We can use a recursive function, that generates a sequence. This function iteratively adds one digit at a time from 0 to k to the current sequence, then checks if the new sequence is present in the set, if not, it adds the sequence into the set and continue the search until all possible combinations are searched. If all combinations has been searched and nothing is found then it backtrack and delete the digit from the sequence and set.

Starting from the node that has original n size password, we branch out to its neighbouring nodes by adding one more digit at the end. We continue the same process for each node and its neighbours until we explored all the nodes.

Pseudo code

  1. Initialize the starting password (a string of size n filled with '0'), and add it to the seen set.
  2. Call the recursive function dfs to construct the minimal password.
  3. The recursive function dfs first checks if the size of the seen set is equal to the number of all possible passwords, if so, implying we covered all the possible passwords in the sequence, so we can return True.
  4. If not all passwords have been covered, we try to append one digit from 0 to k to the current password, and check if it has been added to the seen set. If not, we add the new password to the seen set and continue searching, if we can find a valid sequence, we return True. Otherwise, we will remove the new password from the seen set and try the next digit.
  5. If we tried all possible digits and none of them can help to find a valid sequence, we return False to trigger backtracking.

Python Solution

1
2python
3class Solution:
4 def crackSafe(self, n: int, k: int) -> str:
5    password = "0" * n
6    seen = {password}
7    passwordSize = k ** n
8
9    def dfs():
10        if len(seen) == passwordSize:
11            return True
12        
13        prefix = password[len(password)-n+1:]
14        for i in range(k-1, -1, -1):
15            newPassword = prefix + str(i)
16            if newPassword not in seen:
17                seen.add(newPassword)
18                password += str(i)
19                if dfs():
20                    return True
21                seen.remove(newPassword)
22                password = password[:len(password)-1]
23        return False
24    
25    dfs()
26    return password

We call the dfs function inside the crackSafe function. We add each valid combination to the seen set and also add it to the result string.

Java Solution

1
2java
3class Solution {
4public String crackSafe(int n, int k) {
5    StringBuilder sb = new StringBuilder();
6    HashSet<String> visited = new HashSet<>();
7    StringBuilder start = new StringBuilder();
8
9    for(int i=0; i<n; i++){
10        start.append('0');
11    }
12
13    dfs(start, visited, (int)Math.pow(k,n));
14    return sb.toString()+start;
15}
16
17public void dfs(StringBuilder sb, HashSet<String> visited, int total){
18    if(total == visited.size())
19        return;
20
21    sb.append('0');
22    String sNew = sb.toString();
23    for(int i = 0; i <= 9; i++) {
24        sNew = sNew.substring(0, sNew.length()-1) + i;
25        if(!visited.contains(sNew)) {
26            visited.add(sNew);
27            dfs(sb.append(i), visited, total);
28            if(visited.size()==total)
29                return;
30            sb.deleteCharAt(sb.length()-1);
31            visited.remove(sNew);
32        }
33    }
34}
35}

In Java, we use StringBuilder instead of strings, the StringBuilder being more efficient because it is mutable, allowing in-place modifications.

JavaScript Solution

1
2javascript
3class Solution {
4  crackSafe(n, k) {
5    const seen = new Set();
6    const ans = Array(n).fill('0').join('');
7
8    (function dfs(node) {
9      for (let x = 0; x < k; ++x) {
10        const nei = node + x;
11        if (!seen.has(nei)) {
12          seen.add(nei);
13          dfs(nei.slice(1));
14          ans += x;
15        }
16      }
17    })(ans);
18
19    return ans;
20  }
21}

The JavaScript equivalent uses a Set instead of unordered_Set and Array instead of string to represent the password string.

C++ Solution

1
2cpp
3class Solution {
4public:
5    string crackSafe(int n, int k) {
6        string ans = string(n, '0');
7        unordered_set<string> visited{ans};
8        int total = pow(k, n);
9
10        dfs(ans, visited, total, n, k);
11        return ans;
12    }
13
14private:
15    void dfs(string& path, unordered_set<string>& visited, const int total, const int n, const int k) {
16        if(visited.size() == total) return;
17        
18        string node = path.substr(path.size() - n + 1);
19        for(char c = '0'; c < '0' + k; ++c) {
20            node.push_back(c);
21            if(!visited.count(node)) {
22                visited.insert(node);
23                path.push_back(c);
24                dfs(path, visited, total, n, k);
25                if(visited.size() == total) return;
26                visited.erase(node);
27                path.pop_back();
28                }
29            node.pop_back();
30        }
31    }
32};

In C++, we use unordered_set and string STL to simulate the above approach.

C# Solution

1
2csharp
3public class Solution {
4    public string CrackSafe(int n, int k) {
5        if (n == 1 && k == 1) return "0";
6        var visited = new HashSet<string>();
7        var result = new StringBuilder();
8        result.Append(new string('0', n));
9        visited.Add(result.ToString());
10
11        DFS(n, k, (int)Math.Pow(k,n), visited, result);
12        return result.ToString();
13    }
14    
15    private void DFS(int n, int k, int total, HashSet<string> visited, StringBuilder result)
16    {
17        if (visited.Count == total) return;
18
19        var last = result.ToString(result.Length - n + 1, n - 1);
20        for (char c = '0'; c < '0' + k; c++)
21        {
22            var next = last + c;
23            if (!visited.Contains(next))
24            {
25                visited.Add(next);
26                result.Append(c);
27                DFS(n, k, total, visited, result);
28                if (visited.Count == total) return;
29                visited.Remove(next);
30                result.Remove(result.Length - 1, 1);
31            }
32        }
33    }
34}

In C#, similarly we use a StringBuilder to store our answer and a HashSet for visited nodes. We generate our answer recursively. If all nodes are visited then we return from our recursion. Otherwise, we append a character to our temp answer and visit the node (by adding it to our visited set). If all nodes are still not visited then we backtrack by removing the node from our visited set and removing the last character from our answer.So our final solution in every language involves creating a function which traverses through all possible n digit combinations and stores them. If all combinations have been seen (meaning the box has been opened), we stop. Otherwise we keep adding digits and checking if the current password has already been seen.

This approach is known as Depth First Search (DFS), since we explore as far down each path as possible before backtracking and exploring other options. Here, 'branching out' means adding each of the possible k digits to the current password and seeing if a valid sequence can be found. If after exhausting all k options no valid sequence can be found, we remove the previously added digit (backtrack) and try a new one.

Conclusion

The key to this problem is to realize that we want the smallest length password which is guaranteed to 'crack' the box. Thus, the problem is not as much about finding the right password, but more about ensuring all n digit combinations are covered within some sequence.

Once we realize this, it becomes clear that we're not really finding a password, but more of a 'super password' which contains in it, all possible smaller passwords of n digits based on the k possibilities for each digit. This makes the problem much easier to conceptualize and reduces it to a more straightforward DFS implementation.

This 'super password' will be of size n+k^n-1, if k and n are greater than one otherwise it will be of size n. The extra k^n-1 comes from all the possible passwords of size n.

When k=1 or n=1, we have either one digit or one possibility repeated n times.

In the end, the solution involves using DFS to generate all possible combinations. We explore each possibility by appending a digit and we track all the combinations we've seen already to avoid duplications.

The cracker simply tries an ordered sequence of all possible n digit combinations. It can do this because it knows that at some point, it will stumble upon the correct password.

It's just brute force - but guided with the knowledge of all possible combinations. And that's an art in itself. It is the perfect blend of cleverness and raw power.

Coding it in Python, Java, JavaScript, and others shows us that the implementation may be language specific, but the underlying logic and perspective remains the same - make sure every possibility is covered and you will eventually find the answer.

Thus we see how an initial understanding of the problem can drastically reduce the complexity and guide us in coding an efficient and successful solution.


Got a question? Ask the Teaching Assistant anything you don't understand.

Still not clear? Ask in the Forum,  Discord or Submit the part you don't understand to our editors.


TA 👨‍🏫