简单题就应该往简单想 | 豆包MarsCode AI刷题

151 阅读4分钟

计算从位置 x 到 y 的最少步数(简单)

这是刷题题库中一道简单题,给定起点和终点,求最少步数,这不一眼BFS吗?稳了!我直接就是进行一个公式的套!但是真正上手以后发现有难度。 题目中有这样两个限制:

  • 每一步的移动范围是上一步的-1、0、1
  • 第一步和最后一步的步长为1

那么简单来想,就是在BFS模板的基础上,每次有一个新位置入队列时,要考虑该位置下一次移动的步长,最终达到终点以后还要检查当前步长是否为1

真的是这样吗?并不行,被狠狠地超时了。

这道题我从入营刷题就开始尝试,想了很久也没做出来,最近看它不顺眼,下决心一定攻克!于是我又想到一个扬汤止沸的办法,既然两边都要求步长为1,步长的变化还连续,那么单向的付出自然比不过双向的奔赴!起始时将起点和终点都加入队列,每次从队列中poll元素时,检查是否与另一边相遇,相遇则计算步数。

// 略
// 初始状态(第一步和最后一步的步长都必须为1)
qx.offer(new int[]{xPosition, 1});
qy.offer(new int[]{yPosition, 1});
int steps = 0;

while (true) {
    Set<Integer> visitedX = new HashSet<>();
    Set<Integer> visitedY = new HashSet<>();
    int sizeX = qx.size();
    for (int i = 0; i < sizeX; i++) {
        int[] currentStateX = qx.poll();
        int currentPositionX = currentStateX[0];
        int currentStepLength = currentStateX[1];
        // x 和 y 相遇
        if (visitedY.contains(currentPositionX)) {
            return steps * 2 - 1;
        }
        visitedX.add(currentPositionX);
        // 略
    }

    int sizeY = qy.size();
    for (int i = 0; i < sizeY; i++) {
        int[] currentStateY = qy.poll();
        int currentPositionY = currentStateY[0];
        int currentStepLength = currentStateY[1];
        // y 和 x 相遇
        if (visitedX.contains(currentPositionY)) {
            return steps * 2;
        }
        visitedY.add(currentPositionY);
        // 略
    }
    steps++;
}
// 略

但正如先前所说,这种方法只能算是个小优化,只通过了一部分用例,仍然难逃被超时的命运(哦悲悲)。

苦思冥想一整晚无果,收拾东西回宿舍,在回去的路上,清冷的晚风吹过来,精神一振的我终于还是想到了解决方案。原来之前我的思维被定格在BFS中了,有时候要跳出思维定势,横看成岭侧成峰。

正确思路

既然两边都需要步长为1,且步长的变化是连续的,那么两边的路径应该是对称的,如此便可以简单粗暴地解决,先计算二者之间的距离dist,然后循环减去两个1,两个2,两个3……假设通过这种方式正好能瓜分dist,这样一来最大的步长乘以二即为最少的步数。

但是,but,however,能正好瓜分dist的情况毕竟是少数,走着走着会发现剩余距离不够下一步走了,这时需要减少下一步的步长吗?这里也是想了很久,其实不对,我们应该直接继续走,让dist减为负值(或0),此时的-dist即为多余出来的步长。针对多余出来的步长,我们可以在已经走过的步数中减少步长,每一步最多减少1,不然就不连续了。

如何处理这部分逻辑呢?假设瓜分到最后,最大步长(使得dist减为负值的一步)为len,则总共走了i * 2步(其中包括多余的距离-dist)。每一步都可以进行步长减一的操作,前提是比它长的一步已经减过一次。那我们必须从中间开花,从最长的一步开始减,发现当-dist >= i时,如果只减其中一半,即只减少终点那边的步长,则会将开始第步长为1的那一步减为0,相当于少花费一步。我们当然希望步数越少越好,所以当然要先减其中一半,如果-dist还有剩余,再从另一半开始减。

考虑到-dist最多不会超过i * 2 - 1,在减步长的过程中,不可能把最开始的两步全部减为0,即最多减少一步。最终的代码就是这样:

int dist = Math.abs(xPosition - yPosition);
if (dist == 0) {
    return 0;
}
int i = 1;
while (dist > 0) {
    dist -= i * 2;
    i++;
}
// 此时的最大步长为 i - 1,步数为 (i - 1) * 2
// 多余的步长为-dist,且-dist <= (i - 1) * 2 - 1
// 从已经走过的步长中减去多余的步长
// 如果 -dist >= i - 1,则步数减 1,这是因为第一步的步长被减为 0 了
// 而因为-dist <= (i - 1) * 2 - 1,所以至多有一步被减为 0
return (i - 1) * 2 - (-dist >= (i - 1) ? 1 : 0);

总共十行代码的事儿,果然简单题就应该简单做!