0%

题目大意

洛谷 P5939 [POI 1998 R3] 折线

平面上给 nn 个格点。一条 平直折线 从左到右一笔画过去,每一段和 xx 轴的夹角都在 [45,45][-45^\circ, 45^\circ] 之间。问:最少画多少条平直折线,才能盖住所有点?

数据范围

1n300001 \le n \le 300000xi,yi300000 \le x_i, y_i \le 30000 。时限按常规 1s 算,目标复杂度 O(nlogn)O(n\log n)

样例

样例 1:5 个点 (2,3) (3,4) (4,5) (1,6) (12,27) ,答案 3 。样例 2:6 个点,答案 3 。

思路讲解

一句话

夹角 45\le 45^\circ 等价于斜率绝对值 1\le 1 ,也就是 ΔyΔx|\Delta y| \le \Delta x 。把坐标系转 4545^\circ ,令 u=x+yu = x+yv=xyv = x-y ,"两点能串在同一条折线上"就变成 (u,v)(u,v) 两维都不减 的支配关系;于是一条折线 = 一条 ,最少折线数 = 最小链覆盖 ;由 Dilworth(导弹拦截那一套),它等于 vv 序列的 最长严格下降子序列 长度, O(nlogn)O(n\log n)

第一步:把角度约束代数化

设两个格点 P=(x1,y1)P=(x_1,y_1)Q=(x2,y2)Q=(x_2,y_2) ,不妨 x1x2x_1 \le x_2 。“能用一段满足角度约束的线段把 QQ 接在 PP 后面”,精确条件就是

y2y1x2x1|y_2 - y_1| \le x_2 - x_1

夹角 45\le 45^\circ 嘛,斜率绝对值就 1\le 1 ,竖直跨度不能超过水平跨度。

边界要钉一刀: x1=x2x_1 = x_2 时退化成 Δy0|\Delta y| \le 0 ,即 P,QP,Q 同点——所以两个不同的同 xx 点永远进不了同一条折线(左到右的折线对 xx 严格单调)。后面实现的并列处理会用到这点。

第二步:45° 旋转,变成二维支配

把绝对值拆成两条同时成立的不等式:

y2y1x2x1y1y2x2x1y_2 - y_1 \le x_2 - x_1 \quad\text{且}\quad y_1 - y_2 \le x_2 - x_1

两条都做纯移项,让 PPQQ 彻底分到不等号两侧,冒出来两个组合量。令

u=x+y,v=xy\textcolor{blue}{\boldsymbol{u = x + y}}, \qquad \textcolor{blue}{\boldsymbol{v = x - y}}

条件就清爽成了 uPuQu_P \le u_QvPvQv_P \le v_Q

这里有个 易错点 :是 不下降( \le ,含等号) ,不是"严格上升"。反例 P=(0,0)P=(0,0)Q=(1,1)Q=(1,1)uu 从 0 升到 2, vv 从 0 到 0 没变 ;但 Δy=0Δx=1|\Delta y| = 0 \le \Delta x = 1QQ 完全能接在 PP 后面。按"严格"就会误判。

几何上:第一步那个从 PP 朝右张开的 9090^\circ 锥形,换到 (u,v)(u,v) 系里锥的两条边正好贴着新坐标轴,能接就变成最干净的一句—— QQ 落在 PP 的右上方(含边界),标准的二维支配。

第三步:一条折线 = 一条链 → 最小链覆盖

把 pair 关系抬到整条折线:一条平直折线经过的那串点,按经过顺序看, (u,v)(u,v) 两维都单调不减——这就是 (u,v)(u,v) 支配偏序里的一条

(u,v)(u,v) 字典序排序( uu 升,同 uuvv 升——这个并列方向正好让同 uu 的点能进同一条链,不会被错算)。排完抽出 vv 序列,一条折线就对应 vv 的一个 非降子序列 。于是整道题变成:用最少个 " vv 非降子序列" 覆盖整条 vv 序列。

第四步:Dilworth / 导弹拦截

“用最少个非降子序列覆盖一个序列”——这正是导弹拦截那道经典题的对偶。由 Dilworth 定理:

关键结论:最少非降子序列覆盖数 = 该序列的最长严格下降子序列长度。

所以答案 = vv 序列的最长严格下降子序列长度。实现上不用真去求"下降":把 vv 取负,求 v-v最长严格上升子序列 (patience sorting / lower_boundO(nlogn)O(n\log n) )即可。

📎 动画与源码

完整推导对话录(含锥形配图、 (u,v)(u,v) 支配区配图、Dilworth 方向对照表)见页面末尾 附件mentor.pdf ;完整 AC 源码见下方 AC 代码 折叠块,也在附件里附了 .cpp.txt 方便复制对拍。

AC 代码

AC 提交(vjudge)

源码较长(自带几何模板 + LIS 类),完整 C++ 折叠在下方;同一份也作 .cpp.txt 附件挂在页面末尾。

心路历程(一次方向写反 → AC)

  • Dilworth 方向写反:第一版写成 LIS(uv, false) ——求的是 vv最长非降子序列 ,量根本不对。它和"最小链覆盖"没关系,只是在样例 1 上巧合都等于 3,没暴露。

  • 照出 bug 的反例:输入 2 / 0 0 / 2 0 。两点 (0,0),(2,0)(0,0),(2,0) 在同一条水平折线上(夹角 00^\circ ),正解 = 1 ;错误代码: (u,v)=(0,0),(2,2)(u,v)=(0,0),(2,2)vv 序列 [0,2][0,2] ,最长非降 = 2 → 输出 2 ,错。

  • 修法:改成求 vv 的最长严格下降子序列——uv[i] = -uvA[i].y; 取负后 LIS(uv, true); (strict 走 lower_bound ),等价于求最长严格下降。一发 AC。

  • 沉淀的易错点:(1) Dilworth 是"最少非降覆盖 = 最长 严格下降 “,方向 / 严格性都不能错;(2) 支配条件是"不下降"含等号,不是"严格上升”,同 uu / 同 vv / 重点都得能进同一条链;(3) 排序并列方向(同 uuvv 升)要和"求严格下降"配套,弄反会把可同链的点错算成反链。

