题目描述
给定平面上 nnn 个具有整数坐标的点,判断是否可以用这些点作为顶点构造一个简单的(即不自交的)直角多边形。在直角多边形中,所有边要么是水平的,要么是垂直的;每个顶点恰好连接一条水平边和一条垂直边。多边形中不能有洞。
输入格式:第一行是测试用例数量,每个测试用例以整数 nnn (4≤n≤1000004 \leq n \leq 1000004≤n≤100000) 开始,后跟 nnn 对整数表示点的坐标。
输出格式:对于每个测试用例,如果存在这样的直角多边形,输出其周长;否则输出 −1-1−1。
题目分析
问题本质
这是一个关于构造直角多边形的问题,需要满足以下严格约束条件:
- 直角性:所有边必须是水平或垂直的
- 完整性:所有给定的点都必须作为顶点使用
- 简单性:多边形不能自交
- 无洞性:多边形内部不能包含洞
- 度数约束:每个顶点恰好连接一条水平边和一条垂直边
关键观察
通过分析直角多边形的特性,我们发现以下几个重要性质:
-
奇偶性条件:对于每个 xxx 坐标,具有该 xxx 坐标的点数必须是偶数;对于每个 yyy 坐标,具有该 yyy 坐标的点数也必须是偶数。这是因为垂直边连接相同 xxx 坐标的点,水平边连接相同 yyy 坐标的点。
-
边构建规则:在满足奇偶性条件的前提下,我们可以将相同 yyy 坐标的点按 xxx 坐标排序后两两配对形成水平边,将相同 xxx 坐标的点按 yyy 坐标排序后两两配对形成垂直边。
-
连通性与简单性:在直角多边形的特定约束下,如果所有边形成一个单一的连通分量,那么多边形必定是简单的(不自交且无洞)。
解题思路
初始方案:扫描线算法检测自交
我们最初的解决方案采用了扫描线算法来检测线段是否仅相交于端点:
算法步骤
-
奇偶性检查:统计每个 xxx 坐标和 yyy 坐标的出现次数,确保都是偶数。
-
边构建:
- 水平边:对每个 yyy 坐标,将对应点按 xxx 坐标排序,连接 (x2i,y)(x_{2i}, y)(x2i,y) 和 (x2i+1,y)(x_{2i+1}, y)(x2i+1,y)
- 垂直边:对每个 xxx 坐标,将对应点按 yyy 坐标排序,连接 (x,y2i)(x, y_{2i})(x,y2i) 和 (x,y2i+1)(x, y_{2i+1})(x,y2i+1)
-
自交检测:
- 使用扫描线算法,将线段转换为事件点
- 维护当前活跃的垂直线段集合
- 检查水平线段是否与活跃的垂直线段有非端点相交
-
周长计算:累加所有水平边和垂直边的长度。
复杂度分析
- 排序操作:O(nlogn)O(n \log n)O(nlogn)
- 扫描线算法:O(nlogn)O(n \log n)O(nlogn)
- 总体复杂度:O(nlogn)O(n \log n)O(nlogn)
优化方案:并查集检测连通性
经过深入分析,我们发现了一个重要性质:在满足奇偶性条件的直角多边形中,单连通分量等价于简单多边形。这让我们可以用更高效的并查集来替代复杂的扫描线算法。
算法原理
在直角多边形的约束条件下:
- 如果存在自交,会在交点处产生新的顶点,破坏每个顶点度数为 222 的条件
- 如果存在洞,会形成多个连通分量
- 因此,单连通分量 ⇔ 简单多边形
算法步骤
-
奇偶性检查:同初始方案。
-
边构建与连通性检查:
- 使用并查集数据结构
- 构建水平边时,连接对应端点的并查集
- 构建垂直边时,连接对应端点的并查集
-
简单性验证:检查并查集是否只有一个连通分量。
-
周长计算:在构建边的过程中累加周长。
优势
- 理论保证:在给定约束下,单连通分量保证多边形简单性
- 高效实现:并查集操作接近 O(1)O(1)O(1) 时间复杂度
- 代码简洁:逻辑清晰,易于实现和维护
复杂度分析
- 排序操作:O(nlogn)O(n \log n)O(nlogn)
- 并查集操作:O(nα(n))O(n \alpha(n))O(nα(n)),其中 α(n)\alpha(n)α(n) 是反阿克曼函数
- 总体复杂度:O(nlogn)O(n \log n)O(nlogn),适合 n≤100000n \leq 100000n≤100000 的数据规模
代码实现
方案一:扫描线算法实现(完整但较复杂)
// Rectilinear Polygon
// UVa ID: 11106
// Verdict: Accepted
// Submission Date: 2025-11-30
// UVa Run Time: 1.730s
//
// 版权所有(C)2025,邱秋。metaphysis # yeah dot net
#include <bits/stdc++.h>
using namespace std;
struct Point {
int x, y;
Point(int x = 0, int y = 0) : x(x), y(y) {}
bool operator<(const Point& other) const {
return x < other.x || (x == other.x && y < other.y);
}
bool operator==(const Point& other) const {
return x == other.x && y == other.y;
}
};
namespace std {
template<>
struct hash<Point> {
size_t operator()(const Point& p) const {
return hash<int>()(p.x) ^ (hash<int>()(p.y) << 1);
}
};
}
struct Segment {
int id, x1, x2, y;
bool isHorizontal;
Segment(int id, int x1, int x2, int y, bool horizontal)
: id(id), x1(x1), x2(x2), y(y), isHorizontal(horizontal) {}
};
// 检查多边形是否简单(无洞且不自交)
bool isSimplePolygon(vector<Segment>& horizontalSegments, vector<Segment>& verticalSegments) {
// 构建邻接表
unordered_map<Point, vector<Point>> graph;
// 添加水平边
for (auto& seg : horizontalSegments) {
Point p1(seg.x1, seg.y);
Point p2(seg.x2, seg.y);
graph[p1].push_back(p2);
graph[p2].push_back(p1);
}
// 添加垂直边
for (auto& seg : verticalSegments) {
Point p1(seg.x1, seg.y);
Point p2(seg.x1, seg.x2); // x2存储的是另一个y坐标
graph[p1].push_back(p2);
graph[p2].push_back(p1);
}
// 检查每个顶点的度数是否为2
for (auto& entry : graph) {
if (entry.second.size() != 2) {
return false;
}
}
// 检查是否形成单个环(通过DFS遍历)
if (graph.empty()) return false;
Point start = graph.begin()->first;
unordered_set<Point> visited;
Point current = start;
Point prev = start;
int count = 0;
do {
if (visited.count(current)) {
// 提前遇到已访问点,说明有多个环
return false;
}
visited.insert(current);
count++;
// 找到下一个点(排除前一个点)
Point next;
for (Point neighbor : graph[current]) {
if (!(neighbor == prev)) {
next = neighbor;
break;
}
}
prev = current;
current = next;
} while (!(current == start) && count <= graph.size());
// 如果遍历了所有顶点且回到起点,说明是单个环
return count == graph.size() && current == start;
}
// 扫描线算法检测线段是否仅相交于端点
bool checkNoImproperIntersections(vector<Segment>& horizontalSegments,
vector<Segment>& verticalSegments) {
unordered_map<int, Segment*> verticalMap;
for (auto& seg : verticalSegments)
verticalMap[seg.id] = &seg;
vector<tuple<int, int, int, int>> events;
for (auto& seg : horizontalSegments) {
events.emplace_back(seg.x1, 1, seg.y, seg.id);
events.emplace_back(seg.x2, 1, seg.y, seg.id);
}
for (auto& seg : verticalSegments) {
int y1 = seg.y;
int y2 = seg.x2;
events.emplace_back(seg.x1, 0, min(y1, y2), seg.id);
events.emplace_back(seg.x1, 2, max(y1, y2), seg.id);
}
sort(events.begin(), events.end());
set<pair<int, int>> activeVertical;
for (auto& event : events) {
int x = get<0>(event);
int type = get<1>(event);
int y = get<2>(event);
int id = get<3>(event);
if (type == 0) {
activeVertical.insert({y, id});
} else if (type == 2) {
activeVertical.erase({y, id});
} else if (type == 1) {
for (auto& vert : activeVertical) {
int vertId = vert.second;
Segment* verticalSeg = verticalMap[vertId];
if (verticalSeg->x1 == x) {
int vertMinY = min(verticalSeg->y, verticalSeg->x2);
int vertMaxY = max(verticalSeg->y, verticalSeg->x2);
if (y > vertMinY && y < vertMaxY) {
return false;
}
}
}
}
}
return true;
}
bool solveTestCase() {
int n;
cin >> n;
vector<Point> points(n);
map<int, int> xCount, yCount;
for (int i = 0; i < n; i++) {
cin >> points[i].x >> points[i].y;
xCount[points[i].x]++;
yCount[points[i].y]++;
}
for (auto& p : xCount)
if (p.second % 2 != 0) return false;
for (auto& p : yCount)
if (p.second % 2 != 0) return false;
map<int, vector<int>> byY;
for (auto& p : points)
byY[p.y].push_back(p.x);
vector<Segment> horizontalSegments, verticalSegments;
int segmentId = 0;
long long perimeter = 0;
for (auto& group : byY) {
vector<int>& xs = group.second;
sort(xs.begin(), xs.end());
for (size_t i = 0; i < xs.size(); i += 2) {
int x1 = xs[i], x2 = xs[i + 1];
horizontalSegments.emplace_back(segmentId++, x1, x2, group.first, true);
perimeter += (x2 - x1);
}
}
map<int, vector<int>> byX;
for (auto& p : points)
byX[p.x].push_back(p.y);
for (auto& group : byX) {
vector<int>& ys = group.second;
sort(ys.begin(), ys.end());
for (size_t i = 0; i < ys.size(); i += 2) {
int y1 = ys[i], y2 = ys[i + 1];
verticalSegments.emplace_back(segmentId++, group.first, y2, y1, false);
perimeter += (y2 - y1);
}
}
// 检查是否无自交且是简单多边形
if (!checkNoImproperIntersections(horizontalSegments, verticalSegments) ||
!isSimplePolygon(horizontalSegments, verticalSegments)) {
return false;
}
cout << perimeter << endl;
return true;
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
int testCases;
cin >> testCases;
while (testCases--) {
if (!solveTestCase())
cout << -1 << endl;
}
return 0;
}
方案二:并查集实现(推荐)
// Rectilinear Polygon
// UVa ID: 11106
// Verdict: Accepted
// Submission Date: 2025-11-30
// UVa Run Time: 0.000s
//
// 版权所有(C)2025,邱秋。metaphysis # yeah dot net
#include <bits/stdc++.h>
using namespace std;
struct Point {
int x, y, id;
Point(int x = 0, int y = 0, int id = 0) : x(x), y(y), id(id) {}
};
class UnionFind {
private:
vector<int> parent;
vector<int> rank;
public:
UnionFind(int n) : parent(n), rank(n, 0) {
for (int i = 0; i < n; i++) parent[i] = i;
}
int find(int x) {
if (parent[x] != x) parent[x] = find(parent[x]);
return parent[x];
}
void unite(int a, int b) {
int rootA = find(a);
int rootB = find(b);
if (rootA == rootB) return;
if (rank[rootA] < rank[rootB]) {
parent[rootA] = rootB;
} else if (rank[rootA] > rank[rootB]) {
parent[rootB] = rootA;
} else {
parent[rootB] = rootA;
rank[rootA]++;
}
}
bool hasSingleComponent() {
int root = find(0);
for (int i = 1; i < parent.size(); i++) {
if (find(i) != root) return false;
}
return true;
}
};
bool solveTestCase() {
int n;
cin >> n;
vector<Point> points(n);
map<int, int> xCount, yCount;
for (int i = 0; i < n; i++) {
cin >> points[i].x >> points[i].y;
points[i].id = i;
xCount[points[i].x]++;
yCount[points[i].y]++;
}
// 检查奇偶性条件
for (auto& p : xCount)
if (p.second % 2 != 0) return false;
for (auto& p : yCount)
if (p.second % 2 != 0) return false;
// 按坐标分组构建边
map<int, vector<int>> byY, byX;
for (auto& p : points) {
byY[p.y].push_back(p.id);
byX[p.x].push_back(p.id);
}
UnionFind uf(n);
long long perimeter = 0;
// 构建水平边并连接端点
for (auto& group : byY) {
vector<int>& ids = group.second;
// 按x坐标排序对应的点
sort(ids.begin(), ids.end(), [&](int a, int b) {
return points[a].x < points[b].x;
});
for (size_t i = 0; i < ids.size(); i += 2) {
int id1 = ids[i], id2 = ids[i + 1];
uf.unite(id1, id2);
perimeter += (points[id2].x - points[id1].x);
}
}
// 构建垂直边并连接端点
for (auto& group : byX) {
vector<int>& ids = group.second;
// 按y坐标排序对应的点
sort(ids.begin(), ids.end(), [&](int a, int b) {
return points[a].y < points[b].y;
});
for (size_t i = 0; i < ids.size(); i += 2) {
int id1 = ids[i], id2 = ids[i + 1];
uf.unite(id1, id2);
perimeter += (points[id2].y - points[id1].y);
}
}
// 如果只有一个连通分量,则必定是简单多边形
if (!uf.hasSingleComponent())
return false;
cout << perimeter << endl;
return true;
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
int testCases;
cin >> testCases;
while (testCases--) {
if (!solveTestCase())
cout << -1 << endl;
}
return 0;
}
总结
本题的关键在于发现直角多边形的特殊性质:在满足奇偶性条件的前提下,单连通分量等价于简单多边形。这一发现让我们能够用高效的并查集替代复杂的扫描线算法,大大简化了代码实现并提高了运行效率。
通过这个问题,我们看到了算法优化的重要性——深入理解问题本质往往能找到比通用算法更高效的特定解决方案。
4741

被折叠的 条评论
为什么被折叠?



