开始前,会这些就够了
- 会读排序与列表
- 先看 回溯与动态规划 的小规模枚举
你只有一间会议室,所有申请的开始和结束时间都已给定。如果目标是接纳最多场,优先挑结束最早的申请很有效;若目标改为收入最多,同一个选择却可能让你亏掉大单。
本文固定一个细节:时间段用 [开始,结束) 表示,前一场结束时后一场可以立刻开始,没有清场时间。所有申请持续时间严格大于零,输入是整批已知的申请。
把例子带到自己的电脑上
下载完整练习包
需要 Python 3.10 或更新版本,无第三方依赖;制作时实测 Windows / Python 3.14.0。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。
下载 ZIP · 5 个文件包里有什么
- interval.py:完整可运行示例
- README.md:运行说明与独立练习
- observed.stdout.txt:本次实际输出
- verification.json:源码和产物验证记录
- SHA256SUMS.txt:内容摘要
在这个文件夹打开 PowerShell,逐行运行
python --version
python -B interval.py解压到独立练习文件夹,在含 interval.py 的目录打开终端运行。整批已知申请、单间房、正时长半开区间;数量与非负收益两个目标;收益教学实现O(n²),不改真实日历。
找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。
排得更多,还是总收益更高?
同一间会议室有五个候选任务。结束时刻可以紧接下一个开始时刻,例如 A 在 3 结束,B 在 3 开始,两者不冲突。
始终选最早结束的兼容任务:A、B、D,共 3 个,总收益 11。
本次目标的最优结果:A、B、D;最多 3 个任务。
这个目标只数任务个数,修改 E 的收益不会改变答案。
查看五个任务与完整比较范围
| 任务 | 时间区间 | 收益 | 图中选中 |
|---|---|---|---|
| A | [0, 3) | 4 | 是 |
| B | [3, 6) | 4 | 是 |
| C | [0, 6) | 15 | 否 |
| D | [6, 8) | 3 | 是 |
| E | [2, 7) | 20 | 否 |
页面枚举全部 32 个子集,其中 11 个互不冲突,再分别比较数量与收益;并列最优全部列出,图中显示第一组。
这是单资源、固定起止时刻的五任务实验,收益使用无单位整数。枚举仅适合小输入;正文介绍最早结束的交换论证,并解释带权版本为何需要另选算法。
先把“好”写成一个明确数字
三份申请是 A=[0,2),收益1;B=[2,4),收益1;C=[0,4),收益10。按结束时间先选 A,然后可选 B,共两场、总收益2。选择 C 只有一场,但总收益10。
问场次数量时 A+B 最好,问收益时 C 最好。没有哪份结果凭空算错;错误是把第一道题的保证直接搬到第二道题。程序将两份结果分开输出,避免用同一个 best 字段混淆指标。
为什么最早结束能保证场次最多?
把任意一份场次最多的安排按结束时间排好。它的第一场不可能比全体最早结束的申请更早结束。用我们的第一场替换它,后面原本能接上的场次依旧能接上,因此不减少场次数。
现在锁定这一场,忽略与它冲突的申请,对剩下部分重复同样的替换。每一步都能找到一份场次数最多、且接受当前选择的安排,这就是选择安全的原因。论证依赖每场价值都按一场计算;若替换会损失收益,结论就不再回答收入问题。
考虑最后一场,把收益分成选与不选
先按结束时间编号。考虑当前申请 j:若不接它,最佳收益等于前 j-1 份的最佳收益;若接它,就拿到它的收益,再加上“在它开始之前已结束的申请”能带来的最佳收益。两种情况取较大。
p(j) 表示前面最后一个能在 j 开始时已经结束的申请。递推写成 best[j]=max(best[j-1],value[j]+best[p(j)])。代码用前缀长度作数组下标,因此会出现 p+1;不要机械照抄数学编号。相等收益时本例保留不选方案,只是确定性规则,不声称方案唯一。
用全部子集作参考,不让两份递推互相证明
运行 python -B interval.py。maximum_count_plan 是 [0,1],count_plan_value 是 2;maximum_value_result 是收益 10 与编号 [2]。编号取自原始输入,排序不会重新命名申请。
程序用时间点 0 到 4 形成十个候选区间,并枚举它们组成的全部 1024 份申请集合。参考算法枚举每份集合的所有子集,再逐对判断是否相交,分别找最大数量和最大收益;它不复用贪心顺序或动态规划递推。重复时间段、相邻端点、负时间点、空列表和非法零时长另有检查。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
"""Maximum count is not maximum revenue in interval scheduling."""
from itertools import combinations
import json
def validate(jobs):
jobs = tuple(jobs)
if len(jobs) > 1000:
raise ValueError("teaching limit: 1000 jobs")
for start, end, value in jobs:
if any(type(x) is not int for x in (start, end, value)) or start >= end or value < 0:
raise ValueError("integer start < end and nonnegative integer value required")
return jobs
def earliest_finish(jobs):
jobs = validate(jobs)
chosen, end = [], None
for i in sorted(range(len(jobs)), key=lambda i: (jobs[i][1], jobs[i][0], i)):
if end is None or jobs[i][0] >= end:
chosen.append(i)
end = jobs[i][1]
return chosen
def maximum_value(jobs):
jobs = validate(jobs)
order = sorted(range(len(jobs)), key=lambda i: (jobs[i][1], jobs[i][0], i))
best, plans = [0], [()]
for j, i in enumerate(order):
# Deliberately use a visible linear scan; this version is O(n^2).
p = j - 1
while p >= 0 and jobs[order[p]][1] > jobs[i][0]:
p -= 1
take = jobs[i][2] + best[p + 1]
if take > best[-1]:
best.append(take)
plans.append(plans[p + 1] + (i,))
else:
best.append(best[-1])
plans.append(plans[-1])
return best[-1], list(plans[-1])
def exhaustive(jobs):
# Independent oracle: all subsets, all pairs; no finish-order recurrence.
if len(jobs) > 12:
raise ValueError("oracle limited to twelve jobs")
best_count, best_value = 0, 0
for mask in range(1 << len(jobs)):
chosen = [i for i in range(len(jobs)) if mask >> i & 1]
if all(jobs[a][1] <= jobs[b][0] or jobs[b][1] <= jobs[a][0]
for a, b in combinations(chosen, 2)):
best_count = max(best_count, len(chosen))
best_value = max(best_value, sum(jobs[i][2] for i in chosen))
return best_count, best_value
def main():
checks = 0
def check(ok):
nonlocal checks
if not ok:
raise AssertionError("interval check failed")
checks += 1
pool = [(a, b, (a * 3 + b) % 7) for a, b in combinations(range(5), 2)]
cases = 0
for mask in range(1 << len(pool)):
jobs = [j for i, j in enumerate(pool) if mask >> i & 1]
count, value = exhaustive(jobs)
chosen = earliest_finish(jobs)
dp_value, dp_plan = maximum_value(jobs)
check(len(chosen) == count)
check(dp_value == value)
check(sum(jobs[i][2] for i in dp_plan) == dp_value)
check(all(jobs[a][1] <= jobs[b][0] for a, b in zip(dp_plan, dp_plan[1:])))
cases += 1
demo = [(0, 2, 1), (2, 4, 1), (0, 4, 10)]
chosen = earliest_finish(demo)
check(chosen == [0, 1])
check(maximum_value(demo) == (10, [2]))
check(earliest_finish(iter(demo)) == chosen)
check(len(earliest_finish([(-3, -1, 2), (-1, 0, 2)])) == 2)
check(exhaustive([(0, 2, 1), (0, 2, 3)]) == (1, 3))
for jobs in [[(1, 1, 1)], [(2, 1, 1)], [(0, 1, -1)], [(0, True, 1)]]:
try:
earliest_finish(jobs)
except ValueError:
check(True)
else:
raise AssertionError("invalid interval accepted")
print(json.dumps({"checks_passed": checks, "exhaustive_job_sets": cases,
"jobs_start_end_value": demo, "maximum_count_plan": chosen,
"count_plan_value": sum(demo[i][2] for i in chosen),
"maximum_value_result": maximum_value(demo),
"scope": "one room; fixed half-open intervals [start,end); adjacency allowed; offline"}, indent=2))
if __name__ == "__main__":
main()
怎样运行:在独立练习文件夹中执行 python -B interval.py。2026-10-18 已在 Windows、Python 3.14.0 运行此份源码并通过自带检查。整批已知申请、单间房、正时长半开区间;数量与非负收益两个目标;收益教学实现O(n²),不改真实日历。
运行后对照结果
{
"checks_passed": 4105,
"exhaustive_job_sets": 1024,
"jobs_start_end_value": [
[
0,
2,
1
],
[
2,
4,
1
],
[
0,
4,
10
]
],
"maximum_count_plan": [
0,
1
],
"count_plan_value": 2,
"maximum_value_result": [
10,
[
2
]
],
"scope": "one room; fixed half-open intervals [start,end); adjacency allowed; offline"
}不要把看起来短的代码自动说成 O(n log n)
数量版本排序后只扫描一次,时间 O(n log n),另存排序和选择需要 O(n) 空间。本篇收益版本为了直观看到 p(j),逐项向前扫描,还保存每一项的方案元组,所以时间与保存全部方案的空间最坏都是 O(n²)。
用二分查找 p(j),只保存收益和回溯决定,可把常见实现降到 O(n log n) 时间和 O(n) 空间;这不是当前教学代码已做到的优化。有限枚举检查有利于发现边界错误,所有规模的正确性仍依赖选与不选的完整划分。
现实排期多一条条件,模型就需要重看
如果换场要清理十分钟,可在相容判断里加入固定间隔;如果清理时间由前后两场共同决定,就不能直接沿用这份简单前缀递推。多间房、线上实时到达、允许移动时间或拆分会议,也不在本例内。
本例不改真实日历、不估测收入、不做线上性能测试。它只对已知、固定、半开区间的单资源选择验证两个不同目标。输入上限 1000,指数级参考上限 12。
记住这三点
- 最早结束保证最多场次,收益变化后需重新定义最优。
- 带收益可以按最后一场选与不选做动态规划。
- 端点规则、清场时间和资源数量属于题目条件。
别把例子用过头
- 直接按最高收益或最短时长排序就宣称全局最优。
- 把 start>=end 写成 start>end,误删可首尾相接的会议。
- 复制课件复杂度却忽略当前源码的扫描和方案复制。
自己试一小步
给一个长会、三个短会,你能分别写出数量和收益方案吗?
- 自己设计四份正时长申请,其中至少两份可以首尾相接。
- 选择一组收益,使最多场次方案与最大收益方案不同。
- 把收益统一改成1,比较两个算法得到的最优值。
做到这些,就算完成
- 交付原始区间、端点约定、选择编号和对应指标。
- 允许存在多个同样好的方案;用指标与相容性核验。
- 对自己设计的输入,分别报告数量与收益两个目标的最优值。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- Cornell:Greedy Stays Ahead (新标签页)↗
支持按结束时间选择区间以及逐项不落后的证明。
- Vijay Garg:Weighted Interval Scheduling (新标签页)↗
支持带收益时的选与不选递推。
- Python 3.14:排序技巧 (新标签页)↗
支持排序键和稳定排序语义。