题解 P2491 【[SDOI2011]消防】
知识点 : 树的直径, 单调队列
原题面
P1099 树网的核 的 数据加强版
题目要求 :
给定一棵树 , 边有边权
求一条 边长度和 \(\le s\) 的路径 ,
使其他所有点 到这条路径的距离的 最大值最小 .
分析题意 :
-
对于树的直径 , 有一 性质 :
对于 任意树上的节点 , 距离其最远的点 一定为树的直径的端点-
证明 : 详见此篇博客 : 树的直径 - Luckyblock - 博客园
-
则可 先求得树的直径, 并记录树的直径上的点
由于要记录路径 , 使用 \(\text{DFS}\) 处理较为容易
之后就可以 枚举树的直径上 长度 \(\le s\) 的合法区间
-
-
对于每一个 合法区间 ,
如何求得 其他所有点 到这条路径的距离的最大值 ?
对路径外的点 进行分类讨论 :-
对于在直径上的点 ,
显然, 直径的端点 距离此区间的端点的距离, 比非端点远则只需考虑 直径两端点 到达区间对应两端点的 距离
可以对 直径上的点使用前缀和 维护到达两端点的距离 , 来做到 \(O(1)\) 查询 -
对于在直径外的点 ,
显然, 当直径上 与其距离最近的点 在区间内时 , 才有可能 做出贡献
否则 选择直径的端点 必然比选择它更优则可以维护 从各直径点 , 不经过直径其他点, 能到达的点的 最远距离
-
-
在枚举区间时 需要维护 距选择路径上的点 最远的 直径外的点
显然这是一个滑动窗口最值问题 , 可以使用单调队列维护
算法实现 :
-
先使用 \(\text{DFS}\) 求得直径上的点
可以通过 求得两端点, 然后记录直径上每一个点的前驱, 来记录路径 -
使用 \(\text{BFS}\) 求得 从各直径点 , 不经过直径其他点, 能到达的点的 最远距离
- 先将直径上的点 加入队列, 然后向外进行扩展
- 由于树上两点之间 只有一条简单路径 ,
则按照上述规则进行扩展, 扩展到的最远的点, 一定为 从此点, 不经过直径其他点, 能到达的点的 最远距离
-
使用单调队列, 枚举每一个合法区间, 并取得最优解
-
固定左端点, 找到合法的右端点 ,
并维护 队列中的 距选择路径上的点 最远的 直径外的点 -
将队首元素 与 该区间端点与直径端点的距离 进行比较, 最大值即该合法区间的 答案
取最小的 合法区间答案 作为最终答案 -
区间左端点 右移
-
附代码 :
#include <cstdio>
#include <cstring>
#include <ctype.h>
#include <queue>
#define int long long
#define max(a, b) (a > b ? a : b)
#define min(a, b) (a < b ? a : b)
const int INF = 1e15 + 7;
const int MARX = 3e5 + 10;
//=============================================================
struct edge
{
int u, v, w, ne;
}e[MARX << 1];
int n, num, s, u, v, head[MARX];//建图变量
int dis[MARX], pre[MARX], map[MARX];//求树的直径, dis记录距离, pre记录直径上点的前驱, map存直径上两相邻点的距离
int ans = 1e15, sum[MARX], dis1[MARX];//sum记录直径上距离的前缀和, dis1记录直径上点i到 直径外点的最长距离
int que[MARX] = {INF}, t = 0, h = 0;//单调队列-
//=============================================================
inline int read()
{
int s = 1, w = 0; char ch = getchar();
for(; ! isdigit(ch); ch = getchar()) if(ch == '-') s = -1;
for(; isdigit(ch); ch = getchar()) w = w * 10 + ch - '0';
return s * w;
}
void add(int u, int v, int w)
{
e[++ num].u = u, e[num].v = v, e[num].w = w;
e[num].ne = head[u], head[u] = num;
}
void dfs(int now, int fat, int sum, bool flag)//dfs求得 树的直径
{
if(flag) pre[now] = fat, map[now] = sum;//第二次dfs记录路径 (前驱
dis[now] = dis[fat] + sum;//更新距离
for(int i = head[now]; i; i = e[i].ne)
if(e[i].v != fat) dfs(e[i].v, now, e[i].w, flag);
}
void get_road()//求得 树的直径
{
dfs(1, 0, 0ll, 0); //一次dfs
for(int i = 1, maxdis = 0; i <= n; i ++)//选择 距离最远的点
if(dis[i] > maxdis) u = i, maxdis = dis[i];
dfs(u, 0, 0ll, 1); //二次dfs
for(int i = 1, maxdis = 0; i <= n; i ++)//选择 距离最远的点
if(dis[i] > maxdis) v = i, maxdis = dis[i];
}
void bfs()//bfs处理 以每个直径上的点为起点, 不经过直径上其他点, 能到达的点的 最远距离
{
memset(dis, 63, sizeof(dis));
std :: queue <int> q, from;
for(int i = v; i != 0; i = pre[i]) //将直径上的点加入 队列
q.push(i), from.push(i), dis[i] = 0;
for(; !q.empty();)
{
int now = q.front(), fr = from.front(); q.pop(), from.pop();
for(int i = head[now]; i ; i = e[i].ne)//枚举出边
if(dis[e[i].v] >= INF)//未被更新过
{
dis[e[i].v] = dis[now] + e[i].w;//更新最远距离
dis1[fr] = max(dis1[fr], dis[e[i].v]);
q.push(e[i].v), from.push(fr);
}
}
}
void solve()
{
pre[n + 1] = v;
for(int i = n + 1; i != 0; i = pre[i]) //预处理前缀和
sum[pre[i]] = sum[i] + map[i];
for(int l = v, r = v; l != 0 && r != u; l = pre[l])//当r=u时停止枚举,之后枚举的区间都不合法
{
int last = r; ++ h;
while(sum[r] - sum[l] <= s && r != 0) //枚举右端点
{
last = r, r = pre[r];
if(r != 0 && sum[r] - sum[l] <= s)//单调队列更新
{
for(;dis1[r] >= que[t] && t >= h;) t--;
que[++ t] = dis1[r];
}
}
if(r == 0 || sum[r] - sum[l] > s) r = last;//越界处理
int now = max(sum[l] , sum[u] - sum[r]);//更新答案
now = max(now, que[h]);
ans = min(now, ans);
}
}
//=============================================================
signed main()
{
n = read(), s = read();
for(int i = 1; i < n ;i ++)
{
int u1 = read(), v1 = read(), w1 = read();
add(u1, v1, w1), add(v1, u1, w1);
}
get_road(); bfs(); solve();
printf("%lld", ans);
}