LeetCode-Go 题解:1353. Maximum Number of Events That Can Be Attended(贪心策略与天数坐标化) LeetCode-Go 题解1353. Maximum Number of Events That Can Be Attended贪心策略与天数坐标化【免费下载链接】LeetCode-Go✅ Solutions to LeetCode by Go, 100% test coverage, runtime beats 100% | LeetCode 题解项目地址: https://gitcode.com/GitHub_Trending/le/LeetCode-Go本篇文章围绕 LeetCode 第 1353 题「Maximum Number of Events That Can Be Attended最多可以参加的会议数目」展开以仓库 leetcode/1353.Maximum-Number-of-Events-That-Can-Be-Attended/README.md 为骨架结合同目录下的 Go 解题源码 与 单元测试 做逐行剖析。读完你将掌握「按开始时间排序 优先参加早结束会议」的贪心思路、把天数映射为坐标轴的排期技巧以及如何处理同一时刻堆叠多个会议的特殊边界数据。题目速览题意与约束题目原文给定一个数组events其中events[i] [startDayi, endDayi]。每个会议i从startDayi开始到endDayi结束。你可以在满足startDayi d endDayi的任意一天d参加这个会议注意同一天只能参加一个会议。请返回最多可以参加的会议数目。题目大意给你一个数组events其中events[i] [startDayi, endDayi]表示会议i开始于startDayi结束于endDayi。你可以在满足startDayi d endDayi中的任意一天d参加会议i。注意一天只能参加一个会议。请你返回你可以参加的最大会议数目。数据约束约束项取值events.length1 events.length 10^5单个会议元素events[i].length 2起止天范围1 startDayi endDayi 10^5五个官方示例示例 1题面原图描述三个会议依次首尾衔接可以全部参加Input: events [[1,2],[2,3],[3,4]] Output: 3 Explanation: You can attend all the three events. One way to attend them all is as shown. Attend the first event on day 1. Attend the second event on day 2. Attend the third event on day 3.示例 2存在起止完全相同的两个会议但跨越两天仍可全部参加Input: events [[1,2],[2,3],[3,4],[1,2]] Output: 4示例 3多个会议共享同一天且包含单天会议Input: events [[1,4],[4,4],[2,2],[3,4],[1,1]] Output: 4示例 4跨度极大的单会议Input: events [[1,100000]] Output: 1示例 5七个会议同一天开始Input: events [[1,1],[1,2],[1,3],[1,4],[1,5],[1,6],[1,7]] Output: 7解题思路为什么这是贪心问题关于会议安排、活动安排这类题第一直觉就是贪心。原文档给出的核心策略如下排序规则先按照会议开始时间从小到大排序如果开始时间相同再按照结束时间从小到大排序。贪心选择优先选择参加早结束的会议。原因很直观——一个结束时间晚的会议代表持续时间长先参加马上要结束的会议可以把后面的时间留给持续时间更长的安排从而参加更多会议。从左往右扫描依次扫描各个会议时间段选择结束时间大于起始时间的会议不断累加次数扫描完所有会议最终结果即为可参加的最大会议数。天数坐标化理解排序与比较的关键注意题目给的数据代表的是天数比较大小的时候最好转换成坐标轴上的坐标点。原文档以[1,2]为例这个会议持续 2 天如果在坐标轴上表示是[0,2]——0-1表示第一天1-2表示第二天。因此比较会议时需要把开始时间减一event[0]-1即源码中的start : max(current[0], event[0]-1)选定这个会议以后要把这一天排除例如选择了第二天那么下次对比起始时间需要从坐标2开始因为第二天的时间范围是1-2所以下一轮比较会议前需要把开始时间加一即源码中的current[0] start 1。这套「减一比较、加一排除」的转换是理解仓库实现也是正确编码的枢纽它把「天」这种离散概念统一到了连续的坐标轴上避免了区间判交时的越界错误。源码逐行解析仓库的 Go 实现仓库中的完整实现位于 1353. Maximum Number of Events That Can Be Attended.gomaxEvents定义于第 728 行代码如下package leetcode import ( sort ) func maxEvents(events [][]int) int { sort.Slice(events, func(i, j int) bool { if events[i][0] events[j][0] { return events[i][1] events[j][1] } return events[i][0] events[j][0] }) attended, current : 1, events[0] for i : 1; i len(events); i { prev, event : events[i-1], events[i] if event[0] prev[0] event[1] prev[1] event[1] event[0] { continue } start, end : max(current[0], event[0]-1), max(current[1], event[1]) if end-start 0 { current[0] start 1 current[1] end attended } } return attended } func max(a, b int) int { if a b { return a } return b }逐段解读1. 排序阶段第 813 行sort.Slice(events, func(i, j int) bool { if events[i][0] events[j][0] { return events[i][1] events[j][1] } return events[i][0] events[j][0] })完全对应原文档的排序规则开始时间升序开始时间相同则结束时间升序。排序后所有「早开始、早结束」的会议排在最前面为后续从左往右的贪心扫描奠定基础。由于sort.Slice是原地排序空间开销为 O(1)不计排序内部实现。2. 初始化第 14 行attended, current : 1, events[0]events长度最小为 1因此第一个会议必然能参加计数从 1 开始current记录当前「已占用到」的排期状态初始为第一个会议。3. 主扫描循环第 1526 行for i : 1; i len(events); i { prev, event : events[i-1], events[i] if event[0] prev[0] event[1] prev[1] event[1] event[0] { continue } start, end : max(current[0], event[0]-1), max(current[1], event[1]) if end-start 0 { current[0] start 1 current[1] end attended } }用prev记录前一个会议event记录当前会议start取「当前排期左端」与「当前会议起点坐标化后的值event[0]-1」的较大者end取「当前排期右端」与「当前会议结束天」的较大者。max(current[0], event[0]-1)正是原文档所说「比较会议时需要把开始时间减一」只要end-start 0说明坐标轴上仍存在可安排的间隙就把排期左端推进到start1即原文档的「选定了这个会议以后记得要把这一天排除」计数加一若end-start 0说明当前会议已被之前的排期完全覆盖跳过。边界情况同一天堆叠多个会议的「恶心数据」原文档特别提醒测试数据中有一组很恶心的数据见 test 文件中最后一组数据。这组数据在同一天叠加了多个会议并且起始时间完全一致。这种特殊情况需要加判断条件排除对应的就是代码中的continue条件if event[0] prev[0] event[1] prev[1] event[1] event[0] { continue }该条件同时要求三条成立开始天相同、结束天相同、并且开始等于结束单天会议。也就是说它只剔除「连续重复出现的单天会议」。看测试用例中这组关键数据单元测试 第 5457 行para1353{[][]int{{1, 10}, {2, 2}, {2, 2}, {2, 2}, {2, 2}}}, ans1353{2},排序后为[[1,10],[2,2],[2,2],[2,2],[2,2]]。正确结果是 2参加[1,10]任意一天和唯一的[2,2]只能在第 2 天。如果没有continue剔除重复单天会议模拟如下i1[2,2]与[1,10]合并排期推进计数 1i2再次遇到重复的[2,2]end-start仍 0排期再次推进计数错误地 1依此类推最终会错误地输出 5。为什么只跳过「单天会议」的重复因为跨多天的重复会议是可以全部参加的例如两个[1,2]分别在第一天、第二天各参加一个示例 2 即验证了这一点输出 4而[2,2]只有一个可用的天重复出现时只能算一次。这正是该条件精确匹配event[1] event[0]的原因。排序保证了相同的会议必然相邻因此用event[i-1]与event[i]比较即可覆盖所有重复。测试用例与验证仓库如何保证正确性仓库为本题编写了完整的表驱动测试。测试文件 1353. Maximum Number of Events That Can Be Attended_test.go 的结构如下para1353入参封装events [][]intans1353期望答案one intquestion1353将二者组合成一条用例Test_Problem1353第 25 行起遍历qs用例表打印输入输出并断言。完整的 6 组用例覆盖了常规场景与边界场景输入期望输出覆盖点[[1,2],[2,3],[3,4]]3首尾衔接的顺排场景[[1,4],[4,4],[2,2],[3,4],[1,1]]4单天会议与长跨度会议混合[[1,100000]]1单会议、超大跨度[[1,1],[2,2],[1,3],[1,4],[1,5],[1,6],[1,7]]7同一起点、不同终点的批量会议[[1,2],[2,2],[3,3],[3,4],[3,4]]4起止完全相同的会议对[[1,10],[2,2],[2,2],[2,2],[2,2]]2同一天堆叠多个单天会议文档强调的「恶心数据」运行与覆盖率在仓库根目录下可以用 Go 标准测试命令单独验证本题go test -v ./leetcode/1353.Maximum-Number-of-Events-That-Can-Be-Attended/也可以直接执行仓库自带的测试脚本 gotest.sh它会对全部题解做原子覆盖率采集./gotest.sh脚本内部执行的是go test -covermodeatomic -coverprofilecoverage.txt ./leetcode/...把覆盖数据统一写入根目录的 coverage.txt。在该文件中可以查到本题源码的覆盖率明细例如github.com/halfrost/LeetCode-Go/leetcode/1353.Maximum-Number-of-Events-That-Can-Be-Attended/1353. Maximum Number of Events That Can Be Attended.go:7.36,8.41 1 6这从侧面印证了本题的实现被测试用例充分覆盖项目根 README.md 亦声明该仓库追求 100% 测试覆盖率。复杂度分析与延伸思考从源码结构看本题实现的时间复杂度为排序sort.Slice为 O(n log n)n 为会议数量最大10^5可接受扫描单次线性循环 O(n)空间复杂度原地排序 常数个变量为 O(1)。整体瓶颈在排序符合原文档「先排序再贪心」的定式。值得一提的是本题在算法社区中还存在另一类经典解法把会议按开始时间分组用最小堆优先队列维护当前可参加的会议的结束时间每天从堆中弹出一个结束最早的会议参加复杂度同样为 O(n log n)。仓库此处给出的则是「排序 坐标轴区间合并」的变体其优势在于不依赖额外的堆数据结构、代码紧凑且天然规避了堆解法中「同一天多个会议」的同类边界问题。理解两种思路可以在面试中根据题目变形灵活选用。小结LeetCode 1353 是一道典型的贪心区间调度题。通过本文你可以掌握贪心直觉按开始时间升序、结束时间升序排序优先参加早结束的会议坐标化技巧把天数[1,2]映射为坐标区间[0,2]比较时开始时间减一、选定后左端加一避免日期边界判断出错边界处理排序后连续重复的单天会议需要用continue条件剔除否则会在同一天堆叠的场景下重复计数对应测试中[[1,10],[2,2],[2,2],[2,2],[2,2]]这组数据。仓库中对应的 题解文档、Go 实现 与 表驱动测试 三份文件互相印证是复习该题时可直接复用的完整素材。【免费下载链接】LeetCode-Go✅ Solutions to LeetCode by Go, 100% test coverage, runtime beats 100% | LeetCode 题解项目地址: https://gitcode.com/GitHub_Trending/le/LeetCode-Go创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考