0%

牛客 NC15086 - 向左走

题目大意

牛客 NC15086「向左走」。多组数据(不超过 10 组)。每组给 nn 根杆子( 1n10001\le n\le 1000 ),坐标范围 [1,1000][1,1000] 且互不重合。

你拿着绳子从某根杆子出发,规则有四条:只能向左转或直行(绝不右拐)只能走直线走过的路不能与自己相交每根杆子最多经过一次。起点固定为 yy 最小、 yy 相同取 xx 最小 的那根杆子。

目标:经过尽量多的杆子(其实可以全部经过),输出经过杆子的编号顺序。

样例

4 根杆子 1(3,1)2(7,1)3(5,3)4(5,5)1(3,1)\,2(7,1)\,3(5,3)\,4(5,5) ,输出 1 2 4 3 。从最低点 11 出发,外圈是三角形 {1,2,4}\{1,2,4\} 逆时针描一遍,剩下的 33 在最里面,最后串上。

思路讲解

一句话

洋葱剥皮(onion peeling)螺旋:从最低点出发,沿点集的凸包逆时针描一圈;描完把这一圈的点剥掉,剩下的点又是一个点集,对它再描一圈,像蚊香一样越绕越往里。难点全在两层之间怎么衔接——内层的入口不能随便选,否则会右转或自交。

「只能向左转」= 朝向角单调逆时针旋转

你每到一根杆子要么直行要么左拐、永远不右拐,等价于说:你的前进方向(朝向角)只会朝一个方向累积旋转,绝不回头。这是个很强的约束——它把"路径"逼成一条逆时针卷进去的螺旋

沿最外圈 = 逆时针描凸包

站在最低点、朝向只能左转,那么"贴着最外圈走出来的轨迹"恰好就是把点集的凸包按逆时针描一遍:第一步沿凸包下边迈出去(朝最贴右的方向,使其余所有点都在你左手边),之后每到一个顶点左拐,逆时针绕完外圈。所以第一步方向就被钉死了

关键不变量:起点是全局最低点,外层凸包按逆时针(CCW)输出,第一条边朝向使所有点在左手侧。

钻进内层 = 递归同类子问题

外圈只串起了边界上的杆子,里面还有没碰到的。绕完一圈快回到起点时往里钻一层——而内部剩下的杆子本身又是一个点集,面对的规则一模一样(从它的最外层开始、只能左转、不自交)。所以整个算法就是反复做同一个动作:

  1. 对当前剩余点求凸包(CCW),输出这一层;

  2. 把这层的点删掉,对剩点重复。

关键①:内层入口怎么选(续接式入口,不是重置最低点)

这是本题最容易写错、也是我这次真栽进去的地方。一个自然但错误的写法是:对每一层都调用"找该层字典序最小点、旋转到开头"(即 reorder_polygon)。第一层这么做对(全局最低点就是螺旋起点),但内层这么做就错了——内层会从它自己的最低点起头,而那个点未必接得上上一层的螺旋,会在层边界右转或让连接边穿过内层造成自交

正确做法是续接式入口:设上一层走完停在末顶点 ee ,进入 ee 时的前进方向是 d\overrightarrow{d} 。对内层这个 CCW 多边形,入口顶点 ww使 d\overrightarrow{d} 逆时针转到 we\overrightarrow{w-e} 的转角最小的那一个,再从 ww 起按 CCW 走完内层。直觉就是:朝向只能继续逆时针转,那就贴着当前方向最小幅度地左转钻进去,绳子贴着自己往里收,绝不会右拐、也不会去穿别的边。

w=argminvL ccwAngle(d, ve)w = \arg\min_{v \in L}\ \mathrm{ccwAngle}(\overrightarrow{d},\ \overrightarrow{v - e})

红色结论:内层入口必须续接上一层的前进方向(极角基准 = 上一层末顶点 ee ),绝不能重置成内层自己的最低点。后者就是 WA 的来源。

这条规则在 5000 组随机点集上 100% 产出"全程左转 + 无自交"的合法路径,样例也吻合。

关键②:共线层的退化处理

剥到某一层时,如果剩下的点恰好全共线,凸包退化成一条线段。此时弱凸包(保留共线点)的 Andrew 扫描会折返:上行扫一遍再下行扫回来,输出形如 A B C B 的重复序列——同一个点被走两遍,输出就不是全排列,直接 WA。

处理:对一层的凸包用有向面积判退化(面积为 0 即全共线),退化时这层的点已经按排序顺序排好且互不重复,直接当一条线"单程"返回即可,不折返。这一步是 O(n)O(n) 的,不要写成两两比对的 O(n2)O(n^2) 去重。

复杂度

每层一次凸包 O(nlogn)O(n\log n) ,最多 O(n)O(n) 层,整体 O(n2logn)O(n^2\log n)n1000n\le 1000 、不超过 10 组,稳过。

📎 动画与源码

我做了一个交互式 HTML 演示,可以一步步点 Step 看每个拐点的转向叉积,对比"修正前 vs 修正后"内层入口的差别(右转/自交怎么发生、续接式入口怎么修好)。本体和关键帧截图见附件节。

AC 代码

AC 提交(vjudge)

完整源码较长(含整套几何模板),折叠如下:

心路历程(本题真实 debug 链)

这道题本地样例过、提交却 WA,挖出来是一条挺长的链:

  • 层边界右转:最早用 reorder_polygon 对每层重排到"该层最低点"。第一层对,内层错——构造出 n=5 反例 1(11,9)2(1,11)3(10,2)4(9,8)5(5,9)1(11,9)\,2(1,11)\,3(10,2)\,4(9,8)\,5(5,9) ,外层 {3,1,2}\{3,1,2\} 描完,内层从自己最低点 44 进,路径 2452\to4\to544右转(叉积 4-4 )。随机测 4 万组,约 15% 非法(右转 + 自交两种症状)。

  • 续接式入口:改成"从上一层前进方向逆时针扫,第一个扫到的内层顶点当入口"( ccwAngle\mathrm{ccwAngle} 最小),右转和自交都消失。

  • 两个实现 bug:(1) 取 argmin 时 mn_angle = angle 写反成 angle = mn_angle ,导致永远选最后一个;(2) rotate 转错容器(转了已拼好的答案 ans ,应该转新层 convex_poly )。

  • 共线层折返:修完上面随机测仍有约 0.7% WA,全是"剩余点共线 → 弱凸包折返出重复点 → 非全排列"。加有向面积判退化后,2 万随机 + 2 千全共线专测 100% 通过。

教训:几何题"本地样例过 \ne 对",对拍要主动覆盖共线 / 退化 / 多层嵌套形状,光靠样例和近圆形点集测不出来。

附件

下方挂了交互式演示 HTML 本体(下载后改回 .html 用浏览器打开,可一步步点 Step)、几个关键帧截图,以及完整带教对话录 PDF。

演示初始:从最低点 3 出发,外层凸包 {3,1,2} 逆时针描

逆时针走完外层到末顶点 2

修正前:内层从自己最低点 4 进入,路径 2→4→5 在顶点 4 处右转(cross=-4)违规——这就是 bug。修正后改从 5 进入则全程左转

reorder_polygon_bug.html.txt

完整带教对话录(PDF 版)