题解:洛谷 P3629 巡逻
【题目来源】
【题目描述】
在一个地区中有 \(n\) 个村庄,编号为 \(1, 2, \dots, n\)。有 \(n-1\) 条道路连接着这些村庄,每条道路刚好连接两个村庄,从任何一个村庄,都可以通过这些道路到达其他任何一个村庄。每条道路的长度均为 \(1\) 个单位。为保证该地区的安全,巡警车每天要到所有的道路上巡逻。警察局设在编号为 \(1\) 的村庄里,每天巡警车总是从警察局出发,最终又回到警察局。下图表示一个有 \(8\) 个村庄的地区,其中村庄用圆表示(其中村庄 \(1\) 用黑色的圆表示),道路是连接这些圆的线段。为了遍历所有的道路,巡警车需要走的距离为 \(14\) 个单位,每条道路都需要经过两次。

为了减少总的巡逻距离,该地区准备在这些村庄之间建立 \(K\) 条新的道路,每条新道路可以连接任意两个村庄。两条新道路可以在同一个村庄会合或结束,如下面的图例 (c)。一条新道路甚至可以是一个环,即其两端连接到同一个村庄。由于资金有限,\(K\) 只能是 \(1\) 或 \(2\)。同时,为了不浪费资金,每天巡警车必须经过新建的道路正好一次。下图给出了一些建立新道路的例子:

