🤖 Claude 苏格拉底式带教对话录原稿。每一步都是 mentor 提一个聚焦问题 → me 答 → 反复推进至完整算法。蓝色 callout 是 Claude,橙色 callout 是 zzy,原稿里 mentor 的反例 / 矫枉过正 / 用户吐槽全部逐字保留。
原题:洛谷 P3187 [HNOI2007] 最小矩形覆盖 (紫题)](https://www.luogu.com.cn/problem/P3187 )(紫题))
📝 大对话录正文 + AC 代码 + 附件随后通过 raw API patch 进来。
🤖 Claude 苏格拉底式带教对话录原稿。每一步都是 mentor 提一个聚焦问题 → me 答 → 反复推进至完整算法。蓝色 callout 是 Claude,橙色 callout 是 zzy,原稿里 mentor 的反例 / 矫枉过正 / 用户吐槽全部逐字保留。
原题:洛谷 P3187 [HNOI2007] 最小矩形覆盖 (紫题)
背景
题目链接:洛谷 P3187 [HNOI2007] 最小矩形覆盖 。
题面要点 :给定平面上 n n n 个点,求一个 覆盖所有点的最小面积矩形 (矩形可以任意旋转,不要求轴对齐),输出面积和按逆时针的四个顶点。约束 3 ≤ n ≤ 5 × 1 0 4 3 \le n \le 5\times 10^4 3 ≤ n ≤ 5 × 1 0 4 ,坐标 ∈ [ 0 , 10 ] \in [0,10] ∈ [ 0 , 1 0 ] ,最小矩形面积至少 0.1 0.1 0 . 1 ,误差容忍 1 0 − 4 10^{-4} 1 0 − 4 。
下面按对话方式逐步推进。蓝色 标注首次出现的关键术语 / 符号。
第 1 步:理解题目
🤖 Note :
Claude (Mentor)
先用样例热身。 n = 6 n=6 n = 6 个点
( 1 , 3 ) , ( 1 , 4 ) , ( 2 , 1 ) , ( 3 , 0 ) , ( 3 , 6 ) , ( 6 , 3 ) , (1,3),\ (1,4),\ (2,1),\ (3,0),\ (3,6),\ (6,3),
( 1 , 3 ) , ( 1 , 4 ) , ( 2 , 1 ) , ( 3 , 0 ) , ( 3 , 6 ) , ( 6 , 3 ) ,
标程给的最小矩形顶点是 ( 3 , 0 ) , ( 6 , 3 ) , ( 3 , 6 ) , ( 0 , 3 ) (3,0), (6,3), (3,6), (0,3) ( 3 , 0 ) , ( 6 , 3 ) , ( 3 , 6 ) , ( 0 , 3 ) ,面积 18 18 1 8 。
画一下图(或在脑里走一遍):这 4 个矩形顶点和原始 6 个点放在一起。原始 6 个点里,有几个落在矩形边界 上?分别在哪条边上? (不在边上的就是严格落在矩形内部)
这一步只是想让你看清楚"最小矩形"长什么样——为后面想算法做铺垫。
🧑 Note :
zzy (Me)
这个样例数据范围比较小,不过 O ( n ) O(n) O ( n ) 的算法应该可以做到。然后这个理解题目不是很重要,毕竟这个题目是一道模板题,描述非常简单。再说了,我也没法告诉你,我也不想画这个图,你自己把这个图画出来,用 LaTeX 画出来。
🤖 Note :
Claude (Mentor)
好——我把图补上,复杂度修正一下,然后跳到下一步。
样例可视化 :绿色填充多边形 = 输入 6 点的凸包 (CCW 顺序 ( 3 , 0 ) → ( 6 , 3 ) → ( 3 , 6 ) → ( 1 , 4 ) → ( 1 , 3 ) → ( 2 , 1 ) (3,0)\to(6,3)\to(3,6)\to(1,4)\to(1,3)\to(2,1) ( 3 , 0 ) → ( 6 , 3 ) → ( 3 , 6 ) → ( 1 , 4 ) → ( 1 , 3 ) → ( 2 , 1 ) ,6 边形);蓝色 outline = 标程给的最小覆盖矩形;蓝环 = 矩形 4 顶点;红实心点 = 输入 6 点。
\begin{center}
\begin{tikzpicture}[scale=0.78]
\draw[help lines, gray!25] (-0.5,-0.5) grid (6.5,6.5);
\draw[->, gray!60] (-0.7,0)–(7.0,0) node[right, black]{\scriptsize x x x };
\draw[->, gray!60] (0,-0.7)–(0,7.0) node[above, black]{\scriptsize y y y };
\foreach \i in {1,2,3,4,5,6} {
\draw[gray!60] (\i,0.05)–(\i,-0.05) node[below=1pt, black]{\tiny \i};
\draw[gray!60] (0.05,\i)–(-0.05,\i) node[left=1pt, black]{\tiny \i};
}
\fill[green!30, opacity=0.55]
(3,0) – (6,3) – (3,6) – (1,4) – (1,3) – (2,1) – cycle;
\draw[green!50!black, very thick]
(3,0) – (6,3) – (3,6) – (1,4) – (1,3) – (2,1) – cycle;
\draw[blue!70!black, very thick] (3,0) – (6,3) – (3,6) – (0,3) – cycle;
\foreach \pt in {(3,0), (6,3), (3,6), (0,3)} {
\draw[blue!70!black, very thick] \pt circle (5pt);
}
\foreach \pt in {(1,3), (1,4), (2,1), (3,0), (3,6), (6,3)} {
\fill[red!85!black] \pt circle (2.6pt);
}
\node[blue!50!black, font=\tiny, anchor=north] at (3.0, -0.45) { ( 3 , 0 ) (3,0) ( 3 , 0 ) };
\node[blue!50!black, font=\tiny, anchor=west] at (6.30, 3.0) { ( 6 , 3 ) (6,3) ( 6 , 3 ) };
\node[blue!50!black, font=\tiny, anchor=south] at (3.0, 6.35) { ( 3 , 6 ) (3,6) ( 3 , 6 ) };
\node[blue!50!black, font=\tiny, anchor=east] at (-0.30, 3.0) { ( 0 , 3 ) (0,3) ( 0 , 3 ) };
\node[red!70!black, font=\tiny, anchor=south] at (1.0, 4.20) { ( 1 , 4 ) (1,4) ( 1 , 4 ) };
\node[red!70!black, font=\tiny, anchor=north] at (1.0, 2.78) { ( 1 , 3 ) (1,3) ( 1 , 3 ) };
\node[red!70!black, font=\tiny, anchor=west] at (2.20, 1.0) { ( 2 , 1 ) (2,1) ( 2 , 1 ) };
\node[blue!55!black, font=\scriptsize] at (4.4, 5.0) {最小矩形 };
\node[green!40!black, font=\scriptsize] at (2.5, 2.5) {凸包 };
\end{tikzpicture}
\end{center}
对照 :6 输入点全部在凸包上(凸包就是这 6 个点)。其中 3 个 ( 3 , 0 ) , ( 3 , 6 ) , ( 6 , 3 ) (3,0),(3,6),(6,3) ( 3 , 0 ) , ( 3 , 6 ) , ( 6 , 3 ) 同时也是矩形顶点; ( 1 , 4 ) (1,4) ( 1 , 4 ) 落在矩形顶边 ( 3 , 6 ) − ( 0 , 3 ) (3,6)\!-\!(0,3) ( 3 , 6 ) − ( 0 , 3 ) 上; ( 2 , 1 ) (2,1) ( 2 , 1 ) 落在左边 ( 0 , 3 ) − ( 3 , 0 ) (0,3)\!-\!(3,0) ( 0 , 3 ) − ( 3 , 0 ) 上; ( 1 , 3 ) (1,3) ( 1 , 3 ) 是凸包顶点但严格在矩形内部 。凸包和矩形的偏差区 = 矩形里没被凸包覆盖到的两块小三角,分别贴在 ( 0 , 3 ) (0,3) ( 0 , 3 ) 那个"空角"两侧(绿色和蓝色 outline 不重合的部分肉眼可见)。
复杂度修正 : n ≤ 5 × 1 0 4 n \le 5\times 10^4 n ≤ 5 × 1 0 4 ,本题 O ( n ) O(n) O ( n ) 严格地说做不到——求凸包要 sort 是 O ( n log n ) O(n\log n) O ( n log n ) 。凸包给定后 的旋转卡壳那部分确实是 O ( h ) O(h) O ( h ) 。所以全流程 O ( n log n ) \boldsymbol{O(n\log n)} O ( n l o g n ) 。这里 h h h 表示凸包顶点数。
第 2 步:核心引理 + 暴力骨架
🤖 Note :
Claude (Mentor)
跳到算法骨架。模板的根本依赖是把矩形 方向 (连续的 [ 0 , π / 2 ) [0, \pi/2) [ 0 , π / 2 ) 角度)离散化到有限多个候选——这一步靠的是一条几何引理。
我先把它抛出来给你确认:
核心引理 :覆盖凸包的最小面积矩形,至少有一条边与凸包的某条边共线。
先问一个问题 :接受这条引理后,你能直接说出一个暴力算法 吗——枚举什么、对每个枚举对象做什么、整体复杂度多少?
🧑 Note :
zzy (Me)
不是啊,哪有你这么讲题,让我先接受一个引理啊,这个引理怎么来的?我接受了吗?你代替我接受了是吧?我们也不用严格的证明对吧?但是你不能直接跳过这个引理是怎么来的吧,你这也太搞笑了吧。
🧑 Note :
zzy (Me)
不是啊,你就算这个证明,也需要引导我一次性写出太多东西也是禁止行为。任何回答都必须有引导性,不能一次性吐出太多内容。一次性吐出太多内容是绝对禁止的,懂吗。
🤖 Note :
Claude (Mentor)
懂了,回单步引导。
先做一个具体观察,不抛任何理论 。在你刚才那张样例图上数:
最小矩形的 4 条边 ,每条边上各自经过几个凸包顶点 ?(提醒:这个样例的凸包顶点 = 全部 6 个输入点——之前在图下方写过——所以你只需要数 6 个红点分别落在哪条矩形边/角上。)
🧑 Note :
zzy (Me)
我们也都知道,我们需要解决的问题是什么,就是我们想要证明的就是至少有一条边是和凸包的边是平行的,或者说其实是重合的吧,其实是重合的。就是我们的这个最小覆盖矩形…
🤖 Note :
Claude (Mentor)
对,重合 (collinear / 同一条直线上)比"平行"强——平行只锁方向,重合还锁位置。这就是引理要证的精确陈述。
回到我刚问那个图的问题,因为它直通证明:在图上数,最小矩形的 4 条边,每条边各自经过几个 凸包顶点?
🧑 Note :
zzy (Me)
每条边至少都经过了至少两个红点对,每条边都经过了两个红点。
🤖 Note :
Claude (Mentor)
对,每条边恰好 2 个 凸包顶点——这是个很硬的结构信号。
下一个微观察:刚才数出来的"矩形某条边上的 2 个凸包顶点",比如最小矩形底边 上的 ( 3 , 0 ) (3,0) ( 3 , 0 ) 和 ( 6 , 3 ) (6,3) ( 6 , 3 ) ——它们在凸包上是相邻的吗 ?也就是说,凸包的顶点序列里,它们俩之间是不是直接由一条凸包边连接、中间没有插入别的凸包顶点?
🧑 Note :
zzy (Me)
对,当然是相邻的,当然是相邻的。
🤖 Note :
Claude (Mentor)
好。底边 ( 3 , 0 ) − ( 6 , 3 ) (3,0)\!-\!(6,3) ( 3 , 0 ) − ( 6 , 3 ) 上的 2 个凸包顶点相邻 ⇒ \Rightarrow ⇒ 底边完整包含凸包边 ( 3 , 0 ) → ( 6 , 3 ) (3,0)\!\to\!(6,3) ( 3 , 0 ) → ( 6 , 3 ) ⇒ \Rightarrow ⇒ 底边和该凸包边重合 (共线)。引理在这个样例里被超额 满足——4 条边全部重合于某条凸包边。
切换到一般情形思考 (这个样例太特殊)。把凸包想成一个一般、不对称的凸多边形,外接最小覆盖矩形。
反证 setup 一步:假设 最小覆盖矩形没有任何一条边 和凸包某条边重合。那么矩形每条边 和凸包的接触应该是几个点 ?
🧑 Note :
zzy (Me)
如果假设最小覆盖矩形没有任何一条边和凸包某条边重合,那么矩形每条边和凸包的接触点就只有一个点嘛,每条边和凸包的接触点最多只有一个点。
🤖 Note :
Claude (Mentor)
对——准确说是恰好 1 个点 :
" ≤ 1 \le 1 ≤ 1 "是反证假设 直接给的(不重合 ⇒ \Rightarrow ⇒ 不会经过 2 个相邻凸包顶点;非相邻 2 个顶点更不可能在同一条支撑切线上)。
" ≥ 1 \ge 1 ≥ 1 “是因为矩形必须紧贴 凸包:如果某条边一个凸包顶点都不接触,那条边就能往凸包内推一点,矩形面积变小——它就不是最小的了 ,矛盾。
所以 4 条矩形边各自接触凸包恰好 1 个顶点,命名为 支撑顶点 L , R , T , B L, R, T, B L , R , T , B (左/右/上/下边对应)。
下一步问题 :在 L , R , T , B L, R, T, B L , R , T , B 这 4 个支撑顶点固定不变 的前提下,矩形"覆盖凸包、 L , R , T , B L,R,T,B L , R , T , B 各自落在对应矩形边上”——还剩几个自由度 ?也就是说,可以怎样扰动 这个矩形让它仍然满足这两个条件?
🧑 Note :
zzy (Me)
LRTB 各自落在对应矩形边上,此时还剩几个自由度。嗯,什么叫几个自由度?不太懂你意思,这几个自由度是什么意思啊?不太懂。
🤖 Note :
Claude (Mentor)
好,“自由度"说人话——
自由度 = 保持约束(这里=” L , R , T , B L,R,T,B L , R , T , B 各自落在对应矩形边上")的前提下,矩形还能怎么动 。能独立动几个方向就是几个自由度。
举例:
一个无约束 的矩形在平面上有 5 个自由度——中心 2D(2)+ 旋转角(1)+ 宽(1)+ 高(1)。
加约束"中心必须在原点" ⇒ \Rightarrow ⇒ 砍掉 2 个 ⇒ \Rightarrow ⇒ 剩 3 个自由度。每加 1 个独立约束就砍 1 个自由度。
重新具体问 :4 支撑顶点 L , R , T , B L, R, T, B L , R , T , B 固定。如果我把矩形逆时针旋转一个小角度 ϵ \epsilon ϵ ,同时重新调整 它的位置/宽/高(不局限于原参数),让 L , R , T , B L, R, T, B L , R , T , B 仍然分别落在新矩形对应的 4 条边上——这样的新矩形存在吗 ?(提示:每条新矩形边应该是过对应支撑点、方向沿着新旋转角的那条切线。)
🧑 Note :
zzy (Me)
应该是存在的,就是旋转一个小角度,肯定是可以的。
🤖 Note :
Claude (Mentor)
对——所以 4 支撑固定时,矩形只剩 1 个自由度 :旋转角 θ \theta θ 。每个 θ \theta θ 唯一确定一个矩形(4 条边各自就是过 L , R , T , B L,R,T,B L , R , T , B 沿对应方向的切线),矩形的宽、高、面积都成了 θ \theta θ 的(光滑)函数: width ( θ ) \text{width}(\theta) width ( θ ) 、 height ( θ ) \text{height}(\theta) height ( θ ) 、 A ( θ ) A(\theta) A ( θ ) 。当前矩形对应 θ = θ 0 \theta = \theta_0 θ = θ 0 。
关键问题 :反证假设 “当前矩形是最小的” ⇒ \Rightarrow ⇒ A ( θ ) A(\theta) A ( θ ) 在 θ 0 \theta_0 θ 0 处取局部极小 。这给一阶导数 A ′ ( θ 0 ) A'(\theta_0) A ′ ( θ 0 ) 一个什么必要条件 ?
🧑 Note :
zzy (Me)
那这个一阶导数 A ′ ( θ 0 ) A'(\theta_0) A ′ ( θ 0 ) 理论上应该等于 0 对吧,因为这个极值点嘛,极值点它肯定这个导数值为 0 嘛。
🤖 Note :
Claude (Mentor)
对, A ′ ( θ 0 ) = 0 A'(\theta_0) = 0 A ′ ( θ 0 ) = 0 是必要 条件。但单凭这个还推不出矛盾 —— A ′ = 0 A' = 0 A ′ = 0 既可以是极小、也可以是极大或鞍点。要把矛盾构造出来,得继续做二阶 或结构性 分析。
为了往下走,先把 A ( θ ) A(\theta) A ( θ ) 显式写出来。
记号 :
u = R − L \boldsymbol{u} = R - L u = R − L ("左右"支撑顶点之差,固定向量),
v = T − B \boldsymbol{v} = T - B v = T − B ("上下"支撑顶点之差,固定向量),
n ^ θ = ( cos θ , sin θ ) \hat{n}_\theta = (\cos\theta, \sin\theta) n ^ θ = ( cos θ , sin θ ) (矩形"水平"单位方向),
n ^ θ ⊥ = ( − sin θ , cos θ ) \hat{n}_\theta^{\perp} = (-\sin\theta, \cos\theta) n ^ θ ⊥ = ( − sin θ , cos θ ) (“垂直"方向,与 n ^ θ \hat{n}_\theta n ^ θ 正交)。
下一步问题 :在这个记号下,矩形的宽 width ( θ ) \text{width}(\theta) width ( θ ) 等于 什么内积 ?高 height ( θ ) \text{height}(\theta) height ( θ ) 等于什么内积 ?(提示:宽就是把所有凸包顶点投影到 n ^ θ \hat n_\theta n ^ θ 方向后的"最右"减"最左”——但因为左右支撑就是 L , R L, R L , R ,"最右"投影 = R ⋅ n ^ θ = R \cdot \hat n_\theta = R ⋅ n ^ θ 、“最左” = L ⋅ n ^ θ =L \cdot \hat n_\theta = L ⋅ n ^ θ 。)
🧑 Note :
zzy (Me)
建议你还是把图画出来比较好。你画一个图,我能更清楚地看到这些量。注意画图要检查啊,要检查。然后还有推荐绘图方式好吧。
🤖 Note :
Claude (Mentor)
好,画一张一般情形的示意图。
图设定 :
凸包:6 顶点不对称凸六边形,CCW H 1 = ( 3 , 3 ) , H 2 = ( 7 , 2 ) , H 3 = ( 9 , 5 ) , H 4 = ( 8 , 8 ) , H 5 = ( 5 , 9 ) , H 6 = ( 2 , 6 ) H_1\!=\!(3,3),H_2\!=\!(7,2),H_3\!=\!(9,5),H_4\!=\!(8,8),H_5\!=\!(5,9),H_6\!=\!(2,6) H 1 = ( 3 , 3 ) , H 2 = ( 7 , 2 ) , H 3 = ( 9 , 5 ) , H 4 = ( 8 , 8 ) , H 5 = ( 5 , 9 ) , H 6 = ( 2 , 6 ) 。
取 θ = 3 0 ∘ \theta = 30^\circ θ = 3 0 ∘ :算出 L = H 1 , R = H 4 , T = H 5 , B = H 2 L=H_1,\ R=H_4,\ T=H_5,\ B=H_2 L = H 1 , R = H 4 , T = H 5 , B = H 2 。选 3 0 ∘ 30^\circ 3 0 ∘ 而非更小的角度 是为了把旋转视觉上拉够大——任何 θ ∈ ( 18.4 3 ∘ , 71.5 7 ∘ ) \theta \in (18.43^\circ, 71.57^\circ) θ ∈ ( 1 8 . 4 3 ∘ , 7 1 . 5 7 ∘ ) 都属于同一段平滑段(4 支撑不变),但 θ \theta θ 接近段端点时矩形看起来会和凸包某条边伪 flush,误导读者以为"它们已经重合"; 3 0 ∘ 30^\circ 3 0 ∘ 离两端均够远。
矩形:通过 4 条支撑切线方程算出 4 顶点。
\begin{center}
\begin{tikzpicture}[
scale=0.62, font=\footnotesize,
spt/.style={circle, fill=red!85!black, inner sep=0pt, minimum size=5.5pt},
splbl/.style={red!75!black, font=\small\bfseries},
]
\coordinate (H1) at (3,3); \coordinate (H2) at (7,2); \coordinate (H3) at (9,5);
\coordinate (H4) at (8,8); \coordinate (H5) at (5,9); \coordinate (H6) at (2,6);
\coordinate (BL) at (4.43, 0.52); \coordinate (BR) at (10.35, 3.93);
\coordinate (TR) at (6.82, 10.05); \coordinate (TL) at (0.90, 6.63);
\draw[help lines, gray!18] (-0.5,-0.5) grid (11.0,11.0);
\fill[green!25, opacity=0.55]
(H1) – (H2) – (H3) – (H4) – (H5) – (H6) – cycle;
\draw[green!50!black, very thick]
(H1) – (H2) – (H3) – (H4) – (H5) – (H6) – cycle;
\draw[blue!65!black, very thick] (BL) – (BR) – (TR) – (TL) – cycle;
\draw[->, orange!85!black, line width=1.2pt] (H1) – (H4)
node[pos=0.30, sloped, above=2pt, orange!60!black, font=\small]{ u = R − L \bm u = R\!-\!L u = R − L };
\draw[->, violet!80!black, line width=1.2pt] (H2) – (H5)
node[pos=0.72, sloped, above=2pt, violet!55!black, font=\small]{ v = T − B \bm v = T\!-\!B v = T − B };
\node[spt, label={[splbl, label distance=-3pt]below left: L L L }] at (H1) {};
\node[spt, label={[splbl, label distance=-3pt]above right: R R R }] at (H4) {};
\node[spt, label={[splbl, label distance=-1pt]above: T T T }] at (H5) {};
\node[spt, label={[splbl, label distance=-1pt]below: B B B }] at (H2) {};
\coordinate (O) at (0.0, 0.4);
\draw[gray!45, thin] (O) – ++(1.7, 0);
\draw[->, blue!55!black, line width=1pt] (O) – ++(1.30, 0.75)
node[right=1pt, font=\scriptsize, blue!55!black]{ n ^ θ \hat n_\theta n ^ θ };
\draw[->, blue!55!black, line width=1pt] (O) – ++(-0.75, 1.30)
node[above=1pt, font=\scriptsize, blue!55!black]{ n ^ θ ⊥ \hat n_\theta^{\perp} n ^ θ ⊥ };
\draw[gray!60, thin] ( ( O ) + ( 0.9 , 0 ) (O)+(0.9,0) ( O ) + ( 0 . 9 , 0 ) ) arc (0:30:0.9);
\node[gray!45!black, font=\scriptsize] at ( ( O ) + ( 1.0 , 0.25 ) (O)+(1.0,0.25) ( O ) + ( 1 . 0 , 0 . 2 5 ) ) { θ \theta θ };
\end{tikzpicture}
\vspace{2pt}
{\footnotesize\sffamily\color{gray} 图注:以 θ = 3 0 ∘ \theta = 30^\circ θ = 3 0 ∘ 、不对称凸六边形为例。绿色多边形 = 凸包;蓝色矩形 = 旋转 θ \theta θ 后的覆盖矩形;红点 L , R , T , B L,R,T,B L , R , T , B = 4 支撑顶点(每条矩形边只接触凸包一个顶点 ,没有任何一条边与凸包的某条边重合——这正是反证 setup 假设的状态); u = R − L \bm u = R-L u = R − L (橙色箭头)和 v = T − B \bm v = T-B v = T − B (紫色箭头)是反证 setup 里两个固定向量;左下角"指南针" n ^ θ = ( cos θ , sin θ ) \hat n_\theta = (\cos\theta, \sin\theta) n ^ θ = ( cos θ , sin θ ) 、 n ^ θ ⊥ = ( − sin θ , cos θ ) \hat n_\theta^\perp = (-\sin\theta, \cos\theta) n ^ θ ⊥ = ( − sin θ , cos θ ) 。}
\end{center}
下一步问题 :在这张图上, width ( θ ) \text{width}(\theta) width ( θ ) 等于哪个内积 ? height ( θ ) \text{height}(\theta) height ( θ ) 等于哪个内积 ?(即把宽和高分别写成两个向量的点积形式。)
🧑 Note :
zzy (Me)
我在想一共就两个向量,而且点积是符合交换率的吧,一共就两个向量点积、而且符合交换率的话,这哪能凑得出来两个内积啊?而且我也有一点忘记这个内积的定义了。
🤖 Note :
Claude (Mentor)
点积: a ⋅ b = a 1 b 1 + a 2 b 2 = ∣ a ∣ ∣ b ∣ cos ∠ ( a , b ) \bm a \cdot \bm b = a_1 b_1 + a_2 b_2 = |\bm a|\,|\bm b|\,\cos\angle(\bm a, \bm b) a ⋅ b = a 1 b 1 + a 2 b 2 = ∣ a ∣ ∣ b ∣ cos ∠ ( a , b ) 。
向量数其实 4 个 ,不是 2 个:位移 u = R − L \bm u = R-L u = R − L 、 v = T − B \bm v = T-B v = T − B ,加上单位方向 n ^ θ = ( cos θ , sin θ ) \hat n_\theta = (\cos\theta, \sin\theta) n ^ θ = ( cos θ , sin θ ) 、 n ^ θ ⊥ = ( − sin θ , cos θ ) \hat n_\theta^\perp = (-\sin\theta, \cos\theta) n ^ θ ⊥ = ( − sin θ , cos θ ) 。"宽 / 高"各 = 一个位移 ⋅ \cdot ⋅ 一个方向 ,组合两对。
重问 : width ( θ ) \text{width}(\theta) width ( θ ) 配哪对? height ( θ ) \text{height}(\theta) height ( θ ) 配哪对?
🧑 Note :
zzy (Me)
我在想一个问题。我当然记得点积是指投影的长度,但记得这个投影的长度和 B B B 肯定有关系,不止和 A A A 有关系,和 B B B 也有关系。但你的 n ^ \hat n n ^ 是一个单位向量,但是你在阐述其几何意义的时候,怎么能够用单位向量呢?
或者我可以这么理解吗?我就把 B B B 视为一个单位向量,虽然这个 B B B 它可能长度不唯一,但是我可以把这个 B B B 视为一个单位向量。是这个意思吗?
🤖 Note :
Claude (Mentor)
你的"定义 ∣ B ∣ |B| ∣ B ∣ 为长度单位"是换单位制 的描述:以 ∣ B ∣ |B| ∣ B ∣ 为新单位时 ∣ B ∣ = 1 |B| = 1 ∣ B ∣ = 1 ,点积值 = 投影长度(按新单位测的数值);换回绝对单位再乘 ∣ B ∣ |B| ∣ B ∣ 。本质和"投影长度 × ∣ B ∣ \times |B| × ∣ B ∣ "是同一件事,描述不同。
原问题再问 : width ( θ ) \text{width}(\theta) width ( θ ) 配哪对? height ( θ ) \text{height}(\theta) height ( θ ) 配哪对?
🧑 Note :
zzy (Me)
u ⋅ n ^ θ \bm u \cdot \hat n_\theta u ⋅ n ^ θ 、 v ⋅ n ^ θ ⊥ \bm v \cdot \hat n_\theta^\perp v ⋅ n ^ θ ⊥ 。
🤖 Note :
Claude (Mentor)
对。所以
A ( θ ) = ( u ⋅ n ^ θ ) ( v ⋅ n ^ θ ⊥ ) . A(\theta) \;=\; (\bm u \cdot \hat n_\theta)(\bm v \cdot \hat n_\theta^\perp).
A ( θ ) = ( u ⋅ n ^ θ ) ( v ⋅ n ^ θ ⊥ ) .
这就是反证 setup 下 A A A 的解析式(4 支撑 L , R , T , B L,R,T,B L , R , T , B 固定 ⇒ \Rightarrow ⇒ u , v \bm u, \bm v u , v 固定;只有 θ \theta θ 在变)。
下一步 :把两个内积分量展开。设 u = ( u 1 , u 2 ) \bm u = (u_1, u_2) u = ( u 1 , u 2 ) 、 v = ( v 1 , v 2 ) \bm v = (v_1, v_2) v = ( v 1 , v 2 ) 。 u ⋅ n ^ θ \bm u \cdot \hat n_\theta u ⋅ n ^ θ 用 cos θ , sin θ \cos\theta, \sin\theta cos θ , sin θ 怎么写? v ⋅ n ^ θ ⊥ \bm v \cdot \hat n_\theta^\perp v ⋅ n ^ θ ⊥ 呢?
🧑 Note :
zzy (Me)
u ⋅ n ^ θ = u 1 cos θ + u 2 sin θ \bm u \cdot \hat n_\theta = u_1 \cos\theta + u_2 \sin\theta u ⋅ n ^ θ = u 1 cos θ + u 2 sin θ ; v \bm v v 同法展开 v ⋅ n ^ θ ⊥ = − v 1 sin θ + v 2 cos θ \bm v \cdot \hat n_\theta^\perp = -v_1 \sin\theta + v_2 \cos\theta v ⋅ n ^ θ ⊥ = − v 1 sin θ + v 2 cos θ 。
🤖 Note :
Claude (Mentor)
对,所以
A ( θ ) = ( u 1 cos θ + u 2 sin θ ) ( − v 1 sin θ + v 2 cos θ ) . A(\theta) \;=\; (u_1 \cos\theta + u_2 \sin\theta)\,(-v_1 \sin\theta + v_2 \cos\theta).
A ( θ ) = ( u 1 cos θ + u 2 sin θ ) ( − v 1 sin θ + v 2 cos θ ) .
下一步 :把这个乘积展开 + 化简 (提示:用 cos 2 θ = 1 + cos 2 θ 2 \cos^2\theta = \tfrac{1+\cos 2\theta}{2} cos 2 θ = 2 1 + c o s 2 θ 、 sin 2 θ = 1 − cos 2 θ 2 \sin^2\theta = \tfrac{1-\cos 2\theta}{2} sin 2 θ = 2 1 − c o s 2 θ 、 sin θ cos θ = sin 2 θ 2 \sin\theta\cos\theta = \tfrac{\sin 2\theta}{2} sin θ cos θ = 2 s i n 2 θ 把所有 θ \theta θ 都升到 2 θ 2\theta 2 θ )。 A ( θ ) A(\theta) A ( θ ) 化简后只剩什么形式 ?
🧑 Note :
zzy (Me)
设 a = u 1 cos θ , b = u 2 sin θ , c = v 1 sin θ , d = v 2 cos θ a = u_1\cos\theta,\ b = u_2\sin\theta,\ c = v_1\sin\theta,\ d = v_2\cos\theta a = u 1 cos θ , b = u 2 sin θ , c = v 1 sin θ , d = v 2 cos θ ,则
\begin{align*}
A(\theta) & = (a+b)(-c+d) = -ca - cb + ad + bd
& = -u_1 v_1 \cos\theta\sin\theta - u_2 v_1 \sin^2\theta + u_1 v_2 \cos^2\theta + u_2 v_2 \sin\theta\cos\theta
& = (u_2 v_2 - u_1 v_1),\cos\theta\sin\theta - u_2 v_1 \cdot \tfrac{1-\cos 2\theta}{2} + u_1 v_2 \cdot \tfrac{1+\cos 2\theta}{2}.
\end{align*}
全部升到 2 θ 2\theta 2 θ 后再合并:“这有什么规律?”——展开后得到混着 cos 2 θ \cos 2\theta cos 2 θ 、 sin 2 θ \sin 2\theta sin 2 θ 、常数项的一坨,结构性的东西不显然。
🤖 Note :
Claude (Mentor)
把 /2 因子都收齐后:
A ( θ ) = u 1 v 2 − u 2 v 1 2 ⏟ C 0 + u 2 v 1 + u 1 v 2 2 ⏟ C 1 cos 2 θ + u 2 v 2 − u 1 v 1 2 ⏟ C 2 sin 2 θ . A(\theta) \;=\; \underbrace{\tfrac{u_1 v_2 - u_2 v_1}{2}}_{\displaystyle C_0} \;+\; \underbrace{\tfrac{u_2 v_1 + u_1 v_2}{2}}_{\displaystyle C_1}\cos 2\theta \;+\; \underbrace{\tfrac{u_2 v_2 - u_1 v_1}{2}}_{\displaystyle C_2}\sin 2\theta.
A ( θ ) = C 0 2 u 1 v 2 − u 2 v 1 + C 1 2 u 2 v 1 + u 1 v 2 cos 2 θ + C 2 2 u 2 v 2 − u 1 v 1 sin 2 θ .
规律就在这里 : C 0 + C 1 cos 2 θ + C 2 sin 2 θ C_0 + C_1 \cos 2\theta + C_2 \sin 2\theta C 0 + C 1 cos 2 θ + C 2 sin 2 θ 这种形式可以合并成单一正弦——
A ( θ ) = C 0 + R sin ( 2 θ + φ ) , R = C 1 2 + C 2 2 . A(\theta) \;=\; C_0 + R\sin(2\theta + \varphi), \quad R = \sqrt{C_1^2 + C_2^2}.
A ( θ ) = C 0 + R sin ( 2 θ + φ ) , R = C 1 2 + C 2 2 .
——周期为 π \pi π 的简单正弦波 (频率 2 θ 2\theta 2 θ 的简谐振荡),绕常数 C 0 C_0 C 0 上下浮动,振幅 R R R 。最初看起来杂乱( cos θ , sin θ , cos 2 θ , sin 2 θ \cos\theta, \sin\theta, \cos^2\theta, \sin^2\theta cos θ , sin θ , cos 2 θ , sin 2 θ 全混着),最后塌缩成"常数 + 一个正弦"这个最简结构。
下一步 : A ( θ ) A(\theta) A ( θ ) 既然是 2 θ 2\theta 2 θ 的简单正弦波,它的极小值取在 θ \theta θ 为何值时 ?(提示: sin ( ⋅ ) \sin(\cdot) sin ( ⋅ ) 函数的极小在哪里?)
🧑 Note :
zzy (Me)
极小值在 2 θ + φ = 3 π 2 + 2 k π 2\theta + \varphi = \tfrac{3\pi}{2} + 2k\pi 2 θ + φ = 2 3 π + 2 k π 时取到。
🤖 Note :
Claude (Mentor)
对。等价地 sin ( 2 θ + φ ) = − 1 \sin(2\theta+\varphi) = -1 sin ( 2 θ + φ ) = − 1 ,所以 A min = C 0 − R A_{\min} = C_0 - R A m i n = C 0 − R ,是孤立的极小点 (只要 R > 0 R > 0 R > 0 )。
但等一下 ——我们这一路推下来 A ( θ ) = C 0 + R sin ( 2 θ + φ ) A(\theta) = C_0 + R\sin(2\theta+\varphi) A ( θ ) = C 0 + R sin ( 2 θ + φ ) 是在反证 setup 下:4 支撑 L , R , T , B L,R,T,B L , R , T , B 固定 ⇒ u , v \Rightarrow \bm u, \bm v ⇒ u , v 固定 ⇒ \Rightarrow ⇒ 系数 C 0 , R , φ C_0, R, \varphi C 0 , R , φ 固定。问题是:
这个公式在 θ \theta θ 取哪个范围内有效? 换句话说, θ \theta θ 转到什么时候 L , R , T , B L,R,T,B L , R , T , B 中的某一个会从一个 hull 顶点切换到下一个 hull 顶点?
🧑 Note :
zzy (Me)
这个比较难确定吧。
🤖 Note :
Claude (Mentor)
对——精确长度依赖 hull 几何(每次支撑切换发生在 hull 某个顶点处),不需要算。
反证其实只关心一个性质 :smooth 区间 I I I 是开区间 ——
θ \theta θ 在 I I I 内部 时,4 支撑保持 L , R , T , B L,R,T,B L , R , T , B (4 单点);
θ \theta θ 触到 I I I 端点 时,某个支撑切换 ⇔ \Leftrightarrow ⇔ 矩形某条边和 hull 某条边重合 (flush 状态)。
反证假设 ⇔ \Leftrightarrow ⇔ θ 0 \theta_0 θ 0 不 flush ⇔ \Leftrightarrow ⇔ θ 0 \theta_0 θ 0 是开区间 I I I 的内点 。
下一步 : θ 0 \theta_0 θ 0 是 A ( θ ) A(\theta) A ( θ ) 在开区间 I I I 上的极小,且在 I I I 的内点。这给出 A A A 在 θ 0 \theta_0 θ 0 处什么一阶必要条件 ?写出该等式(用 C 1 , C 2 C_1, C_2 C 1 , C 2 或 u , v \bm u, \bm v u , v 都行)。
🧑 Note :
zzy (Me)
什么叫一阶必要条件????
🤖 Note :
Claude (Mentor)
术语澄清——一阶必要条件 (first-order necessary condition,又叫 费马引理 ):
若 f ( x ) f(x) f ( x ) 在定义域内点 x 0 x_0 x 0 处取局部极值(极大或极小),且 f f f 在 x 0 x_0 x 0 处可导,则 必有
f ′ ( x 0 ) = 0. f'(x_0) = 0.
f ′ ( x 0 ) = 0 .
1D 直观 :极值点处函数曲线切线水平 (左升右降 = 上凸极大;左降右升 = 下凸极小;切线斜率都是 0)。
为什么强调"内点" :在区间端点取极值时, f ′ f' f ′ 不需要为 0(端点处只能往一边走,函数可以是单调的)。例: f ( x ) = x f(x) = x f ( x ) = x 在 [ 0 , 1 ] [0,1] [ 0 , 1 ] 上极小是 x = 0 x = 0 x = 0 ,但 f ′ ( 0 ) = 1 ≠ 0 f'(0) = 1 \neq 0 f ′ ( 0 ) = 1 = 0 。
为什么是"必要"不是"充分" : f ′ ( x 0 ) = 0 f'(x_0) = 0 f ′ ( x 0 ) = 0 不一定保证 x 0 x_0 x 0 是极值——也可能是鞍点 / 拐点 (如 f ( x ) = x 3 f(x) = x^3 f ( x ) = x 3 在 x = 0 x = 0 x = 0 )。所以一阶条件只筛掉了非极值的候选 ,是否真极值还要看二阶导。
回到题目 : A ( θ ) A(\theta) A ( θ ) 是 θ \theta θ 的光滑函数(sinusoid), θ 0 \theta_0 θ 0 是 A A A 在开 区间 I I I 内部 的极小 ⇒ \Rightarrow ⇒ 直接套用费马引理 ⇒ A ′ ( θ 0 ) = 0 \Rightarrow A'(\theta_0) = 0 ⇒ A ′ ( θ 0 ) = 0 。
回到原题 :把 A ( θ ) = C 0 + R sin ( 2 θ + φ ) A(\theta) = C_0 + R\sin(2\theta+\varphi) A ( θ ) = C 0 + R sin ( 2 θ + φ ) 求导,写出 A ′ ( θ 0 ) = 0 A'(\theta_0) = 0 A ′ ( θ 0 ) = 0 的具体形式。
🧑 Note :
zzy (Me)
继续吧,跳过引理证明部分,我们直接来看这个算法证明写。
🤖 Note :
Claude (Mentor)
好——核心引理 就此 buy-in:覆盖凸包的最小面积矩形,至少有一条边与凸包某条边共线 。sinusoid + 费马引理那条严格化路径打住,先把算法跑通;要回填证明随时再开。
已建立的产物保留作直觉 :4 支撑 L , R , T , B L,R,T,B L , R , T , B 固定的平滑段内
A ( θ ) = C 0 + R sin ( 2 θ + φ ) A(\theta) \;=\; C_0 + R\sin(2\theta + \varphi)
A ( θ ) = C 0 + R sin ( 2 θ + φ )
是 2 θ 2\theta 2 θ 的简单正弦——后面写算法时" θ \theta θ 取段端点(也就是某支撑切线 flush 到凸包某条边)"这个结构感会直接复用。
第 3 步:算法骨架(暴力)
🤖 Note :
Claude (Mentor)
接受引理后,最直接的暴力算法枚举什么 对象?
🧑 Note :
zzy (Me)
暴力枚举矩形的一条边和另外两个点呗。
🤖 Note :
Claude (Mentor)
方向对——枚举一条凸包边 e e e 当矩形"底边"(引理保证最优解里至少有这么一条 e e e 存在),然后再确定剩下的支撑点。准确计数是 3 个支撑点不是 2 :
底边方向 由 e e e 给定,矩形方向锁死 。设 n ^ e \hat n_e n ^ e = 沿 e e e 的单位向量, n ^ e ⊥ \hat n_e^\perp n ^ e ⊥ = 垂直 e e e 朝凸包内侧的单位向量。
T (top):凸包顶点中 n ^ e ⊥ \hat n_e^\perp n ^ e ⊥ 投影最大的那个 ⇒ \Rightarrow ⇒ 决定高 。
L, R (left, right):凸包顶点中 n ^ e \hat n_e n ^ e 投影最小 / 最大的两个 ⇒ \Rightarrow ⇒ 共同决定宽 = R R R 投影 − - − L L L 投影。
所以是 1 条边 + 3 个点( T , L , R T, L, R T , L , R )。 L , R L, R L , R 在同一根投影轴上一次扫描就能同时拿到,可能这就是你脑子里把它们合并成"一个点"的来源。
下一问 :固定 e e e 之后,逐个扫描凸包 h h h 个顶点找 T , L , R T, L, R T , L , R 是 O ( h ) O(h) O ( h ) ;外层枚举所有 h h h 条凸包边。这个暴力的总复杂度是多少?
🧑 Note :
zzy (Me)
O ( n 2 ) O(n^2) O ( n 2 ) 的这个级别吧。
🤖 Note :
Claude (Mentor)
对, O ( h 2 ) O(h^2) O ( h 2 ) ≤ \le ≤ O ( n 2 ) O(n^2) O ( n 2 ) (凸包顶点数 h ≤ n h \le n h ≤ n )。 n ≤ 5 × 1 0 4 ⇒ n 2 = 2.5 × 1 0 9 n \le 5 \times 10^4 \Rightarrow n^2 = 2.5 \times 10^9 n ≤ 5 × 1 0 4 ⇒ n 2 = 2 . 5 × 1 0 9 ,2s 时限过不去,要再砍一个数量级。
优化的入口观察 :把凸包边按 CCW 顺序记作 e 0 , e 1 , … , e h − 1 e_0, e_1, …, e_{h-1} e 0 , e 1 , … , e h − 1 。当外层枚举从 e i e_i e i 推进 到 e i + 1 e_{i+1} e i + 1 (底边方向连续微转一点点)时, T , L , R T, L, R T , L , R 这三个支撑顶点在凸包上是怎么动的 ?(提示:从几何上想象底边逆时针偏转一个小角度,三条支撑切线随之偏转,各自的接触顶点会怎么变。)
🧑 Note :
zzy (Me)
我大概理解了,就是维护三个指针就可以解决这个问题。
🤖 Note :
Claude (Mentor)
对——这就是 旋转卡壳(rotating calipers) 。结构性理由(不要跳):
单调性 / monotonicity :底边方向 θ \theta θ 沿 CCW 连续增加时, T , L , R T, L, R T , L , R 三个支撑顶点也只沿凸包 CCW 推进,从不回退 。原因——每个支撑顶点都是凸包上某个"投影极值"顶点( T T T = n ^ e ⊥ \hat n_e^\perp n ^ e ⊥ 投影最大; R R R = n ^ e \hat n_e n ^ e 投影最大; L L L = n ^ e \hat n_e n ^ e 投影最小 = − n ^ e -\hat n_e − n ^ e 投影最大),凸函数的极值点随方向 CCW 旋转只会 CCW 漂移(投影函数在凸包顶点序列上单峰)。
摊还代价 :外层枚举凸包 h h h 条边一圈,每个指针在凸包上最多走一整圈 h h h 步。三个指针合计 O ( h ) O(h) O ( h ) 步,加外层 O ( h ) O(h) O ( h ) 次面积更新 ⇒ \Rightarrow ⇒ 卡壳部分 O ( h ) \boldsymbol{O(h)} O ( h ) 摊还。
全流程复杂度 :求凸包 O ( n log n ) O(n\log n) O ( n log n ) + 旋转卡壳 O ( h ) ≤ O ( n ) O(h) \le O(n) O ( h ) ≤ O ( n ) ⇒ \Rightarrow ⇒ 总 O ( n log n ) \boldsymbol{O(n\log n)} O ( n l o g n ) 。 5 × 1 0 4 log ( 5 × 1 0 4 ) ≈ 8 × 1 0 5 5\times 10^4 \log(5\times 10^4) \approx 8\times 10^5 5 × 1 0 4 log ( 5 × 1 0 4 ) ≈ 8 × 1 0 5 ,2s 绰绰有余。
每个指针的推进判定 (具体写代码会用):以 T T T 为例——当前底边 e i e_i e i = H i → H i + 1 H_i \to H_{i+1} H i → H i + 1 , T T T 当前指 H t H_t H t 。问" T T T 该不该 advance 到 H t + 1 H_{t+1} H t + 1 ?“等价于问” H t + 1 H_{t+1} H t + 1 离底边更远 吗?"——用叉积 表达:
( H i + 1 − H i ) × ( H t + 1 − H i ) ≥ ( H i + 1 − H i ) × ( H t − H i ) , (H_{i+1} - H_i) \times (H_{t+1} - H_i) \;\ge\; (H_{i+1} - H_i) \times (H_t - H_i),
( H i + 1 − H i ) × ( H t + 1 − H i ) ≥ ( H i + 1 − H i ) × ( H t − H i ) ,
也即 ( H i + 1 − H i ) × ( H t + 1 − H t ) ≥ 0 (H_{i+1} - H_i) \times (H_{t+1} - H_t) \ge 0 ( H i + 1 − H i ) × ( H t + 1 − H t ) ≥ 0 。 L , R L, R L , R 类似但用点积 比较投影。
🧑 Note :
zzy (Me)
应该是需要维护 4 个指针吧?一个是边,另外 3 个是这个点,也就是代表这个切线。
🤖 Note :
Claude (Mentor)
对,更准确——4 个指针 ,分两类:
驱动指针 (for 循环):底边索引 i i i ,每轮 i → i + 1 i \to i+1 i → i + 1 强制 advance 1 步,跑遍 h h h 条凸包边作为"当前底边"候选。
跟随指针 (while 循环): T , L , R T, L, R T , L , R 三个支撑点,每轮在自己的 advance 条件不再满足前一直 \verb|++| (mod h h h )。
摊还修正 :4 个指针都沿凸包 CCW、各自最多走一整圈 h h h 步,合计 4 h = O ( h ) 4h = O(h) 4 h = O ( h ) 次操作 ⇒ \Rightarrow ⇒ 卡壳部分仍然 O ( h ) O(h) O ( h ) 。驱动 / 跟随 的区别只是循环类型不同(确定步进 vs 条件步进),单调不回退这一点都满足。
第 4 步:实现
🤖 Note :
Claude (Mentor)
现在去把代码写了——src/main.cpp(或你已有的源文件),用旋转卡壳走一遍。流程:
Andrew 求凸包,CCW 顺序输出 h h h 个顶点。
初始化 T , L , R T, L, R T , L , R 三指针:以 e 0 e_0 e 0 为底边独立扫一遍 找到三个初始位置。
外层 i = 0 → h − 1 i = 0 \to h-1 i = 0 → h − 1 枚举底边 e i e_i e i ;内层各自 while 推进指针到本轮的最优位置;算面积更新答案。
还原最优时的 4 个矩形顶点(底边线 ∩ \cap ∩ 左切线 、 ∩ \cap ∩ 右切线 ;同理上面)。
写完告诉我。
🧑 Note :
zzy (Me)
你直接写一个这个函数参考的实现吧,毕竟我们是在学习嘛。
🤖 Note :
Claude (Mentor)
好,我把参考实现写到 src/P3187\_HNOI2007\_minimun\_rectangle\_cover.cpp 里 minimum\_rectangle\_cover。
相对原 stub 的两处签名调整 :
返回类型 :array<DB, 5> → \to → pair<DB, array<Point, 4>{}>(5 个 DB 装不下面积 + 4 顶点 = 9 个数;Point 数组语义更清晰)。
Real 前置 :函数内部 getintersect 需要浮点除法,必须 Real = DB(模板顶部 using Real = ll; 改成 using Real = DB;)。本题输入也是浮点,本来就要切。
三个写代码的细节 (mentor 推导没明说、写时会卡的):
初始化不能简单赋 0 。 ‘ u p ‘ = ‘ r ‘ = ‘ l ‘ = 0 `up` = `r` = `l` = 0 ‘ u p ‘ = ‘ r ‘ = ‘ l ‘ = 0 + 推进 while 严格 " > > > " 时,cross/dot 都从 0 起步,不会被严格 " > > > " 触发推进 ⇒ \Rightarrow ⇒ 卡死在 0。要在外层 for 之前对 edge = 0 这条底边独立扫一遍 找各自最优起点。
推进条件用双线性化简 。 ‘ c r o s s ‘ ( e , A u + 1 − A e ) > ‘ c r o s s ‘ ( e , A u − A e ) `cross`(e, A_{u+1} - A_e) >` cross`(e, A_u - A_e) ‘ c r o s s ‘ ( e , A u + 1 − A e ) > ‘ c r o s s ‘ ( e , A u − A e ) ‘‘减一下 = = = ‘ c r o s s ‘ ( e , A u + 1 − A u ) > 0 `cross`(e, A_{u+1} - A_u) > 0 ‘ c r o s s ‘ ( e , A u + 1 − A u ) > 0 ‘’ = = = ‘ c r o s s ‘ ( e , e _ o f ( ‘ u p ‘ ) ) > 0 `cross`(e, e\_of(`up`)) > 0 ‘ c r o s s ‘ ( e , e _ o f ( ‘ u p ‘ ) ) > 0 ’’。三个支撑点的判定都能写成 "当前 polygon 边 ⋅ \cdot ⋅ e e e "或"当前 polygon 边 × \times × e e e "形式,比展开式干净一档。
*面积比较不要 除 ∣ e ∣ 2 \sqrt{|e|^2} ∣ e ∣ 2 *。直接乘出 Area ⋅ ∣ e ∣ 2 = ‘ d o t ‘ ( e , A r − A l ) ⋅ ‘ c r o s s ‘ ( e , A u p − A e ) \text{Area} \cdot |e|^2 = `dot`(e, A_r - A_l) \cdot `cross`(e, A_{up} - A_e) Area ⋅ ∣ e ∣ 2 = ‘ d o t ‘ ( e , A r − A l ) ⋅ ‘ c r o s s ‘ ( e , A u p − A e ) 比较;除法和开根都只在最后输出最小答案前做一次。
参考代码 (你自己在 cpp 里照着练):
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 pair<DB, array<Point, 4> > minimum_rectangle_cover (vector<Point> A) { A = make_convex_hull (A); int n = SZ (A); auto e_of = [&](int i) { return A[(i + 1 ) % n] - A[i]; }; int up = 0 , r = 0 , l = 0 ; { Point e0 = e_of (0 ); for (int j = 1 ; j < n; ++j) { if (cross (e0, A[j] - A[0 ]) > cross (e0, A[up] - A[0 ])) up = j; if (dot (e0, A[j] - A[0 ]) > dot (e0, A[r] - A[0 ])) r = j; if (dot (e0, A[j] - A[0 ]) < dot (e0, A[l] - A[0 ])) l = j; } } DB best_area = numeric_limits<DB>::infinity (); int best_edge = 0 , best_up = up, best_r = r, best_l = l; for (int edge = 0 ; edge < n; ++edge) { Point e = e_of (edge); while (cross (e, e_of (up)) > 0 ) up = (up + 1 ) % n; while (dot (e, e_of (r)) > 0 ) r = (r + 1 ) % n; while (dot (e, e_of (l)) < 0 ) l = (l + 1 ) % n; DB w_e = (DB) dot (e, A[r] - A[l]); DB h_e = (DB) cross (e, A[up] - A[edge]); DB area = w_e * h_e / (DB) e.norm (); if (area < best_area) { best_area = area; best_edge = edge; best_up = up; best_r = r; best_l = l; } } Point e = e_of (best_edge); Point e_perp (-e.y, e.x) ; Line bottom (A[best_edge], A[best_edge] + e) ; Line top_ (A[best_up], A[best_up] + e) ; Line right_ (A[best_r], A[best_r] + e_perp) ; Line left_ (A[best_l], A[best_l] + e_perp) ; array<Point, 4> corners = { getintersect (bottom, left_), getintersect (bottom, right_), getintersect (top_, right_), getintersect (top_, left_), }; return {best_area, corners}; }
下一步 :把这份代码塞进 Solve() 里跑题面样例,AC 之后再做 stress test(\textsf{cyaron-stress} skill 的非均匀曲率形状)。
心路历程(mentor 走过的弯路)
第一次抛核心引理时被骂 : 直接说"接受这条引理后请描述暴力",被怼"你代替我接受了?"。整段 setup(参数化 / 平滑段 / sinusoid / 核心断言 / 引理 / 收尾追问)一次性 dump 出去违反「mentor 推进只一步」硬约束,整段 % 行注释回滚作违规档底参考。
mentor 推进路线选错 : 想沿 sinusoid 反证 + 费马引理 A ′ ( θ 0 ) = 0 A'(\theta_0) = 0 A ′ ( θ 0 ) = 0 走严格证明,推到内点极小子情况走不出矛盾——标准证明实际是构造性的(让矩形绕某支撑顶点旋转就能压缩面积)。本 session 选择跳过严格证明,buy-in 引理直接进算法。
指针数搞错 : mentor 数错支撑点数(说"另外 3 个"但 zzy 一开始报"另外 2 个"),后续 zzy 自己纠正成"4 个指针:边 i + T/L/R 三支撑"。
写参考实现位置错 : mentor 把参考实现写进了 cpp 主文件,被 zzy 怒怼"那个文件是我自己练的"。revert,改成 mentor.tex 内联 minted 块。
AC 代码
进行中(cpp 还在敲)—— mentor 给的参考实现已嵌在上方"参考代码"段落,AC 后回来填提交链接和最终代码。
📎 附件
mentor.pdf : 12 页完整带教对话录 PDF 版(xelatex 编译)。
P3187 最小矩形覆盖 mentor 完整带教对话录(12 页 PDF)