椰程信奥·互动课件 专题7·二分(一)
10:00 1 / 14
🧭 椰程信奥 · CSP复赛专题系列

专题7 · 二分(一)

二分不止是「在有序数组里找数」——
它是一种每次砍掉一半搜索范围的通用思想,能用在整数、浮点数甚至答案上。
本讲先把二分的两套模板与两个高频坑彻底讲透。

数的范围(整数二分) 数的三次方根(浮点二分) 两套模板 / 防死循环
本质

有序 + 一刀两断 = 每次砍一半

🔍 顺序查找

从头到尾一个个比,最坏要走 n 步,复杂度 O(n)。

1 亿个元素、查一次就要 1 亿次比较。

⚡ 二分查找

每次拿中间值比一下,根据大小直接排除一半,复杂度 O(log n)。

1 亿个元素只需约 27 次比较——这就是量级差距。

🖼️ 看图:为什么二分快这么多(16 个格子里找一个数)

同一个任务:在 16 个已排好序的格子里找到目标 顺序查找:一个一个试 最坏 16 次 二分查找:每比一次就砍掉一半 第 1 步:看中间 → 砍掉一半,还剩 8 个 第 2 步:再看中间 → 还剩 4 个 第 3 步:还剩 2 个 第 4 步:锁定 ✓ 16 → 8 → 4 → 2 → 1:只要 4 次 📌 每次砍一半,所以 n 越大二分越划算:100 万个只要约 20 次,10 亿个只要约 30 次。
二分的两个前提条件(缺一不可):
① 序列/函数必须单调(有序);
② 每次比较后,能明确判断答案在左半边还是右半边。
很多「二分写不出来」的题,其实是单调性没找对,不是模板不会。
2¹⁰≈10³,2²⁰≈10⁶,2³⁰≈10⁹。所以 n=10⁹ 时二分只要 30 次。看到「10 亿」不要怕,先想二分。
3D🎲 二分 = 每比较一次砍掉一半🖱 拖拽旋转 · 双击复位
为什么用 3D:把 16 个候选摆成一排立体方块,被排除的那一半压扁变灰——「砍掉一半」从一句口号变成看得见的动作。
模型

左闭右闭 vs 左闭右开:别混着写

📘 模型 A:左闭右闭 [l, r]

while(l <= r){
  mid = (l+r)/2;
  if(a[mid] >= q) r = mid-1;
  else          l = mid+1;
}

区间包含 r,所以收缩时要跨过 mid(mid-1 / mid+1)。

📗 模型 B:左闭右开 [l, r)

while(l < r){
  mid = (l+r)/2;
  if(a[mid] >= q) r = mid;
  else          l = mid+1;
}

区间不含 r,所以左半边收缩时 r = mid(不减 1)。

最常见的错误:混用两套模型——while(l<r) 里却写 r=mid-1,或者 while(l<=r) 里写 r=mid。结果是死循环或漏解。选一套,从头到尾只用那一套。
模板

找左边界 / 找右边界

① 找最小满足值(左边界)

while(l < r){
  mid = (l+r)/2;      // 下取整
  if(check(mid)) r = mid;
  else          l = mid+1;
}
return l;

条件成立时,答案可能是 mid 也可能在左边,所以 r=mid。

② 找最大满足值(右边界)

while(l < r){
  mid = (l+r+1)/2;    // ★ 上取整
  if(check(mid)) l = mid;
  else          r = mid-1;
}
return l;

条件成立时,答案可能是 mid 也可能在右边,所以 l=mid。

为什么模板②的 mid 要 +1?当 l+1==r 时,若 mid 取下取整会等于 l,此时 l=mid 毫无变化 → 死循环。上取整让 mid 取到 r,才能推进。这是二分第一大坑。
看 check 成立时是 l = mid 还是 r = mid:只要是 l = mid,mid 就必须上取整(+1)。写成配套的一对,永远不会死循环。
互动

整数二分 · 指针移动模拟器

目标
点击「下一步」,看 l / r 如何把搜索区间一步步砍半。
例题 1 / 数的范围

