【每日一题Day83】LC753破解保险箱 | dfs 贪心

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

破解保险箱【LC753】

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 recent 3 digits is "0", which is incorrect.
    • After typing 1, the most recent 3 digits is "01", which is incorrect.
    • After typing 2, the most recent 3 digits is "012", which is incorrect.
    • After typing 3, the most recent 3 digits is "123", which is incorrect.
    • After typing 4, the most recent 3 digits is "234", which is incorrect.
    • After typing 5, the most recent 3 digits 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 ( k n ) O(k^n) O(kn)
      • 空间复杂度: O ( k n − 1 ) O(k^{n-1}) O(kn1)
贪心
  • 思路:贪心

    上面的搜索过程之所以会出现有部分边未遍历的情况,是因为起始点回的太早,那么如果可以尽可能晚回起点,那么就能遍历更多的边

    • 证明
      • 最终我们回到起点密码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 ( k n ) O(k^n) O(kn)
      • 空间复杂度: O ( k n − 1 ) O(k^{n-1}) O(kn1)
  • 实现:数组代替哈希表,数组的下标是字符串节点的整数值,值为该节点已经选择的最小数值

    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 ( k n ) O(k^n) O(kn)
      • 空间复杂度: O ( k n − 1 ) O(k^{n-1}) O(kn1)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值