【LeetCode 3440. 重新安排会议得到最多空余时间 II】解析

目录

  • LeetCode中国站原文
  • 原始题目
    • 题目描述
    • 示例1:
    • 示例2:
    • 示例3:
    • 示例4:
  • 讲解
    • 1. 新规则,新挑战
    • 2. 收益从何而来?两种可能性的诞生
    • 3. 我们的终极策略
    • 4. 当策略被压缩到极致
      • 第一次遍历:从左到右(向左看)
      • 第二次遍历:从右到左(向右看)
      • 等价的但是我自己尝试的代码
    • 5. 结论

LeetCode中国站原文

https://leetcode.cn/problems/reschedule-meetings-for-maximum-free-time-ii/

原始题目

题目描述

给你一个整数 eventTime 表示一个活动的总时长,这个活动开始于 t = 0 ,结束于 t = eventTime

同时给你两个长度为 n 的整数数组 startTimeendTime 。它们表示这次活动中 n 个时间 没有重叠 的会议,其中第 i 个会议的时间为 [startTime[i], endTime[i]]

你可以重新安排 至多 一个会议,安排的规则是将会议时间平移,且保持原来的 会议时长 ,你的目的是移动会议后 最大化 相邻两个会议之间的 最长 连续空余时间。

请你返回重新安排会议以后,可以得到的 最大 空余时间。

注意,会议 不能 安排到整个活动的时间以外,且会议之间需要保持互不重叠。

注意:重新安排会议以后,会议之间的顺序可以发生改变。

示例1:

输入:eventTime = 5, startTime = [1,3], endTime = [2,5]
输出:2
解释:将 [1, 2] 的会议安排到 [2, 3] ,得到空余时间 [0, 2] 。

示例2:

输入:eventTime = 10, startTime = [0,7,9], endTime = [1,8,10]
输出:7
解释:将 [0, 1] 的会议安排到 [8, 9] ,得到空余时间 [0, 7] 。

示例3:

输入:eventTime = 10, startTime = [0,3,7,9], endTime = [1,4,8,10]
输出:6
解释:将 [3, 4] 的会议安排到 [8, 9] ,得到空余时间 [1, 7] 

示例4:

输入:eventTime = 5, startTime = [0,1,2,3,4], endTime = [1,2,3,4,5]
输出:0
解释:活动中的所有时间都被会议安排满了。

讲解

如果你看过我们之前对 I 系列的讨论(【LeetCode 3439. 重新安排会议得到最多空余时间 I】解析),你会发现这道题有一个关键性的变化:会议的相对顺序可以改变了! 这个变化让上一题精妙的“滑动窗口”解法瞬间失效,迫使我们必须寻找新的出路。

这篇文章将带你从零开始,理清思路,看穿问题的本质,并最终理解一个堪称“神仙”级别的解法。

1. 新规则,新挑战

我们先来回顾一下规则:

  • 能力:可以平移最多 1 个会议。
  • 变化:平移后,会议的顺序可以任意改变
  • 目标:创造出一个尽可能长的连续空闲时间

“顺序可以改变”是这道题的“游戏规则改变者”。这意味着,我们可以从所有会议中,任意挑选一个“倒霉蛋”会议,把它塞进任意一个能容纳它的空隙里。

那么,我们的决策过程就变成了:

  1. 选哪个会议去移动?
  2. 把它移动到哪里去?

我们的目标是让这两个决策的组合,能产生最大的收益。

2. 收益从何而来?两种可能性的诞生

要获得最长的空闲时间,最终的结果只可能从两种情况中产生:

情况一:不移动任何会议

如果我们选择不动,那么最长的空闲时间就是所有原始“空隙”中,最长的那一个。这是我们的保底收益。

情况一:不移动
原始状态
最长空闲 = 空隙2的长度
会议1
空隙1
空隙2
最长
会议2
空隙3

情况二:移动 1 个会议

这是问题的精髓所在。当我们决定移动一个会议,比如「会议i」,会发生什么?

  • 「会议i」原本占用的空间 [startTime[i], endTime[i]] 被释放了。
  • 这个被释放的空间,会和它前后的空隙(「空隙i」和「空隙i+1」)合并,形成一个巨大的新空隙!
