2020_11_11 每日一题 514. 自由之路

视频游戏“辐射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
的下一个字符(下一阶段), 直至完成所有拼写。

示例:

输入: ring = “godding”, key = “gd”

输出: 4

解释:

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

ring 和 key 的字符串长度取值范围均为 1 至 100;

两个字符串中都只有小写字符,并且均可能存在重复字符;

字符串 key 一定可以由字符串 ring 旋转拼出。

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/freedom-trail

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

一开始以为只能王一个方向转想这么简单怎么是困难题
后来发现要求往左右两个方向都可以转…那就得dp了
一开始想的是自底向上的方法。定义矩阵dp[k.length() + 1][ring.length()]
dp[i][j]表示初始ring指向j的情况下要匹配key中第i个和以后所有字母所需的最短旋转次数(按钮次数是固定的,等于key的长度,最后再加)。
为了方便定义一个计算旋转次数的函数:

int minRotate(int from, int to, int l) {
    //字符串长度为l 从from到to最短需要转几下
    int diff = Math.abs(from - to);
    return Math.min(diff, l - diff);
}

一开始想到的递推是,对于dp[pk][pr],把key中字母所有可能的匹配试一下,看看哪一个最后的旋转次数最少。维克能够快速找到所有匹配的字母,先进行一下预处理,把每个字母所在的位置放到一个数据结构中:

List<Integer>[] ringLetter = new List[26];
for(int i = 0; i < 26; ++i) {
    ringLetter[i] = new ArrayList<>();
}
for(int i = 0; i < ring.length(); ++i) {
    ringLetter[ring.charAt(i) - 'a'].add(i);
}

递推的代码:

            int k = key.charAt(pk) - 'a';
            for(int pr = 0; pr < ring.length(); ++pr) {
                int min = Integer.MAX_VALUE;
                for(int i : ringLetter[k]) {
                    min = Math.min(min, minRotate(i, pr, rl) + dp[pk + 1][i]);
                }
                dp[pk][pr] = min;
            }

完整代码:

class Solution {
    int minRotate(int from, int to, int l) {
        //字符串长度为l 从from到to最短需要转几下
        int diff = Math.abs(from - to);
        return Math.min(diff, l - diff);
    }

    public int findRotateSteps(String ring, String key) {
        //范围都不大100 * 100也就10000 允许顺时针或逆时针就比较麻烦了 先试试迭代的dp
        int[][] dp = new int[key.length() + 1][ring.length()];//dp[i][j] 初始ring指向j的情况下要匹配key中第i个字母所需的最短距离 
        //不考虑要按那几下最后再加
        List<Integer>[] ringLetter = new List[26];
        int rl = ring.length();
        for(int i = 0; i < 26; ++i) {
            ringLetter[i] = new ArrayList<>();
        }
        for(int i = 0; i < ring.length(); ++i) {
            ringLetter[ring.charAt(i) - 'a'].add(i);
        }
        for(int pk = key.length() - 1; pk >= 0; --pk) {
            int k = key.charAt(pk) - 'a';
            for(int pr = 0; pr < ring.length(); ++pr) {
                int min = Integer.MAX_VALUE;
                for(int i : ringLetter[k]) {
                    min = Math.min(min, minRotate(i, pr, rl) + dp[pk + 1][i]);
                }
                dp[pk][pr] = min;
            }
        }
        return dp[0][0] + key.length();
    }
}

过了但是事件比较慢45ms 10%不到。尝试优化
第一个尝试是可以贪心地考虑这个问题,在考察sp中每一个元素地时候,如果这一步不用转,肯定是不转更好;如果不得不转,也只有可能是向左转或者向右转最近地两个之一。正确性证明简单说说吧
对于前半句
不转的dp值是dp[pk + 1][pr]
转的dp值是minRotate(i, pr, rl) + dp[pk + 1][i]
而对于dp[pk + 1][pr]来说,方案之一是先转到i 再接着往后转,所以
dp[pk + 1][pr] <= minRotate(i, pr, rl) + dp[pk + 1][i]
所以如果能不转就不需要考虑转的情形

能转转最近的应该和上面的证明差不多,懒得写了
用贪心的方式,更新的代码变为:

