P2680 [NOIP2015 提高组] 运输计划

题目来源

[NOIP2015 提高组] 运输计划 - 洛谷

题目考点

图论   最近公共祖先,LCA   树链部分,树剖

题目背景

公元 2044 年,人类进入了宇宙纪元。

题目描述

公元 2044 年,人类进入了宇宙纪元。

L 国有 n 个星球,还有 n−1 条双向航道,每条航道建立在两个星球之间,这 n−1 条航道连通了 L 国的所有星球。

小 P 掌管一家物流公司, 该公司有很多个运输计划,每个运输计划形如:有一艘物流飞船需要从 ui​ 号星球沿最快的宇航路径飞行到vi​ 号星球去。显然,飞船驶过一条航道是需要时间的,对于航道 j,任意飞船驶过它所花费的时间为 tj​,并且任意两艘飞船之间不会产生任何干扰。

为了鼓励科技创新, L 国国王同意小 P 的物流公司参与 L 国的航道建设,即允许小 P 把某一条航道改造成虫洞,飞船驶过虫洞不消耗时间。

在虫洞的建设完成前小 P 的物流公司就预接了 m 个运输计划。在虫洞建设完成后,这 m 个运输计划会同时开始,所有飞船一起出发。当这 m 个运输计划都完成时,小 P 的物流公司的阶段性工作就完成了。

如果小 P 可以自由选择将哪一条航道改造成虫洞, 试求出小 P 的物流公司完成阶段性工作所需要的最短时间是多少?

输入格式

第一行包括两个正整数 n,m,表示 L 国中星球的数量及小 P 公司预接的运输计划的数量,星球从 1 到 n 编号。

接下来 n−1 行描述航道的建设情况,其中第 ii 行包含三个整数 ai​,bi​ 和 ti​,表示第 i 条双向航道修建在 ai​ 与 bi​ 两个星球之间,任意飞船驶过它所花费的时间为 ti​。

数据保证

接下来 m 行描述运输计划的情况,其中第 j 行包含两个正整数 uj​ 和 vj​,表示第 j 个运输计划是从 uj​ 号星球飞往 vj​号星球。

输出格式

一个整数,表示小 P 的物流公司完成阶段性工作所需要的最短时间。

输入输出样例

输入 #1

6 3 
1 2 3 
1 6 4 
3 1 7 
4 3 6 
3 5 5 
3 6 
2 5 
4 5

输出 #1

11

说明/提示

所有测试数据的范围和特点如下表所示

请注意常数因子带来的程序效率上的影响。

对于 100% 的数据,保证:1≤ai​,bi​≤n,0≤ti​≤1000,1≤ui​,vi​≤n。

题解

这里提供一种比较暴力的做法

因为码量较大,适合对线段树,树状数组,树剖熟悉的同学做


直观地理解题目:

  • 在一颗有边权的树上有m条路径,清零一条边的边权使得m条路径的最大值最小。

对于最暴力的做法:

  • 把n-1条边,每条都做:清零当前边,重新统计路径最小值。

对于上面这种暴力做法,肯定会T,我们先解决几个问题并想想怎么优化:

如何求树上路径的值?

  • 树剖后,对于一条边(x,y),我们选取最深的那个点作为边的代表,易证一个点只会对应一条边且树根没有对应边(画个图很好理解的)。这样就把边的问题转化成点的问题了,加棵线段树统计即可。这个已经是很日常的套路了。

我们必须要试全部的n-1条边嘛?

  • 对于还没有清零过的原图,我们先算出每条路径的值。对于当前最长的路径A如果我们删除A以外的边,此时的最大值肯定还是A。也就是说,我们要删除的边肯定在A上,这样就不一定要试全部的边了(特殊情况可能还是会)。所以说我们记录下最长路径的数据,在这里暂时设为最长路径的数据为(a,b,c)

我们每次都要重新统计嘛?

  • 很容易能想到,如果清零一条边B,在m条路径中,如果某条路径不包含B是不用更新的。

  • 因此我们换个思路,如果我们清零B,要更新答案,只需要知道经过B的路径中最大的那一个不经过B的路径中最大的那一个,分别设为路径C,D。更新答案,只需(取C减去边B的值不经过B最大的路径值)求最大值,再和已统计答案取最小值即可。

  • 即:ans=min(ans,max(C-B,不经过B)) (为了方便描述,用编号代替权值)

  • 对于C,因为我们只在A上清零边,又因为A是最长路径,C=A。那么怎么求D就是关键了:

  • 对于D,我们的定义是不经过当前边的最大路径,为了时间复杂度,我们可以预处理一下每条边对应的D:

  • 设mx[k]为不包含k这条边最长路径权值,对于一条路径,设它包含的边的集合为E,整张图边的集合为R,路径的权值为t。那么mx中的哪些位置需要拿E来更新?显然是mx[R-E]对自己和t求最大值(mx[R-E]=max(mx[R-E],t)),这里的R-E也就是E的补集,即所有不在这条路径上的边

  • 对于补集R-E我们肯定不能一个个更新。因为树剖后,一条路径可以转化成几条链,又因为一条链(不管轻重)上对于的线段树编号肯定是连续的,我们能先在这个路径上跳,记录下第i条链的数据[xi,yi],根据xi或者yi排序后,我们得到几个有序而且不相交的区间[xi,yi],那么就在[1,n]上取这些区间的补集mx上更新路径的值t即可:

  • 即[1,x1-1],[y1+1,x2-1],[y2+1,x3-1]...[y(end)+1,n],注意最前和最后的两个区间,小心x1==1和yn==n,需要特判,不然线段树可能会出错。

  • 求出了mx数组后,我们很容易能想到,如果清零一条边B,设其权值为k,当前所有路径的最大值会变成max(c-k,mx[B]),c为最大路径的权值,c-k就相当于清零B,mx[B]就是不经过B的最长路径。


