题解:洛谷 P2610 旅游
【题目来源】
【题目描述】
到了难得的暑假,为了庆祝小白在数学考试中取得的优异成绩,小蓝决定带小白出去旅游~~
经过一番抉择,两人决定将 T 国作为他们的目的地。T 国的国土可以用一个凸 \(n\) 边形来表示,\(n\) 个顶点表示 \(n\) 个入境/出境口。T 国包含 \(n-2\) 个城市,每个城市都是顶点均为 \(n\) 边形顶点的三角形(换而言之,城市组成了关于 T 国的一个三角剖分)。两人的旅游路线可以看做是连接 \(n\) 个顶点中不相邻两点的线段。

为了能够买到最好的纪念品,小白希望旅游路线上经过的城市尽量多。作为小蓝的好友,你能帮帮小蓝吗?
【输入】
每个输入文件中仅包含一个测试数据。
第一行包含一个正整数 \(n\),\(n\) 的含义如题目所述。
接下来有 \(n-2\) 行,每行包含三个整数 \(p,q,r\),表示该城市三角形的三个顶点的编号(T 国的 \(n\) 个顶点按顺时针方向从 \(1\) 至 \(n\) 编号)。
【输出】
输出文件共包含一行,表示最多经过的城市数目。(一个城市被当做经过当且仅当其与线路有至少两个公共点)
【输入样例】
6
1 2 4
2 3 4
1 4 5
1 5 6
【输出样例】
4
【核心思想】
-
问题分析:给定一个凸 \(n\) 边形的三角剖分(共 \(n-2\) 个三角形),用一条连接多边形两个不相邻顶点的线段(即一条对角线)去穿过这些三角形,要求最大化该线段穿过的三角形数量(一个三角形被计入当且仅当线段与它有至少两个公共点,即线段穿过其内部或沿其边经过)。本质是将对偶图转化为树,并在树上求最长路径的问题。
-
算法选择:
- 对偶图建树:将每个三角形视为一个节点,若两个三角形共享一条边(即该边是多边形内部的一条对角线),则在它们之间连一条无向边。由于三角剖分是对偶树,该图是一棵树。
- 求树的直径(节点数):旅游路线(一条对角线)穿过的三角形序列正好对应这棵树上的一条简单路径。经过的三角形数量 = 路径上的节点数 = 路径边数 + 1。因此,问题等价于求这棵对偶树的直径(最长路径)的节点数。
- 两次 DFS 求直径:任选一点出发,找到最远节点,再从该节点出发找到最远距离,即为直径长度(边数),加 1 即为最多经过的城市数。
-
关键步骤:
- 读入并建图:读入 \(n\) 和 \(n-2\) 个三角形,对每个三角形的三条边(三个顶点对)进行排序统一表示,用
map<pair<int,int>, int>记录每条边首次出现的三角形编号;若某条边第二次出现,则在当前三角形与首次出现的三角形之间加一条无向边。 - 对偶树 DFS:以任意三角形(如编号 1)为起点,进行 DFS,记录每个节点到起点的距离(边数)和父节点,找到最远节点
p。 - 第二次 DFS:从
p出发,再次 DFS,找到最远距离maxd,即为对偶树的直径(边数)。 - 输出答案:
maxd + 1(因为路径上的节点数比边数多 1)。
- 读入并建图:读入 \(n\) 和 \(n-2\) 个三角形,对每个三角形的三条边(三个顶点对)进行排序统一表示,用
-
时间/空间复杂度:
- 时间复杂度:\(O(n \log n)\),主要来自
map查找和排序,\(n \le 2\times 10^5\),可接受。 - 空间复杂度:\(O(n)\),存储邻接表和辅助数组。
- 时间复杂度:\(O(n \log n)\),主要来自
-
对偶树与直径的核心思想:
- 三角剖分的对偶图是树:每个三角形是节点,共享边是边,该图无环且连通,故为树。
- 对角线穿过的三角形序列在树上形成路径:任意一条多边形内部的线段依次穿过的三角形,必然在对偶树上形成一条简单路径。
- 最大化三角形数量 = 树的最长路径节点数:直径路径上的节点数即为能穿过的最多三角形数,无需考虑具体端点,因为任意直径路径的两端对应两个顶点,它们之间的对角线正好穿过这些三角形。
- 适用场景:适用于将几何剖分问题转化为图论树的最长路径问题,是“对偶图”思想的典型应用。
【算法标签】
普及+ #树形DP
【代码详解】
#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII; // 用于map的键:存储两个顶点组成的边
const int N = 200005, M = N * 2; // N: 最大城市数, M: 最大边数
int n; // n: 凸n边形的顶点数
int h[N], e[M], ne[M], idx; // 链式前向星存图
int l, r, p, maxd; // l: 直径左端点, r: 直径右端点, p: 临时最远点, maxd: 当前最大距离
int d[N]; // d[u]: 从某个起点到城市u的距离
int pre[N]; // pre[u]: DFS时城市u的父节点
map<PII, int> mp; // mp[{a, b}]: 边(a,b)第一次出现的城市编号
// 链式前向星:添加无向边
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
// DFS:求对偶树的直径端点,同时记录父节点信息
void dfs(int u, int fa)
{
pre[u] = fa; // 记录u的父节点
// 遍历u的所有邻接城市
for (int i = h[u]; i != -1; i = ne[i])
{
int v = e[i];
if (v == fa) continue; // 跳过父节点
d[v] = d[u] + 1; // 更新v到起点的距离(对偶树中边权为1)
// 更新最远点
if (d[v] > maxd)
{
maxd = d[v];
p = v;
}
dfs(v, u); // 递归DFS
}
}
int main()
{
cin >> n; // 读入顶点数
// 初始化链式前向星
memset(h, -1, sizeof(h));
// 读入n-2个三角形城市
for (int i = 1; i <= n - 2; i++)
{
int p, q, r;
cin >> p >> q >> r;
// 将三个顶点排序,保证边的表示唯一(小边在前)
if (p > q) swap(p, q);
if (p > r) swap(p, r);
if (q > r) swap(q, r);
// 处理边(p,q):如果这条边第一次出现,记录城市编号
// 如果第二次出现,说明两个城市共享这条边,在对偶树中建立连接
if (!mp[{p, q}])
mp[{p, q}] = i;
else
add(i, mp[{p, q}]), add(mp[{p, q}], i);
// 处理边(p,r)
if (!mp[{p, r}])
mp[{p, r}] = i;
else
add(i, mp[{p, r}]), add(mp[{p, r}], i);
// 处理边(q,r)
if (!mp[{q, r}])
mp[{q, r}] = i;
else
add(i, mp[{q, r}]), add(mp[{q, r}], i);
}
// ========== 求对偶树的直径 ==========
// 对偶树:每个城市是一个节点,共享边的城市之间连边
// 旅游路线经过的城市数 = 对偶树上某条路径的长度 + 1
// 第一次DFS:从城市1出发,找到最远点p
dfs(1, 0);
l = p; // l是直径的一个端点
d[l] = maxd = 0; // 重置距离数组,准备第二次DFS
// 第二次DFS:从l出发,找到最远点r
dfs(l, 0);
r = p; // r是直径的另一个端点
// 输出答案:对偶树的直径 + 1
// 直径d[r]表示经过的城市间隔数,+1表示经过的城市数量
cout << d[r] + 1 << endl;
return 0;
}
【运行结果】
6
1 2 4
2 3 4
1 4 5
1 5 6
4
浙公网安备 33010602011771号