List<Integer> l = ringLetter[key.charAt(pk) - 'a'];
int lIndex = 0;
//想开点 其实每一步是可以贪心的 能不转就不转 能转的话也只需要考察最近的左右 正确性证明缓缓
for(int pr = 0; pr < ring.length(); ++pr) {
    if(lIndex == l.size()) {
        //头一个和最后一个
        int i1 = l.get(lIndex - 1);
        int i2 = l.get(0);
        dp[pk][pr] = Math.min(pr - i1 + dp[pk + 1][i1], rl - (pr - i2) + dp[pk + 1][i2]);
    } else if(pr == l.get(lIndex)) {
        //不用转了
        dp[pk][pr] = dp[pk + 1][pr];
        ++lIndex;
    } else {
        int i1 = l.get(lIndex);//一定大于pr
        int i2 = l.get(lIndex == 0 ? l.size() - 1 : lIndex - 1);
        dp[pk][pr] = Math.min(i1 - pr + dp[pk + 1][i1], minRotate(i2, pr, rl) + dp[pk + 1][i2]);
    }
    
}

这样之许哟啊遍历一次pr就好 事件复杂度从O(n3)变成O(n2)。
还是慢,试试自顶向下的方法。因为就k == 0的哪一行来说,其实我只需要一个数就行,其他的算了也白算,所以采用记忆化搜索的方式。更新策略还是贪心的策略:

class Solution {
    int minRotate(int from, int to) {
        //字符串长度为l 从from到to最短需要转几下
        int diff = Math.abs(from - to);
        return Math.min(diff, ring.length() - diff);
    }

    String ring, key;
    Integer[][] dp;
    List<Integer>[] ringLetter;

    //试试自顶向下
    int rec(int pr, int pk) {
        if(pk == key.length()) {
            return 0;
        }
        if(dp[pk][pr] != null) {
            return dp[pk][pr];
        }
        //初始位置在pr 匹配key的第pk个字符和之后的旋转数
        //恰好相等则直接搜索下一步
        if(key.charAt(pk) == ring.charAt(pr)) {
            dp[pk][pr] = rec(pr, pk + 1);
            return dp[pk][pr];
        }

        List<Integer> l = ringLetter[key.charAt(pk) - 'a'];
        if(l.size() == 1) {
            //只能转到那个位置了
            int t = l.get(0);
            dp[pk][pr] = rec(t, pk + 1) + minRotate(t, pr);
        } else if(pr < l.get(0) || pr > l.get(l.size() - 1)) {
            //不扣细节了 总之是头一个和后一个
            int i1 = l.get(0), i2 = l.get(l.size() - 1);
            dp[pk][pr] = Math.min(rec(i1, pk + 1) + minRotate(i1, pr), rec(i2, pk + 1) + minRotate(i2, pr));
        } else {
            //二叉搜索出位置 然后前一个后一个
            int ii = - Collections.binarySearch(l, pr) - 1;
            int i1 = l.get(ii), i2 = l.get(ii - 1);
            dp[pk][pr] = Math.min(rec(i1, pk + 1) + minRotate(i1, pr), rec(i2, pk + 1) + minRotate(i2, pr));
        }
        return dp[pk][pr];
    }


    public int findRotateSteps(String ring, String key) {
        this.ring = ring;
        this.key = key;

        //范围都不大100 * 100也就10000 允许顺时针或逆时针就比较麻烦了 先试试迭代的dp
        dp = new Integer[key.length() + 1][ring.length()];//dp[i][j] 初始ring指向j的情况下要匹配key中第i个字母所需的最短距离 
        //不考虑要按那几下最后再加
        ringLetter = new List[26];
        for(int i = 0; i < 26; ++i) {
            ringLetter[i] = new ArrayList<>();
        }
        for(int i = 0; i < ring.length(); ++i) {
            ringLetter[ring.charAt(i) - 'a'].add(i);
        }
        rec(0, 0);
        return dp[0][0] + key.length();
    }
}

快了好多好多 时间复杂度不是很好分析…(中间还有个二分查找尤其讨厌)
其实把dp矩阵打印一下就知道问什么会快这么多
输入为
ring : “godding”
key : “gdong”
搜索完毕后的dp矩阵为
[
[7, null, null, null, null, null, null],
[7, null, null, null, null, null, null],
[null, null, 5, 6, null, null, null],
[null, 4, null, null, null, null, null],
[null, null, null, null, null, 1, null],
[null, null, null, null, null, null, null]
]
其实只需要算少的可怜的几个 一开始把这个矩阵全算出来确实有些缺心眼…

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值