这样我们就把问题都解决了,所以解题步骤是:

  • 1.读入数据这么多确定不写个快读
  • 2.把求路径的问题转化上。
  • 3.求出路径最长的路径A记录下它的信息(a,b,c)
  • 4.预处理mx数组**(mx要对应到线段树上,因为要多次区间修改)**
  • 5.在(a,b)路径上的每条边都做:清零,更新一次最小的最大路径值,即答案。

=====

  • 对于5,具体步骤是:枚举a,b上的每条边,设答案为ans,当前想清零的边为B,权值为k,那么:
  • ans = min(ans, max(c - k,mx[B]))。(mx[B]用线段树求出)

最后再强调一下需要注意的问题:

  • 1.快读!!!

  • 2.在线段树上求值的时候不要把公共祖先算进去,因为LCA代表的边不在路径上,可以在最后(设两点为x,y且x深度小于y,对应的线段树编号为id[x],id[y])添加成update(id[x]+1,id[y])。

  • 3.取区间补集时特判第一和最后的区间,详情见上面的内容。

  • 4.第15号点,路径会有x==y的情况,用树剖可能需要特判为0.

  • 5.本程序开O2交13号点RE,不开O2可以过??(我不知道)


CODE:

(因为方法几乎都是数据结构,而且步骤已经讲的非常清楚了,所以注释只标个大方向,就不每行解释了,而且代码量也挺大)

​
#pragma warning (disable:4996)
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define RG register
#define mid ((x+y)>>1)
#define lson (pst<<1)
#define rson (pst<<1|1)
using namespace std;
const int maxn = 3e5 + 5, maxm = maxn << 1, inf = 0x7fffffff;
int x[maxn], y[maxn], z[maxn], p[maxn];//x,y,z为每条边的数据,p[x]为x代表边的权值
int head[maxm], nxt[maxm], v[maxm], cnt;//前向星
int son[maxn], dad[maxn], sz[maxn], depth[maxn], root;//树剖dfs1
int id[maxn], top[maxn], rak[maxn], num;//树剖dfs2
int c[maxn], d[maxn], srt[maxn];//记录区间并排序的数组
int ma, mb, mc;//最大路径的记录
int n, m;

struct Binary_Indexed_Tree//求和树状数组
{
	int a[maxn];

	inline int lowbit(int k) { return k & (-k); }
	inline void update(int x, int k) { for (int i = x; i <= n; i += lowbit(i))	a[i] += k; }
	inline int query(int x) { int i = x, ans = 0; for (i = x; i >= 1; i -= lowbit(i))	ans += a[i]; return ans; }
	inline void build(int x) { for (int i = 1; i <= n; i++)	update(i, p[rak[i]]); }
	inline int sum(int l, int r) { return query(r) - query(l - 1); }
}BIT;

inline int max(int x, int y) { return x > y ? x : y; }
inline int min(int x, int y) { return x < y ? x : y; }

struct Segment_Tree//最大值线段树
{
	int mx[maxn << 2], tag[maxn << 2];

	inline void pushdown(int pst)
	{
		if (!tag[pst])	return;
		int k = tag[pst];
		mx[lson] = max(mx[lson], k), mx[rson] = max(mx[rson], k);
		tag[lson] = max(tag[lson], k), tag[rson] = max(tag[rson], k);
		tag[pst] = 0; return;
	}

	inline void pushup(int pst) { mx[pst] = max(mx[lson], mx[rson]); }

	inline void update(int x, int y, int pst, int l, int r, int k)
	{
		if (x > y || y<l || x>r)	return;
		if (l <= x && y <= r) { mx[pst] = max(mx[pst], k), tag[pst] = max(tag[pst], k); return; }
		pushdown(pst);
		update(x, mid, lson, l, r, k), update(mid + 1, y, rson, l, r, k);
		pushup(pst); return;
	}

