0%

原题: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。

原题:POJ 3608 Bridge Across Islands(POJ 评测机太老仅 C++98,强烈推荐 vjudge 上的镜像 OpenJ_Bailian-3851)。本题是两凸多边形最近距离的旋转卡壳模板题,但有一个反直觉的「单调推进准则」选择 —— 第一版贪心在 OJ 上 TLE,调试改 cross 单调推进后又 WA,最终发现单次调用对偶覆盖不完整,必须 (A, B)(B, A) 各跑一次。

题目大意

太平洋中央有一个王国,领土由两座岛屿组成,被洋流冲成两个凸多边形。国王要造一座桥连两岛,问两岛边界的最小距离是多少。

输入

多组测试用例。每组先给两整数 NNMM ( 3N,M100003 \le N, M \le 10000 ),接下来 NN 行各给一对坐标描述第一个凸多边形的一个顶点(已按某种顺序给出,CW 或 CCW),再接 MM 行给第二个凸多边形顶点。坐标 [10000,10000]\in [-10000, 10000]

一行 N=M=0N = M = 0 表示输入结束。

输出

每组输出最小距离,误差 103\le 10^{-3}

样例

1
2
3
4
5
6
7
8
9
10
4 4
0.00000 0.00000
0.00000 1.00000
1.00000 1.00000
1.00000 0.00000
2.00000 0.00000
2.00000 1.00000
3.00000 1.00000
3.00000 0.00000
0 0

输出:

1
1.00000

思路讲解

一句话

两凸多边形最近距离 = 旋转卡壳 + facing 弧双指针 O(N+M)O(N + M) ;推进准则用「cross(a, b') 角度差」、不要用「dis2 < dis1 顶点-边距离单峰」(贪心在背面弧会卡住);最终结果必须 (A, B)(B, A) 各调一次取 min\min 才覆盖完整。

把最近距离归并到「顶点 → 线段」

两个不相交的凸多边形之间,最小距离的实现位置只有三种:

特征对 是否独立 备注
顶点 - 边内部 ✓ 主路径 从某顶点向另一多边形某条边作垂线,垂足落在边内
顶点 - 顶点 退化 dist(vA, seg(eB))\mathrm{dist}(v_A,\ \mathrm{seg}(e_B)) 的垂足落在 eBe_B 外,点-线段距离自动退成点-点距离
边 - 边 退化 仅当两边平行才可能;端点也取得到,被「顶点对线段」覆盖

关键不变量:枚举所有 (顶点, 对面边)(顶点,\ \mathrm{对面边}) 对,算点-线段距离min\min ,三种情形一网打尽。

facing 弧 + 单调推进

固定 AA 上一个顶点 pp ,凸性下 BB 上「真正可能成为离 pp 最近的那条边」有且仅有一条 —— 称它为 ppfacing edge。形象点:从 pp 看过去, BB 朝向 pp 那半弧上正对 pp 的那条边,其它边要么被它挡住、要么转到背面去了。

pp 沿 AA 走 CCW 一圈,那条 facing 边在 BB 上的位置是**「短程单调、长程往返」**:

  • 短程pp 走在 AABB 那半弧上):facing 边在 BB单调推进(不回退)

  • 长程pp 越过 AA 的背面弧): BB 没有新边可选,facing 边只能沿同一段 facing 弧 退回来

  • 整圈轨迹 = 「推过去 → 退回来」 = 往返

算法直接好处:两个指针不需要走完一整圈,各自只走完自己的 facing 弧就够了,总步数 N+M\le N + M

初始 facing pair:A 最低 + B 最高

按代码取:

  • eA\texttt{eA} = AAyy 最小的点的下标

  • eB\texttt{eB} = BByy 最大的点的下标

几何上 eA\texttt{eA} 看上去就是 eB\texttt{eB}eB\texttt{eB} 看下去就是 eA\texttt{eA} —— 这一对是初始 facing pair,也是 AA / BB 各自 facing 弧的一个端点。

不过这个初始只在「 BB 整体在 AA 上方」时直观对齐;其它分布下( AA 在右、 BB 在左之类)yMnA + yMxB 不一定是真 facing pair。没关系 —— 推进准则用 cross 角度差驱动后,初始位置不正确也能在 O(N+M)O(N + M) 步内追上正确的对踵 pair,所以这个朴素初始化就够了。

