LeetCode 每日一题笔记
0. 前言
- 日期:2026.06.02
- 题目:3635. 最早完成陆地和水上游乐设施的时间 II
- 难度:中等
- 标签:排序、二分查找、贪心、预处理
1. 题目理解
问题描述
必须从陆地、水上各挑选1个项目游玩,游玩顺序可选「先陆地后水上」或「先水上后陆地」;项目不能早于自身startTime启动,前一个项目结束后才能开启后一个项目,求全部游玩完毕的最小结束时间。
示例
输入:
landStartTime = [2,8], landDuration = [4,1], waterStartTime = [6], waterDuration = [3]输出:9
解释:选陆地0(2开始+4时长,6结束)+水上0(6开始+3时长,9结束),总耗时9。
2. 解题思路
核心观察
- 两种游玩次序独立计算最优解,最终答案取二者更小值;
- 暴力枚举所有配对复杂度O(NM)O(NM)O(NM),大数据超时,优化方案:排序+二分+前后缀预处理,把复杂度压至O(NlogN+MlogM)O(N\log N+M\log M)O(NlogN+MlogM);
- 对后选的项目按开始时间升序排序,二分拆分两类候选:
- 开始时间≤前项目结束:选其中耗时最短的项目,总耗时=前项目结束+最短时长;
- 开始时间>前项目结束:选其中单独结束时间最早的项目。
算法步骤
- 分别打包陆地、水上项目,按项目开始时间升序排序;
- 预处理前缀最小耗时数组、后缀最小单独结束时间数组;
- 分别枚举先陆地、先水上两种顺序,借助二分查找快速筛选候选项目,计算每种顺序的最小结束时间;
- 两种顺序结果取最小值作为答案。
3. 代码实现
java
package lc3600_lc3699.lc3635;
import java.util.Arrays;
class Solution {
public int earliestFinishTime(int[] landStartTime, int[] landDuration, int[] waterStartTime, int[] waterDuration) {
int n = landStartTime.length;
int m = waterStartTime.length;
// 1. 打包并排序陆地项目(按开始时间升序)
int[][] land = new int[n][3]; // [开始时间, 持续时间, 结束时间]
for (int i = 0; i < n; i++) {
land[i][0] = landStartTime[i];
land[i][1] = landDuration[i];
land[i][2] = landStartTime[i] + landDuration[i];
}
Arrays.sort(land, (a, b) -> a[0] - b[0]);
// 打包并排序水上项目
int[][] water = new int[m][3];
for (int i = 0; i < m; i++) {
water[i][0] = waterStartTime[i];
water[i][1] = waterDuration[i];
water[i][2] = waterStartTime[i] + waterDuration[i];
}
Arrays.sort(water, (a, b) -> a[0] - b[0]);
// 2. 预处理陆地项目的前缀最小时长和后缀最小结束时间
int[] prefixMinDurLand = new int[n];
prefixMinDurLand[0] = land[0][1];
for (int i = 1; i < n; i++) {
prefixMinDurLand[i] = Math.min(prefixMinDurLand[i - 1], land[i][1]);
}
int[] suffixMinFinishLand = new int[n + 1];
suffixMinFinishLand[n] = Integer.MAX_VALUE; // 边界哨兵
for (int i = n - 1; i >= 0; i--) {
suffixMinFinishLand[i] = Math.min(suffixMinFinishLand[i + 1], land[i][2]);
}
// 预处理水上项目的前缀最小时长和后缀最小结束时间
int[] prefixMinDurWater = new int[m];
prefixMinDurWater[0] = water[0][1];
for (int i = 1; i < m; i++) {
prefixMinDurWater[i] = Math.min(prefixMinDurWater[i - 1], water[i][1]);
}
int[] suffixMinFinishWater = new int[m + 1];
suffixMinFinishWater[m] = Integer.MAX_VALUE;
for (int i = m - 1; i >= 0; i--) {
suffixMinFinishWater[i] = Math.min(suffixMinFinishWater[i + 1], water[i][2]);
}
// 3. 提取排序后的开始时间和结束时间数组(方便二分查找)
int[] landStarts = new int[n];
int[] landFinishes = new int[n];
for (int i = 0; i < n; i++) {
landStarts[i] = land[i][0];
landFinishes[i] = land[i][2];
}
int[] waterStarts = new int[m];
int[] waterFinishes = new int[m];
for (int i = 0; i < m; i++) {
waterStarts[i] = water[i][0];
waterFinishes[i] = water[i][2];
}
// 4. 计算两种顺序的最小完成时间
int minLandFirst = calculateMin(landFinishes, waterStarts, prefixMinDurWater, suffixMinFinishWater);
int minWaterFirst = calculateMin(waterFinishes, landStarts, prefixMinDurLand, suffixMinFinishLand);
return Math.min(minLandFirst, minWaterFirst);
}
// 辅助函数:计算"先玩first类,再玩second类"的最小完成时间
private int calculateMin(int[] firstFinishes, int[] secondStarts, int[] secondPrefixMinDur, int[] secondSuffixMinFinish) {
int minTime = Integer.MAX_VALUE;
int m = secondStarts.length;
for (int finish1 : firstFinishes) {
// 二分查找:找到第一个开始时间 > finish1 的项目索引
int k = upperBound(secondStarts, finish1) - 1;
int currentMin = Integer.MAX_VALUE;
// 情况1:有项目在finish1前开放 → 选耗时最短的,结束时间=finish1+最短时长
if (k >= 0) {
currentMin = Math.min(currentMin, finish1 + secondPrefixMinDur[k]);
}
// 情况2:有项目在finish1后开放 → 选最早结束的那个
if (k + 1 < m) {
currentMin = Math.min(currentMin, secondSuffixMinFinish[k + 1]);
}
minTime = Math.min(minTime, currentMin);
}
return minTime;
}
// 二分查找:返回第一个大于target的元素索引
private int upperBound(int[] arr, int target) {
int left = 0, right = arr.length;
while (left < right) {
int mid = left + (right - left) / 2;
if (arr[mid] > target) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
}
4. 代码优化说明
java
class Solution {
public int earliestFinishTime(int[] landStartTime, int[] landDuration, int[] waterStartTime, int[] waterDuration) {
// 分两种游玩次序分别求解,最终取更小值
int landWater = solve(landStartTime,landDuration,waterStartTime,waterDuration);
int waterLand = solve(waterStartTime,waterDuration,landStartTime,landDuration);
return Math.min(landWater,waterLand);
}
/**
* @param firstStartTime 先游玩项目的开始数组
* @param firstDuration 先游玩项目的耗时数组
* @param secondStartTime 后游玩项目的开始数组
* @param secondDuration 后游玩项目的耗时数组
* @return 该游玩顺序下的最小结束时间
*/
public int solve(int[] firstStartTime,int[] firstDuration,int[] secondStartTime,int[] secondDuration) {
int firstGroupMinEndTime = Integer.MAX_VALUE;
int fN = firstStartTime.length;
// 遍历在先的项目,筛选单独游玩结束时间最小的项目
for(int i = 0;i < fN;i++) {
int finishTime = firstStartTime[i] + firstDuration[i];
firstGroupMinEndTime = Math.min(firstGroupMinEndTime,finishTime);
}
int result = Integer.MAX_VALUE;
int sN = secondStartTime.length;
// 用在先项目最优结束时间,逐个搭配在后项目,更新全局最小值
for(int i = 0;i < sN;i++) {
// 后项目实际起始时间不能早于自身开放、不能早于前项目结束
int actualStartTime = Math.max(secondStartTime[i],firstGroupMinEndTime);
result = Math.min(result,actualStartTime + secondDuration[i]);
}
return result;
}
}
5. 复杂度分析
- 二分+预处理原版
时间:O(NlogN+MlogM)O(N\log N+M\log M)O(NlogN+MlogM),排序、预处理、二分遍历均为对数/线性级;
空间:O(N+M)O(N+M)O(N+M),各类预处理数组开销。 - 贪心简化优化版
时间:O(N+M)O(N+M)O(N+M),两轮线性遍历;
空间:O(1)O(1)O(1),仅常数临时变量。
6. 总结
- 核心:拆分两种游玩顺序,分别求最优解再取最小;
- 原版:适配数据范围偏大场景,用排序二分优化枚举;优化版:贪心假设最优搭配为两类单品最优组合,代码极简、线性效率;
- 关键:后项目启动时间 =
max(自身起始时间,前项目结束时间)。