Loading

「刷题记录」[NOIP2015 提高组] 运输计划题解

题目链接:运输计划
看了好长时间题解才终于懂的,有关 lca 和二分答案的题解解释的不详细,一时半会理解不过来,于是自己写一篇解释,尽管解释主要在代码中,希望能对迷茫的小伙伴有帮助
解析(主要为二分答案的解析,lca 只是求距离和找覆盖边时用得到,这里不多说):
由于 \(m\) 个运输计划是同时出发,所以所需要的时间取决于花费最长的时间,因为一个任务在 \(y\) 分钟内完成,那么另一个任务 \(x \le y\) 也一定完成。
问题就转化成了求最长边最短,二分答案。
接下来,就是怎么二分了
想让时间大于答案的任务的时间减小,题目里说了,可以设一条边为虫洞,我们就找这个任务里哪条(些)边可以设为虫洞,这条(些)边就是被覆盖的边,我们将它们记录一下
所以我们只要找这些超时的任务的公共覆盖边就好了,即这条(些)边被记录的次数等于超时的任务数
代码有注释,不懂可以去看一下代码

#include <iostream>
#include <cstdio>
#include <algorithm>
typedef long long ll;
using namespace std;
const ll N=3e5+5;
ll n, m, cnt;
ll h[N], lg[N], sum[N], deep[N], st[N][20];
ll vis[N], up[N];
struct edge//边 
{
	ll v, w, nxt;
} e[N << 1];
struct rw//任务 
{
	ll u, v, lca, dis;
	bool operator < (const rw x)//重载运算符 
	{
		return dis < x.dis;
	}
} g[N];

inline ll read()
{
	ll x = 0;
	bool flag = false;
	char ch = getchar();
	while(ch < '0' || ch > '9')
	{
		if(ch == '-')	flag = true;
		ch = getchar();
	}
	while(ch >= '0' && ch <= '9')
	{
		x=(x << 3) + (x << 1) + (ch ^ 48);
		ch = getchar();
	}
	return flag ? ~x + 1 : x;
}
void add(ll u, ll v, ll w)
{
	e[++cnt].v = v;
	e[cnt].w = w;
	e[cnt].nxt = h[u];
	h[u] = cnt;
}
void dfs(ll u,ll fat)
{
	deep[u] = deep[fat] + 1;//深度更新 
	st[u][0] = fat;//更新st表 
	for(ll i = 1; i <= lg[deep[u]]; ++i)
	{
		st[u][i] = st[st[u][i-1]][i-1];//更新st表 
	}
	for(ll i = h[u]; i; i = e[i].nxt)//遍历 
	{
		ll v = e[i].v, w = e[i].w;
		if(v != fat)//如果不是父节点 
		{
			sum[v] = (sum[u] + w);
			//到根节点的距离就是父节点到根节点的距离加上这条边的边权 
			up[v] = w;//处理到父节点的距离 
			dfs(v, u);//继续搜索 
		}
	}
}
ll LCA(ll x, ll y)//正常求LCA 
{
	if(deep[x] < deep[y])
	{
		x=x^y, y=x^y, x=x^y;//位运算版本的交换 
	}
	while(deep[x] > deep[y])
	{
		x=st[x][lg[deep[x] - deep[y]]];//让两点在同一深度 
	}
	if(x==y)    return x;
	for(ll i = lg[deep[x]]; i >= 0; --i)//找交汇之前最后到达的点 
	{
		if(st[x][i] != st[y][i])//如果不相交,向上跳 
		{
			x = st[x][i];
			y = st[y][i];
		}
	}
	return st[x][0];//返回lca 
}
bool check(ll len)
{
	if(g[m].dis <= len)    return true;
	//如果路程最长的都比不过当前二分的答案,那么这个答案是可行的,直接返回 
	for(ll i = 1; i <= n; ++i)    vis[i] = 0;//每次操作前,数组清0 
	ll cont = 0, maxn = 0;
	//cont 记录比当前答案大的边,maxn记录被覆盖次数最多的边被覆盖的次数
	//(我知道maxn这里我解释的很trouble,不好理解就把他看成每条边被覆盖的次数的最大值) 
	//一定要时刻记住maxn的含义!!! 
	for(ll i = m; i >= 1; --i)//遍历 
	{
		if(g[i].dis <= len)    break;
		//如果当前任务的路程已经小于答案,就没必要继续遍历查找了,退出即可 
		cont++;//当前任务路程比答案大,计数器+1 
		ll u = g[i].u, v = g[i].v;
		ll lca = g[i].lca, c = g[i].dis - len;
		//c 如果这条边大于或等于c,就说明删去它,这个任务的时间就小于或等于答案
		//即这条边可以设为虫洞 
		while(u != lca)//如果起点不是lca,那就向上找,查询被覆盖的边 
		{
			if(up[u] >= c)//大于c,说明在这个任务中,它可以设为虫洞,也就是上文说的覆盖 
			{
				vis[u]++;
				maxn = max(maxn, vis[u]);//更新maxn 
			}
			u = st[u][0];//往上慢慢找,时间限制是1s~2s 
		}
		while(v != lca)//如果终点不是lca,向上找 
		{
			if(up[v] >= c)//和上面一样 
			{
				vis[v]++;
				maxn = max(maxn, vis[v]);//更新maxn 
			}
			v = st[v][0];//慢慢跳(~ ̄▽ ̄)~ 
		}
		if(maxn < cont)	return false;
		//这里maxn只能小于等于cont,因为进行了cont次循环,一条边最多被覆盖cont次 
		//maxn是边被覆盖的次数的最大值
		//如果小于cont,就说明找不到一条边可以被删去后使任务的时间小于等于答案
		//所以这个答案不成立 
		//如果maxn==cont 就说明至少有一条边可以做到删去后使任务的时间小于等于答案 
	}
	return true;
}
int main()
{
	n = read(),m = read();
	for(ll i=1;i<n;++i)
	{
		ll x = read(), y = read(), z = read();
		add(x,y,z);
		add(y,x,z);
	}
	lg[0] = -1;
	for(ll i = 1;i <= n; ++i)
	{
		lg[i] = lg[i >> 1]+1;//预处理log,节约时间 
	}
	dfs(1,0);//搜索 
	for(ll i = 1; i <= m; ++i)//记录每一个计划的信息 
	{
		ll u = read(),v = read(),lca = LCA(u,v);
		g[i].u = u;//起点 
		g[i].v = v;//终点 
		g[i].lca = lca;//lca 
		g[i].dis = sum[u] + sum[v] - (sum[lca] << 1);//这个计划的长度 
	}
	sort(g + 1, g + m + 1);//将任务从小到大排序 
	ll l = 0, r = 3e8, ans;
	while(l < r)
		//注意!!!这里二分的是最终答案,也就是最小花费时间,清楚概念,否则后边就和我一样容易混 
	{
		ll mid = (l + r) >> 1;//二分操作 
		if(check(mid))    r = mid, ans = mid;
		else    l = mid + 1;
	}    
	printf("%lld\n",ans);
	return 0;
} 
posted @ 2022-07-03 17:34  yi_fan0305  阅读(114)  评论(0编辑  收藏  举报