最少侧跳次数 图解题解
这道题到底在问什么
- 输入
- obstacles = [0,1,2,3,0]
- 输出
- 2
- 输入
- obstacles = [0,1,1,3,3,0]
- 输出
- 0(跑道2全程无障碍,不用侧跳)
- 输入
- 本节演示 [0,1,2,3,0,2,0]
- 输出
- 2
最优解:为什么这么做
一句话答案:LeetCode 1824 最少侧跳次数:青蛙在 3 跑道上逐点前进、遇障碍才侧跳。滚动数组记到当前点各道最少侧跳,每点封障碍成 ∞、取 min+1 当侧跳代价、通行道取小,时间 O(n)、空间 O(1)。
这只青蛙为什么非侧跳不可,题目求的是什么
一条路排着 n+1 个点,编号 0 到 n,每个点横摆 3 条跑道。obstacles[i] 为 0 表示点 i 没障碍,为 k 表示跑道 k 上有障碍,一个点最多堵一条道。青蛙从点 0 的跑道 2 出发,沿同一条道能跳到下个点(那点这条道没障碍才行),也能原地侧跳到另一条空道,一次算一跳、不挨着也行。走到点 n 任意一条道即到,问最少侧跳几次;点 0、点 n 三条道都干净。
每个点都能换道,为什么不能把所有跳法试一遍
每个点上除了直行还有两种换道,n 个点连乘走法是指数级,点数上千就试不完;而『从某点某道往后走的最省跳法』被无数条前缀反复重算。这就该用动态规划(把『到某点、停某道最少跳几次』这类小问题的答案算一次存住、后面直接取用,不再重算)。
三条跑道的代价怎么用一个长度 3 的数组记住
用长度 3 的数组 f 当状态(记录当前局面的关键数,这里是三条道各自的代价):f[0]、f[1]、f[2] 表示走到当前点、停在跑道 1、2、3 上时一路最少侧跳几次。点 0 上青蛙站跑道 2、没跳过,f[1]=0;改停另两道得先原地侧跳一次,f[0]、f[2] 都是 1,f 起手 [1,0,1]。
封障碍、取 min 加一、每道取小,这三步凭什么对
从上一个点推到当前点分三步,就是这题的转移(拿上个点的三个代价推出当前点的三个):一,当前点哪条道有障碍就把那条 f 封成 ∞(大到不可能被选中的数),青蛙停不到障碍上;二,算侧跳统一代价 x = min(f) + 1,从当前最省的道侧跳到哪条都只多花 1 次;三,每条没障碍的道拿直行代价和 x 取小。
拿 [0,1,2,3,0] 把三条跑道的代价逐点推出来
起手点 0:f = [1,0,1]。点 1 障碍在跑道 1:封 f[0] 成 ∞ 得 [∞,0,1];x = 0 + 1 = 1,通行的跑道 2、跑道 3 代价 0 和 1 都不比 1 大,定格 [∞,0,1]。点 2 障碍在跑道 2:封 f[1] 得 [∞,∞,1];x = 1 + 1 = 2,跑道 1 的 ∞ 刷成 2,定格 [2,∞,1]。点 3 障碍在跑道 3:封 f[2] 得 [2,∞,∞];x = 2 + 1 = 3,跑道 2 刷成 3,定格 [2,3,∞]。点 4 没障碍:x = 2 + 1 = 3,三条道代价 2、3、∞,跑道 3 的 ∞ 刷成 3,定格 [2,3,3]。取 min(2,3,3) = 2 就是答案。
封障碍要是排到算 x 之后,整列为什么会算歪
封障碍要是挪到算 x 后面,障碍道没清掉的旧代价会混进 min,偏小的 x 一路污染后面每条道,整列全塌。全程从左到右扫一遍,每个点只做常数次比较更新,时间 O(n)(大 O 记号,描述数据规模变大时操作次数怎么涨);每个新点只用得上上一个点的三个代价,一个长度 3 的滚动数组(只留最近用得到的那几项、循环覆盖旧值)就够,空间 O(1)。起手写成全 0 是另一个常见反写——那等于让青蛙零代价凭空站上任意道。哨兵(哨兵=占位用的假极大值)也有讲究:C++、Java 别用 INT_MAX,加 1 溢出成负数反被 min 选中。
▶ 动画逐步走查(共 21 步)——想跟着动画一帧帧对照就展开
- 3记住三步走:有障碍先封成无穷大、再算 x = 最小代价 + 1、每条通行跑道在直行和侧跳之间取小。起手 f = [1,0,1],扫完取 min(f)。
- 4先填最左边点 0 这一列。青蛙就是从点 0 的跑道 2 出发的,所以站在跑道 2 上一次都没跳,代价是 0。如果它一上来就想换到跑道 1 或跑道 3,那得在原地侧跳一次,所以这两格都是 1。这三个数就是我们的起点,后面每到一个新点,都拿上一列的这三个代价往右推。
- 5来到点 1,这里跑道 1 上有障碍。先把点 0 的三个代价原样搬过来,这代表青蛙沿原跑道直行一步、不侧跳。但跑道 1 这个点站不了,所以把它的代价改成无穷大,画面里用 ∞ 表示。你就理解成:想直行停在跑道 1 这个点上是不允许的,先把这条路堵死,免得后面误用它。
- 6现在算这个点上「侧跳换道」的统一代价 x。诀窍是:先在这个点当前的三条跑道里,挑出代价最小的那条,现在是跑道 2,代价 0。从它出发,不管侧跳到另外哪条跑道,都只多花 1 次,所以 x = 0 + 1 = 1。这一个 x 就是「在点 1 侧跳一次能达到的最省代价」,待会儿拿它去更新其它跑道。
- 7最后一步,把 x = 1 拿去和每条通行跑道的直行代价比,取小的留下。通行的跑道2和跑道3直行代价是 0 和 1,都不比 1 大;跑道1被障碍封成 ∞,不参与更新。点 1 这一列最终定格成 ∞ / 0 / 1。
- 8来到点 2,这里跑道 2 上有障碍。先把点 1 的三个代价原样搬过来,这代表青蛙沿原跑道直行一步、不侧跳。但跑道 2 这个点站不了,所以把它的代价改成无穷大,画面里用 ∞ 表示。你就理解成:想直行停在跑道 2 这个点上是不允许的,先把这条路堵死,免得后面误用它。
- 9现在算这个点上「侧跳换道」的统一代价 x。诀窍是:先在这个点当前的三条跑道里,挑出代价最小的那条,现在是跑道 3,代价 1。从它出发,不管侧跳到另外哪条跑道,都只多花 1 次,所以 x = 1 + 1 = 2。这一个 x 就是「在点 2 侧跳一次能达到的最省代价」,待会儿拿它去更新其它跑道。
- 10最后一步,把 x = 2 拿去和每条通行跑道的直行代价比,取小的留下。跑道 1 原来的直行代价比 2 大,说明在这个点侧跳过去更省,于是被刷新。点 2 这一列最终定格成 2 / ∞ / 1。
- 11来到点 3,这里跑道 3 上有障碍。先把点 2 的三个代价原样搬过来,这代表青蛙沿原跑道直行一步、不侧跳。但跑道 3 这个点站不了,所以把它的代价改成无穷大,画面里用 ∞ 表示。你就理解成:想直行停在跑道 3 这个点上是不允许的,先把这条路堵死,免得后面误用它。
- 12现在算这个点上「侧跳换道」的统一代价 x。诀窍是:先在这个点当前的三条跑道里,挑出代价最小的那条,现在是跑道 1,代价 2。从它出发,不管侧跳到另外哪条跑道,都只多花 1 次,所以 x = 2 + 1 = 3。这一个 x 就是「在点 3 侧跳一次能达到的最省代价」,待会儿拿它去更新其它跑道。
- 13最后一步,把 x = 3 拿去和每条通行跑道的直行代价比,取小的留下。跑道 2 原来的直行代价比 3 大,说明在这个点侧跳过去更省,于是被刷新。点 3 这一列最终定格成 2 / 3 / ∞。
- 14点 4 这里干干净净,一条障碍都没有。那三条跑道都可以从点 3 沿原跑道直行一步过来,代价原样照抄,现在跑道 1、2、3 分别是 2、3、∞。没有需要封死的格子,这一步很轻松。不过直行不一定最省,接下来还要看看在这个点侧跳一下会不会更划算。
- 15现在算这个点上「侧跳换道」的统一代价 x。诀窍是:先在这个点当前的三条跑道里,挑出代价最小的那条,现在是跑道 1,代价 2。从它出发,不管侧跳到另外哪条跑道,都只多花 1 次,所以 x = 2 + 1 = 3。这一个 x 就是「在点 4 侧跳一次能达到的最省代价」,待会儿拿它去更新其它跑道。
- 16最后一步,把 x = 3 拿去和每条通行跑道的直行代价比,取小的留下。跑道 3 原来的直行代价比 3 大,说明在这个点侧跳过去更省,于是被刷新。点 4 这一列最终定格成 2 / 3 / 3。
- 17来到点 5,这里跑道 2 上有障碍。先把点 4 的三个代价原样搬过来,这代表青蛙沿原跑道直行一步、不侧跳。但跑道 2 这个点站不了,所以把它的代价改成无穷大,画面里用 ∞ 表示。你就理解成:想直行停在跑道 2 这个点上是不允许的,先把这条路堵死,免得后面误用它。
- 18现在算这个点上「侧跳换道」的统一代价 x。诀窍是:先在这个点当前的三条跑道里,挑出代价最小的那条,现在是跑道 1,代价 2。从它出发,不管侧跳到另外哪条跑道,都只多花 1 次,所以 x = 2 + 1 = 3。这一个 x 就是「在点 5 侧跳一次能达到的最省代价」,待会儿拿它去更新其它跑道。
- 19最后一步,把 x = 3 拿去和每条通行跑道的直行代价比,取小的留下。通行的跑道1和跑道3直行代价是 2 和 3,都不比 3 大;跑道2被障碍封成 ∞,不参与更新。点 5 这一列最终定格成 2 / ∞ / 3。
- 20点 6 这里干干净净,一条障碍都没有。那三条跑道都可以从点 5 沿原跑道直行一步过来,代价原样照抄,现在跑道 1、2、3 分别是 2、∞、3。没有需要封死的格子,这一步很轻松。不过直行不一定最省,接下来还要看看在这个点侧跳一下会不会更划算。
- 21现在算这个点上「侧跳换道」的统一代价 x。诀窍是:先在这个点当前的三条跑道里,挑出代价最小的那条,现在是跑道 1,代价 2。从它出发,不管侧跳到另外哪条跑道,都只多花 1 次,所以 x = 2 + 1 = 3。这一个 x 就是「在点 6 侧跳一次能达到的最省代价」,待会儿拿它去更新其它跑道。
- 22最后一步,把 x = 3 拿去和每条通行跑道的直行代价比,取小的留下。跑道 2 原来的直行代价比 3 大,说明在这个点侧跳过去更省,于是被刷新。点 6 这一列最终定格成 2 / 3 / 3。
- 23整张表推满了。末点 6 三条跑道的代价是 2 / 3 / 3,取最小的那个就是答案 2。顺着倒推能还原出这条最省路线:青蛙在点 1 从跑道 2 侧跳到跑道 3,躲过点 2 跑道 2 的障碍继续直行;到点 2 又从跑道 3 侧跳到跑道 1,之后一路沿跑道 1 直行到底。全程正好 2 次侧跳,和表格算出来的 2 完全对上。
⚠️ 容易写错的地方
✗ 错:先算 x = min(f)+1、再把障碍跑道封成无穷
✓ 对:必须先封障碍、再算 x
顺序反了,障碍跑道的旧代价会被当成侧跳源头算进 min,还可能把青蛙更新到一个不能站的跑道上,答案偏小
✗ 错:起手把 f 设成 [0,0,0] 或 [1,1,1]
✓ 对:f 应初始化成 [1,0,1]
青蛙只从跑道 2 出发,站跑道 2 免跳记 0,跑道 1 和跑道 3 得先侧跳一次才到,记 1;设成 [0,0,0] 相当于允许青蛙起步随便选道
✗ 错:C++ / Java 用整型最大值 INT_MAX 当无穷
✓ 对:用 1 左移 30 位这类留余量的哨兵
INT_MAX 再加 1 会溢出成很小的负数,反而变成「最省」,把答案带偏;Python 的浮点 inf 无此问题
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def minSideJumps(self, obstacles: List[int]) -> int:
f = [1, 0, 1]
for v in obstacles[1:]:
for j in range(3):
if v == j + 1:
f[j] = inf
break
x = min(f) + 1
for j in range(3):
if v != j + 1:
f[j] = min(f[j], x)
return min(f)C++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
int minSideJumps(vector<int>& obstacles) {
const int inf = 1 << 30;
int f[3] = {1, 0, 1};
for (int i = 1; i < obstacles.size(); ++i) {
for (int j = 0; j < 3; ++j) {
if (obstacles[i] == j + 1) {
f[j] = inf;
break;
}
}
int x = min({f[0], f[1], f[2]}) + 1;
for (int j = 0; j < 3; ++j) {
if (obstacles[i] != j + 1) {
f[j] = min(f[j], x);
}
}
}
return min({f[0], f[1], f[2]});
}
};Java
import java.util.*;
class Solution {
public int minSideJumps(int[] obstacles) {
final int inf = 1 << 30;
int[] f = {1, 0, 1};
for (int i = 1; i < obstacles.length; ++i) {
for (int j = 0; j < 3; ++j) {
if (obstacles[i] == j + 1) {
f[j] = inf;
break;
}
}
int x = Math.min(f[0], Math.min(f[1], f[2])) + 1;
for (int j = 0; j < 3; ++j) {
if (obstacles[i] != j + 1) {
f[j] = Math.min(f[j], x);
}
}
}
return Math.min(f[0], Math.min(f[1], f[2]));
}
}复杂度
时间
O(n)
从左到右扫一遍每个点,每个点只在 3 条固定跑道上做常数次比较和更新,总量跟点数 n 成正比,n 到十万也秒出
空间
O(1)
按峰值算:只用一个长度 3 的滚动数组 f,每个新点只依赖上一个点的三个值,不必开二维表,所以是常数空间
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 最少侧跳次数 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么侧跳到不相邻的跑道也只算一次,而不是按跨越的道数收费?+
题目直接规定换一条道就算一次,和跨过几条道无关,所以从跑道 1 直接跳到跑道 3 也只花 1 次。转移里的 x = min(f) + 1 就是按这条规矩写的:站在当前最省的那条道上,不论侧跳到另外哪条,代价都统一是『那条最省的代价 + 1』,不必按目标道分别记账。
f 起手为什么是 [1,0,1] 而不是全 0?+
青蛙的出发位置钉死在点 0 的跑道 2,所以只有跑道 2 的初始代价是 0。想让它一开始就停在跑道 1 或跑道 3,必须在点 0 原地先侧跳一次,这一次得记进代价里,于是 f[0]、f[2] 都是 1。写成全 0 等于让青蛙不花任何一次侧跳就凭空站上任意一条道,会把答案算少。
为什么能一路取 min 往下推,不会因为某步没选全局最优而出错?+
因为这道题满足无后效性(走到某个点、停在某条道之后,后面怎么走只取决于当前在哪条道、已跳几次,跟之前是怎么跳到这里的无关)。f 记的就是『到当前点、停各道的最少侧跳』,推下一个点时只依赖这三个数,过去的路径细节不影响未来,所以每步都基于已算好的最优往下推,不会漏掉更省的走法。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 最少侧跳次数 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。