NOIP2015 运输计划

#$Description$ [题面](https://www.luogu.org/problem/P2680)

给你一颗\(n\)个节点带权的树以及\(m\)条路径端点,你可以将一条边的权值设为\(0\),要求使得操作后\(m\)条路径中的最长路径最短

这道题有很多解法,我在复习一种算法后更新一种

\(Solution1\)

二分+贪心+\(LCA\)+树上差分
这道题显然不是裸的树上差分,但需要树上差分进行\(check\)操作
首先我们预处理出\(m\)条路径的长度,这个是可以使用前缀和\(O(n)\)求的,具体方法是在\(dfs\)过程中记录从根节点到\(u\)的距离\(f[u]\),求一条路径\(u,v\)的长度只需要
\(dis[u]+[v]-2*dis[lca(u,v)]\)即可,画个图就很显然了,这是很重要的前缀和思想

看到题目中"使得最大距离最小"这句话,显然需要二分答案,我们二分操作后(将一条边权值归零)的最大距离,关键在于如何写\(check\)
考虑记录每一条长度比\(mid\)大的路径,用边差分记录一下,假设这样的路径有\(cnt\)条,那么显然这\(cnt\)条路径需要删除一条公共边的权值
所以我们\(dfs\)一遍,对差分数组求前缀和

假设没有一条边被经过\(cnt2\)次,说明这\(cnt\)条路径没有公共边,显然不能通过删一条边来达到\(mid\),直接\(return\ false\)
若有多条边被经过\(cnt2\)次,贪心取最大的一条即可,因为删去的权值越大越能接近合法(显然)。
最后将\(cnt\)条路径长度的最大值减去找到的最大公共边,如果仍然\(>mid\)\(return\ false\),否则\(return \ true\)

总结

1.在\(dfs1\)(预处理\(lca\))的过程中处理到根的距离
2.二分最大长度
3.对大于二分值的路径差分,\(dfs2\)寻找是否可以通过删去一条公共边使得最大长度\(<mid\)
时间复杂度\(O((n+m)logn)\),由于倍增\(LCA\)常数比较大,在当年\(noip\)是拿不到满分的,但是\(luogu\)数据比较水\(AC\)
后面会介绍其他能通过此题(常数比较小)的算法

\(Code1\)

#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#define re register
#define maxn 300010
using namespace std;
inline int read()
{
	int x=0,f=1; char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}
struct Edge{
	int v,w,nxt;
}e[maxn<<2];
int maxx,cnt2,tmp2,es[maxn],guide;
int fr[maxn],to[maxn],num[maxn],sums2;
int a[maxn],z,x,y,cc[maxn];
int cf[maxn],ans,val[maxn],tmp,dep[maxn],head[maxn],cnt,dis[maxn];
int n,m,lg[maxn],num2,f[maxn][23];
inline void add(int u,int v,int w)
{
	e[++cnt].v=v;
	e[cnt].w=w;
	e[cnt].nxt=head[u];
	head[u]=cnt;
}
void pre()
{
	for(re int i=1,num2=0;i<=n;i*=2) lg[i]=num2++; 
	for(re int i=1;i<=n;++i) if(!lg[i]) lg[i]=lg[i-1];
}
void dfs(int u,int fa,int w)
{;
	dep[u]=dep[fa]+1;
	f[u][0]=fa;
	dis[u]=dis[fa]+w;//预处理 
    es[u]=w;//将边权固定到点上 
	for(int i=1;(1<<i)<=dep[u];++i)
	 f[u][i]=f[f[u][i-1]][i-1];
	for(int i=head[u];i;i=e[i].nxt)
	{
		int ev=e[i].v;
		if(ev!=fa) dfs(ev,u,e[i].w);
	}
}
int lca(int x,int y)
{
	if(dep[x]<dep[y]) swap(x,y);
    for(re int i=lg[dep[x]-dep[y]];i>=0;--i)
    {
    	if(dep[f[x][i]]<dep[y]) continue;
    	x=f[x][i];
	}
	if(x==y) return x;
	for(re int i=lg[dep[x]-1];i>=0;--i)
	{
		if(f[x][i]==f[y][i]) continue;
		x=f[x][i],y=f[y][i];
	}
	return f[x][0];
}
void dfs2(int u,int fa)
{
	for(int i=head[u];i;i=e[i].nxt)
	{
		//int sums=0;
		int ev=e[i].v;
		if(ev==fa) continue;
		dfs2(ev,u);
		a[u]+=a[ev]; 
	}
	a[u]+=cf[u];//更新当前的前缀和 
	if(a[u]>=cnt2)
	{
		guide=max(guide,es[u]);
	}
}
bool check(int x)//删去一条边后最大边是否<=mid 
{
	maxx=0,cnt2=0,tmp2=0;
	memset(cf,0,sizeof(cf));
	memset(a,0,sizeof(num));
	for(re int i=1;i<=m;++i)
	{
		if(val[i]>x)
		{
			cf[fr[i]]++;
			cf[to[i]]++;//边差分 
			cf[cc[i]]-=2;
			cnt2++;
			maxx=max(maxx,val[i]);//记录最大长度 
		}
	}
	guide=0;//guide表示最大公共边,guide=0表示没找到 
	dfs2(1,0);
	if(!guide) return false;
	if(maxx-guide>x) return false;
	else return true;
}
int erfen()
{
	int l=0,r=sums2;
	while(l<=r)
	{
		int mid=(l+r)>>1;
		if(check(mid)) r=mid-1;
		else l=mid+1;
	}
	return l;
}
int main()
{
	n=read(),m=read();
	for(re int i=1;i<n;++i)
	{
		x=read(),y=read(),z=read();
		add(x,y,z);
		add(y,x,z);
		sums2+=z;
	}
	for(re int i=1;i<=m;++i) fr[i]=read(),to[i]=read();
	pre();
	dfs(1,0,0);
	for(re int i=1;i<=m;++i)
	{
		
		tmp=lca(fr[i],to[i]);
		cc[i]=tmp;//记录一下最近公共祖先,常数优化 
		val[i]=dis[fr[i]]+dis[to[i]]-dis[tmp]*2;//前缀和O(1)计算 
	}
	printf("%d\n",erfen());
	return 0;
} 
posted @ 2019-10-15 11:21  __Liuz  阅读(121)  评论(0编辑  收藏  举报