附件

下面附件均已直接传入本页面,可在 Notion 内直接预览:

  • mentor.pdf —— 完整带教对话录(PDF 版),含 5 步推导 + 锥形 / 支配区 TikZ 配图 + Dilworth 方向对照表 + 反例。

  • P5939_POI_1998_R3_.cpp.txt —— 完整 AC 源码(下载后改回 .cpp ,含几何模板 + LIS 类,方便复制对拍)。

完整带教对话录(PDF): 5 步推导 + 锥形/(u,v)支配区 TikZ 配图 + Dilworth 方向对照表 + 反例

P5939_POI_1998_R3_.cpp.txt

题目大意

洛谷 P1742 最小圆覆盖:给 NN 个平面点(N105N\le 10^5,坐标实数),求一个半径最小、且能把所有点都包住(含边界)的圆,第一行输出半径、第二行输出圆心坐标,精度 10910^{-9}

样例

6 个点 (8,9) (4,7.5) (1,2) (5.1,8.7) (9,2) (4.5,1) → 半径 5、圆心 (5,5)。这个圆恰由 (8,9)、(1,2)、(9,2) 三点的外接圆决定,其余 3 点严格在内部——暗示了最小圆由至多 3 个边界点钉死。

思路讲解

一句话

随机增量法(Welzl):当前圈外的点必落在新圈边界上,于是逐点加入、三层钉点维护当前最小圆,期望 O(N)O(N)。这道题真正的坑不在算法本身,在内层 for j / for k 必须升序——内层判定下标改对了、循环方向写反,照样直接 WA。

Welzl 灵魂:引理 + 自由度 + 前缀不变量

内核一句话:圈外点 ⇒ 必在新最小圆边界上(圆内部容不下它、又必须刚好罩住它)。每钉一个边界点,圆的自由度 1-1:3 自由度 → 钉 pip_i 剩 2 → 钉 pjp_j 剩 1(圆心锁在中垂线上)→ 钉 pkp_k 剩 0(三点外接圆唯一)。

**关键不变量:**每层 resres = 「覆盖前缀 p0pt1p_0\dots p_{t-1} + 已钉点」的最小圆。正确性全靠它。

三层:L0 钉 0 点扫 ii;L1 钉 pip_ijj;L2 钉 pi,pjp_i,p_jkk,谁在圈外就 res = 外接圆。随机打乱只把复杂度降到期望 O(N)O(N)与正确性无关