被移动的会议i
移动后
移动前
会议i
被塞到其他地方
新产生的巨大空隙
长度 = 空隙i + 会议i + 空隙i+1
会议i-1
会议i+1
空隙i
会议i-1
会议i
空隙i+1
会议i+1

这个新产生的巨大空隙,其长度为 startTime[i+1] - endTime[i-1]

但是,这个美好的情况有一个前提条件:被我们移动的「会议i」必须有地方可去!也就是说,它的时长 (endTime[i] - startTime[i]) 必须小于或等于我们场上存在的某一个原始空隙的长度。

3. 我们的终极策略

结合以上分析,一个清晰的,虽然可能比较慢的策略诞生了:

  1. 预处理

    • 计算出所有会议各自的时长。
    • 计算出所有原始空隙的长度,并找到其中的最大值 max_original_gap
  2. 遍历与决策

    • 遍历每一个会议 i (从 0 到 n-1),假装要移动它。
    • 计算收益:如果移动会议 i,能创造出的新空隙长度为 startTime[i+1] - endTime[i-1]
    • 检查条件:会议 i 的时长,是否 <= max_original_gap
      • 如果条件满足:说明移动是可行的。我们就在这个“收益”和我们已知的“最大收益”之间取一个更大的。
      • 如果条件不满足:说明会议 i 太长了,根本没地方放。我们无法移动它,只能放弃这个方案。
  3. 综合比较:最后,别忘了把“移动会议”能产生的最大收益,和“不移动会议”(即 max_original_gap)本身再比较一次,取最终的胜利者。

这个 O(N) 的预处理 + O(N) 的遍历决策,总时间复杂度为 O(N),空间复杂度也是 O(N)。这是一个非常可靠且正确的解法。

4. 当策略被压缩到极致

现在,让我们来欣赏一下那段极其精炼的、只用两个 for 循环就解决问题的代码。它没有显式地预处理和存储,而是将所有计算都“动态地”揉进了循环里。

这个解法的核心思想是:通过一次从左到右和一次从右到左的遍历,来模拟“向前看”和“向后看”,从而解决移动条件检查的问题。

第一次遍历:从左到右(向左看)

这次遍历,当我们站在会议 i 的位置时,我们只知道在它左边发生的所有事。

// mx 在这里代表“截至目前,我左边见过的最大空隙是多少”
int mx = 0;
for (int i = 0; i < n; i++) {int len = endTime[i] - startTime[i]; // 会议i的时长// 检查移动条件:会议i能放进左边的最大空隙吗?if (len <= mx) {// 如果可以,就计算移动它能产生的收益,并更新全局最大值 resint potential_gap = (i == n-1 ? eventTime : startTime[i+1]) - (i == 0 ? 0 : endTime[i-1]);res = Math.max(res, potential_gap);}// 更新状态,为下一次循环做准备// 1. 让 mx 知道刚刚路过的这个空隙int current_gap = startTime[i] - (i == 0 ? 0 : endTime[i-1]);mx = Math.max(mx, current_gap);// 2. 同时,我们也要考虑不移动的情况,即原始空隙本身也是备选答案res = Math.max(res, current_gap);
}

注意:为了便于理解,我稍微修改了代码逻辑,使其更符合直觉。官方题解的写法更为精炼,但核心思想一致。

这次遍历的盲点在于,它不知道会议 i 右边是否有一个超级大的空隙可以容纳它。

第二次遍历:从右到左(向右看)

为了弥补这个盲点,我们需要一次完全对称的、从右到左的遍历。

