两个最好的不重叠活动 图解题解
这道题到底在问什么
- 输入
- events=[[1,3,2],[4,5,2],[2,4,3]]
- 输出
- 4 (选活动 [1,3,2] 与 [4,5,2],2 加 2)
- 输入
- events=[[1,5,3],[1,5,1],[6,6,5]]
- 输出
- 8 (选 [1,5,3] 与 [6,6,5],3 加 5)
最优解:一步一步想明白
- 3记牢这套:排序,填后缀最大值 f,再逐个活动找第一个开得下的边界、用本价值加边界处的 f。下面按这套一步步填表。
- 4五个活动待处理先把五个活动摊成一张表,每一行是一个活动,列从左到右是开始、结束、价值,右边两列 f 和配对是我们待会儿要算的辅助值,现在先空着。左边那一列是活动的名字 A 到 E。你看开始时间这一列是 3,1,6,2,4,乱的,第一步得把它排好。
- 5已按开始升序按开始时间从小到大排一遍。现在左边活动名的顺序变成了 B、D、A、E、C,开始时间这一列(绿色)是 1、2、3、4、6,规规矩矩升序。排序这一步很关键:排好之后,只要某个活动的开始时间大于我的结束时间,它以及它下面所有活动的开始时间都大于我的结束,天然接得上,后面找边界才能一刀切。
- 6辅助列待填正式开算前,先说清右边两列是干嘛的。f 这一列,表示从这一行往下、价值最高的活动值多少,它帮我们一眼看出结束之后能接的最好活动。配对这一列,是把本活动价值,加上它结束后能接的那个最优价值,得到以这个活动打头的最好组合。两列都从下往上、从上往下分两趟填,先填 f。
- 7f 填到第 5 行从最后一行活动 C 开始填 f。它下面没有别人了,所以从它往下价值最大的就是它自己,f 记成它的价值 6。
- 8f 填到第 4 行往上填活动 E 这一行的 f。比较两个数:下面一行的 f 是 6,本行自己的价值是 3,谁大取谁,f = 6。下面已经有更高价值的活动,f 沿用 6。
- 9f 填到第 3 行往上填活动 A 这一行的 f。比较两个数:下面一行的 f 是 6,本行自己的价值是 7,谁大取谁,f = 7。本行价值更大,f 被自己刷新了。
- 10f 填到第 2 行往上填活动 D 这一行的 f。比较两个数:下面一行的 f 是 7,本行自己的价值是 5,谁大取谁,f = 7。下面已经有更高价值的活动,f 沿用 7。
- 11f 填到第 1 行往上填活动 B 这一行的 f。比较两个数:下面一行的 f 是 7,本行自己的价值是 4,谁大取谁,f = 7。下面已经有更高价值的活动,f 沿用 7。
- 12f 列就绪f 这一列填完了,从上到下是 7、7、7、6、6。它的含义再强调一遍:比如第四行 E 的 f 是 6,意思是从 E 往下(也就是 E 和 C 里)价值最高的是 6。有了这一列,任何活动只要知道结束后从哪一行接得上,就能立刻查出那之后能拿的最高价值。接下来逐个活动算配对。
- 13为 B 找搭档轮到活动 B 当打头的,它结束于 2。往下找第一个开始时间严格大于 2 的活动,是活动 A,它的开始是 3(紫色是本活动结束,黄色是接得上的分界)。从这一行往下的活动都不会和 B 撞。
- 14B 配对 = 11接得上就好办了。从分界那一行往下最高价值就是那行的 f,等于 7。用 B 自己的价值 4 加上 7,配对总价值是 11(绿色),记到配对列。目前所有配对里最大的是 11。
- 15为 D 找搭档轮到活动 D 当打头的,它结束于 4。往下找第一个开始时间严格大于 4 的活动,是活动 C,它的开始是 6(紫色是本活动结束,黄色是接得上的分界)。从这一行往下的活动都不会和 D 撞。
- 16D 配对 = 11接得上就好办了。从分界那一行往下最高价值就是那行的 f,等于 6。用 D 自己的价值 5 加上 6,配对总价值是 11(绿色),记到配对列。目前所有配对里最大的是 11。
- 17为 A 找搭档轮到活动 A 当打头的,它结束于 5。往下找第一个开始时间严格大于 5 的活动,是活动 C,它的开始是 6(紫色是本活动结束,黄色是接得上的分界)。从这一行往下的活动都不会和 A 撞。
- 18A 配对 = 13接得上就好办了。从分界那一行往下最高价值就是那行的 f,等于 6。用 A 自己的价值 7 加上 6,配对总价值是 13(绿色),记到配对列。目前所有配对里最大的是 13。
- 19为 E 找搭档轮到活动 E 当打头的,它结束于 6。可它已经排在很后面,下面再没有哪个活动的开始时间大于 6 了,谁都接不上。那它只能单独参加。
- 20E 配对 = 3接不上,活动 E 只能自己参加,配对总价值就是它自己的价值 3(绿色)。目前所有配对里最大的仍是 13。
- 21为 C 找搭档轮到活动 C 当打头的,它结束于 7。可它已经排在很后面,下面再没有哪个活动的开始时间大于 7 了,谁都接不上。那它只能单独参加。
- 22C 配对 = 6接不上,活动 C 只能自己参加,配对总价值就是它自己的价值 6(绿色)。目前所有配对里最大的仍是 13。
- 23答案 = 13五个活动的配对都算完了,配对这一列从上到下是 11、11、13、3、6。里头最大的是 13,出现在活动 A 那一行(绿色)。这个 13 就是最终答案。
- 24最优 = 13把这个 13 拆开看是谁凑的。打头的是活动 A,开始 3 结束 5,价值 7;它结束于 5,接上开始于 6 的活动 C,价值 6。两段时间不碰,价值 7 加 6 等于 13,这就是最优组合。
⚠️ 容易写错的地方
✗ 错:把不重叠写成开始 ≥ 前一个结束
✓ 对:必须开始严格大于前一个结束
结束时间也算占用,结束于 t 的活动,下一个要从 t 加 1 起,写成大于等于会误判两个挨着的活动可以同选
✗ 错:忘了只参加一个也算答案
✓ 对:配对里天然含单独参加(接不上时配对就是本价值)
有时一个高价值大活动比任何两个小活动加起来都强,漏掉会算小
✗ 错:找边界时忘了排序前提
✓ 对:先按开始时间排序再二分
二分要求开始时间单调,不排序直接二分找到的分界是错的
✗ 错:f 从前往后填
✓ 对:f 是后缀最大值,必须从后往前填
f 在某行要用到它下面一行的结果,顺序反了就取不到正确的后缀最大值
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def maxTwoEvents(self, events: List[List[int]]) -> int:
events.sort()
n = len(events)
f = [events[-1][2]] * n
for i in range(n - 2, -1, -1):
f[i] = max(f[i + 1], events[i][2])
ans = 0
for _, e, v in events:
idx = bisect_right(events, e, key=lambda x: x[0])
if idx < n:
v += f[idx]
ans = max(ans, v)
return ansC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
int maxTwoEvents(vector<vector<int>>& events) {
sort(events.begin(), events.end());
int n = events.size();
vector<int> f(n + 1);
for (int i = n - 1; ~i; --i) {
f[i] = max(f[i + 1], events[i][2]);
}
int ans = 0;
for (const auto& e : events) {
int v = e[2];
int left = 0, right = n;
while (left < right) {
int mid = (left + right) >> 1;
if (events[mid][0] > e[1]) {
right = mid;
} else {
left = mid + 1;
}
}
if (left < n) {
v += f[left];
}
ans = max(ans, v);
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int maxTwoEvents(int[][] events) {
Arrays.sort(events, (a, b) -> a[0] - b[0]);
int n = events.length;
int[] f = new int[n + 1];
for (int i = n - 1; i >= 0; --i) {
f[i] = Math.max(f[i + 1], events[i][2]);
}
int ans = 0;
for (int[] e : events) {
int v = e[2];
int left = 0, right = n;
while (left < right) {
int mid = (left + right) >> 1;
if (events[mid][0] > e[1]) {
right = mid;
} else {
left = mid + 1;
}
}
if (left < n) {
v += f[left];
}
ans = Math.max(ans, v);
}
return ans;
}
}复杂度
时间
O(n log n)
排序是 O(n log n),是主导项。填后缀最大值 f 扫一遍是 O(n)。之后枚举每个活动、各做一次二分找边界,是 O(n log n)。合起来 O(n log n)
空间
O(n)
按峰值算。额外开了一列长度约 n 的后缀最大值数组 f,所以是 O(n)。排序本身的额外开销三语言不同,但都不超过这一列的量级
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 两个最好的不重叠活动 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这题的核心思路是什么?+
最多选两个不重叠的活动求最大价值和。枚举第一个活动,难点是快速知道它结束之后能接的最好活动。先按开始时间排序,再预处理一列后缀最大值 f,f 在某行表示从这行往下的最高价值。枚举每个活动时,二分找到第一个开始时间严格大于它结束的行,用本活动价值加上那行的 f 更新答案。只选一个的情况天然包含在接不上的配对里。
除了排序加二分,还有别的解法吗?+
可以把所有活动的开始和结束打散成事件,按时间扫描,用一个变量维护到当前时刻为止已经结束的活动里的最高价值,遇到一个活动开始时就用它的价值加上这个最高价值更新答案,这是扫描线加优先队列的思路。也有先离散化时间再做动态规划的写法。复杂度都是 O(n log n) 量级,排序加后缀最大值加二分是最直接的一种。
为什么答案里只选一个活动的情况不会漏?+
枚举每个活动当打头时,如果它结束后没有任何活动接得上,配对总价值就是它自己的价值,这本身就覆盖了只参加一个的情况。所以对所有活动的配对取最大,自然把单独参加和两两组合都比进去了,不会漏。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 两个最好的不重叠活动 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。