题解 P2491 【[SDOI2011]消防】

知识点 : 树的直径, 单调队列

原题面

P1099 树网的核 的 数据加强版

题目要求 :

给定一棵树 , 边有边权
求一条 边长度和 \(\le s\) 的路径 ,
使其他所有点 到这条路径的距离的 最大值最小 .

分析题意 :

  • 对于树的直径 , 有一 性质 :
    对于 任意树上的节点 , 距离其最远的点 一定为树的直径的端点

    • 证明 : 详见此篇博客 : 树的直径 - Luckyblock - 博客园

    • 则可 先求得树的直径, 并记录树的直径上的点
      由于要记录路径 , 使用 \(\text{DFS}\) 处理较为容易
      之后就可以 枚举树的直径上 长度 \(\le s\) 的合法区间

  • 对于每一个 合法区间 ,
    如何求得 其他所有点 到这条路径的距离的最大值 ?
    对路径外的点 进行分类讨论 :

    1. 对于在直径上的点 ,
      显然, 直径的端点 距离此区间的端点的距离, 比非端点远

      则只需考虑 直径两端点 到达区间对应两端点的 距离
      可以对 直径上的点使用前缀和 维护到达两端点的距离 , 来做到 \(O(1)\) 查询

    2. 对于在直径外的点 ,
      显然, 当直径上 与其距离最近的点 在区间内时 , 才有可能 做出贡献
      否则 选择直径的端点 必然比选择它更优

      则可以维护 从各直径点 , 不经过直径其他点, 能到达的点的 最远距离

  • 在枚举区间时 需要维护 距选择路径上的点 最远的 直径外的点
    显然这是一个滑动窗口最值问题 , 可以使用单调队列维护

算法实现 :

  1. 先使用 \(\text{DFS}\) 求得直径上的点
    可以通过 求得两端点, 然后记录直径上每一个点的前驱, 来记录路径

  2. 使用 \(\text{BFS}\) 求得 从各直径点 , 不经过直径其他点, 能到达的点的 最远距离

    • 先将直径上的点 加入队列, 然后向外进行扩展
    • 由于树上两点之间 只有一条简单路径 ,
      则按照上述规则进行扩展, 扩展到的最远的点, 一定为 从此点, 不经过直径其他点, 能到达的点的 最远距离
  3. 使用单调队列, 枚举每一个合法区间, 并取得最优解

    • 固定左端点, 找到合法的右端点 ,
      并维护 队列中的 距选择路径上的点 最远的 直径外的点

    • 将队首元素 与 该区间端点与直径端点的距离 进行比较, 最大值即该合法区间的 答案
      取最小的 合法区间答案 作为最终答案

    • 区间左端点 右移


附代码 :

#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);
}
posted @ 2019-11-01 08:00  Luckyblock  阅读(143)  评论(0编辑  收藏  举报