0
点赞
收藏
分享

微信扫一扫

自由之路:拨动表盘ring,按出密码key,所需最小步数或最小代价是多少?

自由之路:拨动表盘ring,按出密码key,所需最小步数或最小代价是多少?

提示:dfs的经典运用


文章目录

题目

电子游戏“辐射4”中,任务 “通向自由” 要求玩家到达名为 “Freedom Trail Ring” 的金属表盘,并使用表盘拼写特定关键词才能开门

给定一个字符串 ring ,表示刻在外环上的编码;
给定另一个字符串 key ,表示需要拼写的关键词。您需要算出能够拼写关键词中所有字符的最少步数

最初,ring 的第一个字符与 12:00 方向对齐。您需要顺时针或逆时针旋转 ring 以使 key 的一个字符在 12:00 方向对齐,然后按下中心按钮,以此逐个拼写完 key 中的所有字符。

旋转 ring 拼出 key 字符 key[i] 的阶段中:

您可以将 ring 顺时针或逆时针旋转 一个位置 ,计为1步。旋转的最终目的是将字符串 ring 的一个字符与 12:00 方向对齐,并且这个字符必须等于字符 key[i] 。
如果字符 key[i] 已经对齐到12:00方向,您需要按下中心按钮进行拼写,这也将算作 1 步。按完之后,您可以开始拼写 key 的下一个字符(下一阶段), 直至完成所有拼写。


一、审题

图1

示例1:输入: ring = “godding”, key = “gd”
输出: 4
解释:
对于 key 的第一个字符 ‘g’,已经在正确的位置, 我们只需要1步来拼写这个字符。
对于 key 的第二个字符 ‘d’,我们需要逆时针旋转 ring “godding” 2步使它变成 “ddinggo”。
当然, 我们还需要1步进行拼写,也就是按下button按钮。
因此最终的输出是 4。

示例2:
输入: ring = “godding”, key = “godding”
输出: 13


二、解题

观察,ring中可能有重复的字符,因此我们在按之前,要旋转多少步,肯定是旋转最少的步骤,
不妨设,你现在12点中方向对着ring中的i位置【比如g】,ring总共有N个字符【godding,N=7】,
你下次要旋转到目标字符(key[k])【gd中要寻找去对d字符的话】在ring中j位置【在godding中,j=2是最近的位置】,
那你旋转的最小代价其实很好算:j-i的绝对值即可
当然,你不能排除往左(逆时针找)找的情况,也就是先向左走i个位置,再顺着N-1往左走N-j个位置
看图:
图2
因此,我们知道了ring的长度N,知道了此刻ring对准12点方向的位置是ring的i,则求下一个key[k]在ring中的位置j以后,我们可以算出最小代价为:min(|j-i|,i-0 + N-j),即顺时针(右边)找,逆时针(左边)找的的代价最小值:

//假设12点顶针在i处,需要将j转到顶针处,请问需要旋转多少步,实际上不是顺时针就是逆时针
    //如果str的长为N,则从i转到j有两条路:
    //顺时针:|j-i|就是步数
    //逆时针先去0,再绕到N-1那,|N-j|+|i-0|就是
    //也就是N-max(i,j)+min(i,j)-0,很简单的一个公式
    public static int dial(int i, int j, int N){
        return Math.min(Math.abs(i - j),
                N - Math.max(i, j) + Math.min(i, j));
    }

本题的解题算法大流程是这样的:
最开始,观察ring,12点指向0字符,也就是说i=0,我们将这个目前被对齐12点的位置i改称preIndex
你有一个key字符串,肯定第一次要找哪个字符呢?自然找key[0],找哪个字符,我们定义为找keyIndex那个,即第一次从keyIndex=0开始找;

那很显然,来一个字符key[keyIndex],ring中可能存在多个key[keyIndex]字符,
那我们需要挨个都算一下代价,然后取最小代价,
因为你不知道究竟拨动到哪个字符更近?拨动的代价更小?
那就得在宏观调度中,挨个遍历一下所有可能的位置,然后取最小代价。
【也就是dfs一条道找,找完一个j,算一次代价,然后每次找完都更新最小代价】。

这就涉及到我们要摸清楚ring中究竟哪些位置j是key[keyIndex]字符的位置,也就是找到可能存在的j,
这些j,自然要用列表保存在哈希表中:
map:key是字符,value取这个key字符对应的ring中的所有下标【列表】

HashMap<Character, List<Integer>> map = new HashMap<>();

有了这个map
那么要找下一个字符key[keyIndex]
就只需要遍历ring中key[keyIndex]字符对应的位置下标j们,拨动ring,从preIndex去j,算一个拨动代价,按动button的代价,然后+ 再找下一个字符的最小代价。
与其他的位置j得到的代价,相比,取最小代价返回。

