算法
(二分,树的直径,贪心,树的遍历) $O(NlogS)$
题目中说所有直径的中点均重合。
偏心距也有类似的性质:不管从哪条直径来求最小偏心距,答案都是唯一的。
因此我们可以先找出任意一条直径,这里可以用两次找最长路的方式:
- 任选一点作为起点,找出距离起点的最远点
u
; - 再找出距离
u
最远的点v
,则u
和v
之间的路径就是树的一条直径。
然后枚举最小偏心距,判断在直径上是否存在一段长度不超过 $s$ 的路径,使得其余所有点到路径的距离小于等于枚举的值。
如果偏心距等于 $d$ 是满足的,那么当偏心距大于 $d$ 时也一定可以满足,因此我们可以通过二分来枚举。
接下来在直径上找到与 u
的距离不超过 mid
的前提下,距离u
最远的节点,作为节点 p
。类似地,在直径上找到与 v
的距离不超过 mid
的前提下,距离 v
最远的节点,作为节点 q
。
根据直径的最长性,任何从 u, p
之间分叉离开直径的子树,其最远点与 p
的距离都不会比 u
更远。所以 p, q
就是在满足直径两侧的那部分节点偏心距不超过 mid
的前提下,尽量靠近树网中心的节点。
接下来判断 p, q
之间的距离是否不超过 s
,以及p, q
之间的所有点到其他所有点的最短距离是否不超过mid
即可。
时间复杂度
假设所有边长的总和是 $S$,树中节点数是 $N$,则我们一共会二分 $logS$ 次,每次判断需要 $O(N)$ 的时间,因此总时间复杂度是 $O(NlogS)$。
C++ 代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
const int N = 500010, M = N * 2;
int n, s;
int h[N], e[M], w[M], ne[M], idx;
int q[N], dist[N], pre[N];
vector<PII> path;
bool st[N];
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
void bfs(int start)
{
int hh = 0, tt = 0;
memset(dist, 0x3f, sizeof dist);
memset(pre, -1, sizeof pre);
dist[start] = 0;
q[0] = start;
while (hh <= tt)
{
int t = q[hh ++ ];
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if (dist[j] > dist[t] + w[i])
{
pre[j] = t;
dist[j] = dist[t] + w[i];
q[ ++ tt] = j;
}
}
}
}
int bfs_max_dist(int start)
{
int res = 0;
int hh = 0, tt = 0;
q[0] = start;
while (hh <= tt)
{
int t = q[hh ++ ];
res = max(res, dist[t]);
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if (!st[j])
{
st[j] = true;
dist[j] = dist[t] + w[i];
q[ ++ tt] = j;
}
}
}
return res;
}
int get_max()
{
int t = 1;
for (int i = 1; i <= n; i ++ )
if (dist[t] < dist[i])
t = i;
return t;
}
bool check(int mid)
{
int u = 0, v = path.size() - 1;
while (u + 1 < path.size() && path[u + 1].second <= mid) u ++ ;
while (v - 1 >= 0 && path.back().second - path[v - 1].second <= mid) v -- ;
if (u > v) return true;
if (path[v].second - path[u].second > s) return false;
memset(st, false, sizeof st);
memset(dist, 0, sizeof dist);
for (auto p : path) st[p.first] = true;
for (int i = u; i <= v; i ++ )
if (bfs_max_dist(path[i].first) > mid)
return false;
return true;
}
int main()
{
scanf("%d%d", &n, &s);
memset(h, -1, sizeof h);
for (int i = 0; i < n - 1; i ++ )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c), add(b, a, c);
}
bfs(1);
int u = get_max();
bfs(u);
int v = get_max();
while (v != -1)
{
path.push_back({v, dist[v]});
v = pre[v];
}
reverse(path.begin(), path.end());
int l = 0, r = 2e9;
while (l < r)
{
int mid = (LL)l + r >> 1;
if (check(mid)) r = mid;
else l = mid + 1;
}
printf("%d\n", r);
return 0;
}
这题空间限制不能放的大一点吗
为什么枚举u~v的结点再DFS找最远距离这一过程是 $O(n)$ 的呢?
说错了,BFS
因为这是一棵树,被st[]分成多段了,每一次只遍历其中的未被标记的那一段
终于看懂这道题了。。O(n)的做法太难写了
yxc nb!!!!!!!
不能发图,就口述一下想法:
假设z为所有直径交点,以z为根去dfs整棵树处理每个点到z的距离d,因为直径一定过z点,所以离z点最远的点一定是直径的端点之一,假设离z最远的点为p:
如果p>2个,则选择一条直径(d[p1],d[p2])后,还会剩至少一个d[p3],d[p3]>d[x],x是p之外的任意一点,所以这个时候偏心距应该是d[p3]。
如果p=2个,则只有一条直径,偏心距肯定是唯一确定的。
如果p=1个,则直径可以分成p~z段和z~q段,无论怎么选直径一定会把p~z段选上去,因为p~z是距离最大的,z~q段长度和其余长度相加一定会小于p~q段长度。
如果q只有一个,则只有一条直径,则偏心距肯定是唯一确定的。
如果q不止一个,则直径至少有两条,选择一条直径(d[p],d[q1])后,至少还会剩一段d[q2]满足d[q2]>d[x],x是除p,q外的任意一点,因为如果d[x]>d[q2],那直径就应该是p~x而不是p~q了,所以上式一定成立,此时偏心距应该是d[q2],也是唯一确定的。
很棒!
不对啊应该是在直径上取两个端点而不是取整个直径啊。
为什么求两次最长距离就能确定直径
这是一个经典知识点,可以参考1072. 树的最长路径。
太强了 膜拜
谢谢hh
不过弱弱问一句,偏心距的最大值应该不会超过直径长,二分上界改为直径长应该可以吧?
那个多直径的偏心距,是不是因为如果核不经过中点,那么偏心距必然大于直径的一半,而经过中点,偏心距就是直径的一半。所以多直径时核一定经过中点且偏心距是直径的一半?
%%%yxc太强了
谢谢2333
# 前排资瓷+好评
谢谢hh