移动石子直到连续 II 图解题解
这道题到底在问什么
- 输入
- stones = [7,4,9]
- 输出
- [1,2]
最优解:为什么这么做
一句话答案:LeetCode 1040 移动石子直到连续 II 用排序后的两招求解:最大步数取放弃某条端缝后剩下的空位数,最小步数用长 n 的滑动窗口找覆盖石子最多的位置,瓶颈在排序 O(n log n)。
石子在数轴上挪来挪去,最后要报哪两个数
给一排位置互不相同的石子 stones,每步只能拿起当前最左或最右的端点,放进某个空位,且放完后它不能仍是端点;石子连成一段连续位置即停。返回长度 2 的数组 [最少步数, 最多步数]。以题面 stones=[7,4,9] 为例,排序后 4、7、9,最少 1 步(4 挪到 8 得 7、8、9)、最多 2 步,答案 [1,2]。
为什么不能真的一颗颗搬着去试
最直接的想法是模拟搬端点、穷举每步的放法直到连续。可每步有多个空位、还要递归很多层,移动序列随石子数指数级膨胀,稍大就跑不完。让它变简单的前提是先排序——端点、相邻间距、区间能装几颗,全靠位置有序才能直接读出,所以算最大最小前第一步都是把 stones 排成从小到大。
最大步数:放弃一条端缝,把剩下的空格填满
第一步必然动某个端点,它一旦往里搬,外侧那条通向远处的空隙(端缝)就永久作废。于是只有两种选择:放弃最左,剩下 n-1 颗挤进第二颗到最后一颗的区间;或放弃最右,挤进第一颗到倒数第二颗的区间。哪个区间空位多就能多走几步,取大者即最大步数——参考代码正是数每个区间首尾之间能站几个位置、各减掉 n-1 颗石子。
最小步数:拿一个长 n 的窗口滑过去数石子
终局是 n 颗石子占满 n 个连续位置,所以任何宽度不超过 n 的区间都可能是最后落点。让一个长 n 的滑动窗口贴着石子滑,它是一段宽度不超过 n、从左往右挪的区间:窗内就位的石子越多,要从窗外搬进来的越少,本窗步数就是 n 减窗内石子数。右指针每纳入一颗,宽度一超 n 就把左指针右移、压回 n 以内;每个窗口的『n 减窗内数』取最小即最少步数。
那个『差一点就排好』的位置,公式为什么少算一步
有一种排布会坑这条公式:n-1 颗已连成一片,只剩最后一颗落在远处。窗口罩住那 n-1 颗,按『n 减窗内数』像是只差 1 步,可这步走不通——一步收尾只能把远处那颗端点挪到紧邻空位,而那位置又成了新端点,违反『挪完不能还是端点』。正确要 2 步:先把连续段一端挪出一格,再把远处那颗填进中间。n-1 颗连成片时首尾跨度就是 n-2,所以窗内恰好 n-1 颗、且窗口跨度等于 n-2 时,单独把这窗记成 2 步。
排序后的 4、7、9,最大最小分头验一遍
排序得 4、7、9,n=3。最大:放弃最左 4,两颗挤进 7 到 9 共 3 位装 2 颗、1 空位,1 步;放弃最右 9,挤进 4 到 7 共 4 位装 2 颗、2 空位,2 步,取大得 2。最小:长 3 窗口滑到 7、9(宽度 9-7+1=3 没超 n),窗里 2 颗只需搬 4,1 步;跨度 9-7=2 不等于 n-2=1,没踩特例按 3-2=1 记。答案 [1,2]。
不排序、漏减 n-1、忘判特例,答案就偏了
不排序直接套公式最隐蔽:端点、间距、窗口宽度全依赖有序位置,乱序算的量不可信。算最大若只算一侧、或忘末尾减 n-1,会漏掉『放弃较小端缝更划算』的走法。算最小若漏掉那个『n-1 已连续、差一空位、最后一颗在远处』的特例,会把 2 步误报成 1 步。石子本就连续时输出 0。排序 O(n log n) 是瓶颈,最大 O(1)、滑窗 O(n),空间 O(1)。
▶ 动画逐步走查(共 25 步)——想跟着动画一帧帧对照就展开
- 3记住两句:最大「移一端、丢小端缝、数空格」;最小「滑长 n 窗口、数窗内最多石子、特例留空位算 2 步」。后面每帧都在套它。
- 4先看原始输入,6 颗石子位置乱序排列。所有套路都建立在「排好序」之上,所以第一步永远是排序。
- 5排好序后变成 2、3、5、6、8、11。最左是 2、最右是 11,整段从 2 到 11 一共 10 个位置,里面只占了 6 颗。接下来先算最大、再算最小。
- 6先算最大步数。整段 10 个位置里有 4 个空格。最理想是每一步只填一个空格、慢慢挪。但第一步必须把一个端点往里搬,会放弃它那一侧的端缝,所以要在两种放弃里挑划算的。
- 7第一种:第一步动最左的 2,等于放弃左端缝,往后所有石子都挤进 3 到 11 这段。这段有 4 个空格,每步填一个,最多走 4 步。
- 8第二种:第一步动最右的 11,等于放弃右端缝,石子挤进 2 到 8 这段。这段只有 2 个空格,最多走 2 步。
- 9两种放弃里挑空格多的:4 和 2 取大,最大步数就是 4。直觉上就是「丢掉较小的那条端缝,剩下的空格能填多少步就走多少步」。
- 10再算最小。终局是 6 个挨着的位置。我们拿一个宽度不超过 6 的窗口在数轴上滑,窗口里已经就位的石子越多,要从外面搬进来的就越少。右指针 j 往右扩,一旦窗口宽度超过 6,左指针 i 就跟着右移。
- 11右指针扩到位置 2,窗口宽度 1,还在 6 以内,左指针不动。
- 12窗内已就位 1 颗,外面还有 5 颗要搬进来,本窗需要 5 步。比之前更少,最少刷新成 5。
- 13右指针扩到位置 3,窗口宽度 2,还在 6 以内,左指针不动。
- 14窗内已就位 2 颗,外面还有 4 颗要搬进来,本窗需要 4 步。比之前更少,最少刷新成 4。
- 15右指针扩到位置 5,窗口宽度 4,还在 6 以内,左指针不动。
- 16窗内已就位 3 颗,外面还有 3 颗要搬进来,本窗需要 3 步。比之前更少,最少刷新成 3。
- 17右指针扩到位置 6,窗口宽度 5,还在 6 以内,左指针不动。
- 18窗内已就位 4 颗,外面还有 2 颗要搬进来,本窗需要 2 步。比之前更少,最少刷新成 2。
- 19右指针扩到位置 8,宽度一度超过 6,左指针往右移了 1 步,缩到位置 3,让窗口宽度回到 6,控制在 6 以内。
- 20窗内已就位 4 颗,外面还有 2 颗要搬进来,本窗需要 2 步。没比当前最少 2 更优,继续滑。
- 21右指针扩到位置 11,宽度一度超过 6,左指针往右移了 2 步,缩到位置 6,让窗口宽度回到 6,控制在 6 以内。
- 22窗内已就位 3 颗,外面还有 3 颗要搬进来,本窗需要 3 步。没比当前最少 2 更优,继续滑。
- 23滑完一遍,最划算的窗口已就位 4 颗,只要再搬 2 颗,最少就是 2 步。配上前面算的最大 4 步,主例答案是 [2, 4]。
- 24单独看这个最容易错的特例:1、2、3、4 已经连成一片,只有 9 落在远处。窗口里能装下 4 颗连续石子,看着像只差 1 颗、1 步就完事,真的吗?
- 25朴素地套公式:窗口里有 4 颗,n 是 5,5 减 4 等于 1,好像 1 步就行。问题出在这个 1 步根本走不通,下一帧看为什么。
- 26想 1 步收尾,只能把端点 9 挪到位置 5,凑成 1、2、3、4、5。可规则要求端点挪完不能还是端点,而 5 恰好是新的最右端,这步违规。所以 1 步走不通。
- 27正确走法要 2 步:先把最左的 1 挪到 6,变成 2、3、4、6、9;再把端点 9 挪到 5,凑成 2、3、4、5、6 连续收工。所以「n-1 已连续、只差一个空位、最后一颗在远处」这个特例,最少是 2 步,公式里要单独判它。
⚠️ 容易写错的地方
✗ 错:最小直接用 n 减窗内石子,忘了特例
✓ 对:判 n-1 连续且差一空位时记 2 步
1、2、3、4 加远处一颗这种,1 步收尾会让端点仍是端点、违规
✗ 错:最大忘了减 n-1、或只算一个窗口
✓ 对:两个候选窗口跨度各减 n-1 再取大
只算一侧会漏掉「放弃较小端缝」更优的那种走法
✗ 错:不排序直接套公式
✓ 对:一切前提是先排序
端点、相邻间距、窗口宽度都依赖有序位置
完整代码(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 numMovesStonesII(self, stones: List[int]) -> List[int]:
stones.sort()
mi = n = len(stones)
mx = max(stones[-1] - stones[1] + 1, stones[-2] - stones[0] + 1) - (n - 1)
i = 0
for j, x in enumerate(stones):
while x - stones[i] + 1 > n:
i += 1
if j - i + 1 == n - 1 and x - stones[i] == n - 2:
mi = min(mi, 2)
else:
mi = min(mi, n - (j - i + 1))
return [mi, mx]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:
vector<int> numMovesStonesII(vector<int>& stones) {
sort(stones.begin(), stones.end());
int n = stones.size();
int mi = n;
int mx = max(stones[n - 1] - stones[1] + 1, stones[n - 2] - stones[0] + 1) - (n - 1);
for (int i = 0, j = 0; j < n; ++j) {
while (stones[j] - stones[i] + 1 > n) {
++i;
}
if (j - i + 1 == n - 1 && stones[j] - stones[i] == n - 2) {
mi = min(mi, 2);
} else {
mi = min(mi, n - (j - i + 1));
}
}
return {mi, mx};
}
};Java
import java.util.*;
class Solution {
public int[] numMovesStonesII(int[] stones) {
Arrays.sort(stones);
int n = stones.length;
int mi = n;
int mx = Math.max(stones[n - 1] - stones[1] + 1, stones[n - 2] - stones[0] + 1) - (n - 1);
for (int i = 0, j = 0; j < n; ++j) {
while (stones[j] - stones[i] + 1 > n) {
++i;
}
if (j - i + 1 == n - 1 && stones[j] - stones[i] == n - 2) {
mi = Math.min(mi, 2);
} else {
mi = Math.min(mi, n - (j - i + 1));
}
}
return new int[] {mi, mx};
}
}复杂度
时间
O(n log n)
瓶颈是排序;之后的最大计算 O(1)、滑动窗口一遍 O(n)
空间
O(1) 起
只用 i、j、mi、mx 几个变量;排序内部栈 C++/Java 约 O(log n)、Python 最坏 O(n)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 移动石子直到连续 II 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
最大步数为什么只用比较两个候选?+
因为第一步一定要动一个端点,它会让那一侧的端缝彻底作废。要么放弃最左、保住右边 n-1 颗从第二颗到末颗的跨度,要么放弃最右、保住左边 n-1 颗从首颗到倒数第二颗的跨度,其余石子不会在第一步就被永久跳过。所以只有『放弃哪条端缝』这两种局面,各自算出能填多少空位、取大的就是最大步数。
滑动窗口的宽度为什么卡在 n?+
终局是 n 颗石子占满 n 个连续位置,这片区域的宽度恰好是 n。宽度超过 n 的窗口,里面容不下一个合法的连续终局,不可能是最后落点,所以一旦右指针让窗口宽过 n,就得把左指针右移、把它压回 n 以内。只有宽度不超过 n 的窗口,才值得拿来数『里面已有几颗、还差几颗要搬』。
最小步数直接用 n 减窗内石子,什么时候会算错?+
只有一种情形:n-1 颗已经连成一整段、剩最后一颗落在远处。这时窗口罩住那 n-1 颗,公式给出 1 步,但唯一的 1 步走法是把远处那颗端点挪到紧邻空位,而它落下后又变成新的端点,违反规则。真实要 2 步——先把连续段一端挪出一格,再把远处那颗填进中间。判定就是窗内恰好 n-1 颗、且窗口跨度等于 n-2,命中就把这一窗记 2 步。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 移动石子直到连续 II 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。