UVa 248 Cutting Corners
题目概述
题目背景是自行车快递服务公司需要为配送路线定价,要求计算出从起点到终点的最短可行路径长度。快递员可以在建筑物之间穿行,甚至可以紧贴建筑物边缘行驶,但不能穿过建筑物内部。
题目分析
问题描述
在二维平面上,给定若干矩形建筑物(由 nnn 个矩形表示,0≤n≤200 \leq n \leq 200≤n≤20),以及起点和终点坐标。要求计算从起点到终点的最短路径长度,路径不能穿过任何建筑物的内部(但可以紧贴建筑物边界行走)。
关键约束条件
- 建筑物的定义:每个建筑物由三个顶点坐标给出(矩形的一个顶点和相邻两个顶点),可以推导出第四个顶点。
- 建筑物的关系:相交的矩形被视为同一建筑物,不相交的矩形代表不同建筑物。
- 行走规则:可以在建筑物之间的空隙穿行,可以紧贴建筑物边界行驶(包括直角转弯处紧贴角落)。
- 数据范围:所有坐标在 [0,1000][0, 1000][0,1000] 之间,n≤20n \leq 20n≤20。
- 输入终止:以 −1-1−1 表示最后一个场景的结束。
输入输出格式
- 输入:每个场景第一行为整数 nnn,第二行为起点和终点坐标 x1 y1 x2 y2x_1\ y_1\ x_2\ y_2x1 y1 x2 y2,接下来 nnn 行每行三个顶点坐标 x1 y1 x2 y2 x3 y3x_1\ y_1\ x_2\ y_2\ x_3\ y_3x1 y1 x2 y2 x3 y3。
- 输出:每个场景输出一行,格式为
Scenario #k route distance: x.xx,场景之间输出空行分隔。
解题思路
核心思想:图论建模 + 最短路径
本题的关键是将几何问题转化为图论问题。由于路径可以紧贴建筑物边界,因此最短路径必然由起点、终点以及所有建筑物的顶点之间的线段构成。理由如下:
- 如果路径中存在一段不经过任何特殊点的直线段,我们可以将其平移至经过某个顶点,距离不会变长。
- 由于只能绕开建筑物,路径的转折点必然发生在建筑物顶点处。
因此,解题步骤为:
- 收集所有关键点:起点、终点、所有建筑物的 444 个顶点。
- 在这些点之间建立图:如果两点之间的线段不穿过任何建筑物内部,则在两点之间连一条边,边权为两点间的欧几里得距离。
- 在图上运行最短路径算法(Dijkstra\texttt{Dijkstra}Dijkstra 算法),求起点到终点的最短距离。
关键问题处理
1. 矩形顶点的推导
题目只给出矩形的三个顶点。矩形是轴对齐的吗?题目没有明确说明,但从输入和代码实现来看,矩形可以是任意方向(非轴对齐)。代码中的 getPointAndRectangle 函数完成了第四个顶点的推导:
- 先找出三条边中最长边对应的两个顶点作为矩形的对角顶点。
- 然后利用平行四边形法则计算第四个顶点:p4=p1−(p3−p2)p_4 = p_1 - (p_3 - p_2)p4=p1−(p3−p2)。
2. 判断两点连线是否与建筑物相交
这是本题的核心几何计算。一个线段 ABABAB 是可行的,当且仅当它不与任何建筑物的内部相交。判断方法:
对于每个建筑物(矩形):
- 首先判断 ABABAB 是否完全在矩形的一侧(使用方向函数 d1,d2,d3,d4d_1, d_2, d_3, d_4d1,d2,d3,d4 同号),如果是,则不可能与矩形相交,跳过。
- 否则,检查以下两种情况:
- 如果 AAA 或 BBB 在矩形内部,则线段与建筑物相交(不可行)。
- 如果线段与矩形的任意一条边相交(且不是端点重合的情况),则不可行。
代码中的 intersected 函数实现了这一逻辑。
3. 方向函数
double direction(point a, point b, point c)
{
return (c.x - a.x) * (b.y - a.y) - (b.x - a.x) * (c.y - a.y);
}
该函数计算向量 ab→\overrightarrow{ab}ab 与 ac→\overrightarrow{ac}ac 的叉积。返回值为正表示 ccc 在 ababab 左侧,为负表示右侧,为 000 表示共线。
4. 点是否在矩形内部
使用方向法判断:对于矩形 RRR(顶点按顺时针或逆时针顺序),点 PPP 在矩形内部当且仅当 PPP 位于四条边的同一侧(即四个方向值同号)。代码中的 pointInRectangle 函数实现了这一判断。
5. 精度处理
由于坐标是实数,直接比较存在浮点误差。代码中使用了 EPSILON=10−6\texttt{EPSILON} = 10^{-6}EPSILON=10−6 进行容差判断。例如:
- 判断方向值是否为 000:
fabs(value) <= EPSILON - 判断小于关系:
value + EPSILON < 0
6. 最短路径算法
代码使用的是 Moore-Dijkstra\texttt{Moore-Dijkstra}Moore-Dijkstra 算法(即朴素 Dijkstra\texttt{Dijkstra}Dijkstra),时间复杂度 O(V2)O(V^2)O(V2)。由于顶点数最多为 2+20×4=822 + 20 \times 4 = 822+20×4=82,朴素版本完全够用。
代码实现细节
数据结构
struct point { double x, y; }; // 点
struct segment { point start, end; }; // 线段
struct rectangle { // 矩形(四个顶点)
point top_left, top_right, bottom_left, bottom_right;
};
struct edge { int label; double dist; }; // 图的边
主要函数说明
| 函数名 | 功能 |
|---|---|
direction |
计算叉积,判断点的方位 |
distanceOfPoints |
计算两点欧氏距离 |
getPointAndRectangle |
由三个顶点推导矩形并存储 |
segmentsIntersect |
判断两线段是否相交(规范相交) |
pointInRectangle |
判断点是否在矩形内部 |
intersected |
判断线段是否与任何建筑物相交 |
prepare |
构建图:在所有点之间添加可行边 |
mooreDijkstra |
运行朴素 Dijkstra 算法求最短路 |
代码注释
// Cutting Corners
// UVa ID: 248
// Verdict: Accepted
// Submission Date: 2016-11-29
// UVa Run Time: 0.040s
//
// 版权所有(C)2016,邱秋。metaphysis # yeah dot net
#include <bits/stdc++.h>
using namespace std;
const double MAX_DISTANCE = 1000000.0, EPSILON = 1e-6;
// 点结构体
struct point
{
double x, y;
};
// 线段结构体
struct segment
{
point start, end;
};
// 矩形结构体,存储四个顶点
struct rectangle
{
point top_left, top_right, bottom_left, bottom_right;
};
// 图的边结构体
struct edge
{
int label; // 目标顶点索引
double dist; // 边长(距离)
};
point starting, stopping; // 起点和终点
vector<point> allPoints; // 所有点的集合
vector<rectangle> allRectangles; // 所有矩形的集合
vector<vector<edge>> edges; // 图的邻接表
vector<double> distances; // 最短距离数组
vector<bool> visited; // 访问标记数组
// 计算向量 ab 和 ac 的叉积,判断 c 在 ab 的左侧还是右侧
double direction(point a, point b, point c)
{
return (c.x - a.x) * (b.y - a.y) - (b.x - a.x) * (c.y - a.y);
}
// 计算两点间的欧几里得距离
double distanceOfPoints(point p1, point p2)
{
return sqrt(pow(p1.x - p2.x, 2) + pow(p1.y - p2.y, 2));
}
// 由三个顶点推导出矩形的第四个顶点,并存储矩形和所有顶点
void getPointAndRectangle(point p1, point p2, point p3)
{
// 找出最长边对应的两个顶点作为对角顶点
if (distanceOfPoints(p1, p2) + EPSILON < distanceOfPoints(p2, p3))
getPointAndRectangle(p2, p3, p1);
else if (distanceOfPoints(p1, p2) + EPSILON < distanceOfPoints(p1, p3))
getPointAndRectangle(p1, p3, p2);
else
{
// 平行四边形法则计算第四个顶点
point p4 = (point){p1.x - (p3.x - p2.x), p1.y - (p3.y - p2.y)};
// 存储四个顶点
allPoints.push_back(p1);
allPoints.push_back(p2);
allPoints.push_back(p3);
allPoints.push_back(p4);
// 根据方向确定矩形的顶点顺序(保证一致性)
if (direction(p1, p2, p3) > EPSILON)
allRectangles.push_back((rectangle){p1, p3, p2, p4});
else
allRectangles.push_back((rectangle){p1, p4, p2, p3});
}
}
// 判断两条线段是否相交(规范相交,即交点在内部)
bool segmentsIntersect(segment a, segment b)
{
double d1, d2, d3, d4;
d1 = direction(a.start, a.end, b.start);
d2 = direction(a.start, a.end, b.end);
d3 = direction(b.start, b.end, a.start);
d4 = direction(b.start, b.end, a.end);
// 规范相交:两线段互相跨立
return ((d1 * d2 < 0) && (d3 * d4) < 0);
}
// 判断点是否在矩形内部(不包括边界)
bool pointInRectangle(point p, rectangle r)
{
double d1 = direction(r.top_left, r.bottom_left, p);
double d2 = direction(r.bottom_left, r.bottom_right, p);
double d3 = direction(r.bottom_right, r.top_right, p);
double d4 = direction(r.top_right, r.top_left, p);
// 点在内部当且仅当在四条边的同一侧
return (d1 < 0 && d2 < 0 && d3 < 0 && d4 < 0);
}
// 判断线段 s 是否与任何建筑物相交(包括端点在内部的情况)
bool intersected(segment s)
{
for (int i = 0; i < allRectangles.size(); i++)
{
rectangle r = allRectangles[i];
// 判断线段两端点相对于矩形各边的方向
double d1 = direction(s.start, s.end, r.top_left);
double d2 = direction(s.start, s.end, r.top_right);
double d3 = direction(s.start, s.end, r.bottom_left);
double d4 = direction(s.start, s.end, r.bottom_right);
// 如果线段完全在矩形的一侧,则不可能相交
if ((d1 >= -EPSILON && d2 >= -EPSILON && d3 >= -EPSILON && d4 >= -EPSILON) ||
(d1 <= EPSILON && d2 <= EPSILON && d3 <= EPSILON && d4 <= EPSILON))
continue;
else
{
// 如果端点在矩形内部,则相交
if (pointInRectangle(s.start, r) || pointInRectangle(s.end, r))
return true;
// 检查是否与矩形的任意一条边相交
if (segmentsIntersect(s, (segment){r.top_left, r.bottom_left}) ||
segmentsIntersect(s, (segment){r.bottom_left, r.bottom_right}) ||
segmentsIntersect(s, (segment){r.bottom_right, r.top_right}) ||
segmentsIntersect(s, (segment){r.top_right, r.top_left}))
return true;
}
}
return false;
}
// 构建图:在所有点之间添加可行边
void prepare()
{
edges.clear();
edges.resize(allPoints.size(), vector<edge>());
// 枚举所有点对
for (int i = 0; i < allPoints.size(); i++)
for (int j = i + 1; j < allPoints.size(); j++)
{
segment s = (segment){allPoints[i], allPoints[j]};
// 如果线段不穿过任何建筑物,则添加边
if (!intersected(s))
{
double dist = distanceOfPoints(s.start, s.end);
edges[i].push_back((edge){j, dist});
edges[j].push_back((edge){i, dist});
}
}
}
// 朴素 Dijkstra 算法求最短路,起点为 start
void mooreDijkstra(int start)
{
visited.clear();
distances.clear();
for (int i = 0; i < edges.size(); i++)
{
visited.push_back(false);
distances.push_back(MAX_DISTANCE);
}
distances[start] = 0.0;
// 每次选择一个未访问且距离最小的顶点进行松弛
while (!visited[start])
{
visited[start] = true;
// 松弛当前顶点的所有邻接边
for (int i = 0; i < edges[start].size(); i++)
{
edge e = edges[start][i];
if (!visited[e.label] && distances[start] + e.dist + EPSILON < distances[e.label])
distances[e.label] = distances[start] + e.dist;
}
// 选择下一个未访问的距离最小的顶点
double minDistance = MAX_DISTANCE;
for (int i = 0; i < edges.size(); i++)
if (!visited[i] && minDistance > distances[i] + EPSILON)
{
minDistance = distances[i];
start = i;
}
}
}
int main(int argc, char *argv[])
{
int n, cases = 0;
while (cin >> n, n >= 0)
{
// 初始化数据
allPoints.clear();
allRectangles.clear();
// 读入起点和终点
cin >> starting.x >> starting.y >> stopping.x >> stopping.y;
allPoints.push_back(starting);
allPoints.push_back(stopping);
// 读入 n 个矩形的三个顶点
point p1, p2, p3;
for (int i = 1; i <= n; i++)
{
cin >> p1.x >> p1.y;
cin >> p2.x >> p2.y;
cin >> p3.x >> p3.y;
getPointAndRectangle(p1, p2, p3);
}
// 构建图并求最短路
prepare();
mooreDijkstra(0); // 起点索引为 0
// 输出结果
if (cases > 0) cout << '\n';
cout << "Scenario #" << ++cases << '\n';
cout << " route distance: ";
cout << fixed << setprecision(2) << distances[1] << '\n';
}
return 0;
}
算法复杂度分析
- 顶点数 VVV:最多 2+4×20=822 + 4 \times 20 = 822+4×20=82 个顶点。
- 边数 EEE:最多 O(V2)≈6724O(V^2) \approx 6724O(V2)≈6724 条边(完全图)。
- 建图复杂度:每次判断线段是否与矩形相交需遍历所有 nnn 个矩形,每个矩形需进行常数次几何计算,总复杂度 O(V2⋅n)O(V^2 \cdot n)O(V2⋅n),在本题范围内完全可以接受。
- 最短路径复杂度:朴素 Dijkstra\texttt{Dijkstra}Dijkstra 为 O(V2)O(V^2)O(V2),约 822=672482^2 = 6724822=6724 次操作。
样例验证
样例输入
5
6.5 9 10 3
1 5 3 3 6
6 5.25 2 8 2
8 3.5 6 10 6
12 9 12 7 6
11 6 11 8 10
7 11 7 11 11
-1
样例输出
Scenario #1 route distance: 7.28
代码输出与样例一致。
总结
本题是一道经典的几何图论问题,核心思路是将几何约束转化为图的可达性判断,然后使用最短路径算法求解。解题的关键在于:
- 准确判断线段是否与矩形内部相交。
- 正确处理浮点精度。
- 将所有建筑物顶点纳入图节点。
掌握了这些技巧,可以解决类似的可视性图(Visibility Graph\texttt{Visibility Graph}Visibility Graph)问题。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)