// mx 重置,代表“截至目前,我右边见过的最大空隙是多少”
mx = 0; 
for (int i = n - 1; i >= 0; i--) {int len = endTime[i] - startTime[i];// 检查移动条件:会议i能放进右边的最大空隙吗?if (len <= mx) {int potential_gap = (i == n-1 ? eventTime : startTime[i+1]) - (i == 0 ? 0 : endTime[i-1]);res = Math.max(res, potential_gap);}// 更新状态int current_gap = (i == n-1 ? eventTime : startTime[i+1]) - endTime[i];mx = Math.max(mx, current_gap);res = Math.max(res, current_gap);
}```### 最终代码呈现将上述思想融合,并采用官方题解的精炼写法,就得到了最终的答案:```java
class Solution {public int maxFreeTime(int eventTime, int[] startTime, int[] endTime) {int n = startTime.length;int res = 0;// 第一次遍历:从左到右int mx_left_gap = 0; // 记录左侧的最大空隙int last_end = 0;for (int i = 0; i < n; i++) {int len = endTime[i] - startTime[i];int potential_new_gap = (i == n - 1 ? eventTime : startTime[i + 1]) - last_end;if (len <= mx_left_gap) {res = Math.max(res, potential_new_gap);} else {// 如果不能移动,我们能获得的最大空隙,// 就是把当前会议的时长从 potential_new_gap 中挖掉res = Math.max(res, potential_new_gap - len);}mx_left_gap = Math.max(mx_left_gap, startTime[i] - last_end);last_end = endTime[i];}// 第二次遍历:从右到左int mx_right_gap = 0; // 记录右侧的最大空隙int last_start = eventTime;for (int i = n - 1; i >= 0; i--) {int len = endTime[i] - startTime[i];int potential_new_gap = last_start - (i == 0 ? 0 : endTime[i - 1]);if (len <= mx_right_gap) {res = Math.max(res, potential_new_gap);} else {res = Math.max(res, potential_new_gap - len);}mx_right_gap = Math.max(mx_right_gap, last_start - startTime[i]);last_start = startTime[i];}return res;}
}

等价的但是我自己尝试的代码

由于本人的算法知识并不算非常强,在一开始写左右分别看的代码出现了很多问题,这里给出一个可能冗余但是能看懂逻辑的相同的“向左看”+向右看的代码

class Solution {// time = O(n), space = O(n)public int maxFreeTime(int eventTime, int[] startTime, int[] endTime) {// 当前会议的长度,从startTime或者endTime任意一个数组的长度取值都可以。int meetingCount = startTime.length;// 记录最终的答案int ans = 0;// 临时的最大值int tempMaxGap = 0;// 接下来要从左向右看。// 记录下来走过来最长的路int maxGap = 0;for(int i=0; i< meetingCount; i++){// 是否是第一个会议boolean isFirstMeeting = i==0;// 是否是最后一个会议boolean isLastMeeting = i == meetingCount - 1;// 左边的空隙int leftGap = isFirstMeeting ? startTime[0] : startTime[i] - endTime[i-1];// 本次循环中会议的持续时间int meetingDuration = endTime[i] - startTime[i];if(maxGap >= meetingDuration){// 如果当前会议的时长比走过的最大间隔还要大,或者等于最大时长,代表可以插入// 上一个会议的结束点int lastMeetingEnd = isFirstMeeting ? 0 : endTime[i-1];// 下一个会议的开始时间int nextMeetingStart = isLastMeeting ? eventTime : startTime[i+1];// 新的巨大空隙tempMaxGap = nextMeetingStart - lastMeetingEnd;}else{// 右边的空隙int rightGap = isLastMeeting ? eventTime - endTime[i]: startTime[i+1] - endTime[i];// 新的空隙(为左边的空隙+右边的空隙)tempMaxGap = leftGap + rightGap;}ans = Math.max(ans, tempMaxGap);// 更新记忆maxGap = Math.max(maxGap, leftGap);}// 更新ansans = Math.max(ans, maxGap);// 重置记忆maxGapmaxGap = 0;for(int i=meetingCount - 1; i>=0; i--){// 是否是第一个会议boolean isFirstMeeting = i==0;// 是否是最后一个会议boolean isLastMeeting = i == meetingCount - 1;// 右边的空隙int rightGap = isLastMeeting ? eventTime - endTime[i]: startTime[i+1] - endTime[i];// 本次循环中会议的持续时间int meetingDuration = endTime[i] - startTime[i];if(maxGap >= meetingDuration){// 如果当前会议的时长比走过的最大间隔还要大,或者等于最大时长,代表可以插入// 上一个会议的结束点int lastMeetingEnd = isFirstMeeting ? 0 : endTime[i-1];// 下一个会议的开始时间int nextMeetingStart = isLastMeeting ? eventTime : startTime[i+1];// 新的巨大空隙tempMaxGap = nextMeetingStart - lastMeetingEnd;}else{// 左边的空隙int leftGap = isFirstMeeting ? startTime[0] : startTime[i] - endTime[i-1];// 新的空隙(为左边的空隙+右边的空隙)tempMaxGap = leftGap + rightGap;}ans = Math.max(ans, tempMaxGap);// 更新记忆maxGap = Math.max(maxGap, rightGap );}return ans;}
}

5. 结论

这道题是对问题分析和抽象能力的绝佳考验。它告诉我们:

