NOIP 2015 运输计划 二分+差分 || 前缀和O(n)

题目链接:运输计划

主要思路1:

(注:以下路径耗时与路径长度是同一回事)

由于本题求的是所有任务中所需时间最长的时间的最小值,故可以想到二分答案。二分所需的最长时间记为lim,check函数就找出所有任务所需时间超过lim的任务。并将其路径用差分在树上标记(区间覆盖-->差分)。不难知道只有让被所有不满足任务路径所覆盖的路变成虫洞才有可能使这个lim可行。故在这种边中找一条最大的边的长度记为mx,用MX(即为所有任务中所需时间最长的任务的所需时间)-mx与lim比较,若MX-mx>lim则不可行,若不存在这种边也是不可行。反之则可行。(这样子时间有点久,故在redfs时直接逆序for一遍dfs序而不是递归)

AC代码:

#include<cstdio>
#include<cstring>
#include<algorithm>
#define M 300005
using namespace std;
struct E{
	int to,nx,d;
}edge[M<<1];
int tot,head[M];
void Addedge(int a,int b,int d){
	edge[++tot].to=b;
	edge[tot].nx=head[a];
	edge[tot].d=d;
	head[a]=tot;
}
struct Que{
	int st,ed,d;
}Q[M];
int fa[M],sz[M],son[M],dep[M],dis[M],P[M],T,D[M];
void dfs(int now) {
	P[++T]=now;
	sz[now]=1;
	son[now]=0;
	for(int i=head[now]; i; i=edge[i].nx) {
		int nxt=edge[i].to;
		if(nxt==fa[now])continue;
		fa[nxt]=now;
		dep[nxt]=dep[now]+1;
		dis[nxt]=dis[now]+edge[i].d;
		D[nxt]=edge[i].d;
		dfs(nxt);
		sz[now]+=sz[nxt];
		if(sz[nxt]>sz[son[now]])son[now]=nxt;
	}
}
int top[M];
void dfs_top(int now) {//跳重链求LCA 
	if(son[now]) {
		top[son[now]]=top[now];
		dfs_top(son[now]);
	}
	for(int i=head[now]; i; i=edge[i].nx) {
		int nxt=edge[i].to;
		if(nxt==fa[now]||nxt==son[now])continue;
		top[nxt]=nxt;
		dfs_top(nxt);
	}
}
int LCA(int a,int b) {
	while(top[a]!=top[b]) {
		if(dep[top[a]]<dep[top[b]])b=fa[top[b]];
		else a=fa[top[a]];
	}
	return dep[a]<dep[b]?a:b;
}
int lca[M];
int get_dis(int a,int b,int x) {
	return dis[a]+dis[b]-2*dis[x];
}
int cnt[M],mx,Mx,ner;
bool ok;
void redfs(){
	for(int i=T;i>=2;i--){//因为DFS常数太大,故用DFS序枚举即可 
		int now=P[i];
		for(int j=head[now];j;j=edge[j].nx){
			if(edge[j].to==fa[now])continue;
			cnt[now]+=cnt[edge[j].to];
		}
		if(cnt[now]==ner)Mx=max(D[now],Mx),ok=1;//若他被所有不可行的路径覆盖 
	}
}
bool check(int X,int n,int m) {
	ner=0;
	memset(cnt,0,sizeof(cnt));
	for(int i=1; i<=m; i++)if(Q[i].d>X)cnt[Q[i].st]++,cnt[Q[i].ed]++,cnt[lca[i]]-=2,ner++;//边i表示其到达i这个点的边 
	ok=0;
	Mx=0;
	redfs();
	if(mx-Mx>X)return false;
	if(!ok)return false;//若不存在这样的边 
	return true;
}
void solve(int n,int m) {
	int l=0,r=mx,ans=0;//二分最大的路径长度 
	while(l<=r) {
		int mid=(l+r)>>1;
		if(check(mid,n,m)) {
			ans=mid;
			r=mid-1;
		} else l=mid+1;
	}
	printf("%d\n",ans);
}
void Init(int n,int m) {
	top[1]=1;
	dfs(1);
	dfs_top(1);
	for(int i=1; i<=m; i++)lca[i]=LCA(Q[i].st,Q[i].ed);//预处理出LCA 
	for(int i=1; i<=m; i++) {
		Q[i].d=get_dis(Q[i].st,Q[i].ed,lca[i]);//预处理出每条路径的长度 
		mx=max(Q[i].d,mx);
	}
}
int main() {
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=1;i<n;i++){
		int a,b,d;
		scanf("%d%d%d",&a,&b,&d);
		Addedge(a,b,d);
		Addedge(b,a,d);
	}
	for(int i=1;i<=m;i++)scanf("%d%d",&Q[i].st,&Q[i].ed);
	Init(n,m);
	solve(n,m);
	return 0;
}

主要思路2:

进一步思考题目可以知道:

性质1:最后建设虫洞的边一定是在最长的路径上的。(不将最长路径上的边变为虫洞那么ans肯定还为这条最长路径的长度)

性质2:建设虫洞后的答案为max(不经过这条虫洞的最长路径,最长的任务路径-这条建设虫洞的边的长度,不与最长的路径有公共边的路径的最大值)。

知道以上性质这道题目就挺简单了。

1.把任务中耗时最长的路径(像直径一样)拉出来,并标号。(以下称为直径)

2.把直径上每一个点向下DFS(不回到直径),并标记每个点是从直径上的哪个点下来的,存入KIND数组中。