数的范围:两次二分找左右边界

升序序列中,对每个询问 q,输出 q 第一次和最后一次出现的位置;不存在输出 −1 −1。

🔍 左边界

找第一个 ≥ q 的位置。
用模板①(找最小满足值)。

关键:找到后必须验证该位置的值是否真的等于 q,否则是 -1。

🔍 右边界

找最后一个 ≤ q 的位置。
用模板②(找最大满足值)。

同样要验证值等于 q。

样例:a=[1,2,3,3,4,4],查 3 → 左边界 3、右边界 4;查 2 → 2、2;查 5 → −1 −1。
二分不会告诉你"存不存在",它只会告诉你"应该插在哪"。所以模板跑完必须回头看一眼 a[ans]==q。漏掉这步,不存在的数会返回一个错误的下标。
例题 1

数的范围 · 代码走读

int lowerPos(int n, int q){          // 第一个 >= q 的位置
  int l=1, r=n, ans=-1;
  while(l <= r){
    int mid = (l+r)/2;
    if(a[mid] >= q){ ans=mid; r=mid-1; }
    else            { l=mid+1; }
  }
  return (ans!=-1 && a[ans]==q) ? ans : -1;   // ★ 验证
}
int upperPos(int n, int q){          // 最后一个 <= q 的位置
  int l=1, r=n, ans=-1;
  while(l <= r){
    int mid = (l+r)/2;
    if(a[mid] <= q){ ans=mid; l=mid+1; }
    else            { r=mid-1; }
  }
  return (ans!=-1 && a[ans]==q) ? ans : -1;      // ★ 验证
}
三个坑:① mid=(l+r)/2 在 l、r 接近 2×10⁹ 时可能溢出,保险写法 l+(r-l)/2;② 数组要开到 10⁶+5 且放全局;③ 必须 ios::sync_with_stdio(false),10⁵ 次查询不用快读会超时。
拓展

浮点二分:把「砍一半」用在实数上

🆚 与整数二分的差别

  • 没有「相邻」概念,不能用 l+1==r 结束。
  • 循环条件改为精度控制:while(r-l > 1e-8)。
  • 收缩时直接 r=mid / l=mid,不需要 ±1。
  • 不会出现死循环,也不存在模板选择问题。

🧠 精度怎么定

题目要求保留 6 位小数,循环精度取 1e-8(比要求再小两位),留足安全余量。

或者干脆固定跑 100 次:区间每次减半,100 次后精度远超 1e-8,简单又好记。

样例核对:n=1000,在 [−100,100] 上二分 x³ 与 1000 的大小关系,最终 l→10.000000。第 10 页模拟器会展示区间逐步收敛的过程。
例题 2

数的三次方根 · 代码走读

double l = -100, r = 100, mid;      // 100^3 已远大于数据上限
while(r - l > 1e-8){                  // ★ 精度控制,不是 l<=r
  mid = (l + r) / 2;
  if(mid*mid*mid >= n) r = mid;      // x^3 单调递增
  else                   l = mid;
}
cout << fixed << setprecision(6) << l << "\n";

✅ 关键点

f(x)=x³ 在整个实数域单调递增,所以能二分。

区间必须覆盖答案:n 最大 10⁴,³√10⁴ ≈ 21.5,取 ±100 足够。

⚠️ 输出格式

必须 fixed << setprecision(6),否则默认有效数字输出,格式不对就是 WA。

头文件别忘 <iomanip>。

负数怎么办?区间取 [−100,100] 即可,不用特判负数——这是单调函数的好处。若只取 [0,100],负数输入就全错了。
互动

浮点二分 · 区间收敛模拟器

目标
点击「下一步」,看区间如何一步步收紧到答案。
诊断

二分高频易错辨析

① 模板②(找最大满足值)里 mid 为什么写成 (l+r+1)/2?

② 二分跑完得到位置 ans,接下来必须做什么?

③ 浮点二分的循环条件应该写成?

拿分

一道题 100 分是分档给的——不会正解也要先拿保底