注意,当keyIndex=N之后,没有字符,代价为0,返回即可,整体返回需要int。

仔细领略下面代码:从i位置拨动,找key中keyIndex字符在ring中的位置j们,计算每一个j的代价,对比最小返回

//递归流程:
    //从顶针i出发,去拨动出key的keyIndex处的字符,
    // 随身携带map,key,和N,ring的个数,表盘字符个数,能得到的最小代价是
    public static int minStep(int i, int keyIndex, char[] key,
                              HashMap<Character, List<Integer>> map, int N){
        if (keyIndex == key.length) return 0;//搞定了所有不要代价返回

        //从顶针i位置去拨动,到j,这些j位置是map中放的key[keyIndex]所记录的位置,DFS遍历找到所有情况
        //然后计算代价的最小值

        int min = Integer.MAX_VALUE;//来到每个顶针i,都只要i到j的最小值
        for(Integer j : map.get(key[keyIndex])){
            //枚举所有可能的位置
            int cost = 1 + dial(i, j, N) +
                    minStep(j, keyIndex + 1, key, map, N);//搞定keyIndex这个字符代价+后续字符的代价
            min = Math.min(min, cost);//每个j位置要代价的最低值
        }

        //搞定以后,返回min
        return min;
    }

主函数,先把ring各个字符的位置们放入map
然后调用递归

//主函数,拿着ring和key处理
    public static int findMinSteps(String ring, String key){
        char[] r = ring.toCharArray();
        char[] k = key.toCharArray();

        int N = r.length;
        HashMap<Character, List<Integer>> map = new HashMap<>();
        for (int i = 0; i < N; i++) {
            if (!map.containsKey(r[i])) map.put(r[i], new ArrayList<>());
            map.get(r[i]).add(i);//直接抽出列表,添加就行了
        }

        return minStep(0, 0, k, map, N);
    }

修改暴力递归为傻缓存代码:

//改为记忆化搜索DP
    //递归流程:
    //从顶针i出发,去拨动出key的keyIndex处的字符,随身携带map,key,和N,ring的个数,表盘字符个数,能得到的最小代价是
    public static int minStepDP(int i, int keyIndex, char[] key, HashMap<Character, List<Integer>> map, int N,
                                int[][] dp){
        if (dp[i][keyIndex] != -1) return dp[i][keyIndex];


        if (keyIndex == key.length) {
            dp[i][keyIndex] = 0;
            return dp[i][keyIndex];//搞定了所有不要代价返回
        }

        //从顶针i位置去拨动,到j,这些j位置是map中放的key[keyIndex]所记录的位置,DFS遍历找到所有情况
        //然后计算代价的最小值

        int min = Integer.MAX_VALUE;//来到每个顶针i,都只要i到j的最小值
        for(Integer j : map.get(key[keyIndex])){
            //枚举所有可能的位置
            int cost = 1 + dial(i, j, N) + minStepDP(j, keyIndex + 1, key, map, N, dp);//搞定keyIndex这个字符代价+后续字符的代价
            min = Math.min(min, cost);//每个j位置要代价的最低值
        }

        dp[i][keyIndex] = min;
        //搞定以后,返回min
        return dp[i][keyIndex];
    }

    //主函数,拿着ring和key处理
    public static int findMinStepsDP(String ring, String key){
        char[] r = ring.toCharArray();
        char[] k = key.toCharArray();

        int N = r.length;
        int M = k.length;
        HashMap<Character, List<Integer>> map = new HashMap<>();
        for (int i = 0; i < N; i++) {
            if (!map.containsKey(r[i])) map.put(r[i], new ArrayList<>());
            map.get(r[i]).add(i);//直接抽出列表,添加就行了
        }

        //i从0---N-1,出发,拨到key的keyIndex位置字符从0--M,最小的代价是多少,返回dp[0][0]
        int[][] dp = new int[N][M + 1];
        for (int i = 0; i < N; i++) {
            for (int j = 0; j <= M; j++) {
                dp[i][j] = -1;//没有填写过
            }
        }

        return minStepDP(0, 0, k, map, N, dp);
    }

测试代码:

public static void test(){
        String ring = "abcde";//N=5
        String key = "ac";//按一下a,转2步,按下c4

//        char[] str = ring.toCharArray();
//        System.out.println(dial(0, 3, str.length));

        System.out.println(findMinSteps(ring, key));
        System.out.println(findMinStepsDP(ring, key));
    }


    public static void main(String[] args) {
        test();
    }

总结

提示:重要经验:

1)熟悉本题,这是一个很常规的题目,循环拨动表盘,算最小代价,自然要遍历相同字符的可能所有位置j,取最小值返回
2)map记录字符的位置j们,这里要熟悉;

举报
0 条评论