3.将一一枚举其他路径,记路径两端点为的Kind值为x,y(x<=y).若x==y则这条路径不与我们的直径相交,记录这种路径长度的最大值。否则更新aft[x]=max(aft[x],这条边的长度),pre[y]=max(pre[y],这条边的长度)(aft[i]为路径直径上i这个点右边的所有任务的耗时最大值,pre[i]为路径在直径上i这个点左边所有任务的耗时最大值)

4.for一遍直径上的所有点aft[i]=max(aft[i],aft[i+1]),pre[i]=max(pre[i],pre[i-1])(即预处理前缀和)

5.一一枚举直径上的点,(现在要建设虫洞的边为i->i+1)ans=min(ans,max(最长的任务路径-这条建设虫洞的边的长度,不与最长的路径有公共边的路径的最大值,aft[i+1],pre[i]));(此处max(pre[i],aft[i+1])即为不经过这条虫洞的最长路径)

注意初始值ans=最长路径的长度,否则碰到m==1&&这条边起点和终点重合会挂。

以下有一张图方便理解(图有点丑,将就一下)

若还有不懂,可以自己画图试一试理解一下。

AC代码:

#include<cstdio>
#include<algorithm>
#define max4(a,b,c,d) max(max(a,b),max(c,d))
#define M 300005
using namespace std;
struct E{
	int to,nx,d;
}edge[M<<1];
int tot,head[M];
void Addedge(int a,int b,int d){
	edge[++tot].to=b;
	edge[tot].nx=head[a];
	edge[tot].d=d;
	head[a]=tot;
}
struct line{
	int st,ed,lca,d;
}Q[M];
int fa[M],sz[M],son[M],dep[M],dis[M];
void dfs(int now){
	son[now]=0;
	sz[now]=1;
	for(int i=head[now];i;i=edge[i].nx){
		int nxt=edge[i].to;
		if(nxt==fa[now])continue;
		fa[nxt]=now;
		dep[nxt]=dep[now]+1;
		dis[nxt]=dis[now]+edge[i].d;
		dfs(nxt);
		sz[now]+=sz[nxt];
		if(sz[son[now]]<sz[nxt])son[now]=nxt;
	}
}
int top[M];
void dfs_top(int now){//跳重链求LCA 
	if(son[now]){
		top[son[now]]=top[now];
		dfs_top(son[now]);
	}
	for(int i=head[now];i;i=edge[i].nx){
		int nxt=edge[i].to;
		if(nxt==fa[now]||nxt==son[now])continue;
		top[nxt]=nxt;
		dfs_top(nxt);
	}
}
int LCA(int a,int b){
	while(top[a]!=top[b]){
		if(dep[top[a]]<dep[top[b]])b=fa[top[b]];
		else a=fa[top[a]];
	}
	return dep[a]<dep[b]?a:b;
}
int chain[M],id;
int stk[M],Top;
bool mark[M];
int Kind[M];
void redfs(int now,int kind){
	Kind[now]=kind;
	for(int i=head[now];i;i=edge[i].nx){
		int nxt=edge[i].to;
		if(nxt==fa[now]||mark[nxt])continue;
		fa[nxt]=now;
		redfs(nxt,kind);
	}
}
int pre[M],aft[M];
void solve(int n,int m){
	int a=Q[1].st,b=Q[1].ed,x=Q[1].lca;
	while(a!=x){//把这条路径拉出来 
		mark[a]=1;
		chain[++id]=a;
		a=fa[a];
	}
	chain[++id]=x;
	mark[x]=1;
	while(b!=x){
		mark[b]=1;
		stk[++Top]=b;
		b=fa[b];
	}
	while(Top)chain[++id]=stk[Top--];
	int res=-1;
	for(int i=1;i<=id;i++){//对于每一个点染色 
		fa[chain[i]]=-1;
		redfs(chain[i],i);
	}
	for(int i=2;i<=m;i++){
		int from=Kind[Q[i].st],to=Kind[Q[i].ed];
		if(from==to){//这条路径与最长路径没有公共边 
			res=max(res,Q[i].d);
			continue;
		}
		if(from>to)swap(from,to);//保证from<to 
		pre[to]=max(pre[to],Q[i].d);
		aft[from]=max(aft[from],Q[i].d);
	}
	for(int i=1;i<=id;i++)pre[i]=max(pre[i],pre[i-1]);//处理前缀 
	for(int i=id;i>=1;i--)aft[i]=max(aft[i],aft[i+1]);//处理后缀
	int ans=Q[1].d;//初始值,否则碰到m==1&&只有一条起点和终点重合的任务会挂 
	for(int i=1;i<id;i++){
		int D=dis[chain[i]]-dis[chain[i+1]];//D为这条路径的长度 
		if(D<0)D=-D;
		ans=min(ans,max4(pre[i],aft[i+1],Q[1].d-D,res));
	}
	printf("%d\n",ans);
}
void Init(){
	dfs(1);
	top[1]=1;
	dfs_top(1);
}
int main(){
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=1;i<n;i++){
		int a,b,d;
		scanf("%d%d%d",&a,&b,&d);
		Addedge(a,b,d);
		Addedge(b,a,d);
	}
	Init();
	int mxid=0,mxd=0; 
	for(int i=1;i<=m;i++){
		scanf("%d%d",&Q[i].st,&Q[i].ed);
		Q[i].lca=LCA(Q[i].st,Q[i].ed);
		Q[i].d=dis[Q[i].st]+dis[Q[i].ed]-2*dis[Q[i].lca];
		if(mxd<Q[i].d)mxd=Q[i].d,mxid=i;
	}
	swap(Q[mxid],Q[1]);//把最长边与第一条边交换一下。 
	solve(n,m);
	return 0;
}

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值