LeetCode 849. 到最近的人的最大距离
题目描述
题意分析
一排座位用 0/1 数组给出,1 表示已有人、0 表示空位。要在某个空位坐下,使得「自己到最近的那个人的距离」尽可能大,返回这个最大值。距离按下标差计算。
题目保证至少有一个 1、也至少有一个 0,所以答案一定存在且至少为 1,不需要考虑无解。
关键在于目标函数的形状:坐下之后,到最近的人的距离只取决于左边最近的 1 和右边最近的 1,与更远的人无关。这意味着整排座位可以按 1 的位置切成若干互不影响的空位段,每段单独求最优,再取全局最大。这是一个「局部可分解」的信号——一次线性扫描就够,不需要任何数据结构。
段的类型必须分开讨论,这是全题唯一的难点。夹在两个 1 之间的空位段,坐在正中间时左右距离最平衡;而贴着数组左端或右端的空位段,一侧根本没有人,可以一路坐到最边上,距离等于整段长度。把这两类混为一谈是本题最主要的失分点。
约束里 $n$ 最大 2 万,$O(n)$ 绰绰有余;数组元素只有 0 和 1,不需要排序也不需要哈希。
边界要盯住:
[1,0,0,0]这种右端开口的情况答案是 3;[0,0,1]这种左端开口的答案是 2;[1,0,1]中间段长度为 1,答案是 1;[0,1,0]两端各有一个长度为 1 的开口段,答案也是 1。
解法:一次扫描统计 0 段长度
核心思路
暴力做法是对每个空位分别向左向右各扫一遍,找最近的 1,取两者较小值,再对所有空位取最大。这个做法完全正确,但每个空位最坏要扫 $O(n)$,总共 $O(n^2)$,在 2 万规模下就有 4 亿次操作,明显浪费。
瓶颈在于同一段空位里的每个位置,重复地在扫描同一对邻居。既然一段连续 0 的左右边界(那两个 1)对段内所有位置都是同一对,那就没必要逐个位置去找,直接对整段一次性算出最优答案。
由此得到分段的观察:把数组按 1 切开,每一段连续的 0 属于以下三类之一。设这一段占据下标区间
[i, j),长度len = j - i。第一类,左端开口段(
i == 0):左边没有任何人,坐在下标 0 处,最近的人是seats[j],距离为j - 0 = len。第二类,右端开口段(
j == n):右边没有任何人,坐在下标n - 1处,最近的人是seats[i-1],距离为(n-1) - (i-1) = len。第三类,中间夹段(
i > 0且j < n):两侧下标i-1与j处都有人,两人相隔j - (i-1) = len + 1。坐在正中间时,到两侧的距离分别是 $\lfloor \frac{len+1}{2} \rfloor$ 和 $\lceil \frac{len+1}{2} \rceil$,取较小者即 $\lfloor \frac{len+1}{2} \rfloor$,写成整数运算就是(len + 1) / 2。不变量:外层循环每次迭代开始时,
i指向一个尚未处理的下标,且answer已经等于「下标小于i的所有空位中,到最近的人的距离的最大值」。每处理完一段 0(或跳过一个 1),不变量继续成立。扫描结束时i == n,answer即为全局答案。注意「两个开口段可以并存」:
[0,0,1,0,0,0]的左段长 2、右段长 3,都按len算,答案是 3。也要注意开口段的判定用的是下标位置而不是「这段前面有没有 1」的记忆变量,写起来更不容易错。
解题步骤
- 初始化
answer = 0、i = 0:answer取 0 作初值是安全的,因为题目保证至少有一个空位,任何一段都会给出至少 1 的候选值,0 不会成为最终答案。- 遇到 1 就跳过:
seats[i] == 1时i++并continue。有人的位置不能坐,也不构成段,直接略过。- 用
j探出整段 0 的右边界:从j = i出发,只要j < n && seats[j] == 0就j++。循环结束时j要么等于n,要么指向段右侧第一个 1。用左闭右开区间[i, j)表示这一段,长度直接是j - i,避免了加一减一的混乱。- 判段型并更新答案:
i == 0 || j == n说明这段贴着数组的某一端,候选值取len;否则是中间夹段,候选值取(len + 1) / 2。用「或」是因为一段既可能贴左端也可能贴右端(整排只有一段 0 时两者同时成立),任一成立就按开口段算。- 推进
i = j:直接跳到段末,保证每个下标只被外层访问一次,总复杂度线性。这里绝不能写成i++,那样会对同一段重复计算len次。- 返回
answer。以
seats = [1,0,0,0,1,0,1]走一遍,n = 7。
i = 0:seats[0] = 1,跳过,i = 1。
i = 1:seats[1] = 0,探边界,j从 1 走到 4(seats[4] = 1停下)。len = 4 - 1 = 3。i = 1 != 0且j = 4 != 7,是中间夹段,候选(3 + 1) / 2 = 2。answer由 0 更新为 2。含义是:左右两人在下标 0 和 4,相隔 4,坐在下标 2 时左右各距 2。i = 4。
i = 4:seats[4] = 1,跳过,i = 5。
i = 5:seats[5] = 0,j从 5 走到 6(seats[6] = 1停下)。len = 1。中间夹段,候选(1 + 1) / 2 = 1,不大于 2,answer保持 2。i = 6。
i = 6:seats[6] = 1,跳过,i = 7 == n,循环结束,返回 2。再看开口段的用例
seats = [1,0,0,0]:i = 1时j一路走到 4 即n,len = 3,因为j == n走开口分支,候选 3,答案为 3——坐在最右端下标 3,最近的人在下标 0,距离正是 3。如果这里误用中间段公式会得到(3+1)/2 = 2,直接答错。
代码实现
class Solution {
public int maxDistToClosest(int[] seats) {
int n = seats.length;
int answer = 0;
int i = 0;
while (i < n) {
if (seats[i] == 1) {
i++;
continue;
}
int j = i;
while (j < n && seats[j] == 0) {
j++;
}
int len = j - i;
if (i == 0 || j == n) {
answer = Math.max(answer, len);
} else {
answer = Math.max(answer, (len + 1) / 2);
}
i = j;
}
return answer;
}
}
func maxDistToClosest(seats []int) int {
n := len(seats)
answer := 0
i := 0
for i < n {
if seats[i] == 1 {
i++
continue
}
j := i
for j < n && seats[j] == 0 {
j++
}
length := j - i
if i == 0 || j == n {
if length > answer {
answer = length
}
} else {
cand := (length + 1) / 2
if cand > answer {
answer = cand
}
}
i = j
}
return answer
}
复杂度分析
- 时间复杂度:$O(n)$。内层探边界的
j与外层的i合起来只在数组上单调右移一遍,i = j的跳跃保证没有任何下标被访问两次。- 空间复杂度:$O(1)$。只用了
answer、i、j、len四个整数,不开任何辅助数组,也没有递归。
关键点总结
- 目标只依赖左右最近的两个人,说明问题可按 1 的位置切成互不影响的段——识别出「可分解」结构,就能把 $O(n^2)$ 的逐点扫描降成 $O(n)$ 的逐段计算。
- 端点段与中间段的公式不同(
len对(len+1)/2),根源是端点段只有一侧有约束。凡是「距离/覆盖」类题目,都要先问一句「边界处是不是少了一侧约束」。- 用左闭右开区间
[i, j)描述段,长度直接是j - i,段型判定直接是i == 0与j == n,比维护「上一个 1 的位置」少一半的加减一错误。- 中间段的
(len + 1) / 2本质是「两人间距len + 1的一半向下取整」,写成(len + 1) / 2而不是len / 2 + 1之类,才能在len为偶数时正确。- 扫完一段后必须
i = j而不是i++,这是把线性算法写成平方算法的常见分水岭。- 面试视角:主动说出「答案候选只有三类段」并给出每类的闭式解,比写完代码再解释更有说服力;面试官往往会追问「为什么端点段是
len而不是len - 1」,答案是可以坐到下标 0 或n-1。
易错点总结
- 端点段套用中间段公式:
[1,0,0,0]会算出(3+1)/2 = 2,正确答案是 3。这是本题第一大错误。- 中间段写成
len / 2:[1,0,1]中len = 1,1/2 = 0,返回 0,而正确答案是 1(坐在下标 1,左右各距 1)。- 中间段写成
(len + 1) / 2之外的取整方向:写成(len + 2) / 2,[1,0,0,1]会得到 2,但实际坐在下标 1 或 2 时最近距离都只有 1,答案被高估。- 段型判定只判
i == 0漏掉j == n:[1,0,0,0]中i = 1 != 0,会走中间分支得到 2,右端开口被当成夹段。- 推进写成
i++而不是i = j:[1,0,0,0,0,0,1]这类长段会对同一段重复计算,答案虽然仍对(取的是最大值),但复杂度退化到 $O(n^2)$,n = 20000且大段全 0 时超时。- 探边界循环忘记
j < n的越界保护:[1,0,0]中j会一直走到 3 并访问seats[3],直接数组越界异常。answer初值设成Integer.MAX_VALUE或用min更新:本题求最大值,初值应为 0 并用max更新;用min会在[1,0,0,0,1,0,1]上返回 1 而不是 2。- 把「距离」理解成中间空位的个数:
[1,0,1]会算成 1 个空位即距离 1(巧合正确),但[1,0,0,0]会算成 3 个空位对 3(也巧合正确);真正暴露问题的是[1,0,0,1],按空位数会得到 2,而下标差最大只有 1。- 误以为要返回最优座位的下标:题目要的是最大距离这个数值,返回下标会全部答错。
- 试图先把所有 1 的下标收集到列表再两两求差:思路可行但要额外处理首尾两个「虚拟边界」,漏掉其中一个就会在
[0,0,1]或[1,0,0]上答错,且多花 $O(n)$ 空间,不如原地分段扫描。
相似题目
| 题目 | 难度 | 考察点 |
|---|---|---|
| 605. 种花问题 | 简单 | 同样按 0 段分类计数,端点段能多种一朵,公式随段型变化 |
| 485. 最大连续 1 的个数 | 简单 | 只需求最长 1 段长度,不区分段型,是本题分段扫描的最简形态 |
| 487. 最大连续1的个数 II | 中等 | 允许翻转一个 0,需要记住前一段长度或改用滑动窗口 |
| 1004. 最大连续1的个数 III | 中等 | 翻转次数推广到 k 次,标准可变长滑动窗口,与分段思路分道扬镳 |
| 1493. 删掉一个元素以后全为 1 的最长子数组 | 中等 | 必须删一个元素,答案要减一,边界处理与本题端点段的讨论同源 |
| 1552. 两球之间的磁力 | 中等 | 同样最大化「最小距离」,但位置可选且要放多个球,改用二分答案 |
| 424. 替换后的最长重复字符 | 中等 | 段的概念升级为窗口内众数,考察窗口收缩条件而非闭式公式 |