「NOIP2018」赛道修建 - 二分答案 + 贪心

题意描述

洛谷链接

LibreOJ 链接

C 城将要举办一系列的赛车比赛。在比赛前,需要在城内修建 \(m\) 条赛道。

C 城一共有 \(n\) 个路口,这些路口编号为 \(1,2, \cdots , n\),有 \(n − 1\) 条适合于修建赛道的双向通行的道路,每条道路连接着两个路口。其中,第 \(i\) 条道路连接的两个路口编号为 \(a_i\)\(b_i\),该道路的长度为 \(l_i\)。借助这 \(n − 1\) 条道路,从任何一个路口出发都能到达其他所有的路口。

一条赛道是一组互不相同的道路 \(e_1, e_2, \cdots , e_k\),满足可以从某个路口出发,依次经过道路 \(e_1, e_2, \cdots , e_k\)(每条道路经过一次,不允许调头)到达另一个路口。一条赛道的长度等于经过的各道路的长度之和。为保证安全,要求每条道路至多被一条赛道经过。

目前赛道修建的方案尚未确定。你的任务是设计一种赛道修建的方案,使得修建的 \(m\) 条赛道中长度最小的赛道长度最大(即 \(m\) 条赛道中最短赛道的长度尽可能大)。

解题思路

本题求的是最小赛道的最大值,可二分答案。而难点则在二分答案的判定上。

设二分时赛道长度为 \(\text{limit}\),我们需要考虑一颗子树上形成的长度满足条件的赛道最多有多少个。对于这个问题,我们可以贪心解决。

设对于 \(v\) 为树上一节点,\(u\)\(v\) 的父节点。设以 \(u\)\(v\) 为根的子树分别最多有 \(f_u\)\(f_v\) 个赛道,\(u\) 可利用 \(v\) 的最长路径为 \(g_v\)

对于 \(u \stackrel{w}{\longrightarrow} v\),我们有两种情况:

  • \(w + g_v \ge \text{limit}\),此时我们可直接将该边与 \(v\) 中剩余的路径连起来形成一条新的赛道。即 \(f_u = f_v + 1\)
  • \(w + g_v \ge \text{limit}\),此时我们无法形成一条完整的赛道。此时我们需要将这个路径保存以便后续使用。

对于节点 \(u\) 保存的路径,我们有两种情况:

  1. 由于保存的路径端点均为 \(u\),我们可将两条路径连在一起形成一条赛道;
  2. 将路径保存至 \(g_u\),以便 \(u\) 的父节点使用。

我们可以证明:我们尽量满足情况 \(1\),对于无法满足情况 \(1\) 的路径中取最长路径满足情况 \(2\)。这种方法不劣。因为对于 \(u\) 的父节点,我们至多只能形成一条利用 \(u\) 的赛道。而对于对 \(u\) 的父节点有用的路径,使用情况 \(1\) 和情况 \(2\) 对答案的贡献是等价的。对于对 \(u\) 的父节点无用的路径,使用情况 \(1\) 显然更优。

对于路径的保存,我们用 multiset 保存,用 lower_bound 查询可连接成赛道的路径即可。时间复杂度 \(O(n \log n \log L)\),其中 \(L\) 为边权和。

代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include <cstdio>
#include <iostream>
#include <vector>
#include <set>

const int MAXN = 5e4;

struct Node {
std::vector<struct Edge> e;
std::multiset<int> s;
} N[MAXN + 1];

struct Edge {
Node *s, *t;
int w;

Edge(Node *s, Node *t, int w) : s(s), t(t), w(w) {}
};

int n, m;
int cnt = 0;

inline void addEdge(int s, int t, int w) {
N[s].e.push_back(Edge(&N[s], &N[t], w));
N[t].e.push_back(Edge(&N[t], &N[s], w));
}

int dfs(Node *v, int limit, Node *f = NULL) {
v->s.clear();
for (Edge *e = &v->e.front(); e && e <= &v->e.back(); e++) {
if (e->t == f) continue;
int res = dfs(e->t, limit, v);
if (res + e->w >= limit) cnt++;
else v->s.insert(res + e->w);
}

int max = 0;
while (!v->s.empty()) {
if (v->s.size() == 1) return std::max(max, *v->s.begin());
std::multiset<int>::iterator it = v->s.lower_bound(limit - *v->s.begin());
if (it == v->s.begin() && v->s.count(*it) == 1) it++;
if (it == v->s.end()) {
max = std::max(max, *v->s.begin());
v->s.erase(v->s.begin());
} else {
cnt++;
v->s.erase(it);
v->s.erase(v->s.begin());
}
}

return max;
}

int main() {
scanf("%d %d", &n, &m);

int max = 0;
for (int i = 1; i <= n - 1; i++) {
int a, b, l;
scanf("%d %d %d", &a, &b, &l);
addEdge(a, b, l);
max += l;
}

int l = 0, r = max;
while (l < r) {
int mid = (l + r + 1) / 2;
cnt = 0;
dfs(&N[1], mid);
if (cnt >= m) l = mid;
else r = mid - 1;
}

printf("%d\n", l);

return 0;
}