  1. 明确规则变化:”相对顺序可变“是解题的钥匙。
  2. 分解问题:将复杂问题分解为“移动”和“不移动”两种独立的、可分析的情况。
  3. 优化实现:思考如何用更少的空间和时间复杂度来实现策略。两次遍历的解法正是这种极致优化的产物。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/88979.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/88979.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C++卸载了会影响电脑正常使用吗?解析C++运行库的作用与卸载后果

卸载C运行库可能导致常用软件瘫痪&#xff01;这些不起眼的组件为Photoshop、游戏等提供关键支持&#xff0c;多个版本共存是正常现象&#xff0c;随意清理会引发程序报错甚至闪退。一、前言&#xff1a;C不是“编程语言”那么简单很多用户在电脑中看到“Microsoft Visual C Re…

前端vue对接海康摄像头流程

1、拆包摄像头、插电源2、下载SADP&#xff08;设备网络搜索&#xff09;&#xff0c;连接设备&#xff0c;获取ip地址 下载地址&#xff1a;https://partners.hikvision.com/tools 找到自己的设备类型DS开头3、摄像头链接wifi、网线 登录设备预览配置网页-配置网络-可预览等 4…

org.casic.javafx.control.PaginationPicker用法

org.casic.javafx.control.PaginationPicker 是 CASIC&#xff08;或某位作者&#xff09;基于 JavaFX 自制的分页控件&#xff0c;功能比官方 Pagination 更完整&#xff0c;支持&#xff1a;首页 / 上一页 / 下一页 / 尾页按钮页码快速跳转每页条数自定义总数据量、当前页码、…

下载 | Win10 2021精简版,预装应用极少!(7月更新、Win 10 IoT LTSC 2021版、适合老电脑安装)

⏩ 【资源A047】Win10 IoT LTSC 2021精简版 &#x1f536;Windows 10 IoT 企业版 LTSC 2021 正式版更新中。LTSC是长期服务渠道版本&#xff0c;网友俗称“老坛酸菜版”&#xff0c;相当于精简版Win10&#xff0c;精简了很多预装应用&#xff0c;同时更新频率也更低&#xff0c…

Web3:Foundry使用指南

Foundry目录1. 前言2. 什么是Foundry3. 安装与环境配置1. 安装工具2. 重新加载 .bashrc3. 检查环境变量 PATH4. 手动运行 foundryup4. Foundry的基本使用1.创建一个新的Foundry项目2. 编写智能合约3. 编译智能合约4. foundry.toml 主要作用5.部署智能合约5. Cli参考1. forge2. …

uniapp+unipush推送配置

APP推送记录 一、使用框架 Uniappunipush推送插件 二、需要提前准备的 1.准备自有证书 可以用这个网站—香蕉云编&#xff08;用于安卓 ios证书生成&#xff09;https://www.yunedit.com/update/androidzhengshu/list 安卓证书生成后&#xff0c;下载证书&#xff0c;除了原文…

CentOS系统哪些版本?分别适用于那些业务或网站类型?

CentOS&#xff08;Community ENTerprise Operating System&#xff09;是一款开源的企业级 Linux 操作系统&#xff0c;因其稳定性、安全性和长期支持周期&#xff0c;广泛应用于服务器环境。以下是 CentOS 的主要版本及其适用场景的详细介绍。1. CentOS 主要版本CentOS 的版本…

【前端】【Iconify图标库】【vben3】createIconifyIcon 实现图标组件的自动封装

&#x1f9e9; Vue 图标管理全攻略&#xff1a;Iconify createIconifyIcon 封装最佳实践 在前端项目中&#xff0c;图标无处不在。按钮需要图标&#xff0c;导航需要图标&#xff0c;提示信息也少不了图标。如何优雅、高效地使用图标&#xff0c;是每个中大型 Vue 项目不可回…

数据可视化全流程设计指南

一、需求定义阶段1. 明确核心目标回答关键问题&#xff1a;2. 确定数据特性import pandas as pd data pd.read_csv(your_data.csv) print(f""" 数据概览: - 维度: {data.shape[1]}列 {data.shape[0]}行 - 类型分布: {data.dtypes.value_counts()} - 缺失值: …

Llama系列:Llama1, Llama2,Llama3内容概述

前言 参考视频&#xff1a;大模型修炼之道(三): Llama系列讲解 Llama1&#xff0c;Llama2, Llama3_哔哩哔哩_bilibili 本博客是基于视频的学习笔记&#xff0c;以及相关知识点的扩充 Llama1 1. 动机 使用完全开源数据&#xff0c;性能媲美GPT3研究开源&#xff0c;禁止商用…

Docker 搭建本地Harbor私有镜像仓库

Docker 搭建本地Harbor私有镜像仓库 一、Harbor 核心价值与企业级特性解析 在容器化技术普及的背景下&#xff0c;镜像仓库作为容器生命周期的核心组件&#xff0c;其可靠性直接影响开发效率与生产稳定性。Docker 官方的 Registry 虽能实现基础镜像存储&#xff0c;但存在明显短…

AI 助力:如何批量提取 Word 表格字段并导出至 Excel

在日常办公中&#xff0c;我们经常需要处理大量的 Word 文档中的表格数据&#xff0c;如学生登记表、客户信息表、报名表等。然而这些表格往往格式各异、字段命名不统一&#xff08;如“姓名”“名字”“Name”&#xff09;&#xff0c;甚至含有合并单元格或多余空白行&#xf…

在 Azure Linux 上安装 RustFS

本文分享在 Azure Linux 上安装并使用对象存储 RustFS 的过程。 关于 RustFS RustFS 是一款用 Rust 语言编写的分布式存储系统&#xff0c;兼容 S3 协议&#xff0c;是 MinIO 的国产化平替。详情可以前往 RustFS 官网。目前&#xff0c;RustFS 支持二进制、Docker 安装方式&am…

实现在线预览pdf功能,后台下载PDF

<!-- PDF预览模态框 --><n-modalv-model:show"pdfModalVisible"title"投诉统计报告预览":closable"false":mask-closable"false"positive-click"closePdfModal"positive-text"关闭":width"900"…

华为VS格行VS中兴VS波导随身WIFI6怎么选?流量卡OR随身WIFI,长期使用到底谁更香?

在移动互联时代&#xff0c;流量焦虑成为现代人的通病。面对"办流量卡还是随身WiFi"的抉择&#xff0c;许多人陷入两难。本文从实际需求出发&#xff0c;用数据和场景帮你精准决策&#xff0c;尤其这五类人群建议直接选择正规随身WiFi。一、这五类人&#xff0c;随身…

AI网络搜索

作为AI应用程序开发人员在了解函数调用&#xff08;Function Calling&#xff09;特性调用本地函数时可能注意到列表型参数tools中每一个元素都携带有一个type值。而在大多数函数调用示例程序中&#xff0c;这个type值一直被设定为“function”&#xff0c;这意味着它还可能存在…

39.Sentinel微服务流量控制组件

雪崩问题 微服务调用链路中某个服务故障,引起整个链路中的所有微服务都不可用。 解决方案 1.超时处理:设置一个超时时间,请求超过一定时间没有响应就返回错误信息,不会无休止的等待。(只能起到缓解作用,并不能从根本上解决问题) 2.舱壁模式:限定每个业务能使用的线程…

基于hadoop的竞赛网站日志数据分析与可视化(下)

【基于hadoop的竞赛网站日志数据分析与可视化&#xff08;上&#xff09;】讲解了如何用hadoop对数据进行初步处理&#xff0c;本篇主要讲解用python对结果数据进行可视化分析。 ------------------------------------------------------------------------------------------…

Python爬虫打怪升级:数据获取疑难全解析

一、引言 **​​​ 在大数据时代,数据就是价值的源泉。而 Python 爬虫,作为数据获取的得力助手,凭借 Python 简洁的语法和丰富强大的库,在众多领域发挥着重要作用。无论是电商领域的价格监测、市场调研中的数据收集,还是学术研究里的文献获取,Python 爬虫都能大显身手。…

基于R语言的极值统计学及其在相关领域中的实践技术应用

极值统计学就是专门研究自然界和人类社会中很少发生&#xff0c;然而发生之后有着巨大影响的极端现象的统计建模及分析方法&#xff1b;在水文、气象、环境、生态、保险和金融等领域都有着广泛的应用。一&#xff1a;独立假设下的极值统计建模 1.广义极值模型. 2.极小值的处理.…