三层结构(核心循环,升序版)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
template<class T>
Circle<DB> min_circle_cover(vector<Point<T> > A) {
// 先打乱
shuffle(all(A), rng);
Circle<DB> res(A[0], 0);
for (int i = 1; i < SZ(A); ++i) {
if (!res.is_point_in_circle(A[i])) {
res = Circle<DB>(A[i], 0);
// 注意,我们的这个后缀点之前已经考虑过了,实际上我们是要覆盖前缀点
for (int j = 0; j < i; ++j) {
if (!res.is_point_in_circle(A[j])) {
res = circle_from_diameter(A[i], A[j]);
for (int k = 0; k < j; ++k) {
if (!res.is_point_in_circle(A[k])) {
res = outcircle_triangle(A[i], A[j], A[k]);
}
}
}
}
}
}
return res;

⚠️ 内层必须升序,倒序为什么 WA(本题关键)

升序让「已扫过的点」恒等于引理担保仍在圈内的「前缀」{p0pj1}\{p_0\dots p_{j-1}\},两集合重合、归纳一步步闭合。

倒序时(for(j=i-1;j>=0;--j)):在某个 jj 处重建圆,引理只担保覆盖前缀 p0pj1p_0\dots p_{j-1};但你实际已扫过的是后缀 pj+1pi1p_{j+1}\dots p_{i-1}(大下标先遍历)。这批后缀点没人保证还在新圈里,而 jj 只减不回头、永不复查 → 被甩出就永久丢失。

  • **同样例 6 点实测:**升序连跑 30/30 全 = r=5r{=}5、圆心 (5,5)。

  • 倒序飘忽:r=5r{=}5(对) / r=6.23r{=}6.23 圆心 (5,9.375)(盖住但非最小 = WA) / r=3.876r{=}3.876 圆心 (7.05,5.35)(连 (1,2) 都没盖 = 非法解)。

  • **两种坏法:**漏点(已扫点被甩出 → 非法),或后续 circle_from_diameter 重置把圆撑大(非最小)。

机制图:以 j=3 重建为例。升序「已扫」=「引理担保前缀」两段重合(归纳闭合);倒序引理只担保前缀 p0..p2,实际已扫的却是后缀 p4..p6,错位 → 不变量断裂。

几何后果:绿实线 = 正解最小圆 (5,5) r=5;红虚线 = 倒序某次实际跑出的圆 (7.05,5.35) r=3.876,(1,2) 和 (4.5,1) 都被甩在圈外 → 非法解。

🎬 Note: 交互式演示:下面 3 张关键帧出自 mc_order_demo.html(完整可交互版在末尾「附件」,可自己点 Step、切升/倒序、拖引理点、滑前缀/后缀滑条)。

step 0 — L0 起手:res = 单点 p0(蓝实线 r=0);绿虚线 = 真最小圆 r=5 参照物;右栏阶段 L0。

中间帧 — L2:钉 p_i,p_j 后扫 p_k 算外接圆;右栏实时列「引理担保前缀」vs「实际已扫」+ 重合/错位 verdict。

末帧 — 升序跑完:蓝圈收敛到绿圈,r=5 正解 ✓。切「倒序」按钮重跑会停在错的圆(非最小 / 漏点)。

📎 动画与源码

完整可交互 HTML 演示、AC 源码、完整带教对话录 PDF 都在页面末尾「附件」节,直接点开 / 下载。

AC 代码

AC 提交链接:vjudge.net/solution/69974356

心路历程(WA → AC)

  • **第一次:**内层判定误写成 A[i] 而非 A[j] / A[k],res 退化成半径 0 的点圆,只要有点在外答案就全错。

  • **改对下标后仍 WA:**内层循环写成了倒序 for(j=i-1;j>=0;--j) / for(k=j-1;k>=0;--k)

  • **现象飘忽:**同样例点不同 shuffle 跑出 r=5(对)、r=6.23(盖住但非最小=WA)、r=3.876(连 (1,2) 都没盖=非法),一开始以为是精度/eps 问题,其实是结构错。

  • **根因:**倒序破坏「前缀不变量」——引理重建只担保前缀,倒序实际已扫的是后缀,被甩出的点 j 只减不回头、永不复查。

  • **修复:**内层改升序 for(j=0;j<i;++j) / for(k=0;k<j;++k),固定样例点序回归 30/30 稳定 r=5、圆心 (5,5)。

**教训:**随机增量法的正确性不靠随机(随机只管期望复杂度),靠「已扫集合恒为前缀」这条不变量;扫描方向本身是正确性的一部分,不是风格选择。

附件

本题相关产物(可直接点开 / 下载):

mentor.tex.txt

P1742_mini_circle.cpp.txt

题目大意

比赛:The 2024 CCPC National Invitational Contest (Northeast) / The 18th Northeast Collegiate Programming Contest(2024 CCPC 东北邀请赛 / 第 18 届东北赛区)

原题链接Gym-105173 M House(vjudge)CF Gym 原题

AC 提交vjudge submission 69955475

题面

平面上给定 nn 个互不相同的整点,从中选 5 个不同的点 (A,B,C,D,E)(A, B, C, D, E) 组成一个 “房子”

  • ABCDABCD 是一个矩形(按顺序,即 ABCDAB \parallel CDADBCAD \parallel BCADABAD \perp AB

  • CDECDE 是以 CDCD 为底的等腰三角形( CE=DE|CE| = |DE|

  • EECDCD 直线远离 ABAB 那一侧(数学条件: DADE<0\overrightarrow{DA} \cdot \overrightarrow{DE} < 0

两个房子被认为不同当且仅当存在至少一个点的坐标不同(按点集判,不按 tuple 顺序)。问能凑出多少个不同的房子。

数据范围

  • 5n3005 \le n \le 300

  • xi,yi109|x_i|, |y_i| \le 10^9 (整数坐标)

样例

样例 2: n=7n = 7 ,点集 {(0,2),(4,0),(2,4),(0,0),(2,0),(4,4),(4,2)}\{(0,2), (4,0), (2,4), (0,0), (2,0), (4,4), (4,2)\} ,答案 =5= 5

思路讲解

一句话

n300n \le 300O(n3)O(n^3) 留足空间。把"枚举 5 个点"压缩为"枚举矩形/三角形共享的那条底边 (C,D)(C, D) ",剩下 AAEE 各自独立地在 C,DC, D 限定的两条特定直线上检查——一对底边 O(n)O(n) 扫一遍点就够了。

关键观察:把 5 元组解耦成两个 1 元组

5 个点同时枚举是 O(n5)O(n^5) ,连暴力的边都摸不到。关键观察:把 C,DC, D 当主角,固定 (C,D)(C, D) 这条底边后:

  • AA 必须满足 ADCDAD \perp CDB=A+(CD)B = A + (C - D) 也在点集 —— AA 完全由"在过 DD 的垂线上 + BB 是否凑得齐"决定,跟 EE 无关

  • EE 必须满足 CE=DE|CE| = |DE| ,即 EECDCD 的中垂线上 —— 跟 AA 也无关

  • 唯一耦合的条件是" AAEECDCD 异侧"

于是只要固定 (C,D)(C, D) AA 候选和 EE 候选按所在侧别分桶,最后一组 AA×\times 异侧的 EE 桶就是这一对 (C,D)(C, D) 的贡献。

7 个点的样例 —— 看上去能拼出几个?官方答案是 5 个。

Step 1: 样例 2 的 7 个点。看上去能拼出几个?官方答案是 5 个。

算法骨架( O(n3)O(n^3)

枚举有序对 (C,D)(C, D)O(n2)O(n^2) 对),对每一对扫一遍剩下 n2n - 2 个点 PP ,做两个 O(1)O(1) 判定:

判定 条件 归类
PP 能当 AA 吗? (PD)(CD)=0(P - D) \cdot (C - D) = 0P+(CD)P + (C - D) 也在点集 cross(CD,PD)\mathrm{cross}(C - D, P - D) 符号 → ALA_L / ARA_R
PP 能当 EE 吗? $\ P - C\ ^2 = \ P - D\ ^2$ (在 CDCD 中垂线上) 按同一 cross 符号 → ELE_L / ERE_R

这一对 (C,D)(C, D) 的贡献是 ALER+ARELA_L \cdot E_R + A_R \cdot E_L ——把 AAEE 强行配到 CDCD 两侧。

关键不变量:异侧 = cross(CD,PD)\mathrm{cross}(C - D, P - D) 符号相反,等价于点积 DADE<0\overrightarrow{DA} \cdot \overrightarrow{DE} < 0 那个题面条件,但 cross 是整数运算不会出精度。

Step 8: 固定 (C,D) = ((4,2), (0,2))。绿色虚线 = A/B 必在过 C/D 的两条垂线上;紫色虚线 = E 必在 CD 中垂线上。

AA 候选的两条轨迹 + EE 候选的一条轨迹

固定 (C,D)(C, D) 之后,几何直观特别清晰:

  • 绿色虚线(两条):过 C,DC, D 各画一条 CD\perp CD 的垂线 —— AA 必在过 DD 那条上, BB 必在过 CC 那条上

  • 紫色虚线(一条): CDCD 的中垂线 —— EE 必在它上面

  • 按每个候选点落在 CDCD 哪一侧(cross 的符号)分到 LL / RR 桶里

算法等价于"按轨迹切片 + 异侧匹配"。

Step 14: 汇总。A_L=0, A_R=1 (P=(0,0) 配出 B=(4,0)), E_L=1 (P=(2,4)), E_R=1 (P=(2,0))。贡献 = A_L·E_R + A_R·E_L = 0+1 = 1。

关于"为何要除以 2 / 为何 cpp 没除"

HTML demo 走的版本是有序对 (C,D)(C, D) 枚举, (C,D)(C, D)(D,C)(D, C) 数同一个房子两次,最后 ÷2\div 2

AC 的 cpp 走的是无序对 {ci,di}\{c_i, d_i\}for cifor di = ci + 1),固定一个方向 (c,d)(c, d) ,但用 ALER+ARELA_L \cdot E_R + A_R \cdot E_L 自动把两侧都数了进去 —— 每个房子恰好数一次,不用除 2 。两种写法等价,cpp 这种省去末尾 /2/ 2 一步。

复杂度 + 溢出注意

  • 复杂度O(n2)O(n^2)×\times O(n)O(n) 扫点 =O(n3)= O(n^3)n=300n = 300 时约 2.7×1072.7 \times 10^7 次基本操作,常数小,稳过

  • 点集查询unordered_map<pair<ll,ll>, int> 或先排序后二分都行(cpp 走的是 sort + binary_search

  • 溢出:坐标到 10910^9 。判 EEPC2PD2=(CD)(2PCD)|P - C|^2 - |P - D|^2 = -(C - D) \cdot (2P - C - D) 展开后内部乘积量级 4×10184 \times 10^{18} ,两项加和会越界 long long 。 用 __int128 即可(模板里 dot / cross 在整数路径下已经 promote 到 __int128,直接调)

📎 交互式 demo

下方"附件"节挂了 house_count.html.txt —— 下载后改回 .html 用浏览器打开,可以一步步点 下一步 看每一帧的判定过程(15 帧覆盖:7 点初始 → 5 个房子轮播 → 算法枚举 (C, D) → 5 个候选点逐个分桶 → 汇总贡献 → 求和 ÷ 2 的全流程)。

AC 代码

AC 提交链接:vjudge submission 69955475

核心 Solve() 函数(完整代码见下方附件 House_Count.cpp.txt):

心路历程

  • 5 个点同时枚举想都别想O(n5)=2.4×1012O(n^5) = 2.4 \times 10^{12} ,肯定要解耦。题目让 (A,B,C,D,E)(A, B, C, D, E) 5 个点中 C,DC, D 同时是矩形的边和三角形的底 → 自然以 C,DC, D 为主角, A,B,EA, B, E 都跟着定下来

  • BB 是被 AA 顺便确定的:注意 BA=CDB - A = C - D (矩形对边等长平行),所以扫到一个 AA 候选时立刻知道 BB 应该在哪,查一次点集即可 —— 不用单独枚举 BB

  • 异侧条件靠 cross 符号实现:题面给 DADE<0\overrightarrow{DA} \cdot \overrightarrow{DE} < 0 看起来是浮点不等式,但本质就是 " A,EA, ECDCD 直线两侧",等价于 cross(CD,PD)\mathrm{cross}(C - D, P - D) 异号 —— 整数判完美

  • 有序对 vs 无序对:第一次写很容易写成有序对枚举然后忘了 ÷2\div 2 ,或者无序对枚举但只算 ALERA_L \cdot E_R 漏数一半。核心是想清楚 {c,d}\{c, d\} 作为无序对枚举一次后, ALER+ARELA_L E_R + A_R E_L 已经覆盖了 AA 在两侧 × EE 在异侧的全部 4 种 (label, side) 组合,等价于有序对求和后 ÷2\div 2

  • 溢出 __int128:坐标 10910^9 平方 101810^{18} 还在 long long 内,但两个平方相减 / 相加4×10184 \times 10^{18} 就溢了。模板里 cross / dot / Point::norm() 整数路径下自动走 __int128,所以直接调 distancePPNorm 比较两个 __int128 就行,不用手动加 (__int128) 强转

  • 点集查询用 sort + binary_searchn300n \le 300 完全没必要上 unordered_map<pair<ll,ll>>sort + binary_search<Point<ll>> 走默认 operator<(先 x 后 y)就够了,常数还更小

附件

完整 AC 源码(含中文注释,下载后改回 .cpp 用 g+±15 编译):

House_Count.cpp.txt

交互式 HTML demo(15 帧讲解动画,解压得到 house_count.html,用浏览器打开点 下一步 逐帧看):

house_count_html.zip

vjudge 链接 · Codeforces Gym 原题 · 2025 CCPC 河南省赛 & 全国邀请赛(郑州,黄河科技学院)

题目大意

二维平面上 n 个互不相同的点(1 ≤ n ≤ 6666,坐标 |x|, |y| ≤ 10⁹)。从这 n 个点中有放回地选 3 个点,标记为 A, B, C,最小化点积:

ABAC=(xBxA)(xCxA)+(yByA)(yCyA)\overrightarrow{AB} \cdot \overrightarrow{AC} = (x_B - x_A)(x_C - x_A) + (y_B - y_A)(y_C - y_A)

A, B, C 可以选相同的点,所以答案的下界是 0(取 A = B = C 任意点都给 0)。

输入 / 输出 / 数据范围

  • 第 1 行:n(1 ≤ n ≤ 6666)

  • 后 n 行:每行 x y(|x|, |y| ≤ 10⁹)

  • 输出一行一个整数,最小点积(可能为负,需要 long long / int128

样例 1

1
2
3
4
5
6
5
0 2
0 -2
3 0
-3 0
0 -1

输出 -8(最优 A = p5 = (0, -1)(hull 内部点)、B = p3 = (3, 0)、C = p4 = (-3, 0),dot = 3·(-3) + 1·1 = -8)。

思路讲解

一句话

凸包 + 双指针的「投影版」:B、C 都在凸包上(调整法证),固定 A 后让 B 沿 hull CCW 走、C 用双指针单调跟进。整体 O(n·h) ≤ O(n²)

⚠️ 关键边界(test 44 WA 根因):A 在凸包顶点上时,B 的起点必须是 A 的 CCW 后继 A_next不能从 hull[0] 起步——否则方向序断层破坏双指针单调性。光「跳过 B = A」治标不治本。

把"最小点积"翻译成"在某方向上最极端"

固定 A,把点积写成投影形式:

ABAC=ABprojAB(AC)\overrightarrow{AB} \cdot \overrightarrow{AC} = |\overrightarrow{AB}| \cdot \mathrm{proj}_{\overrightarrow{AB}}(\overrightarrow{AC})

其中 proj 是 AC 在方向 AB 上的有向投影长度(同向为正、反向为负)。要让点积越负越好,就要让 AC 在方向 -AB 上的距离越远越好

关键不变量:凸集上线性函数的极值在顶点取到。所以最优 C 必然在凸包上;对称地 B 也在凸包上。

A 自己不一定在凸包上! 样例 1 的最优 A = (0, -1) 就在 hull 内部(题目唯一的内部点)。这是「必须枚举所有原始点当 A、不能只在 hull 上枚举 A」的原因——一个非常容易写错的实现细节。

算法三阶段

阶段 干什么 跳过的代价
1. 凸包预处理 对全部 n 个点跑 Andrew O(n log n) B/C 在全集枚举 → O(n³) TLE
2. 枚举 A 原始点集(是 hull)每点当 A 只在 hull 上枚举 A → 漏内部最优 A(样例 1 答案变 -7 而不是 -8)
3. 双指针扫 (B, C) 固定 A 后 B 沿 hull CCW 走、C 单调跟进 二重循环 → O(n·h²) 大数据 TLE

阶段 2 / 3 的「分裂」是核心理解点:枚举的是原始点(n 个),扫的是凸包上的点(h 个),两者不一样。

双指针单调性 = 旋转卡壳的对偶

固定 A,写出内层「对 B 求 C」函数:

C(B)=argminCH  ABAC=argminCH  ABprojAB(AC)C^*(B) = \arg\min_{C \in H}\; \overrightarrow{AB} \cdot \overrightarrow{AC} = \arg\min_{C \in H}\; |\overrightarrow{AB}| \cdot \mathrm{proj}_{\overrightarrow{AB}}(\overrightarrow{AC})

C*(B) 就是凸包在方向 -AB 上的支撑点

灵魂对偶:本题就是旋转卡壳的「投影版」——卡壳卡的是「到边的距离」,本题卡的是「沿方向的有向投影」,主结构同构。

结构件 旋转卡壳(求直径) 本题(最小点积)
主循环对象 凸包边 e_i 凸包顶点 B(视作方向 AB)
跟进对象 对踵点 C*(离 e_i 最远) 支撑点 C*(方向 -AB 上最远)
单调性根源 e_i 法方向沿 H 单调旋转 AB 方向随 B 沿 H 单调旋转
判定工具 cross 比 C 到边距离 dot 比 C 沿 AB 投影

A 在凸包上时的边界处理(test 44 WA 根因)

这一节是 ICPC 题解里最容易写错的点,也是题解 PDF 一笔带过那句的真意:

如果 A 在凸包上,枚举 B 时就不要枚举 A 了,从 A 的下一个点开始枚举 B,转回来到 A 的上一个点结束枚举。 —— 命题组 sol.pdf

为什么不能"光跳过 B = A"了事

if (A.id == convex[b].id) continue; 只解决 AB = 0 退化(该轮 dot 全是 0),没解决方向序断层

真正的死因:双指针单调性要求「随 B 沿 hull 走,方向 AB 必须单调旋转」,这样 C* 才能跟着单向推进。当 A 在 hull 顶点 h_{i_A} 上时,按 hull 原序 [h_0, h_1, ..., h_{h-1}] 遍历 b:

  • h_0, h_1, ..., h_{i_A - 1} 这一段在「从 A 看的 CCW 极角序」里属末段

  • 真正的 CCW 起点是 h_{i_A + 1}(A 的 next)

代码从 h_0 起扫等于「方向倒着来」,下一步跳到 h_{i_A + 1} 时方向回退 90° 以上C* 单向 c++ 跟不上 → WA

实证反例(17 点,本会话抓到)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
17
16 100
104 -9
-18 94
-86 45
-87 -47
54 89
-30 -94
-60 -78
52 -90
2 -103
-98 9
-97 -14
96 -36
74 67
-61 83
96 31
65 -72

真最优:A = p11 = (-97, -14)(hull[1])、B = p6 = (-30, -94)(hull[4])、C = p14 = (-61, 83)(hull[14]),dot = -5348

「只 continue 跳过 b=A」的错代码输出 -5338,差 10——正是 b = h0 方向 ≈ +92.5° → b = h2 方向 ≈ -73°,方向断层 165° 让 c 跟丢了 c = h14。

正确修法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 找 A 在 hull 上的位置 aPos (-1 = 不在 hull 上)
int aPos = -1;
for (int i = 0; i < H; ++i)
if (A[a].id == convex[i].id) { aPos = i; break; }
int bStart = (aPos == -1) ? 0 : (aPos + 1) % H;
int bCnt = (aPos == -1) ? H : H - 1;

// 初始 c 以 convex[bStart] 为 b 基准 (不是 convex[0]!)
int c = bStart;
ll tar = dot(A[a], convex[bStart], convex[c]);
for (int i = 0; i < H; ++i) {
ll lans = dot(A[a], convex[bStart], convex[i]);
if (lans < tar) { tar = lans; c = i; }
}

for (int t = 0; t < bCnt; ++t) {
int b = (bStart + t) % H;
auto check = [&]() {
return dot(A[a], convex[b], convex[c % H]) >=
dot(A[a], convex[b], convex[(c + 1) % H]);
};
while (check()) c++;
ans = min(ans, (ll) dot(A[a], convex[b], convex[c % H]));
}

3 个改动:

  1. bStart = (aPos + 1) % H:让 b 从真正的 CCW 起点开始

  2. bCnt = H - 1:走 h-1 步绕回 A_prev 终止(A 自己天然跳过)

  3. check >= 而不是 >:dot 平台时推到末尾,避免 c 卡在平台前段让下一个 b 跟不上

「为什么 hull[0] 是幸运 case」

如果 A = h0(hull 最左下顶点),那 A_next = h1——代码从 b = h0 起扫,第一轮 b = A 直接被 continue 跳,第二轮 b = h1 恰好就是真正的 CCW 起点,方向断层不出现。这就是为什么「只 continue 跳过 b=A」的写法样例 1 过了(样例 1 hull 是矩形 + 内部点,所有 hull 顶点对称,A 在内部时根本没断层;A 在 hull 上时恰好都是 hull[0] 的对称 case,运气好)。

但样例不能覆盖「A 在 hull 上但不是 hull[0]」这种邪恶 case——test 44 就是用这个数据。

📎 动画与源码

下面 3 张截图(位于「附件」节)来自 astral_decay_demo.html 的关键帧。HTML 本体在附件节末尾(zip 包,解压后用浏览器打开 .html = 完整交互演示,含 4 个 mini-demo + 主算法 step-by-step)。

AC 代码

AC 提交链接(Codeforces Gym 105941) 见 vjudge submission 记录

完整 .cpp 含 700 行几何模板 + 50 行 Solve()。完整源码见附件 Astral_Decay.cpp.txt(下载后改回 .cpp)。下面只展开核心 Solve():

心路历程(WA → AC)

按真实的 debug 过程列:

  1. 第一版按题解写:枚举 A、b 沿 hull 走 h 步、c 双指针。样例 1 过,WA on test 44

  2. if (A.id == convex[b].id) continue; 跳过 b = A 退化轮样例 1 仍过,仍 WA on test 44

  3. cyaron 风格 stress test:本机 gen 17~30 点全在 hull 上的随机数据(椭圆采点 + 强制 h ≥ 5)+ brute O(n³) 对拍。第 204 次种子抓到 17 点反例

  4. 手算反例确认根因:A = p11 在 hull[1](不是 hull[0]!),b 按 hull 顺序走方向不单调(+92.5° → -73° 回退 165°),c 单向推进跟不上。

  5. 正确修法:b 起点改 (aPos + 1) % H、走 H-1 步;c 初始扫描以 convex[bStart] 为 b 基准(不再用 convex[0]);check>=

  6. AC

关键 take-away

  • 「光看样例 AC」≠「算法正确」——样例 1 的 hull 是对称矩形 + 1 内部点,所有特殊 case(A 在 hull 上非 hull[0])都不触发;样例 2 是随机 10 点稀疏分布,也不触发。

  • 「跳过 b=A」是表面修法,「b 起点改 A_next****」才是治本——前者样例过但 test 44 仍 WA,后者通过。

  • 对拍 gen 要能触发「A 在 hull 顶点上但不是 hull[0]」:均匀随机点云通常 A 在 hull 上的概率 ≈ h/n 很低;必须强制让 hull 顶点占比 ≥ 50%。本会话用的 gen:椭圆采 h ≥ 5 点 + 调过 phi/asp 让 hull 形状非对称。

  • 几何模板里的 gen_convex_polygon 当 N ≤ 2 必死循环(N=1: j=(0+1)%1=i 让 A[i] == A[j] 永真;N=2: k=(0+2)%2=i 让 cross 三点退化恒 0)。这次顺便发现 + 修了(加 N ≤ 2 退化分支撒不重点)。

附件

demo 关键帧 1: 初始状态, 5 个点全识别, 凸包未建

demo 关键帧 2: step 8, A=p1 在 hull 顶点, B=h0=p3, C*=h2=p2, 当前 dot=-5, 历史最小 -5

demo 关键帧 3: 算法完成, 历史最小收敛到 -8 (最优 A=p4 内部点, B=p2, C=p3)

astral_decay_demo.html.zip

Astral_Decay.cpp.txt

官方题解 PDF (CCPC 河南省赛 2025 全题题解, 命题组 BUAA)

原题:Gym 105158 D 距离之比vjudge

AC:vjudge solution 69896828

题目大意

平面上 nn 个互不重合的点 P1,,PnP_1, \dots, P_n ,求

max1i<jndis1(Pi,Pj)dis2(Pi,Pj)\max_{1 \le i < j \le n} \frac{\mathrm{dis1}(P_i, P_j)}{\mathrm{dis2}(P_i, P_j)}

其中 dis1\mathrm{dis1} 是曼哈顿距离 xixj+yiyj|x_i - x_j| + |y_i - y_j|dis2\mathrm{dis2} 是欧几里得距离 (xixj)2+(yiyj)2\sqrt{(x_i - x_j)^2 + (y_i - y_j)^2}

数据范围

  • 1T1051 \le T \le 10^5 组数据

  • 2n2×1052 \le n \le 2 \times 10^5n2×105\sum n \le 2 \times 10^5

  • xi,yi109|x_i|, |y_i| \le 10^9

  • 输出绝对/相对误差 109\le 10^{-9} 即可

思路讲解

一句话

比值 dis1/dis2\mathrm{dis1} / \mathrm{dis2} 只取决于两点连线的方向角, ±45\pm 45^\circ 方向最大 2\sqrt{2} 、坐标轴方向最小 11 。所以题目等价于"找方向最接近 ±45\pm 45^\circ 的点对"。一个中值不等式引理把它直接砍到 O(nlogn)O(n \log n) :按 x+yx + y 排序扫相邻、再按 xyx - y 排序扫相邻就完事。

第 1 步:比值只取决于方向

θ\thetaPiPj\overrightarrow{P_i P_j}xx 轴的夹角。把 dis2\mathrm{dis2} 当分母提出来:

dis1dis2=cosθ+sinθ=2sin ⁣(θ+π4)\frac{\mathrm{dis1}}{\mathrm{dis2}} = |\cos\theta| + |\sin\theta| = \sqrt{2}\sin\!\left(|\theta'| + \tfrac{\pi}{4}\right)

折叠到一个象限后看,整个比值是关于 θ\theta 的纯三角函数——也就是跟两点距离绝对长度无关、只看方向

关键不变量θ\theta 越接近 ±45\pm 45^\circ±135\pm 135^\circ ,比值越大;极大值 21.4142\sqrt{2} \approx 1.4142θ=0\theta = 0π/2\pi/2 时退化为 11

第 2 步:旋转到 (u, v) 化为 Chebyshev

做仿射变换

u=x+y,v=xyu = x + y, \qquad v = x - y

也就是把 xyxy 平面旋转 45-45^\circ 再放缩 2\sqrt{2} 。神奇的事情发生了—— L1L^1 距离变成了 Chebyshev 距离

dis1=max(Δu,Δv),dis2=Δu2+Δv22\mathrm{dis1} = \max(|\Delta u|, |\Delta v|), \qquad \mathrm{dis2} = \sqrt{\frac{\Delta u^2 + \Delta v^2}{2}}

±45\pm 45^\circ 方向在 (u,v)(u, v) 平面变成 uu 轴 / vv 轴方向。代入比值:

dis1dis2=2max(Δu,Δv)Δu2+Δv2\frac{\mathrm{dis1}}{\mathrm{dis2}} = \sqrt{2} \cdot \frac{\max(|\Delta u|, |\Delta v|)}{\sqrt{\Delta u^2 + \Delta v^2}}

不妨设 ΔuΔv|\Delta u| \ge |\Delta v| ,记 t=Δv/Δu[0,1]t = |\Delta v| / |\Delta u| \in [0, 1] ,比值化为 2/1+t2\sqrt{2} / \sqrt{1 + t^2} ——单调地越接近某根坐标轴( t0t \to 0 )越大

问题转化:在 (u,v)(u, v) 平面找点对,使其连线方向最接近 uu 轴或 vv 轴。

第 3 步:核心引理 — 中值不等式

转化后的问题:找一对点使 (uiuj)/(vivj)\left| (u_i - u_j) / (v_i - v_j) \right| 最大(以 vv 为横轴看"斜率绝对值最大")。

引理:把点按 vv 升序排得 v(1)v(2)v(n)v_{(1)} \le v_{(2)} \le \dots \le v_{(n)} 。最优点对必然出现在相邻位,即存在最优解 (P(k),P(k+1))(P_{(k)}, P_{(k+1)})

证明(中值不等式):取三点 i,k,ji, k, j 满足 vi<vk<vjv_i < v_k < v_j 。记 sab=(ubua)/(vbva)s_{ab} = (u_b - u_a) / (v_b - v_a) 。则

sij=(ukui)+(ujuk)(vkvi)+(vjvk)s_{ij} = \frac{(u_k - u_i) + (u_j - u_k)}{(v_k - v_i) + (v_j - v_k)}

分母两项同号,所以 sijs_{ij}sik,skjs_{ik}, s_{kj} 的加权"中值":

min(sik,skj)sijmax(sik,skj)\min(s_{ik}, s_{kj}) \le s_{ij} \le \max(s_{ik}, s_{kj})

sijmax(sik,skj)|s_{ij}| \le \max(|s_{ik}|, |s_{kj}|)

跨过中间点 kk 的非相邻对,其斜率绝对值不会超过两个跨界子对之一。一路归纳下去,最优解总能退化到 vv 排序后的相邻对。 \square

中值不等式那一招很优美——它说"跨过去更长 \Rightarrow 不更陡",本质上是凸性:把分子分母都看成"长度向量"沿 vv 轴的累加,斜率永远夹在子段斜率之间。这是一个比凸包/旋转卡壳更对路的工具,专门针对"对所有点对极大化方向相关量"这一类几何题。

第 4 步:完整算法

引理只处理了"方向最接近 uu 轴"。对称的"方向最接近 vv 轴"同理——按 uu 排序扫相邻对就能覆盖。所以:

  1. u=x+yu = x + y 升序排,遍历相邻对更新最大比值

  2. v=xyv = x - y 升序排,遍历相邻对更新最大比值

  3. 两次累计的最大值就是答案

两次 sort + 两次 O(n)O(n) 扫描,单组 O(nlogn)O(n \log n) 。每对的比值直接用原 (x,y)(x, y) 坐标算(无需真把点替换成 (u,v)(u, v) )。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void Solve() {
ll N; cin >> N;
vector<Point<ll>> A(N);
for (auto& p : A) cin >> p;

auto upd = [&](Point<ll> a, Point<ll> b) {
return (DB) distancePPL1(a, b) / distancePP(a, b);
};

// 阶段 1:按 u = x+y 升序排
sort(all(A), [](auto& aO, auto& bO) {
Point<ll> a(aO.x + aO.y, aO.x - aO.y);
Point<ll> b(bO.x + bO.y, bO.x - bO.y);
return a < b;
});
DB ans = 0;
for (int i = 0; i + 1 < SZ(A); ++i)
ans = max(ans, upd(A[i], A[i+1]));

// 阶段 2:按 v = x-y 升序排(swap 后复用同一个 Point 字典序)
sort(all(A), [](auto& aO, auto& bO) {
Point<ll> a(aO.x + aO.y, aO.x - aO.y);
Point<ll> b(bO.x + bO.y, bO.x - bO.y);
swap(a.x, a.y); swap(b.x, b.y);
return a < b;
});
for (int i = 0; i + 1 < SZ(A); ++i)
ans = max(ans, upd(A[i], A[i+1]));

cout << fsp(12) << ans << "\n";
}

几个实现细节:

  • distancePPL1 返回的是 __int128_t (我新加进几何模板的"曼哈顿距离"),所以 (DB) 强转一次再除浮点,不转的话整数除法直接出 1 全错

  • x,y109|x|, |y| \le 10^9dx2+dy28×1018dx^2 + dy^2 \le 8 \times 10^{18} 卡在 long long 上界 9.2×10189.2 \times 10^{18} 内,不会爆 ll。要稳一点 __int128_t 兜底也行

  • 全程用 long doublesqrtl 保精度, cout << fixed << setprecision(12) 输出

  • 多组 T105T \le 10^5ios::sync_with_stdio(false) 必须关掉缓冲

📎 动画与源码

完整的可交互 HTML 讲解我做了一份。覆盖 4 个 demo:

  1. 方向决定比值 :拖一个点看比值随方向变化,验证"只看方向不看长度"

  2. xyxyuvuv 双视图:直观看到 ±45\pm 45^\circ 方向变 u,vu, v 轴的几何过程

  3. 中值不等式三点 demo :拖中间点 kkuu 坐标,看 sijs_{ij} 永远夹在 sik,skjs_{ik}, s_{kj} 之间

  4. 主算法 6 点演示 :Step / Auto-play 走完两阶段排序 + 邻点扫,最优对正好在 P1-P2(u 都等于 7,Δu = 0,方向沿 v 轴 → 比值取到 2\sqrt{2} 上限)

HTML 本体 + 3 张关键帧截图都嵌在「附件」节里。

AC 代码

AC 提交 — vjudge solution 69896828

完整源码包含本次新沉淀进几何模板的:

  • Point::normL1() / Point::normLinf() 两个成员(曼哈顿 / 切比雪夫范数)

  • distancePP / distancePPL1 / distancePPLinf 三个自由函数(开方欧氏 / 曼哈顿 / 切比雪夫距离)

完整源码见末尾「📎 完整源码」附件 + 下方折叠块(折叠块走 raw API 单独追加,因为 30 KB 超 markdown 单段限制)。

心路历程

这道题没什么"卡 WA"的心路——主要是思路推导一条线下来

  1. 看到 dis1/dis2\mathrm{dis1} / \mathrm{dis2} 直觉就反应"这跟方向有关",写成 cosθ+sinθ|\cos\theta| + |\sin\theta| 后就锁死"找最接近 ±45\pm 45^\circ 方向的点对"

  2. 但是 O(n2)O(n^2) 枚举显然不行( n=2105\sum n = 2 \cdot 10^5 ),第一反应想凸包——结论:凸包做不了。反例:最优对可以在凸包内部。比如三角形 A=(0,0),B=(10,10),C=(10,10)A = (0, 0), B = (10, 10), C = (10, -10) 加两个内部点 D=(5,0),E=(5,1)D = (5, 0), E = (5, 1)D,ED, EΔu=0\Delta u = 0 比值直接 2\sqrt{2} 顶天,但 D,ED, E 都不在凸包上

  3. 直接看了官方题解才知道是中值不等式那一招。证明优美但"赛场上能不能想到"是另一回事——下次见到「对所有点对极大化方向相关量」类几何题先把这条工具拿出来用

  4. 实现时本来要在 Solve 里手写 L1 / L2 距离,顺手沉淀进几何模板(这次加了 distancePP / distancePPL1 / distancePPLinf 三个自由函数 + Point::normL1 / normLinf 两个成员),后续题目直接复用

Take-away:「最大化 / 最小化 f(θij)f(\theta_{ij}) 」( θij\theta_{ij} 是点对方向角)这一类问题,先想"排序 + 邻点扫"配中值不等式,比凸包 / 旋转卡壳更对路。

附件

下面是这道题的可交互 HTML 讲解 + 3 张关键帧截图 + 完整源码下载。

distance_ratio.html.txt

Step 0:初始状态。6 个点已读入,未排序未扫描。

Step 6:u-排序阶段扫到最后一对 P5-P3 时,累计最优已锁定在 P1-P2(u 都 = 7,Δu = 0,方向沿 v 轴)—— 比值 1.414214 触顶 √2。

Step 12:算法完成。两阶段共扫了 10 对相邻对,最优对始终是 P1-P2。