LEETCODE 818. Race Car 解题思路分析

题目大意:

赛车

你的赛车起始停留在位置 0,速度为 +1,正行驶在一个无限长的数轴上。(车也可以向负数方向行驶。)

你的车会根据一系列由 A(加速)和 R(倒车)组成的指令进行自动驾驶 。

当车得到指令 “A” 时, 将会做出以下操作: position += speed, speed *= 2

当车得到指令 “R” 时, 将会做出以下操作:如果当前速度是正数,则将车速调整为 speed = -1 ;否则将车速调整为 speed = 1。  (当前所处位置不变。)

例如,当得到一系列指令 “AAR” 后, 你的车将会走过位置 0->1->3->3,并且速度变化为 1->2->4->-1。

现在给定一个目标位置,请给出能够到达目标位置的最短指令列表的长度

示例 1: 
输入: target = 3
输出: 2
解释: 最短指令列表为 "AA" 位置变化为 0->1->3
示例 2: 
输入: target = 6
输出: 5
解释: 最短指令列表为 "AAARA" 位置变化为 0->1->3->7->7->6

说明:

  • 1 <= target(目标位置) <= 10000

如果想查看本题目是哪家公司的面试题,请参考以下免费链接: https://leetcode.jp/problemdetail.php?id=818

解题思路分析:

这是一道Hard题目,最优解使用的是DP动态规划,但笔者实在是才疏学浅,看了几篇文章均没能摸到其公式精髓。隧改用BFS,这样思路一下子变得清晰易懂。执行效率虽不及最优解,但也可以勉强混到击败50%以上的成绩(这里需要感谢未达到50%解法的大力支持)。

言归正传,一般使用bfs时,多数情况下会在一个数组或是树形结构里进行搜索,边界条件也就是数组的边界或是树的叶子节点。然而这道题,赛车的可移动范围并不局限于起点到目标终点的范围内,如果不加限制,bfs很容易无限循环至天荒地老。。。

因此,首先考虑清楚边界条件是本解法的关键,想清这点之后,剩下的bfs部分都是常规操作。那么边界到底在哪里?我们先看起点这个位置,如果一上来我们就向着反方向开,势必会离我们的终点越来越远,再加上反向操作的步数,显然我们已经输在了起跑线上。因此可以明确,起点0是我们的左边界。接下来考虑右边界,题目的例子已经给出我们很好的例证,即使超出终点再掉头回来也是有可能存在最优解的,那么最多超出多少是我们可以承受的范围呢?这个数字不好计算,我们只能用一个可以简单被证明是正确的数字作为边界,即终点target乘以2。也就是说我们的bfs搜索范围是在 0 到 target * 2的范围内。简单来说明一下,这个target*2是如何定义的,首先声明这并不一定是最优的边界范围,但是是一个我们可以承受的搜索范围。因为target*2到target的距离刚好等于起点到target的距离,既然从起点处不能向后开,同理在target*2处继续向前开同样会增加操作的步数。写本篇文章前,我在利用反复提交代码测试了一下这个边界值,在target*1.3左右的范围内是可以通过所有TC,因此实际上1.3应该是比较接近最优边界值的数。不过不论边界是target的2倍或是1.3倍,都不会影响到最终的结果,我们使用2倍只不过多搜索了一些非最优解而已。执行速度上也是相差无几。

有了边界值,剩下就是如何做bfs,我们知道在边界范围内,每一步我们都有两种操作的可能,或者加速向前,或者转向,将这两种可能放入bfs中继续演化出更多的可能,直到找到目标终点为止,bfs操作的次数即是结果。

另外,bfs必须同时配搭memo记忆数组,防止大量的重复计算出现。也就是说不要在同一路径上反复折返。这里memo的key不能仅仅记录当前位置,还要记录当前速度,因为即使在相同位置,速度不同也会产生不同的结果。

我们看下详细的代码:

public int racecar(int target) {
    // memo用来记录已经走过的点,防止重复走回头路
    // memo中的值我们用position_speed形式的字符串记录
    Set<String> memo = new HashSet<>();
    // 因为在起点是边界值,不能向回走,将其存入memo
    memo.add("0_-1");
    // 同时第一步只能走出向前加速,因此将0_1存入memo
    memo.add("0_1");
    // 定义bfs用的Queue。泛型Pair中存的是当前坐标和速度
    Queue<Pair> q = new LinkedList<Pair>();
    // 将起点和起始速度加入Queue中,作为bfs的开始。
    q.offer(new Pair(0, 1));
    // 用于记录bfs的步数
    int count = 0;
    // bfs开始
    while (q.size() > 0) {
        // 记录当前bfs开始前Queue的size大小
        int size = q.size();
        while (size > 0) {
            // 每搜索完一个节点,size减一。
            // size变为0时,说明当前层的bfs搜索全部结束,此时count可以加一
            size--;
            // 从Queue中取出一个节点
            Pair pair = q.poll();
            // 该节点位置
            int position = pair.position;
            // 该节点速度
            int speed = pair.speed;
            // 当前节点加速的情况下,位置变为position + speed
            int aPosition = position + speed;
            // 如果加速后正好达到终点,返回count+1。
            // +1是本次加速的操作
            if (aPosition == target) {
                return count + 1;
            }
            // 加速后速度变为原来的2倍
            int aSpeed = speed * 2;
            // 如果加速后的位置在边界之内,我们将此处位置和速度存入Queue中
            if (aPosition > 0 && aPosition < target * 2) {
                q.offer(new Pair(aPosition, aSpeed));
            }

            // 反向的情况下,速度变为1或者-1
            int rSpeed = speed > 0 ? -1 : 1;
            // 我们利用当前位置和速度作为key,存入memo数组,避免之后重复走
            String key = position + "_" + rSpeed;
            // 如果memo中不存在该key,说明曾经没有以相同的速度开到过这里
            if (!memo.contains(key)) {
                // 将key存入memo
                memo.add(key);
                // 同时将此处位置和速度存入Queue中
                q.offer(new Pair(position, rSpeed));
            }
        }
        // size变为0时,说明当前层的bfs搜索全部结束,此时count可以加一
        count++;
    }
    // 返回结果
    return count;
}

class Pair {
    int position;
    int speed;

    Pair(int p, int s) {
        position = p;
        speed = s;
    }
}

最后再补充说明一下代码中的一个小地方,细心的小伙伴会发现,在向Queue中添加A操作和R操作的条件不一致,加入A操作时,满足边界条件即可,然而加入R操作则没有检查边界,却增加了memo的重复check。其实严谨的写法是不论A还是R,我们都需要检查边界以及memo,这样写没有任何问题。然而这里各省略一个条件的原因是,R操作只是改变速度,位置不变,不存在越界的可能,因此可以省略。另外在A操作时没有加memo重复判断的原因是,只要我们在反向时加了重复判断,保证我们不在同一个地方掉头两次,那么我们也不可定能以相同的速度加速到达某一个点2次。

本题解法用时68ms,想要学习最优解的话建议看下DP的解法。

本网站文章均为原创内容,并可随意转载,但请标明本文链接
如有任何疑问可在文章底部留言。为了防止恶意评论,本博客现已开启留言审核功能。但是博主会在后台第一时间看到您的留言,并会在第一时间对您的留言进行回复!欢迎交流!
本文链接: https://leetcode.jp/818-race-car-解题思路分析/
此条目发表在leetcode分类目录,贴了, , , , 标签。将固定链接加入收藏夹。

发表评论

您的电子邮箱地址不会被公开。