复赛一道题的 100 分由十几到二十几个测试点组成, 前面几档专门为「没想出正解的人」准备。四道题各拿 30~40 分, 加起来往往就是一等奖和二等奖的分界。

第 1 档 特判 5~10 分 约 2 分钟 第 2 档 暴力 20~30 分 约 8 分钟 第 3 档 特殊性质 40~60 分 约 15 分钟 第 4 档 正解 100 分 约 25 分钟

① 特判档:先别想算法,看数据范围表

题目给的数据范围表就是出题人给你的送分清单。看到「n = 1」「只有一组数据」这类一行, 就先写个 if 直接输出答案。2 分钟换 5~10 分,全场最划算。

② 暴力档:按题意最直白地写一遍

多重循环、DFS 全枚举、朴素 O(n³)……不要优化,只要保证最小那档全对。 它既是保底分,又是后面对拍的标尺——没有它你无法证明正解是对的。

③ 特殊性质档:题目里那句「若……」

常见的送分性质:数据已经有序、所有值完全相同、规模小到可以 O(n²)、 只出现一种类型。题目写出来就是让你拿的,专门写一份即可。

④ 正解档:思路定了,就赢了八成

考场上的时间几乎都花在想思路上,不是写代码。写完先跑样例, 再用第 ② 档的暴力版造小数据对拍——这是唯一能证明你思路对的办法。

考场纪律:每档设时间上限,到点还没调通就 立刻提交当前版本,保住已有分数,再往上冲。最常见的翻车是"正解写了 50 分钟没过,连暴力分都没交上去"。
拿分

本讲 2 道题,各自的四档怎么走

数的范围 · RANGE

第1档 · 2 分钟
数组长度 1 → 判等后输出 0 0 或 -1 -1

第2档 · 8 分钟
从左往右扫找第一个、从右往左扫找最后一个,n ≤ 10⁵ 稳过

第3档 · 10 分钟
只做第一问(找第一次出现),先拿一半分

第4档 · 15 分钟
两套模板:找左端点 a[mid]>=x → r=mid;找右端点 a[mid]<=x → l=mid

数的三次方根 · CUBERT

第1档 · 2 分钟
n == 0 → 0.000000;n 是完全立方数先手算一遍

第2档 · 8 分钟
以 1e-4 为步长暴力扫一遍定位区间,再逐步细化到 1e-6

第3档 · 10 分钟
n ≤ 1 → 答案就是 n 本身

第4档 · 15 分钟
浮点二分 while (r−l > 1e-7),比较 mid³ < n,输出 %.6f

50 分钟时间盒(一题的节奏):
0–5 分钟 读题 + 圈出数据范围表里最小的那几档 → 5–15 分钟 写完第 1、2 档并先交一次(保住 20~30 分)→ 15–40 分钟 冲第 3、4 档 → 40–50 分钟 造小数据对拍、检查 freopen 与文件名。
课堂上别让学生"从头想正解"。先要求每个人 15 分钟内交出第 1、2 档, 再放开冲正解。这个顺序一换,平均分通常能涨一大截——因为它杜绝了"想了 40 分钟,交上去 0 分"。
小结

本讲知识树与自检清单

🌳 知识树

前提:单调 + 可判定左右。

整数:两套模板 —— 找左边界(r=mid,下取整)、找右边界(l=mid,上取整)。

浮点:精度控制 + r=mid / l=mid,无死循环。

STL:lower_bound / upper_bound / binary_search。

✅ 考场 N 步自检

  • 单调性找对了吗?说不出单调性就别写二分。
  • 模板选的是哪一套?全程别混用。
  • 是 l = mid 吗?是的话 mid 必须 +1。
  • mid 会不会溢出?用 l + (r-l)/2 更稳。
  • 找到后验证了吗?(整数二分必查)
  • 浮点题 fixed << setprecision 加了吗?
下机前:默写两套模板 + 数的范围(两次二分+验证)+ 三次方根(精度控制)。能默写 ≈ 本讲过关。

目录