破解保险箱【LC753】
There is a safe protected by a password. The password is a sequence of
ndigits 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
ndigits 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.Return any string of minimum length that will unlock the safe at some point of entering it.
有点难的…
欧拉回路:通过图中所有边一次且仅一次行遍所有顶点的回路称为欧拉回路
dfs
-
思路:
-
题意要求输入一个字符串,能够打开保险柜,密码的长度为n,每位数字小于k,因此题意可以转化为找到一个最短字符串,其包含了n位k进制所有的排列组合
-
由于每次值取最后一个字符进行移动,那么对于n位密码,将n-1位的所有可能组合看做一个点,对于每个节点会有k条入边和k条出边,因此它一定存在一个欧拉回路
-
如何找到最短字符串?
从00出发进行dfs搜索没有加入过的边,直至回到了该节点。此时图中仍有边未遍历到,那么从某个节点v开始,继续搜寻,然后将该路径嵌入,直至所有路径全部被添加。注意:需要将前缀加入结果集末尾
-
-
实现
- 使用HashSet存储已经遍历过的n位k进制组合,然后
class Solution { Set<Integer> seen = new HashSet<Integer>(); public String crackSafe(int n, int k) { StringBuilder sb = new StringBuilder(); int nodeNum = (int)Math.pow(k, n - 1); dfs(0, k, sb, nodeNum); for (int i = 1; i < n; i++){ sb.append('0'); } return new String(sb); } public void dfs(int node, int k, StringBuilder sb, int nodeNum){ for (int i = 0; i < k; i++){ int x = node * k + i; if (seen.add(x)){ dfs(x % nodeNum, k, sb, nodeNum); sb.append(i); } } } }- 复杂度
- 时间复杂度:O(kn)O(k^n)O(kn)
- 空间复杂度:O(kn−1)O(k^{n-1})O(kn−1)
贪心
-
思路:贪心
上面的搜索过程之所以会出现有部分边未遍历的情况,是因为起始点回的太早,那么如果可以尽可能晚回起点,那么就能遍历更多的边
- 证明
- 最终我们回到起点密码0000,因为是从大到小取数的,说明之前已经有k-1次走到状态000,取走了1~k-1。
- 那这k次到达状态000是怎么来的呢?是尝试过密码x000后走到的。也就是说,我们一定尝试过所有k个形如x000的密码。
- 同理,当我们尝试某个密码x000时一定是第k次到达状态x00,所以我们一定尝试过所有形如yx00的密码。
- 以此类推,我们一定已经尝试过所有可能的密码。
- 证明
-
HashMap实现
选择00…作为起始点,每次要选择添加的数字时,从大数字开始,尽可能晚回到起始点
class Solution { public String crackSafe(int n, int k) { StringBuilder sb = new StringBuilder(); int edgesNum = (int)Math.pow(k, n); Map<String,Integer> strToVal = new HashMap<>();// 存储节点已经选择的最小数值 下一个可以选择的数值是其减1 // 字符串的最短长度为 n - 1 + edgesNum for (int i = 1; i < n; i++){// 初始0 节点为n-1位 sb.append('0'); } // 遍历所有边 while (edgesNum > 0){ String str = sb.substring(sb.length() - n + 1, sb.length()); strToVal.put(str, strToVal.getOrDefault(str, k) - 1); // if (!strToVal.containsKey(str)){ // strToVal.put(str, k - 1); // }else{ // strToVal.put(str, strToVal.get(str) - 1); // } sb.append(strToVal.get(str)); edgesNum--; } return new String(sb); } }- 复杂度
- 时间复杂度:O(kn)O(k^n)O(kn)
- 空间复杂度:O(kn−1)O(k^{n-1})O(kn−1)
- 复杂度
-
实现:数组代替哈希表,数组的下标是字符串节点的整数值,值为该节点已经选择的最小数值
class Solution { public String crackSafe(int n, int k) { int edgesNum = (int)Math.pow(k, n), nodeNum = (int)Math.pow(k, n - 1); int[] nodeToVal = new int[nodeNum];// 存储节点已经选择的最小数值 下一个可以选择的数值是其减1 Arrays.fill(nodeToVal, k);// 初始化值为k 下一个可选择的数值为k-1 StringBuilder sb = new StringBuilder(); // 字符串的最短长度为 n - 1 + edgesNum for (int i = 1; i < n; i++){// 添加初始0 n-1个 sb.append('0'); } // 遍历所有边 for (int i = 0, idx = 0; i < edgesNum; i++){ int edge = --nodeToVal[idx]; sb.append(edge); idx = (idx * k + edge) % nodeNum;// 去除最高位加入新的一位后节点的值 } return new String(sb); } }- 复杂度
- 时间复杂度:O(kn)O(k^n)O(kn)
- 空间复杂度:O(kn−1)O(k^{n-1})O(kn−1)
- 复杂度

文章讨论了一种破解保险箱密码的方法,其中密码是由n位数字组成,范围在[0,k-1]之间。通过建立图论模型,利用深度优先搜索(DFS)和贪心策略寻找最短的字符串,确保在输入过程中能解锁保险箱。两种方法都涉及到遍历所有可能的n位k进制组合,一种基于DFS寻找欧拉回路,另一种通过贪心策略延迟回起点,保证所有边都被遍历。复杂度均为O(k^n)空间复杂度和O(kn)时间复杂度。

被折叠的 条评论
为什么被折叠?



