LeetCode 每日一题笔记 日期:2026.06.02 题目:3635. 最早完成陆地和水上游乐设施的时间 II

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. 解题思路

核心观察

  1. 两种游玩次序独立计算最优解,最终答案取二者更小值;
  2. 暴力枚举所有配对复杂度O(NM)O(NM)O(NM),大数据超时,优化方案:排序+二分+前后缀预处理,把复杂度压至O(Nlog⁡N+Mlog⁡M)O(N\log N+M\log M)O(NlogN+MlogM);
  3. 对后选的项目按开始时间升序排序,二分拆分两类候选:
    • 开始时间≤前项目结束:选其中耗时最短的项目,总耗时=前项目结束+最短时长;
    • 开始时间>前项目结束:选其中单独结束时间最早的项目。

算法步骤

  1. 分别打包陆地、水上项目,按项目开始时间升序排序;
  2. 预处理前缀最小耗时数组、后缀最小单独结束时间数组;
  3. 分别枚举先陆地、先水上两种顺序,借助二分查找快速筛选候选项目,计算每种顺序的最小结束时间;
  4. 两种顺序结果取最小值作为答案。

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(Nlog⁡N+Mlog⁡M)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(自身起始时间,前项目结束时间)
相关推荐
Lsk_Smion2 小时前
力扣实训 _ [102].层序遍历--前序--后续_递归与非递归的实现
数据结构·算法·leetcode
小满Autumn2 小时前
MVVM Light 架构笔记:定位器、命令、消息与 IoC 实践
笔记·学习·架构·c#·上位机·mvvm
小欣加油3 小时前
leetcode3751 范围内总波动值I
java·数据结构·c++·算法·leetcode
kobesdu3 小时前
【ROS2实战笔记-24】ROS2 Launch 实用技巧:条件逻辑与节点动态生成
笔记·ros·slam
小满Autumn4 小时前
CommunityToolkit.Mvvm 架构笔记:现代 MVVM、源生成器与工程化实践
笔记·架构·c#·.net·wpf·mvvm
Halo_tjn4 小时前
反射与设计模式1
java·开发语言·算法
V搜xhliang02465 小时前
临床科研新范式:从选题到投稿,AI智能体如何接管全流程?
运维·数据结构·人工智能·算法·microsoft·数据挖掘·自动化
计算机安禾5 小时前
【算法分析与设计】第46篇:近似难度与不可近似性理论
网络协议·算法·ssl
小bo波6 小时前
Java Swing 可视化素数筛:动态演示 1~120 质数筛选【附完整源码】
java·算法·可视化·swing·素数