cross(a, b') 推进准则(核心)

设:

  • a=Ai+1Ai\vec{a} = A_{i+1} - A_iAAAiA_iCCW 出边方向

  • b=BjBj+1\vec{b}' = B_j - B_{j+1}BBBjB_j 的 CCW 出边 反向(因为 BB 的 facing 弧逆 BB 的 CCW 序前进)

推进准则

cross(a, b){>0 ++i (推进 A)<0 ++j (推进 B)=0两个都试一下取 min\mathrm{cross}(\vec{a},\ \vec{b}') \quad \begin{cases} > 0 & \Rightarrow\ \mathtt{++i}\ \text{(推进 A)} \\ < 0 & \Rightarrow\ \mathtt{++j}\ \text{(推进 B)} \\ = 0 & \text{两个都试一下取 min} \end{cases}

几何意义:初始 facing pair 处 a\vec{a}b\vec{b}' 都大约朝 +x+x ,几乎平行同向;两者沿各自多边形 CCW 推进时都向 CCW 方向旋转(凸性)。 cross(a, b)\mathrm{cross}(\vec{a},\ \vec{b}') 衡量两者角度差:

  • >0> 0b\vec{b}'a\vec{a} 的 CCW 一侧 \Rightarrow a\vec{a} 角度落后 \RightarrowAA 推一步追上

  • <0< 0 :反之,让 BB 推一步

为什么这样不会 TLE:每步刚好推一个指针、各自只能 forward。 ii 走完 AA 一圈、 jj 走完 BB 一圈就停,总步数 N+M\le N + M 。背面弧问题自动消失 —— cross 一直按角度差驱动,根本不依赖「dis 谁更小」这种局部贪心。

为什么必须 (A, B) + (B, A) 各调一次

单次 cal_min_dis_two_convex(A, B) 只覆盖**「 AA 顶点 → BB 边」**这组对偶配置 —— 也就是「枚举 AA 上每个顶点,找 BB 上对应的 facing 边」。

但反对称几何里答案可能取在另一边:细长 AA 戳向粗 BB 的边中间, AA 的端点垂足落在 BB 的边内部 —— 这种「 AA 顶点 - BB 边」抓得到;反过来「 BB 顶点 - AA」抓不到。

解决:把同一份代码以 (B, A) 参数顺序再跑一遍,补上反向对偶,最后两次取 min\min

`c++

DB ans = INFINITY;

ans = min(ans, cal_min_dis_two_convex(A, B));

ans = min(ans, cal_min_dis_two_convex(B, A));

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16



漏一次会在 stress test 第 24 个 seed 翻车(**已实测**,反例见 `counterexample_seed24.html` 附件,期望 $62.29$ 实测 $63.7$ )。

## 📎 动画与源码

- $\vec{a}$ / $\vec{b}'$ 向量 + `cross` 推进逐步演示:`cross_product_demo.html`(见附件)
- 单次调用对偶不完整反例:`counterexample_one_call_only.html`、`counterexample_seed24.html`

# AC 代码

[**AC 提交链接(vjudge OpenJ\_Bailian-3851)**](https://vjudge.net/solution/69793877)

核心函数:

// 两凸多边形最近距离(旋转卡壳, O(n + m))

// 单次调用只覆盖「A 边 → B 顶点」配置,

// 调用方需以 (A, B) 和 (B, A) 各调一次取 min。

template

DB cal_min_dis_two_convex(vector<Point> A, vector<Point> B) {

reorder_polygon(A);

reorder_polygon(B);

ll yMnA = min_element(all(A), [](const Point<T> &a, const Point<T> &b) {

    return a.y < b.y;

}) - A.begin();

ll yMxB = max_element(all(B), [](const Point<T> &a, const Point<T> &b) {

    return a.y < b.y;

}) - B.begin();

DB ans = INFINITY;

ll eA = yMnA, eB = yMxB;

for (int _ = 0; _ < SZ(A) + SZ(B); ++_) {

    auto check = [&]() {

        auto vecA = A[(eA + 1) % SZ(A)] - A[eA];

        auto vecB = B[(eB + 1) % SZ(B)] - B[eB];

        return cross(vecA, vecB) > 0;

    };

    while (check()) ++eB, eB %= SZ(B);

    ans = min(ans, distanceSS(Line(A[(eA + 1) % SZ(A)], A[eA]),

                              Line(B[(eB + 1) % SZ(B)], B[eB])));

    eA = (eA + 1) % SZ(A);

}

return ans;

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

完整源码(含几何模板、读入、Solve 框架):

<details>
<summary>展开完整 C++ 源码(Bridge_Across_Islands.cpp)</summary>

完整源码 ~840 行较长,含整套几何模板(`Point` / `Line` / `cross` / `distanceSS` / `reorder_polygon` 等)+ `cal_min_dis_two_convex` + 多组数据 main。直接下载页面末尾附件 `Bridge_Across_Islands.cpp.txt`(去掉 `.txt` 后缀即为 `.cpp`)。

</details>

# 心路历程(TLE → WA → AC)

1. **第一版 TLE**:用「`dis2 < dis1` 顶点-边距离贪心」推进 —— 在 $A$ 朝 $B$ 那半弧上确实单调,但当 $i$ 越过 $A$ 的背面弧后单调性破裂, $j$ 想往回走但只能 forward,**陷在 $B$ 上转圈**,OJ 测出 TLE。
2. **第二版 WA**:换成 `cross(a, b')` 角度判定,单次 `cal_min_dis_two_convex(A, B)` 调用 —— 本地 cyaron stress 用近圆形测 5000 case 全过、提交 OJ WA。`cyaron-stress` skill 警告过:**cyaron `Polygon.convex_hull` 默认近圆形是对拍盲区**,必须主动用 ellipse / spiked / heart / lemon 等曲率非均匀形状才能触发反例。换成椭圆 + spike 形状后,stress seed 24 立刻给出 `yMnA + yMxB` 配对失效的反例(见附件 `counterexample_seed24.html`)。
3. **第三版 AC**:加上 `(B, A)` 反向调用取 $\min$ 补对偶。

\textcolor{red}{**收获心法**}:

- 旋转卡壳推进准则用 **`cross` 角度差** 而不是 **dis 单峰**。「dis 单峰」是教材级 fact 但只在曲率均匀分布下成立。
- 单次调用对偶不完整 —— 看 mentor.tex 里 me/mentor 第 3 步对话录有详细推导,需要 `(A, B)` + `(B, A)` 反向。
- 对拍**必须**用非均匀曲率形状(椭圆 / spike / heart / lemon)破盲区;cyaron `Polygon.convex_hull` 默认值就是盲区本身。

# 附件

Bridge_Across_Islands_html_demos.zip

mentor.tex 编译产物 PDF —— me/mentor 5 步推导对话录(题面 / 三类特征对 / facing 弧 / 旋转卡壳骨架 / cross 推进准则)

Bridge_Across_Islands.cpp.txt

题目大意

原题:CF Round 1093 Div2 D2 - Unique Values (Hard version)(rating 1900)

交互题。有一个长度 2n+12n+1 的隐藏数组 aa ,元素值在 [1,n][1, n] 之间;其中有且仅有一个值出现 3 次,其余每个值都恰好出现 2 次。要求找出那个出现 3 次的值的三个下标

每次询问可以选一个下标子集 SS ,judge 会返回 {ai:iS}\{a_i : i \in S\} 这个多重集里恰好出现一次的值的个数(即把一对一对的去掉以后还剩几种)。

Easy version: 50\le 50 次询问。Hard version(本题)33\le 33 次询问。 n1000n \le 1000

思路讲解

一句话

设那个特殊值(出现 3 次的)为 TT ,三个位置 p1<p2<p3p_1 < p_2 < p_333 次预算 = 3 段二分 × 11 次——每段用 bit-trick 在长度 2n+12001\le 2n+1 \le 2001 的范围里锁一个位置, log22001=11\lceil \log_2 2001 \rceil = 11 。三段分别独立设计一个关于 kk 单调的奇偶谓词,bit-trick 收敛。

核心 trick:贡献奇偶表

对任一询问集合 SS ,记 len=S\text{len} = |S|res\text{res} 为 judge 返回值。每个值 vvlenres\text{len} - \text{res} 的贡献只取决于它在 SS 中的 count:

count 贡献 = count − [count==1] 奇偶
0 0
1 0
2 2
3 3

关键不变量:只有 count = 3 那档贡献奇数。本题里能让某个值 count = 3 的,只有那个出现 3 次的特殊值 TT 。所以 (lenres)mod2=1    T 在 S 里凑齐 3 个 position(\text{len} - \text{res}) \bmod 2 = 1 \iff T \text{ 在 } S \text{ 里凑齐 } 3 \text{ 个 position}

其他值(出现 2 次的 double)count 2\le 2 ,贡献全偶,自动从奇偶位上消失——这就是奇偶谓词的威力,doubles 不来添乱。

第 1 段:找 p3p_3

询问 Sk=[1..k]S_k = [1..k]TTSkS_k 里 count = 3 当且仅当 kp3k \ge p_3 (三个位置全进来)。

(kres)mod2=0    k<p3(k - \text{res}) \bmod 2 = 0 \iff k < p_3

谓词在 [0,p31][0, p_3 - 1] 真, [p3,N][p_3, N] 假。bit-trick 从 k=0k = 0 起按 2i2^i 加位,找最大的真 kk = p31p_3 - 1 ,加 1 即 p3p_3

1
2
3
4
5
6
7
8
ll pos3 = 0;
for (int i = __lg(N); i >= 0; --i) {
ll k = pos3 + (1ll << i);
if (k > N) continue;
ll res = query(1, k);
if ((k - res) % 2 == 0) pos3 = k;
}
pos3++;

第 2 段:找 p1p_1

知道 p3p_3 后,询问 Sk=[k..p3]S_k = [k..p_3] (后缀)。 TTSkS_k 里 count = 3 当且仅当 kp1k \le p_1 (三个位置都在 [k..p3][k..p_3] 里)。

(lenres)mod2=0    k>p1(\text{len} - \text{res}) \bmod 2 = 0 \iff k > p_1

谓词在 [p1+1,p3][p_1+1, p_3] 真, [1,p1][1, p_1] 假。bit-trick 从 k=p3k = p_3降序减位,找最小的真 kk = p1+1p_1 + 1 ,减 1 即 p1p_1

1
2
3
4
5
6
7
8
9
ll pos1 = pos3;
for (int i = __lg(pos3); i >= 0; --i) {
ll k = pos1 - (1ll << i); // ← 用当前 pos1,不是 pos3
if (k < 1) continue;
ll res = query(k, pos3);
ll len = pos3 - k + 1;
if ((len - res) % 2 == 0) pos1 = k;
}
pos1--;

第 3 段:找 p2p_2 (最绕的一段)

p1,p3p_1, p_3 已知,找 p2(p1,p3)p_2 \in (p_1, p_3) 。这一段最容易写错——不要用「query [1..k]{p1,p3}[1..k] \setminus \{p_1, p_3\} 看 res 是否为 0」这种谓词,那个不单调。

正确设计:让 TT 在询问集合里的 count 在 kk 跨过 p2p_2从 2 跳到 3(正好覆盖贡献表里的奇偶分界点)。

Sk={p3}[p1..k],k[p1,p31]S_k = \{p_3\} \cup [p_1..k], \quad k \in [p_1, p_3 - 1]

T 在 SkS_k 里: p1p_1 必入(在 [p1..k][p_1..k] 里), p3p_3 必入(显式塞), p2p_2 当且仅当 kp2k \ge p_2 时入。

kk vs p2p_2 TT count (lenres)mod2(\text{len}-\text{res}) \bmod 2
k<p2k < p_2 2 0(偶)
kp2k \ge p_2 3 1(奇)

谓词     k<p2\iff k < p_2 ,bit-trick 从 k=p1k = p_1 起加位,找最大真 kk = p21p_2 - 1 ,加 1 即 p2p_2

1
2
3
4
5
6
7
8
ll pos2 = pos1;
for (int i = __lg(pos3 - pos1); i >= 0; --i) {
ll to = pos2 + (1ll << i);
if (to >= pos3) continue;
auto [len, res] = query(pos1, to, pos1, pos3); // [pos1..to] ∪ {pos1, pos3}
if ((len - res) % 2 == 0) pos2 = to;
}
pos2++;

询问数核账

范围 bit 数上界
p3p_3 [1,2n+1][1, 2n+1] log2(2n+1)11\lceil \log_2 (2n+1) \rceil \le 11
p1p_1 [1,p3][1, p_3] log2p311\lceil \log_2 p_3 \rceil \le 11
p2p_2 [p1,p31][p_1, p_3 - 1] log2(p3p1)11\lceil \log_2 (p_3 - p_1) \rceil \le 11

合计 33\le 33正好卡到题目限制。Easy version 给 50 次相对宽松,Hard 这 33 次每段都不能浪费一次询问——所以三个 bit-trick 必须用 continue 越界保护省下不必要的查询。

AC 代码

AC 提交链接

心路历程(WA → MLE → AC,被 D2 教做人的 4 个小时)

这道题头一次过 D1(Easy 50 次询问)那版还挺顺,三段二分各用 17 次差不多卡进去就完了。到 D2 把预算压到 33 次才开始遭罪——核心不是"次数压紧"这件事,而是 p2p_2 那段我第一版完全写歪了思路,后面几次提交几乎都是在为最初这个错误的设计买单。

❌ 第一版:用 !res 当谓词,谓词根本不单调

第一版的想法是「query [1..k]{p1,p3}[1..k] \setminus \{p_1, p_3\} ,看 res 是不是 0」。直觉是「subset 里所有值都成对了 = res = 0 = 还没碰到 p2p_2 」,然后 bit-trick 找最大的 res = 0 的 kk

第一次提交:WA on test 1。看了一会才发现一个特别傻的低级错误

1
2
plus(pos2, 1);   // 这行返回值被丢了!
cout << "! " << pos1 << " " << pos2 << " " << pos3 << endl;

plus 是个按值收 pos2 的纯函数返回新值,我没赋值回来。pos2 全程是 0,输出 ! 1 0 3 直接非法。改成 pos2 = plus(pos2, 1)

❌ 修了 line 179 后才发现:!res 这个谓词本身就不单调

修完那个赋值,跑反例 a=[1,2,3,2,1,3,1]a = [1, 2, 3, 2, 1, 3, 1]T=1T=1p2=5p_2 = 5 )发现:

kk subset 值 res
0 \varnothing 0
2 {2}\{2\} 1
3 {2,3}\{2, 3\} 2
4 {2,3,1}\{2, 3, 1\} 3
5 {2,3,1,1}\{2, 3, 1, 1\} 2
6 {2,3,1,1,2}\{2, 3, 1, 1, 2\} 1

res = 0 只在 k=0k = 0 出现一次,bit-trick 完全收敛不到 p21p_2 - 1res = 0 要求 (a) p2>kp_2 > k 且 (b) 所有 doubles 在 [1..k][1..k] 里都凑成对——条件 (b) 完全取决于数组排列,不单调,bit-trick 没救。

✅ 重新设计:让 TT 的 count 落在贡献奇偶分界点 232 \to 3

被这个反例打醒以后回去想,关键是奇偶性技巧。每个值对 lenres\text{len} - \text{res} 的贡献只看 count,count = 3 那档唯一贡献奇数。所以谓词应该让 TT 的 count 在 kk 跨过 p2p_2正好从偶(=2)跳到奇(=3)

解法:强制把 p1p_1 p3p_3 塞进询问当 baseline,让 TT count 起步 = 2;可变前缀 [p1+1..k][p_1+1..k] 决定 p2p_2 进不进来。doubles 全偶贡献,自动失声。

这一刻才意识到为什么 Easy 给 50 次而 Hard 卡 33 次——Hard 是逼你想出贡献奇偶表那张图,否则你写一段「 res==0\text{res} == 0 」走 32 次询问的解法在 Easy 能水过去(其实也水不过去因为不收敛 ),但 Hard 必须靠奇偶性拿满 11 + 11 + 11。

❌ 第二版:query 函数签名想得不干净

写了个 query(pos1, to, pos1, pos3) 4-arg 重载,意思是 [pos1..to] ∪ {pos1, pos3}。Claude 提醒 pos1 重复传了(既是区间端点又是 extra),但实测因为内部用 set<ll> 去重,功能上没坏。算是一个"看起来丑但实际能跑"的设计——code review 视角不优雅,但提交跑下来对的,就先不动它了。

❌ 第三版:to 没卡上界 → MLE on test 2

写完 p2p_2 那段提交:MLE on test 2,262100 KB / 256 MB。看了一会想不通:算法里 vector<ll> 都是局部变量,每次 query 完就析构,per-query 内存几 KB,单次 Solve \le 100 KB,怎么会到 256 MB?

最后定位到 line 173 没卡 to < pos3

1
2
3
4
5
6
for (int i = __lg(pos3 - pos1); i >= 0; --i) {
ll to = pos2 + (1ll << i);
// 没有越界保护!
auto [len, res] = query(pos1, to, pos1, pos3);
...
}

bit-trick 推进时,trial 值 to 可以超过 pos3 甚至 N = 2n+1——比如 p32000p_3 \approx 2000 , p2,actual1500p_{2,\text{actual}} \approx 1500 , p1=1p_1 = 1 这种局,几次 take 之后 pos2 ~ 1985,下一轮 i=5i=5 算出 to = 2017 > N,把 2002,...,20172002, ..., 2017 这种不存在的位置全发给 judge。Judge 返 -1,但代码不退出继续读流,stream 关掉以后所有 cin 都 fail(C++11+ 落 res = 0),predicate 退化成"按 k 奇偶决定 take",状态彻底污染。

Per problem statement: After receiving such a response, your program should immediately terminate.

我没退出,导致后面跑成什么样都不可控——MLE 大概率是 stdout pipe 被 judge 关掉以后 iostream 缓冲区累积,或者多 test cases 在烂状态下连续跑出来的 cascade。根因是单次 invalid query 没立刻退出

修法:

1
2
ll to = pos2 + (1ll << i);
if (to >= pos3) continue; // ← 补这一行

顺手把 p3p_3if (k > N) continue;p1p_1if (k < 1) continue; 也补了——这俩同样能越界,只是没造成 MLE。

❌ 第四版:p_1 段 bit-trick 写错变量

补完三处 continue 还过不了。再仔细看 p1p_1 段:

1
2
3
4
5
for (int i = __lg(pos3); i >= 0; --i) {
ll k = pos3 - (1ll << i); // ← 用了 pos3 不是 pos1!
...
if (...) pos1 = k;
}

bit-trick 降序的标准套路是每轮用当前的 pos1****(已经被前几轮压低过)减 2i2^i 当 trial 值,让 pos1 单调下降收敛到 p1,actual+1p_{1,\text{actual}} + 1 。我写成 pos3 - (1ll << i)p3p_3 整个循环不变,每轮 trial 都是固定的 p32ip_3 - 2^i 那 11 个偏移量——根本不是 bit-trick,最后 pos1 等于最后一次 predicate 真的那个 kk ,几乎一定是 p31p_3 - 1 (除非 p1,actual=p31p_{1,\text{actual}} = p_3 - 1 )。

为什么 sample 蒙过了:sample 是 p1=1,p3=3p_1 = 1, p_3 = 3 ,循环只 2 轮,恰好 i=0i=0k=p31=2k = p_3 - 1 = 2 就是 p1,actual+1p_{1,\text{actual}} + 1pos1 = 2pos1-- = 1。完美对的——但完全是巧合。

改成 ll k = pos1 - (1ll << i);,提交:AC。提交号 374001488

总结一下踩过的坑

  1. lambda 返回值丢了——低级 typo 但确实写过,下次 IDE warning 多看一眼

  2. !res 不单调——奇偶谓词 vs 等于零谓词的认知差。等于零是偶发性的、依赖排列;奇偶性才是结构性的,doubles 自动失声

  3. bit-trick trial 值没卡上界——3 个段都要保护:k > N / k < 1 / to >= p_3。MLE 不是因为单次内存,是因为没有遵守 read -1 → exit 的协议,状态污染后续的 cascade 才把内存搞炸的

  4. bit-trick 用了静态变量代替累积变量——pos3 - 2^i vs pos1 - 2^i,sample 巧合下还能过,靠肉眼看完全看不出来,必须自己手算几个反例

  5. Easy → Hard 不是单纯次数压紧——往往 Hard 在逼你换更结构化的 idea。这道题 Hard 的 33 次是直接告诉你"每段必须 log2N\lceil \log_2 N \rceil 不能浪费",反推奇偶谓词的设计

附件

📄 p2 奇偶谓词的设计思路(独立讲解 PDF,含贡献奇偶表 + 反例数组完整 trace)

D2_Unique_Values_Hard_version.cpp.txt

📄 p2 奇偶谓词的设计思路(独立讲解 PDF,含贡献奇偶表 + 反例数组完整 trace)

D2_Unique_Values_Hard_version.cpp.txt

杭电 2026 春季联赛 7 第 1 题(HDU 1001)。原题:https://acm.hdu.edu.cn/contest/problem?cid=1203&pid=1001

题目大意

无向连通图 nnmm 边,每条边有正权 ww 表示走这条边耗费的时间。每个点 ii 上有一瓶魔力药水 aia_i ,喝一瓶可以让魔力值 ±ai\pm a_i (任意次、不耗时间)。从 11 号点出发、初始魔力 00 ,目标让魔力值恰好等于 VV ,求最少总移动时间;不可达输出 -1

约束 1n,m1041 \le n, m \le 10^41V,ai,w1091 \le V, a_i, w \le 10^9 。题面保证 a1a_1 的所有质因数都在 VV 的质因数集合中(注意这不蕴含 a1Va_1 \mid V )。

样例

1
2
3
4
5
1
3 2 18
2 3 4
1 2 10
2 3 20

输出 0V=18=9a1V = 18 = 9 \cdot a_1 ,起点直接喝 9 次正向药水就行,不用动。

思路讲解

一句话

走过的点的 aia_i 通过裴蜀定理可以拼出 gcd\gcd 的所有倍数;问题等价于找一条从 11 出发的路径,让经过点的 gcd\gcd 整除 VV ,最小化总边权gcd\gcd 状态只有 O(logV)O(\log V) 种,分层图 Dijkstra 直接跑。

裴蜀定理转化

走过点集 SS 时,每个点 iSi \in S 都可以喝若干次正向 / 反向药水。最终魔力值是

iSkiai,kiZ.\sum_{i \in S} k_i \cdot a_i, \quad k_i \in \mathbb{Z}.

也就是关于 {ai}iS\{a_i\}_{i \in S} 的整数线性组合

🧮 Note: 裴蜀定理(Bézout){ai}\{a_i\} 的整数线性组合恰好覆盖 gcd(ai:iS)\gcd(a_i : i \in S) 的全部整数倍。

所以走过点集 SS 的可达魔力值集合 == gcd(ai:iS)\gcd(a_i : i \in S) 的全体整数倍。目标 VV 可达 等价于 gcd(S)V\gcd(S) \mid V

殊途同归一句:经过一个点不强制喝它的药水,但跳过 == 把它从计入 gcd 的集合里去掉,gcd 只会变大、整除关系只会更难满足。所以最优策略就是把走过的点全喝,状态干脆只追"目前路径上点的 gcd"一个量。

gcd 的状态数 == O(logV)O(\log V)

  • 起点 gcd =a1= a_1

  • 每加入一个新点 vv , gcd 变成 gcd(g,av)\gcd(g, a_v) :要么不变( ava_vgg 的倍数),要么至少缩小一半(变成 gg 的真因子)

a1a_1 一路降到 11 最多 log2a1=O(logV)\log_2 a_1 = O(\log V) 步。出现过的 gcd 全是 a1a_1 的因数,每个节点最多 O(logV)O(\log V) 种 gcd 状态

关键不变量:状态空间上界 nO(logV)10430=3×105n \cdot O(\log V) \approx 10^4 \cdot 30 = 3 \times 10^5 ,分层图 Dijkstra 完全跑得动。

分层图 Dijkstra

状态 (u,g)(u, g) :当前在节点 uu 、当前路径上点的 gcd 是 gg

转移:从 (u,g)(u, g) 沿边 (u,v,w)(u, v, w)(v,gcd(g,av))(v, \gcd(g, a_v)) , dist 加 ww

起点 (1,a1)(1, a_1) , dist =0= 0

答案:扫所有可达状态 (u,g)(u, g) ,其中满足 Vmodg=0V \bmod g = 0 的最小 dist 即为答案。

边权全正、状态空间有限,标准 Dijkstra(pq + lazy stale check)就够。

实现要点

  • vector<map<ll, ll>> mp(n+2) 存每个节点的 gcd → dist

  • pq pop 出来 必须 先判 step > mp[u][gcd] 跳过 stale 态——否则同一个状态反复被 push 出来展开邻居,状态量爆炸 TLE

  • 起点先特判 V % a_1 == 0 直接输出 0

  • 找到 ans 后加剪枝 step + w >= ans continue

  • ans 永远是 LLONG_MAX 时输出 -1 ——题面保证 a1a_1 的质因数 V\subseteq V 的质因数,但蕴含 a1Va_1 \mid V ,反例 a1=4,V=18a_1 = 4, V = 18 下若所有 ai{4,8,12,}a_i \in \{4, 8, 12, \dots\} 永远凑不出 1818 ,必须返回 -1

AC 代码

AC 提交链接

心路历程(TLE → WA → AC)

第一版 TLE

priority_queue + map<ll, map<ll, ll>> 直接交,TLE 。原因:

  • pop 出来没做 stale check ,直接处理邻居 → 同一个 (u, g) 状态被反复 push、pop、展开邻居,pq 状态量指数膨胀

  • map<ll, map<ll, ll>> 嵌套红黑树,每次 contains / operator[] 都是 O(log²) ,常数惨

修法:

  1. pop 之后立刻 if (step > mp[u][gcd]) continue;

  2. 外层换成 vector<map<ll, ll>> (节点下标天然是 vector 索引)

  3. 顺手加 step + w >= ans 早剪枝

第二版 WA

修完 TLE 跑通样例,交上去 WA 。原因:

  • ll ans = LLONG_MAX ,不可达时没转 -1 直接打印了 9223372036854775807

  • 题面要求不可达输出 -1

修法:

1
cout << (ans == LLONG_MAX ? -1 : ans) << "\n";

题面"保证 a1a_1 的质因数 V\subseteq V 的质因数"乍看像在保证可达,其实蕴含 a1Va_1 \mid V :例如 a1=4,V=18a_1 = 4, V = 18{2}{2,3}\{2\} \subseteq \{2, 3\} 满足前提,但 4184 \nmid 18 。如果整张图所有 aia_i 都是 44 的倍数(gcd 永远是 44 的倍数)就永远凑不出 1818 ,必须返回 -1

加上这一行后 AC 。

附件

(暂无)

题目大意

CCPC 2023 Guangdong Provincial M - Computational Geometry(CF Gym 104369 镜像)。

给定逆时针顺序凸 NN 边形顶点 A0,A1,,AN1A_0, A_1, \ldots, A_{N-1} ,选两个不相邻的顶点 Ai1,Aj1A_{i_1}, A_{j_1} 把多边形切成两段 sub-polygon QQRR ,最小化

diam(Q)2+diam(R)2\mathrm{diam}(Q)^2 + \mathrm{diam}(R)^2

数据范围 N5000N \le 5000 ,多组数据。

样例

  • N=4N = 4{(1,0),(2,0),(1,1),(0,0)}\{(1,0), (2,0), (1,1), (0,0)\} ,期望 4\mathbf{4} :切 (0,2)(0, 2) 把菱形切成两个三角形,每半 d2=2d^2 = 2

  • N=6N = 6 ,spike-3 hull,期望 44\mathbf{44} :切 (2,4)(2, 4)QQ 直径 2=10^2 = 10RR 直径 2=34^2 = 34

思路讲解

普通 RC 求凸多边形直径为什么对

直观理解:拿一把卡尺(两条平行 supporting line)夹住凸多边形旋转一圈,最大间距就是直径。算法上对应「外循环 r 跑遍每条边、内层 l 跟随推进」——每条边都被卡过一遍,所以直径必出现在某次「边-反极顶点」的 candidate 里,必被 collect。

本题改造为什么有问题——边覆盖不全

子弧 + 闭合 chord 是一个闭子多边形 RR' ,对它跑经典 RC 是对的——但前提是把 RR'所有边都卡过。

我的改造里 edgeii 起步靠 need_edge_move 单调向前推进,永远停留在原多边形的相邻顶点对 (Ae,Ae+1)(A_e, A_{e+1}) 上。但 RR' 的边集 = 子弧上的原边 + 一条非相邻的 chord (Aj,Ai)(A_j, A_i) ——chord 是非相邻顶点对,edge 数值永远不会取到它。

卡尺没把 RR' 的所有边卡过一遍,自然漏直径。

区间 DP 正解

状态

dp[i][j]\mathrm{dp}[i][j] = 以子弧 AiAi+1AjA_i \to A_{i+1} \to \cdots \to A_j (CCW 沿原多边形顺序,含两端)为顶点集的闭子多边形的直径² ——即该顶点集上所有点对的 ApAq2\|A_p - A_q\|^2 最大值。

递推

dp[i][j]=max(dp[i+1][j], dp[i][j1], AiAj2)\mathrm{dp}[i][j] = \max\big(\,\mathrm{dp}[i{+}1][j],\ \mathrm{dp}[i][j{-}1],\ \|A_i - A_j\|^2\,\big)

正确性:把顶点集 {i,i+1,,j}\{i, i{+}1, \ldots, j\} 上所有无序点对 (p,q)(p, q) 按是否含两端点 i,ji, j 分三类:

  • 不含 ii :点对 {i+1,,j}\subseteq \{i{+}1, \ldots, j\} ,覆盖在 dp[i+1][j]\mathrm{dp}[i{+}1][j]

  • 不含 jj :点对 {i,,j1}\subseteq \{i, \ldots, j{-}1\} ,覆盖在 dp[i][j1]\mathrm{dp}[i][j{-}1]

  • 两端都含:唯一点对 (i,j)(i, j) ,单独的 AiAj2\|A_i - A_j\|^2

三类不重不漏、取 max 即全集直径²。

为什么 DP 走得通、RC 走不通

关键对比:这个递推式完全不依赖凸性——它就是个「枚举所有点对取 max」的 O(N2)O(N^2) 朴素 DP。「凸」在 RC 路线里是刚需(卡尺单峰让对蹵点单调推进 → O(N)O(N) );DP 路线不贪「凸」这点便宜、只用上「顶点集是连续子弧」这个事实。一句话:RC 吃结构红利换 O(N)O(N) ,DP 不吃红利 O(N2)O(N^2) 但保命

循环顺序是硬约束

跟一般区间 DP 一样:len 在外、 ii 在内。奇心把 ii 写在外会踩心路历程 7 那个坑( 2N2N pass 兜底也救不回)。

切法枚举

枚举不相邻顶点对 (i1,j1)(i_1, j_1) 作为 chord:

ans=minj1i1+2, j1≢i1±1(modN)(dp[i1][j1]+dp[j1][i1])\mathrm{ans} = \min_{j_1 \ge i_1 + 2,\ j_1 \not\equiv i_1 \pm 1 \pmod N} \big(\,\mathrm{dp}[i_1][j_1] + \mathrm{dp}[j_1][i_1]\,\big)

两项分别是切后两段子弧的直径²——dp 表索引是循环的, dp[j1][i1]\mathrm{dp}[j_1][i_1] 自动覆盖另一段。cross 检查 0\ne 0 顺手筛掉两类退化: wrap-adjacent切(如 (0,N1)(0, N-1) 在循环视角下其实是相邻点),与共线退化切(chord 踩在原多边形某条边上)。

复杂度

填 dp 表 O(N2)O(N^2) 个状态 × O(1)O(1) 转移、枚举切法 O(N2)O(N^2) 、总 O(N2)O(N^2)N5000N \le 5000 下填表 2.5×1072.5 \times 10^7 次贴近 TL,多组数据下实测 3312 ms / 6s 限制。

AC 代码

AC 提交:codeforces.com/gym/104369/submission/373844988

切到 O(N2)O(N^2) 区间 DP 后 AC(3312 ms / 192600 KB, N5000N \le 5000 多组数据贴近 TL)。

心路历程

  1. 直觉把「凸多边形直径 → 旋转卡壳」和「切法枚举」硬嫁接,写出 vanilla RC + 子弧增长版

  2. sample 1 ( n=4n=4 ) 期望 44 实测 33 ,开始 debug

  3. 第一发现: dp[2][0]=1\mathrm{dp}[2][0] = 1 漏了 (A2,A3)(A_2, A_3) 这对距离——up 起步 i+2i+2 ,arc 起点附近的 pair 永远不进 candidate

  4. 第二发现:dp[edge][up] = ans 索引用动态 edge 而非外层切起点 iouteri_\mathrm{outer} ,dp 矩阵大量被错位写入(附带 bug)

  5. spike-3 对拍反例(差 7.45%7.45\% )让「算法是结构性错的、不是 off-by-one」心服口服

  6. 抓到根因:RC 之所以对,因为它每条边都卡过 + 每个顶点都反极过;改造版两条都破了

  7. DP 重写又踩循环顺序坑——RC 死了之后转 O(N2)O(N^2) 区间 DP( dp[i][j]\mathrm{dp}[i][j] = 子弧 AiAjA_i \to A_j 凸子多边形直径² ),递推 dp[i][j]=max(dp[i+1][j], dp[i][j1], AiAj2)\mathrm{dp}[i][j] = \max(\mathrm{dp}[i+1][j],\ \mathrm{dp}[i][j-1],\ \|A_i - A_j\|^2) 。第一发先把 ii 写在外、 len\mathrm{len} 写在内,再跑 2N2N 遍当兜底——

  • 当前算 dp[i][j]\mathrm{dp}[i][j] 需要 dp[i+1][j]\mathrm{dp}[i+1][j] (len 1-1起点变了)。 ii 外 len 内的顺序下,处理 i=0i=0dp[1][]\mathrm{dp}[1][\cdot] 整行都还是 00 ,根本没轮到 i=1i=1 那层
  • 2N2N pass 兜底也救不回来:pass 1 全程用 00 推导出脏值;pass 2 处理 i=0, len=3i=0,\ \mathrm{len}=3 时要 dp[1][3]\mathrm{dp}[1][3] ,可这个值是 pass 1 用 dp[2][3]=0\mathrm{dp}[2][3]=0 算出来的脏值——pass 2 来不及修
  • 正确写法:len 外、 ii ,一遍即可

心法:「len 小的之前都被计算了」这个直觉成立的前提是 len 在外ii 在外的话,给定 ii 你确实按 len 升序填了 dp[i][]\mathrm{dp}[i][\cdot] ,但同 len 不同起点的 dp[i+1][]\mathrm{dp}[i+1][\cdot] 还压根没碰过——「 2N2N pass 兜底」是错觉:每跑一遍正确性最多渗透一层 len,最坏要 NN 遍且仍受脏值污染。

附件

详细 debug 记录与图示见下方嵌入 PDF —— rc_breakdown.pdf ,5 页,含凸多边形 + 反极顶点单峰示意 / 闭子多边形 RR' / forbidden up + chord ×\times 标记 / dp[3][1]\mathrm{dp}[3][1] trace 表。

rc_breakdown.pdf — 5 页 debug 记录:经典 RC 为什么对(凸多边形 + 反极顶点 + 距离单峰)/ 闭子多边形 R’ / forbidden up + chord ✗ 标记 / dp[3][1] trace 表