01 / 本课学习路线
本课学习路线
阅读与推演约 116 分钟,练习约 60 分钟,进阶练习另需约 45 分钟
02 / 学习目标与先修自测
学完本课你能做什么,以及开始前需要会什么
先决定二分判定的是元素大小还是方案可行性,再分清要找最小可行值还是最大可行值,据此选择边界更新方式。
| 学完后能做的事 | 正文位置 | 检查方式 |
|---|---|---|
| 不看资料写出 lower_bound,并说出结束时 lo 两侧的不变量 | 第 03、04 节 | 自查第 2 条、必做任务 1 |
| 解释半开区间写法为什么每轮必缩、不死循环 | 第 03 节补充 | 自查第 3 条 |
| 见到「最小的最大值 / 最低速度」先写 check 再写二分,通过(AC)P3305 | 第 05、08 节 | 自查第 4 条、必做任务 3 |
| 整数上取整写成 (p + k − 1) // k | 第 05 节 | 自查第 5 条、练习 4 |
| 求最大可行值时把收缩方向和 mid 的取整一起镜像,做出 P3303 | 第 06 节 | 练习 5、进阶练习 1 |
先修自测:下面 5 题请先自己写答案,再展开对照。答不出的按括号里的位置补看再回来。本课假定你已完成「复合排序与并列规则」与上一课。
| 题号 | 题目 | 补看位置 |
|---|---|---|
| 自测 1 | 7 // 4、(7 + 4 - 1) // 4、8 // 4、(8 + 4 - 1) // 4 各是多少? | 数字与运算符 |
| 自测 2 | 列表 [93, 95, 97, 100, 102, 123, 155] 是升序的吗?第一个 ≥ 110 的元素下标是多少(从 0 数)? | 列表 |
| 自测 3 | lo, hi = 0, 7,循环 while lo < hi 里每轮要么 hi = mid、要么 lo = mid + 1(mid 在 [lo, hi) 内),循环一定会停吗? | while 循环 |
| 自测 4 | 输入一行 93,95,97 怎样读成整数列表?第二行单独一个整数怎样读? | 标准输入输出与首次独立提交 |
| 自测 5 | 写一个函数 ok(k),当 k * 3 >= 10 时返回 True,否则 False;k 从 1 到 5 的返回值各是什么? | 函数与布尔值 |
展开先修自测答案
自测 1:1、2、2、2。(p + k - 1) // k 是整数上取整:7/4 = 1.75 → 2;8/4 恰好整除 → 2,不会多算。
自测 2:是升序;第一个 ≥ 110 的是 123,下标 5。P3306 要输出从 1 起的位置,所以是 6。
自测 3:一定停。mid 满足 lo ≤ mid < hi,hi = mid 让右端至少左移一格,lo = mid + 1 让左端至少右移一格,区间每轮至少短一格。写成 lo = mid 就不保证了(第 03 节补充)。
自测 4:[int(x) for x in input().split(",")];第二行 int(input())。P3306 就是这两行的组合;P3308 的题面文字写「逗号隔开」、题目页示例却用空格:先把逗号替换成空格再用不带参数的 split(),两种排版都能读。
自测 5:k=1、2、3 → False,k=4、5 → True。一旦为 True,后面全是 True——这种「从某一点起全为真」就是二分答案要求的单调性。
03 / 概念与术语
区间不变量、边界、可行性判断、单调性
先把 lower_bound 写熟练;确认判定具有单调性后,再设置答案范围,把判定从「比大小」换成「可行吗」,并按可行性结果收缩边界。
| 术语 | 含义 | 代码写法 |
|---|---|---|
| 半开区间 [lo, hi) | 候选下标是 lo..hi−1;hi 本身不是候选 | lo, hi = 0, len(nums) |
| 区间不变量 | lo 左边全部不满足条件,hi 及右边全部满足条件;每轮收缩后仍成立 | if nums[mid] >= target: hi = mid / else: lo = mid + 1 |
| lower_bound | 第一个 ≥ target 的位置;不存在时为 n(也是保持有序的插入位置) | 循环结束返回 lo |
| 可行性判断 check | 把一个候选答案 v 代入题目,回答「可行吗」 | def check(v) -> bool |
| 单调性 | 候选从小到大,check 的结果只从 False 变到 True(或反过来)一次 | 二分答案成立的前提 |
| 最小可行值 | check 为 True 的最左端:True 收 hi=mid,False 抬 lo=mid+1 | P3305、P3308 |
| 最大可行值 | check 为 True 的最右端:True 抬 lo=mid,False 收 hi=mid−1,mid 上取整 | P3303 |
| 整数上取整 | p 除以 k 向上取整 | (p + k - 1) // k |
一个模板,两种用法
有序数组找边界时,判定是 nums[mid] >= target;二分答案时,判定换成 check(mid)。两者的收缩规则一模一样:判定为真的候选构成右半段,要找的就是它的左端点。第 05 节的模板把 check 独立成函数,二分主体不变。
补充学习(选学)为什么这个模板不会死循环约 5 分钟区间每轮严格变短的论证与经典死循环
在 [lo, hi) 里取中点 mid = (lo+hi)//2,一定有 lo ≤ mid < hi。执行更新(lo = mid+1)时左端至少右移一格;执行更新(hi = mid)时右端至少左移一格——区间长度每轮至少减一,到 0 必停。
以下写法会导致区间无法继续缩小:把收缩写成 lo = mid(丢了 +1)。用 [1,2] 找 2:hi 收到 1 后 mid=0、1<2 走 lo=mid,lo 停在 0 永不前进。+1 是合法的:判定(nums[mid] < target)已把 mid 本身排除出答案。求最大可行值要 lo = mid 时,mid 必须上取整 (lo+hi+1)//2,同一个道理的镜像。
04 / 有序数组找边界:P3306
lower_bound 的逐轮区间表
P3306「小明找位置」:第一行逗号分隔、已按学号升序的队列(最多 10000 人),第二行小明的学号;输出他应插入的位置(从 1 起)。要找的就是第一个 ≥ 学号的下标,再加 1。
lower_bound 手推:[1,3,3,5] 找 3
区间 [0,4): mid=2, a[2]=3 ≥ 3 → hi=2 区间 [0,2): mid=1, a[1]=3 ≥ 3 → hi=1 区间 [0,1): mid=0, a[0]=1 < 3 → lo=1 lo == hi == 1 → 第一个 3 在下标 1
| 轮 | 区间 [lo, hi) | mid | nums[mid] | 判定 | 更新 |
|---|---|---|---|---|---|
| 1 | [0, 7) | 3 | 100 | 100 < 110 | lo = 4 |
| 2 | [4, 7) | 5 | 123 | 123 ≥ 110 | hi = 5 |
| 3 | [4, 5) | 4 | 102 | 102 < 110 | lo = 5 |
| 停 | [5, 5) | lo == hi | 输出 5 + 1 = 6 |
与题面一致:110 应排在 102 之后、123 之前,位置 6。结束时下标 0..4 全 < 110、下标 5..6 全 ≥ 110,就是不变量。
| 学号 | 过程 | lo | 输出 |
|---|---|---|---|
| 90 | 每轮都是 nums[mid] ≥ 90 → hi 一路左移 | 0 | 1 |
| 200 | 每轮都是 nums[mid] < 200 → lo 一路右移到 n | 7 | 8 |
lower_bound 返回 n 表示「没有 ≥ target 的元素」,此时插入位置就是队尾,不需要特判。
05 / 二分答案:P3305
可行性判断函数与二分主体
P3305「孙悟空吃蟠桃」:第一行 N 个整数(每棵树的蟠桃数),第二行 H 小时;每小时只能选一棵树吃 K 个(不够 K 就吃完这棵、本小时不再吃别的);求 H 小时内吃完的最小整数速度 K;树比小时多时无解输出 0。
速度 K 越大,每棵树用的小时数只会不增,总小时数单调不增——所以 check(K) = 「总小时数 ≤ H」从某个 K 起全为真,可以二分。每棵树的小时数是 N[i] 除以 K 向上取整:(N[i] + K − 1) // K。上界取 max(N[i]):速度等于最大堆时每棵树恰好 1 小时,只要树不比小时多就一定可行;下界 1。
| 轮 | 区间 [lo, hi] | mid | 各树小时数 | 总小时 | ≤ 8? | 更新 |
|---|---|---|---|---|---|---|
| 1 | [1, 11] | 6 | 1 + 1 + 2 + 2 | 6 | 是 | hi = 6 |
| 2 | [1, 6] | 3 | 1 + 2 + 3 + 4 | 10 | 否 | lo = 4 |
| 3 | [4, 6] | 5 | 1 + 2 + 2 + 3 | 8 | 是 | hi = 5 |
| 4 | [4, 5] | 4 | 1 + 2 + 2 + 3 | 8 | 是 | hi = 4 |
| 停 | lo == hi == 4 | 输出 4 |
这里 hi 是「已知可行」的候选(初值取最大堆 11,一定可行),lo 是候选下界;循环 while lo < hi,可行收 hi = mid,不可行抬 lo = mid + 1。check(3) = 10 小时超出、check(4) = 8 小时恰好,所以 4 是最小可行速度。
无解与另一组:树比小时多 / [30, 11, 23, 4, 20]
piles=[3,6,7,11], H=3:4 棵树 > 3 小时,每小时最多一棵 → 输出 0(先于二分判断) piles=[30,11,23,4,20], H=5:5 棵 5 小时 → 每棵只能 1 小时 → K = max = 30 piles=[30,11,23,4,20], H=6:check(23) = 2+1+1+1+1 = 6 ✓;check(22) = 2+1+2+1+1 = 7 ✗ → 23
二分答案模板(check 独立成函数)
Pythonimport sys
def check(v, data) -> bool:
# 步骤 1:以 v 为假设答案判断可行性(判定必须单调)
...
def main():
data = sys.stdin.read().split()
...
lo, hi = 1, 10 ** 9 # 步骤 2:按题目的数据范围收紧上下界
while lo < hi:
mid = (lo + hi) // 2
if check(mid, data):
hi = mid # 步骤 3:求最大可行值时改为 lo = mid(mid 上取整)
else:
lo = mid + 1
print(lo)
main()二分收缩的框架可以复用,但要重新确定查找区间、单调方向和返回值含义:判定从 nums[mid] < target 换成 check(mid),可行的取值构成右半段,要找的就是它的左端点。
上取整用 (p + k − 1) // k,不要用 p // k
ceil(7/4) 写成 7//4 得 1,总时长被低估,算出的速度偏小、实际摘不完。整数上取整的标准写法是 (p + k − 1) // k——这一处错足够让整题答案错误(第 09 节错误表:[3,6,7,11]、H=8 会输出 3 而不是 4)。
补充学习(选学)check 函数的三条要求约 4 分钟单调、独立验证、贪心配合
一要单调:混入「恰好等于」类条件可能出现 v=4 可行、v=5 不可行、v=6 又可行,二分会收敛到错误值。二要独立验证:先拿样例手工算 check 的返回,再套二分模板。三是 check 的形态由题目的分配规则决定——P3303 植树的 check(d) 是一次贪心(按间距 ≥ d 从左到右种树,数够不够);P3308 排期的题目说明写明「需求可以任意分给 N 个人」,check(t) 就是装箱可行性:能否把所有工作量放进 N 个容量为 t 的箱子。任务不要求连续,就不能用「按输入顺序连续分组、超过上限时切换到下一组」的连续切分——那是另一道题的 check。
补充学习(选学)二分在阈值扫描与批大小探测中的应用约 4 分钟阈值扫描与批大小上限
分类器要找「预测为正的数量不超过预算 B」的最低阈值:阈值抬高,正例数单调不增——这就是蟠桃题,速度空间换成阈值空间。显存里探最大批大小(batch size):先倍增探上界、再二分收缩,判定就是实际运行一次。查找空间到 10⁹ 也只要 30 次判定,分类指标与阈值选择课(模块 6 · 第 5 课)做阈值扫描时会再用到这套方法。
06 / 求最大可行值:P3303
镜像模板:可行就抬 lo,mid 上取整
P3303「最佳植树距离」:第一行位置数 M,第二行 M 个坐标,第三行要种的棵数 N;选 N 个位置让相邻两棵的最小间距尽可能大,输出这个最大的最小间距。间距 d 越小越容易种够——check(d) 从某个 d 起全为假,要找的是最后一个为真的 d。
check(d) 是一次贪心:坐标排序后,第一个位置一定种;之后每个位置只要与上一棵种下的距离 ≥ d 就种,数一数是否够 N 棵。方向与蟠桃题相反:可行就抬 lo = mid,不可行就收 hi = mid − 1;因为 lo = mid 不一定让区间变短,mid 必须上取整 (lo + hi + 1) // 2。
| 轮 | 区间 [lo, hi] | mid = (lo+hi+1)//2 | 贪心种树 | 棵数 | 够 3? | 更新 |
|---|---|---|---|---|---|---|
| 1 | [1, 8] | 5 | 1 → 8(2、4 距 1 不足 5;9 距 8 不足) | 2 | 否 | hi = 4 |
| 2 | [1, 4] | 3 | 1 → 4 → 8 | 3 | 是 | lo = 3 |
| 3 | [3, 4] | 4 | 1 → 8(4 距 1 只有 3) | 2 | 否 | hi = 3 |
| 停 | lo == hi == 3 | 输出 3 |
上界取最大跨度 9 − 1 = 8(只种两端时的间距),下界 1。第 3 轮如果 mid 用 (3+4)//2 = 3,check(3) 为真走 lo = 3,区间 [3, 4] 不变——就是死循环,所以求最大可行值 mid 必须上取整。
同一模板、不同 check:项目排期 [8,7,6,5]、N=2 人
题目说明:需求可以任意分给 N 人(不要求连续)→ check(t) 是装箱可行性 check(13):降序 8,7,6,5 → 8 进甲;7 进乙;6 甲放不下(14)→乙(13);5 进甲(13) ✓ 可行 check(12):8 进甲;7 进乙;6 两边都放不下 ✗ 区间 [8, 26]:…收敛到 13 → 答案 13 若误用「按输入顺序连续分组、当前组超过上限时切换到下一组」:t=13 时 8 | 7 | 6+5 要 3 人 ✗,二分会收敛到 15——把可行的 13 判成不可行,整题答案错误
07 / check 的形态:P3308
精确的装箱判断:最大件归当前人,再枚举同放的组合
P3308「项目排期」:第一行 M 个工作量(M < 30,每个 < 200,空格分隔),第二行人数 N;每个需求只能由一个人独立完成,求最快完成天数(即最小化「工作量最多的人」的总量)。二分天数 t,check(t) 判断能否把全部工作量分给 N 个人、每人 ≤ t。
装箱可行性没有简单的贪心:题目页参考题解用「工作量降序、逐个放进第一个放得下的人」的首次适应贪心,它在题面样例上得到 28,与精确答案一致;但对 3 11 7 5 10 3、N=2,它得到 21,而正确答案是 20({10, 7, 3} 与 {11, 5, 3})——贪心把 11 和 7 放在一起后,10、5、3 就再也凑不成 ≤ 20 的两组。本课的 check 用精确搜索,按下表六个要点剪枝;这种搜索最坏仍需枚举大量组合,剪枝不能保证所有大输入都能快速完成。
| 要点 | 做法 | 为什么 |
|---|---|---|
| 最大件归当前人 | 剩余工作量降序,取第一件必然属于「当前正在装的人」 | 谁先拿都一样,直接指定消除对称 |
| 枚举同放组合 | 从其余件里按下标递增挑选若干件,总和 ≤ t − 最大件 | 相同工作量只按顺序取,避免重复组合 |
| 只保留装到最满的组合 | 剩下的件里还有能放进这个人的,就不把当前组合交给下一个人 | 把放得进的件挪进来,其他人只会更轻,不会错过可行方案 |
| 公约数降上限 | 所有工作量的最大公约数为 g 时,把 t 降到不超过 t 的 g 的倍数 | 每个人的总量都是 g 的倍数:全是偶数、上限却是奇数时,真正可用的上限少 1 |
| 剩余总量剪枝 | 剩余工作量总和 > 剩余人数 × t 立即失败 | 多数候选 t 在这一步就被排除 |
| 失败状态记忆 | 记下已证明分不下去的(剩余多重集合, 剩余人数) | 不同组合可能留下同一批剩余,不重复搜索 |
| 轮 | 区间 [lo, hi] | mid | 判断 | 更新 |
|---|---|---|---|---|
| 1 | [28, 55] | 41 | 可行,例如 {11,9,7,7,6,1}=41,其余 14 | hi = 41 |
| 2 | [28, 41] | 34 | 可行,例如 {11,9,7,7}=34,其余 21 | hi = 34 |
| 3 | [28, 34] | 31 | 可行,例如 {11,9,7,4}=31,其余 24 | hi = 31 |
| 4 | [28, 31] | 29 | 可行,例如 {11,9,7,2}=29,其余 26 | hi = 29 |
| 5 | [28, 29] | 28 | 可行,例如 {11,9,7,1}=28,其余 27 | hi = 28 |
| 停 | lo == hi == 28 | 输出 28(与题面一致:28 天 + 27 天) |
下界取 max(最大单件, 总量除以人数的上取整) = max(11,28) = 28,上界取总和 55,与下方程序一致。表中给出每轮可行分配的一个例子,搜索找到的具体分组不一定相同;这五轮都可行,最后收敛到 28。
一组结构性输入:M=29 件工作量是 2、4、6、…、58(全部偶数,总和 870),N=2。平均 435,但每个人的总量都是偶数,最大负载至少 436;[58, 56, 54, 52, 50, 48, 46, 44, 28] 合计 436、其余 434,所以答案是 436。二分会遇到上限 435:只靠「剩余总量 ≤ 剩余人数 × 上限」判不掉它(870 ≤ 2×435),组合搜索会在「凑出恰好 435」上反复失败——公约数降上限把 435 先降成 434,2×434 = 868 < 870,一步排除。公约数剪枝先排除数学上不可能的容量,避免继续枚举这些无解组合。
08 / 从步骤到程序
四道题的完整参考程序,每一步落在哪几行
先自己写完并提交一次,再展开对照。P3303 与 P3308 是进阶题,它们的程序也在这里。
| 题 | 步骤 | 代码 | 说明 |
|---|---|---|---|
| P3306 | 半开区间 lower_bound,输出 lo + 1 | if nums[mid] >= target: hi = mid / else: lo = mid + 1 | 位置从 1 起 |
| P3305 | 先判无解,再在 [1, max] 上二分 | if len(piles) > h: print(0);hours += (p + k - 1) // k | 可行收 hi = mid |
| P3303 | 排序、贪心 check、求最大可行值 | mid = (lo + hi + 1) // 2;可行 lo = mid,否则 hi = mid - 1 | mid 上取整 |
| P3308 | 降序、装箱 check、求最小可行值 | solve(items, people) 递归 + fill 枚举组合 + failed 记忆 | 下界取最大单件与平均值上取整的较大值,上界取总量 |
展开完整参考程序 1:P3306 小明找位置(先自己写完并提交一次,再展开对照)
完整程序:P3306(标准输入 → 标准输出)
Pythonimport sys
lines = sys.stdin.read().split("\n")
nums = [int(x) for x in lines[0].strip().split(",")] # 已按学号升序
target = int(lines[1].strip())
lo, hi = 0, len(nums) # 半开区间 [lo, hi)
while lo < hi:
mid = (lo + hi) // 2
if nums[mid] >= target: # mid 满足「≥ 学号」→ 答案在 mid 或左边
hi = mid
else:
lo = mid + 1 # mid 不满足 → 答案在 mid 右边
print(lo + 1) # 题目位置从 1 起自测用例:题面示例(110 → 6)与两个边界(90 → 1、200 → 8)。
展开完整参考程序 2:P3305 孙悟空吃蟠桃
完整程序:P3305(标准输入 → 标准输出)
Pythonimport sys
lines = sys.stdin.read().split("\n")
piles = [int(x) for x in lines[0].split()] # 第一行:每棵树的蟠桃数
h = int(lines[1].strip()) # 第二行:小时数
def check(k): # 速度 k 能否在 h 小时内吃完
hours = 0
for p in piles:
hours += (p + k - 1) // k # 上取整:这棵树要几个小时
return hours <= h
if len(piles) > h: # 每小时最多一棵树:树比小时多必无解
print(0)
else:
lo, hi = 1, max(piles) # 速度 = 最大堆时每棵 1 小时,一定可行
while lo < hi:
mid = (lo + hi) // 2
if check(mid):
hi = mid # 可行 → 更小的速度可能也行
else:
lo = mid + 1 # 不可行 → 至少要比 mid 大
print(lo)自测用例:第 05 节三组手算(→ 4、0、30)与练习 3 的 H=6(→ 23)。
展开完整参考程序 3:P3303 最佳植树距离(进阶)
完整程序:P3303(标准输入 → 标准输出)
Pythonimport sys
data = sys.stdin.read().split()
m = int(data[0])
trees = sorted(int(x) for x in data[1:1 + m]) # 坑位坐标,先排序
n = int(data[1 + m]) # 要种的棵数
def check(d): # 最小间距为 d 时能否种下 n 棵
count = 1 # 第一个坑一定种
last = trees[0]
for x in trees[1:]:
if x - last >= d:
count += 1
last = x
if count >= n:
return True
return count >= n
lo, hi = 1, trees[-1] - trees[0] # 答案在 [1, 最大跨度] 内
while lo < hi:
mid = (lo + hi + 1) // 2 # 求最大可行值:mid 上取整
if check(mid):
lo = mid # 可行 → 更大的间距可能也行
else:
hi = mid - 1 # 不可行 → 比 mid 小
print(lo)自测用例:第 06 节逐轮表(→ 3)、练习 5(→ 9)、全部坑位都种的 3 / 1 2 3 / 3(→ 1)。输入格式按题目页参考题解(M / 坐标 / N)。
展开完整参考程序 4:P3308 项目排期(进阶,精确装箱)
完整程序:P3308(标准输入 → 标准输出)
Pythonimport sys
from math import gcd
lines = sys.stdin.read().split("\n")
work = sorted((int(x) for x in lines[0].replace(",", " ").split()), reverse=True) # 工作量降序;先把逗号换成空格,两种排版都能读
n = int(lines[1].strip()) # 人数
g = 0
for x in work:
g = gcd(g, x) # 所有工作量的最大公约数:每个人的总量一定是它的倍数
def check(limit): # 每人不超过 limit,能否把全部需求分给 n 个人
limit -= limit % g # 总量只能是 g 的倍数:上限先降到 g 的倍数(全是偶数配奇数上限就靠这一步排除)
if work[0] > limit:
return False
if sum(1 for x in work if 2 * x > limit) > n: # 超过半容量的件互相不能同人
return False
failed = set() # 记住已证明「分不下去」的状态:(剩余工作量元组, 剩余人数)
def solve(items, people): # items:还没分配的工作量(降序元组);people:还剩几个人
if not items:
return True
if people == 0 or sum(items) > people * limit: # 剩余总量装不进剩余的人
return False
if (items, people) in failed:
return False
first, rest = items[0], items[1:] # 最大的一件必须归「当前这个人」(谁先拿都一样,直接指定)
k = len(rest)
def fill(start, cap, taken): # 从 rest[start:] 里再挑一些和 first 一起放,剩余容量 cap
for j in range(start, k):
if rest[j] > cap:
continue
if j > start and rest[j] == rest[j - 1] and (j - 1) not in taken:
continue # 相同工作量的两件,只按顺序取,避免重复组合
taken.add(j)
if fill(j + 1, cap - rest[j], taken):
return True
taken.discard(j)
# 不再挑了:只有「剩下的件一件也放不进」这种装到最满的组合才值得往下试
for j in range(k - 1, -1, -1):
if j not in taken:
if rest[j] <= cap: # 还有件放得进却没放:这种组合不如把它放进来,跳过
return False
break
remaining = tuple(x for j, x in enumerate(rest) if j not in taken)
return solve(remaining, people - 1) # 这个人的组合定型,剩下的交给下一个人
ok = fill(0, limit - first, set())
if not ok:
failed.add((items, people))
return ok
return solve(tuple(work), n)
lo, hi = max(work[0], (sum(work) + n - 1) // n), sum(work) # 下界 = max(最大单件, 平均值上取整),上界 = 全部给一个人
while lo < hi:
mid = (lo + hi) // 2
if check(mid):
hi = mid # 可行 → 上限还能更小
else:
lo = mid + 1 # 不可行 → 至少要比 mid 大
print(lo)自测用例:题面示例(→ 28)、第 06 节的 8 7 6 5 / 2(→ 13)、第 07 节的 3 11 7 5 10 3 / 2(→ 20)和偶数构造输入 2 4 … 58 / 2(→ 436);小规模输入可以再写一个「枚举每件归谁」的全枚举对拍。搜索最坏仍是指数级,M < 30 也不能保证快速完成。工作量相近、可选组合多时尤其要留意运行时间;先用小规模全枚举验证答案,再按题目的数据范围与时限检查性能。题目页参考题解使用更快的贪心,但它会在第 07 节的反例上给出偏大答案,不能作为精确搜索的正确性依据。
09 / 边界、反例与复杂度
错误做法在具体输入上各输出什么
下面每一行都是一个具体的错误程序,给出输入、错误输出与正确输出;把参考程序改成对应写法就能复现。
| 错误做法 | 输入 | 错误输出 | 正确输出 | 判题结果 |
|---|---|---|---|---|
| P3306 输出 lo 而不是 lo + 1(位置从 0 起) | 题面示例 | 5 | 6 | 答案错误(WA) |
| P3306 用不带参数的 split() 读逗号行 | 题面示例 | 抛出 ValueError | 6 | 运行错误(RE) |
| P3306 收缩写成 lo = mid | 1,2 / 2 | 区间停在 [0, 1) 不再缩小 | 2 | 超时(TLE) |
| P3305 用 p // k 代替上取整 | 3 6 7 11 / 8 | 3(check(3) 被算成 1+2+2+3 = 8) | 4 | 答案错误(WA) |
| P3305 漏掉「树比小时多」的判断 | 3 6 7 11 / 3 | 11 | 0 | 答案错误(WA) |
| P3303 不排序就贪心种树 | 5 / 1 2 8 4 9 / 3 | 1 | 3 | 答案错误(WA) |
| P3303 间距判断写成 x − last > d | 4 / 1 10 3 7 / 2 | 8 | 9 | 答案错误(WA) |
| P3303 求最大可行值却用 mid = (lo+hi)//2 配 lo = mid | 5 / 1 2 8 4 9 / 3 | 区间停在 [3, 4] 不再缩小 | 3 | 超时(TLE) |
| P3308 按输入顺序连续切分 | 8 7 6 5 / 2 | 15 | 13 | 答案错误(WA) |
| P3308 首次适应贪心(题目页参考写法) | 3 11 7 5 10 3 / 2 | 21 | 20 | 答案错误(WA) |
第六行的 1:不排序时按 1,2,8,4,9 的顺序种,从 2 到 8 再到 4 距离为负,任何 d ≥ 2 都种不够 3 棵,只剩 d = 1。第九行来自第 06 节末尾的推演。
| 做法 | 判定次数 | 每次判定 | 本课的题 |
|---|---|---|---|
| 有序数组二分 | log₂ n(n = 10⁴ 时约 14 次) | O(1) | P3306 |
| 二分答案 + 线性 check | log₂(答案范围)(10⁴ 时约 14 次) | O(n) | P3305、P3303 |
| 二分答案 + 搜索 check | log₂(总和)(约 13 次) | 最坏指数级;六条剪枝对多数输入有效,快慢取决于输入结构 | P3308 |
| 逐个试答案 | 答案范围(最多 10⁴ 次) | O(n) | P3305 也能过,但不可迁移到 10⁹ |
10 / 渐进练习与参考答案
跟做 → 改一个条件 → 独立实现 → 迁移
每题先在纸上或文件里做完,再展开答案。
练习 1(跟做):按第 04 节表的格式,对 P3306 输入 93,95,97,100,102,123,155 分别找 100 与 156,逐轮写出区间与输出。
展开练习 1 答案
找 100:[0,7) mid=3 值 100 ≥ 100 → hi=3;[0,3) mid=1 值 95 < 100 → lo=2;[2,3) mid=2 值 97 < 100 → lo=3;输出 4(第一个 ≥ 100 的就是 100 本身)。找 156:每轮都 < 156,lo 一路到 7,输出 8。
练习 2(改一个条件):把 lower_bound 改成「最后一个 ≤ target 的下标(不存在返回 −1)」。用 [1, 3, 3, 5] 找 3 与找 0 验证。
展开练习 2 答案
最后一个 ≤ target = 第一个 > target 的位置减 1:把判定改成 nums[mid] > target 求出第一个 > target 的下标 p,返回 p − 1。找 3:第一个 > 3 是下标 3(值 5),答案 2;找 0:第一个 > 0 是下标 0,答案 −1。不要另写一套循环,复用同一模板换判定即可。
练习 3(改一个条件):P3305 输入 30 11 23 4 20,H 分别为 5 与 6,按第 05 节表的格式手算最小速度。
展开练习 3 答案
H = 5:5 棵树 5 小时,每棵只能占 1 小时,速度必须 ≥ 30 → 30(二分:所有 < 30 的 mid 都不可行,lo 抬到 30)。H = 6:[1,30] mid=15 → 2+1+2+1+2 = 8 ✗ lo=16;[16,30] mid=23 → 2+1+1+1+1 = 6 ✓ hi=23;[16,23] mid=19 → 2+1+2+1+2 = 8 ✗ lo=20;[20,23] mid=21 → 2+1+2+1+1 = 7 ✗ lo=22;[22,23] mid=22 → 2+1+2+1+1 = 7 ✗ lo=23 → 23。
练习 4(独立实现):完成「必做任务 1」的 lower_bound 与 check,再各加断言:lower_bound([1,3,3,5], 0) 应为 0、空列表应为 0;check(11, [3,6,7,11], 4) 应为 True。
展开练习 4 答案
lower_bound / check 的参考实现(自带断言)
Pythondef lower_bound(nums, target):
lo, hi = 0, len(nums) # 半开区间 [lo, hi)
while lo < hi:
mid = (lo + hi) // 2
if nums[mid] >= target: # mid 满足 → 答案在 mid 或左边
hi = mid
else: # mid 不满足 → 答案在右边
lo = mid + 1
return lo # 结束时 lo 左边全 < target,lo 及右边全 ≥ target
assert lower_bound([1, 3, 3, 5], 3) == 1
assert lower_bound([1, 3, 3, 5], 6) == 4 # 比所有数都大 → n
assert lower_bound([1, 3, 3, 5], 0) == 0 # 比所有数都小 → 0
assert lower_bound([], 7) == 0 # 空数组
def check(k, piles, h):
hours = 0
for p in piles:
hours += (p + k - 1) // k # 整数上取整
return hours <= h
assert check(4, [3, 6, 7, 11], 8) is True # 1+2+2+3 = 8
assert check(3, [3, 6, 7, 11], 8) is False # 1+2+3+4 = 10
assert check(11, [3, 6, 7, 11], 4) is True # 速度 = 最大堆:每棵 1 小时七条断言覆盖:找到、越界(大 / 小)、空数组、可行、不可行、速度等于最大堆。
练习 5(迁移):P3303 输入 4 / 1 10 3 7 / 2,按第 06 节表的格式逐轮手算,并说明这题为什么是「求最大可行值」。
展开练习 5 答案
排序后 [1, 3, 7, 10],种 2 棵,上界 9。[1,9] mid=5 → 1 → 7 两棵 ✓ lo=5;[5,9] mid=7 → 1 → 10(3、7 距 1 不足 7)两棵 ✓ lo=7;[7,9] mid=8 → 1 → 10 ✓ lo=8;[8,9] mid=9 → 1 → 10 ✓ lo=9 → 9(只种两端)。是求最大可行值:间距越小越容易种够,可行的 d 在左半段,要找它的右端点,所以可行就抬 lo、mid 上取整。
11 / 读题要求与复习自评
四道题的要求对照,以及完成本课之后怎么复习
提交前把下表过一遍;题目页的题面与样例是最终依据。P3303 的输入格式按题目页参考题解整理。
| 项目 | P3306 | P3305 | P3303 | P3308 |
|---|---|---|---|---|
| 输入 | 逗号分隔的升序学号;第二行学号 | N 个整数;第二行 H | M;M 个坐标;N | M 个工作量(空格);第二行 N |
| 输出 | 位置(从 1 起) | 最小速度 K;无解 0 | 最大的最小间距 | 最少天数 |
| 方向 | 有序数组找边界 | 最小可行值 | 最大可行值(mid 上取整) | 最小可行值 |
| check | nums[mid] ≥ 学号 | Σ 上取整(N[i]/K) ≤ H | 贪心种树数 ≥ N | 精确装箱 |
| 数据范围 | 队列 ≤ 10000 | N、H < 10000 | 以题目页为准 | M < 30,每件 < 200 |
| 样例 | 93,95,97,100,102,123,155 / 110 → 6 | 自拟 3 6 7 11 / 8 → 4 | 自拟 5 / 1 2 8 4 9 / 3 → 3 | 6 2 7 7 9 3 2 1 3 11 4 / 2 → 28 |
题解入口:需要对照解法时,先展开本课第 08 节的四份完整参考程序;四道题的题目页另有思路与参考代码,可在题目页查看——其中 P3308 的题目页参考题解是贪心,在少数输入上与精确答案不同(第 07 节给出了具体输入)。复习与自评:本课算完成 = 两道必做题 P3306、P3305 都通过判题,并勾选全部六条「学习完成检查」(含复习题 P4202 那一条);进阶练习不影响完成状态。六条检查是自评,不改变题目的通过(AC)状态。复习时用三个问题自测:① 不看正文,写出 lower_bound 并说出结束时 lo 两侧各满足什么;② 不看表格,重算 [3,6,7,11]、H=8 的 check(3) 与 check(4);③ 说出求最大可行值时 mid 为什么要上取整。答不出哪一条,就回到对应的节重读,再做第 10 节对应的练习。
基础加练(选做)
同一主题的 5 道基础题
这组题目采用标准输入输出(ACM 模式):程序从标准输入读取数据、把结果写到标准输出。它们难度低于必做题,适合在必做题之前热身,或在未通过时回来巩固;不计入本课完成标准。每道题给出练习重点、完成标准与提示;已在本站通过的题目会直接显示为已通过。
- H100057未开始
在排序数组中查找元素的第一个和最后一个位置
基础练习 2:两次二分 · 预计 20 分钟
- 练习重点:
- 用下界查找(
lower_bound)确定目标值(target)的左边界,再将target + 1的下界位置减一,得到右边界 - 完成标准:
- 能解释第二次查
target + 1为什么不用写新模板
提示
先查目标值(
target)的左边界:越界或值不相等就输出 -1 -1;存在时再查target + 1的左边界,减一即为右边界。 - H100059未开始
寻找旋转排序数组中的最小值
进阶练习:找旋转点 · 预计 20 分钟
- 练习重点:
- 与
nums[hi]比较,把最小值限定在当前区间内 - 完成标准:
- 能说明这套写法为什么选
nums[hi]作比较对象;改用nums[lo]比较时,判定条件与收缩方向要整套一起改
提示
nums[mid] > nums[hi]说明最小值在右半,令lo = mid + 1;否则mid本身也可能是最小值,令hi = mid。
12 / 练习
按顺序完成本课的任务
必做题已通过 0/2 道;进阶练习已通过 0/2 道
实现有序数组的边界查找与蟠桃可行性判断(lower_bound、check)
必做任务 1:代码自测自主练习练习重点:半开区间不变量;ceil 的整数写法;预计用时:15 分钟
完成标准:能说出循环结束时 lo 左右两侧各满足什么
需要时查看提示
lower_bound 按模板写;check 里每棵树 (p + k − 1) // k。四个断言分别检验「找到」「越界」「可行」「不可行」。参考实现在第 10 节练习 4 的展开区。
自测代码(复制到你的代码文件中运行,检查输出是否一致)
def lower_bound(nums, target):
# 请在这里实现:半开区间 [lo, hi),返回第一个 >= target 的位置
...
assert lower_bound([1, 3, 3, 5], 3) == 1
assert lower_bound([1, 3, 3, 5], 6) == 4 # 比所有数都大 → n
def check(k, piles, h):
# 请在这里实现:速度 k 时总小时数 = 每棵 ceil(N[i]/k) 之和,是否 <= h
...
assert check(4, [3, 6, 7, 11], 8) is True # 1+2+2+3 = 8
assert check(3, [3, 6, 7, 11], 8) is False # 1+2+3+4 = 10P3306 · 小明找位置
必做任务 2练习重点:lower_bound 找插入位置;预计用时:15 分钟
完成标准:能解释 target 不存在时 lo 停在哪里、为什么就是答案
需要时查看提示
题目要求的就是「第一个 ≥ 学号的位置」。输出从 1 起(题面示例 110 → 6);第一行按逗号拆。第 04 节把题面示例逐轮列出。
P3305 · 孙悟空吃蟠桃
必做任务 3练习重点:在速度空间二分,check = 总小时数 ≤ H;预计用时:30 分钟
完成标准:能解释总小时数函数(hours)随 K 增大单调不增,所以能二分
需要时查看提示
先判树比小时多 → 0。上界取 max(N[i])(速度再大也一小时一棵)。check 用 (p + k − 1) // k 累加;可行收 hi = mid,不可行抬 lo = mid + 1。第 05 节有逐轮判定表。
P3303 · 最佳植树距离
进阶练习 1进阶练习练习重点:最大化最小间距:check(d) 是一次贪心种树;预计用时:20 分钟
完成标准:能说出这是「求最大可行值」,收缩方向与蟠桃相反
需要时查看提示
坐标先排序。check(d):从第一棵开始,间距 ≥ d 才种下一棵,数够不够。求最大可行值:check 过走 lo = mid,mid 用 (lo+hi+1)//2 上取整。第 06 节有逐轮表,先做第 10 节练习 5。
P3308 · 项目排期
进阶练习 2进阶练习练习重点:最小化最大工作量:check(t) 判断 M 件任务能否任意分给 N 人、每人 ≤ t;预计用时:25 分钟
完成标准:能说清为什么这题不能用「按输入顺序连续分组、超过上限时切换到下一组」,并写出带剪枝的精确装箱判断
需要时查看提示
题目说明明确写出需求「任意分配、不要求连续」。下界 = max(最大单件, 总工作量除以人数的上取整),上界 = 总和。check(t) 按第 07 节六个要点写:最大件归当前人、枚举同放组合且只保留装到最满的、上限先降到公约数的倍数、剩余总量 > 剩余人数×t 直接失败、记住失败状态。不要写成顺序累加:[8,7,6,5]、N=2 时它会把可行的 13 判成不可行;题目页的首次适应贪心对 3 11 7 5 10 3、N=2 会得 21 而不是 20。
提交结果
提交结果说明与处理方法
- WA
答案错误
先测 target 比所有数都小、都大、不存在三种情况;二分答案题核对上下界是否把答案框住;上取整、无解判断、位置基准各是一处易错点——第 09 节的表给出了每种错误的具体输出
- RE
运行错误
hi 初值和区间写法配套:半开用 n、闭区间用 n−1;读取数组元素(nums[mid])前确认不越界;逗号行不要用不带参数的
split()- TLE
超时
先检查区间是否每轮严格缩短(求最大可行值时 lo=mid 要配上取整),再检查 check 的时间复杂度和剪枝是否满足数据范围
- PE
格式错误
输出下标的基准(0 基/1 基)按题目样例定
- AC
通过
再测单元素、答案在边界(K=1 或 K=max)两种情况;口述一遍 check 的单调性——做了 P3308 的话,再说一遍它的 check 为什么是装箱而不是顺序切分
13 / 学习完成检查
本课学习完成检查
完成本课需要:必做题全部通过,并勾选本课的全部学习完成检查;进阶练习、基础加练与复习题单独统计,不影响完成状态。登录后,勾选记录会保存到账号,并更新课程总览的完成状态。