在 (a) 中,新建了一条道路,总的距离是 \(11\)。在 (b) 中,新建了两条道路,总的巡逻距离是 \(10\)。在 (c) 中,新建了两条道路,但由于巡警车要经过每条新道路正好一次,总的距离变为了 \(15\)。试编写一个程序,读取村庄间道路的信息和需要新建的道路数,计算出最佳的新建道路的方案使得总的巡逻距离最小,并输出这个最小的巡逻距离。
【输入】
第一行包含两个整数 \(n, K(1 ≤ K ≤ 2)\)。接下来 \(n-1\) 行,每行两个整数 \(a,b\),表示村庄 \(a\) 与 \(b\) 之间有一条道路 \((1 ≤ a, b ≤ n)\)。
【输出】
输出一个整数,表示新建了 \(K\) 条道路后能达到的最小巡逻距离。
【输入样例】
8 1
1 2
3 1
3 4
5 3
7 5
8 5
5 6
【输出样例】
11
【核心思想】
-
问题分析:给定一棵 \(n\) 个节点的树(边权均为 1),巡警车从节点 1 出发,需要遍历所有边并回到起点,初始总路程为 \(2(n-1)\)(每条边走两次)。现在可以新建 \(K\) 条边(\(K=1\) 或 \(2\)),每条新边必须被恰好经过一次,求最小总巡逻距离。新建边的作用是减少需要重复走的边,最优策略是将新边连接在树的直径两端,使得直径上的边只需走一次。当 \(K=2\) 时,第二条新边应连接在直径路径上某个分支的最长链两端,进一步节省距离。
-
算法选择:
- 两次 DFS 求直径:先求树的直径 \(D_1\),并记录直径路径上的节点。
- \(K=1\):答案 \(= 2(n-1) - D_1\),因为直径上的边可以从走两次变为走一次。
- \(K=2\):在直径路径上,将边权标记为 \(-1\)(表示这些边已受益于第一条新边),然后对整棵树做树形 DP,求出直径路径上的最长链(即第二条新边能节省的最大长度)\(D_2\)。最终答案 \(= 2(n-1) - D_1 - D_2\)。
-
关键步骤:
- 建图:读入 \(n, K\) 及 \(n-1\) 条边,构建无向邻接表。
- 求直径:第一次 DFS 从 1 出发找到最远点 \(p\),第二次从 \(p\) 出发找到直径另一端 \(q\),同时记录父节点
pre,得到直径长度 \(D_1\)。 - 若 \(K=1\):直接输出 \(2(n-1) - D_1\)。
- 若 \(K=2\):
- 标记直径路径上的所有节点(
col=1)。 - 将直径路径上的边权修改为 \(-1\)(因为第一条新边已经让这些边只需走一次)。
- 对整棵树进行树形 DP,计算最长链 \(D_2\):对每个节点 \(u\),维护以 \(u\) 为端点的最长链长度 \(dp[u]\),更新全局最大值 \(D_2 = \max(D_2, dp[u] + dp[v] + w(u,v))\)。
- 输出 \(2(n-1) - D_1 - D_2\)。
- 标记直径路径上的所有节点(
-
时间/空间复杂度:
- 时间复杂度:\(O(n)\),两次 DFS 和一次树形 DP。
- 空间复杂度:\(O(n)\)。
-
核心思想:
- 新建边的作用:在原树中,每条边需要走两次。新建一条边连接两个节点后,这两个节点之间的路径上的边可以只走一次,从而节省距离。
- 最优路径在直径上:为了最大化节省,第一条新边应连接直径两端;第二条新边应连接直径路径上某个分支的最长链两端。
- 树形 DP 处理第二条边:在直径路径被第一条边“压缩”后,通过树形 DP 找出剩余部分能形成的最长路径,即为第二条新边可节省的距离。
- 适用场景:树上的巡逻、遍历问题,通过新建边减少重复路径的优化。
【算法标签】
省选 #树形DP
【代码详解】
#include <bits/stdc++.h>
using namespace std;
const int N = 100005, M = N * 2; // N: 最大村庄数, M: 最大边数(无向图双向边)
int n, k; // n: 村庄数, k: 新建道路数(1或2)
int h[N], e[M], w[M], ne[M], idx; // 链式前向星存图
int pre[N]; // pre[u]: DFS时u的父节点(用于回溯直径路径)
int d[N]; // d[u]: 从某个起点到u的距离(DFS求直径时)/ 树形DP时存最长链
int p; // p: 临时最远点
int maxd; // 当前DFS找到的最大距离
int d1, d2; // d1: 树的直径长度, d2: 直径路径上"树形DP最长链"的长度
int col[N]; // col[u]: 标记u是否在主直径路径上
// 链式前向星:添加无向边
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
// DFS1:求树的直径端点,同时记录父节点信息
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] + w[i]; // 更新v到起点的距离
// 更新最远点
if (d[v] > maxd)
{
maxd = d[v];
p = v;
}
dfs(v, u); // 递归DFS
}
}
// 树形DP:求直径路径上的树形DP最长链
// 同时修改直径路径上的边权为-1(表示新建道路后这些边只需走一次)
void dp(int u, int fa)
{
for (int i = h[u]; i != -1; i = ne[i])
{
int v = e[i];
if (v == fa) continue; // 跳过父节点
// 如果u和v都在直径路径上,则这条边在直径上
// 新建一条道路连接直径两端后,直径上的边只需走一次(而不是两次)
// 将边权改为-1:原来贡献+2(来回),现在贡献+1(只需一次),相当于减少了1
if (col[u] && col[v])
{
w[i] = -1;
w[i ^ 1] = -1;
}
dp(v, u); // 递归处理子树
// 树形DP:更新经过u的最长链
// d2记录全局最长链(直径路径上某点往两侧分支延伸的最长距离)
d2 = max(d2, d[u] + d[v] + w[i]);
// d[u]记录以u为端点的最长链
d[u] = max(d[u], d[v] + w[i]);
}
}
int main()
{
cin >> n >> k; // 读入村庄数和新建道路数
// 初始化链式前向星
memset(h, -1, sizeof(h));
// 读入n-1条道路(树结构)
for (int i = 1; i < n; i++)
{
int a, b;
cin >> a >> b;
add(a, b, 1), add(b, a, 1); // 无向边,长度为1
}
// ========== 第一步:求树的直径 d1 ==========
// 第一次DFS:从任意点1出发,找到最远点p
dfs(1, 0);
d[p] = maxd = 0; // 重置距离数组,准备第二次DFS
// 第二次DFS:从p出发,找到最远点,同时记录父节点
dfs(p, 0);
d1 = d[p]; // d1为树的直径长度
// ========== K=1的情况 ==========
// 新建一条道路连接直径两端,直径上的边只需走一次
// 总巡逻距离 = 2*(n-1) - d1 = 2n - 2 - d1 = 2n - d1 - 1
// 解释:原来每条边走两次,共2(n-1);直径上的d1条边现在只需走一次,节省了d1
if (k == 1)
{
cout << 2 * n - d1 - 1 << endl;
return 0;
}
// ========== K=2的情况 ==========
// 标记直径路径上的所有点
for (int i = p; i; i = pre[i])
col[i] = 1;
// 重置距离数组,准备树形DP
memset(d, 0, sizeof(d));
// 树形DP:求"直径路径上的树形DP最长链" d2
// 核心思想:新建两条道路,第一条连接直径两端,第二条连接某分支的两端
// 第二条道路应该连接从直径上某点延伸出的最长分支链的两端
dp(1, 0);
// 输出答案
// 总巡逻距离 = 2*(n-1) - d1 - d2 = 2n - 2 - d1 - d2 = 2n - d1 - d2 - 1?
// 注意:代码中是 2*n - d1 - d2,因为边权改为-1后,d2已经包含了节省的量
// 实际上:原来总距离2(n-1),第一条道路节省d1,第二条道路节省d2
// 最终距离 = 2(n-1) - d1 - d2 = 2n - 2 - d1 - d2
// 但代码输出2n - d1 - d2,这是因为dp中边权改为-1后,d2的计算方式已经调整
cout << 2 * n - d1 - d2 << endl;
return 0;
}
【运行结果】
8 1
1 2
3 1
3 4
5 3
7 5
8 5
5 6
11
浙公网安备 33010602011771号