	inline int query(int x, int y, int pst, int p)
	{
		if (x == y)	return mx[pst];
		pushdown(pst);
		if (p <= mid)	return query(x, mid, lson, p);
		else return query(mid + 1, y, rson, p);
	}
}ST;

inline void addline(int x, int y) { v[cnt] = y, nxt[cnt] = head[x], head[x] = cnt++; }

inline int read()
{
	RG char c = getchar(); RG int x = 0;
	while (c<'0' || c>'9')	c = getchar();
	while (c >= '0'&&c <= '9')	x = (x << 3) + (x << 1) + c - '0', c = getchar();
	return x;
}

inline void dfs1(int x, int f, int d)//树剖
{
	dad[x] = f, depth[x] = d, sz[x] = 1;
	for (RG int i = head[x]; ~i; i = nxt[i])
	{
		if (v[i] == f)	continue;
		dfs1(v[i], x, d + 1);
		sz[x] += sz[v[i]];
		if (sz[v[i]] > sz[son[x]])	son[x] = v[i];
	}
	return;
}

inline void dfs2(int x, int t)//树剖
{
	top[x] = t, id[x] = ++num, rak[id[x]] = x;
	if (!son[x])	return;
	dfs2(son[x], t);
	for (RG int i = head[x]; ~i; i = nxt[i])
		if (v[i] != dad[x] && v[i] != son[x])	dfs2(v[i], v[i]);
	return;
}

inline int sum(int x, int y)//求某条路径的权值
{
	RG int tx = top[x], ty = top[y], ans = 0;
	while (tx != ty)
	{
		if (depth[tx] >= depth[ty])	ans += BIT.sum(id[tx], id[x]), x = dad[tx], tx = top[x];
		else ans += BIT.sum(id[ty], id[y]), y = dad[ty], ty = top[y];
	}
	if (id[x] <= id[y])	ans += BIT.sum(id[x] + 1, id[y]);
	else ans += BIT.sum(id[y] + 1, id[x]);
	return ans;
}

inline bool cmp(int x, int y) { return c[x] < c[y]; }

inline void update(int x, int y, int z)//更新mx数组(其实是更新最大值线段树)
{
	RG int tx = top[x], ty = top[y], t = 0;
	while (tx != ty)
	{
		if (depth[tx] >= depth[ty]) c[++t] = id[tx], d[t] = id[x], x = dad[tx], tx = top[x];
		else c[++t] = id[ty], d[t] = id[y], y = dad[ty], ty = top[y];
	}
	if (id[x] <= id[y])	c[++t] = id[x] + 1, d[t] = id[y];
	else c[++t] = id[y] + 1, d[t] = id[x];
	for (int i = 1; i <= t; i++)	srt[i] = i;
	sort(srt + 1, srt + t + 1, cmp);
	if (c[srt[1]] > 1)	ST.update(1, n, 1, 1, c[srt[1]] - 1, z);
	if (d[srt[t]] < n)	ST.update(1, n, 1, d[srt[t]] + 1, n, z);
	for (int i = 1; i < t; i++)	ST.update(1, n, 1, d[srt[i]] + 1, c[srt[i + 1]] - 1, z);
	return;
}

inline int find_ans(int x, int y)//在最大路径上遍历并清零边求答案
{
	RG int ans = inf;
	if (x == y)	return 0;
	if (depth[x] < depth[y])	swap(x, y);
	while (depth[x] != depth[y])	ans = min(ans, max(mc - p[x], ST.query(1, n, 1, id[x]))), x = dad[x];
	while (x != y)
	{
		if (depth[x] > depth[y])	ans = min(ans, max(mc - p[x], ST.query(1, n, 1, id[x]))), x = dad[x];
		else ans = min(ans, max(mc - p[y], ST.query(1, n, 1, id[y]))), y = dad[y];
	}
	return ans;
}

int main(void)
{
	memset(head, -1, sizeof(head));
	n = read(), m = read();
	for (int i = 1; i < n; i++)	x[i] = read(), y[i] = read(), z[i] = read();
	for (int i = 1; i < n; i++)	addline(x[i], y[i]), addline(y[i], x[i]);
	root = rand() % n + 1, dfs1(root, 0, 1), dfs2(root, root);//树剖
	for (int i = 1; i < n; i++)
	{
		if (depth[x[i]] > depth[y[i]])	p[x[i]] = z[i];//把深度大的点作为一条边的代表
		else p[y[i]] = z[i];
	}
	BIT.build(n);
	for (int i = 1; i <= m; i++)
	{
		RG int a = read(), b = read(), temp;
		temp = sum(a, b), update(a, b, temp);
		if (temp >= mc)	ma = a, mb = b, mc = temp;//求最大路径
	}
	printf("%d\n", find_ans(ma, mb));
	return 0;
}

​

真的很用心写的一篇题解,留个赞再走吧QwQ

  • 6
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值