NOIP2018D1T3赛道修建

题目描述

一道让人受益匪浅的树形DP+贪心二分题

C 城将要举办一系列的赛车比赛.在比赛前,需要在城内修建m条赛道.

C 城一共有n个路口,这些路口编号为1,2,…,n,有 n-1 条适合于修建赛道的双向通行的道路,每条道路连接着两个路口.其中,第i条道路连接的两个路口编号为 a i {a_i} ai b i {b_i} bi,该道路的长度为 l i {l_i} li​.借助这n-1条道路,从任何一个路口出发都能到达其他所有的路口。
一条赛道是一组互不相同的道路 e 1 , e 2 , … , e k {e_1,e_2,…,e_k} e1,e2,,ek,满足可以从某个路口出发,依次经过 道路 e 1 , e 2 , … , e k {e_1,e_2,…,e_k} e1,e2,,ek.

(每条道路经过一次,不允许调头)到达另一个路口.一条赛道的长度等于经过的各道路的长度之和.为保证安全,要求每条道路至多被一条赛道经过。

目前赛道修建的方案尚未确定.你的任务是设计一种赛道修建的方案,使得修建的m条赛道中长度最小的赛道长度最大.(即m条赛道中最短赛道的长度尽可能大)

输入格式

输入文件第一行包含两个由空格分隔的正整数n,m,分别表示路口数及需要修建的赛道数。

接下来n-1行,第i行包含三个正整数 a i , b i , l i {a_i,b_i,l_i} ai,bi,li表示第i条适合于修建赛道的道 路连接的两个路口编号及道路长度。保证任意两个路口均可通过这n-1条道路相互到达。每行中相邻两数之间均由一个空格分隔。

输出格式

输出共一行,包含一个整数,表示长度最小的赛道长度的最大值.

说明/提示
【输入输出样例 1 说明】

所有路口及适合于修建赛道的道路如下图所示:
道路旁括号内的数字表示道路的编号,非括号内的数字表示道路长度. 需要修建1条赛道.可以修建经过第3,1,2,6条道路的赛道(从路口4到路口7), 则该赛道的长度为 9+10+5+7 = 31,为所有方案中的最大值.
在这里插入图片描述
【输入输出样例 2 说明】
所有路口及适合于修建赛道的道路如下图所示:
需要修建3条赛道。可以修建如下3条赛道:
经过第 1,6条道路的赛道(从路口1到7),长度为 6+9=15;
经过第5,2,3,8条道路的赛道(从路口6到9),长度为4+3+5+4=16;
经过第7,4条道路的赛道(从路口8到5),长度为7+10=17.长度最小的赛道长度为15,为所有方案中的最大值。
https://cdn.luogu.com.cn/upload/pic/43163.png

数据规模与约定

2 ≤ n ≤ 5 × 1 0 4 ,   1 ≤ m ≤ n − 1 ,   1 ≤ a i , b i ≤ n ,   1 ≤ l i ≤ 1 0 4 {2 \le n \le 5\times 10^4, \ 1 \le m \le n − 1,\ 1 \le a_i,b_i \le n,\ 1 \le l_i \le 10^4} 2n5×104, 1mn1, 1ai,bin, 1li104.

解题思路

本题适合分点解决问题,即先拿部分暴力分,然后逃再思考正解.
这种方法可以较容易地帮助我们在考场上拿到较高得分.

1. b i = a i + 1 ( 图 为 一 条 链 ) o p t s ( 20 分 ) {1.b_i=a_i+1(图为一条链)opts(20分)} 1.bi=ai+1()opts(20).
这种情况很简单,即将一个序列分为m个区间,求可以分到的最小区间和的最大值.(经典贪心问题,部分代码如下)时间复杂度: O ( n   l o g n ) {O(n~logn)} O(n logn)

inline bool isok2(int kk)//核心部分
{
	int num=0,zhi=0;
	for(int i=1;i<n;i++)
	{
		if(zhi+b[i]>=kk)
		{
			zhi=0;
			num++;
		}
		else zhi+=b[i];
		if(num>=m)return true;
	}
	return false;
}

int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<n;i++)
	{
		scanf("%d%d%d",&a[i].x,&a[i].y,&a[i].val);
		b[a[i].x]=a[i].val;//便于写一些,无实质作用.
		if(a[i].x!=a[i].y-1)tal2=0;
	}
	sort(a+1,a+n,cmp1);
	l=1,r=1e9+7;
	while(l<r)
	{
		mi=(l+r+1)>>1;
		if(isok2(mi))l=mi,ans=mi;
		else r=mi-1;
	}
	printf("%d\n",ans);
	return 0;
}

2. a i = 1 ( 图 是 菊 花 图 ) o p t s ( 20 分 ) {2.a_i=1(图是菊花图)opts(20分)} 2.ai=1()opts(20).
这种情况也很容易处理,可以把问题转化为在一堆数中取m次数,每次取取=一到两个数,使得取出的最小数和最大.(还是一个经典二分,方法就是先将 l i {l_i} li从小到大排序,然后用指针在首尾各取一个再让指针逐渐向中间靠拢即可)时间复杂度: O ( n   l o g n ) {O(n~logn)} O(n logn)

#include<bits/stdc++.h>
#define N 50005
using namespace std;

int n,m,tal1=1,tal2=1,ans,l,r,mi;
struct llj{
int x,y,val;}a[N];
int b[N];

inline bool cmp(const llj &a,const llj &b)
{
	return a.val<b.val;
}

inline bool isok1(int kk)//菊花图核心代码
{
	int num=0,zhi=0,ll=1,rr=n-1;
	while(ll<=rr)
	{
		if(num>=m)return true;
		zhi=a[rr].val,rr--;
		if(zhi>=kk)
		{
			zhi=0;
			num++;
			continue;
		}
		else
		{
			while(zhi+a[ll].val<kk&&ll<=rr+1)ll++;
			if(ll>rr)return false;
			ll++;
			zhi=0;
			num++;
		}
	}
	if(num>=m)return true;
	return false;
}

inline bool isok2(int kk)
{
	int num=0,zhi=0;
	for(int i=1;i<n;i++)
	{
		if(zhi+b[i]>=kk)
		{
			zhi=0;
			num++;
		}
		else zhi+=b[i];
		if(num>=m)return true;
	}
	return false;
}

int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<n;i++)
	{
		scanf("%d%d%d",&a[i].x,&a[i].y,&a[i].val);
		b[a[i].x]=a[i].val;
		if(a[i].x!=a[i-1].x&&i>1)tal1=0;//判是否为菊花图
		if(a[i].x!=a[i].y-1)tal2=0;
	}
	if(tal1==1)
	{
		sort(a+1,a+n,cmp);
		l=1,r=1e9+7;
		while(l<r)
		{
			mi=(l+r+1)>>1;
			if(isok1(mi))l=mi,ans=mi;
			else r=mi-1;
		}
	}
	else if(tal2==1)//处理
	{
		l=1,r=1e9+7;
		while(l<r)
		{
			mi=(l+r+1)>>1;
			if(isok2(mi))l=mi,ans=mi;
			else r=mi-1;
		}
	}
	printf("%d\n",ans);
	return 0;
}

然后40分到手!接下来我们继续向高分迈进.
3. m = 1. ( 求 树 的 直 径 ) o p t s ( 15 − 20 分 ) {3.m=1.(求树的直径)opts(15-20分)} 3.m=1.()opts(1520)
这就是板子(不会点这里),我就不过多的讲述了.(部分代码如下)
时间复杂度: O ( n ) {O(n)} O(n).


inline void lian(int x,int y,int z)
{
	nxt[++tot]=fi[x];
	fi[x]=tot;
	to[tot]=y;
	w[tot]=z;
}

inline int Dfs(int u,int fa)//核心部分树形DP
{
	int sum1=0,sum2=0;
	for(int i=fi[u];i;i=nxt[i])
	{
		int v=to[i];
		if(v==fa)continue;
		sum2=max(sum2,Dfs(v,u)+w[i]);
		if(sum1<sum2)swap(sum1,sum2);
	}
	ans=max(ans,sum1+sum2);
	return sum1;
}

int main()
{
	for(int i=1;i<n;i++)
	{
		lian(a[i].x,a[i].y,a[i].val);
		lian(a[i].y,a[i].x,a[i].val);
		rd[a[i].x]++,rd[a[i].y++];
	}
	Dfs(1,0);
	printf("%d\n",ans);
	return 0;
}

然后55分暴力就到手了.接下来我们想正解.
容易想到的是用树形DP,同时题面要求的是“最短长度最长”,就是明显的二分答案.
那么我们取min(a[i])为左边界, Σ i = 1 n a [ i ] {\Sigma_{i=1}^{n}a[i]} Σi=1na[i]​, a [ i ] {a[i]} a[i]为右边界进行二分答案.
用两个数组 f [ i ] {f[i]} f[i] g [ i ] {g[i]} g[i]分别表示以i为根节点的子树中有多少条长度>=mid,去掉满足条件的路径后从ii往上连最长的长度.
在转移时将当前节点所有子节点的 g [ i ] {g[i]} g[i]加上 w [ i ] {w[i]} w[i]后加入一个multiset中,因为我们之后要先将长度满足>=mid的加到答案中,所以需要从大到小排序,而且不能去重,所以multiset是一个很好的选择.
加入后从后往前扫multiset的元素,如果>=mid加到答案(即f数组中)否则退出循环.
然后我们从前往后扫multiset中的元素,然后用lower_bound在multiset中找能够与它配对的元素,找不到的话就用这个元素来更新 g [ i ] {g[i]} g[i],这样就可以得到最优解了.(AC代码如下)

#include<bits/stdc++.h>
#define N 150005
#define re register int
using namespace std;

int n,m,tal1=1,tal2=1,ans,l,r,mi,tot,sum;
struct llj{
int x,y,val;}a[N];
int b[N],fi[N],nxt[N],to[N],w[N],f[N],g[N];
multiset<int>s;
multiset<int>::iterator tt;

inline bool cmp1(const llj &a,const llj &b)
{
	return a.val<b.val;
}

inline void lian(int x,int y,int z)
{
	nxt[++tot]=fi[x];
	fi[x]=tot;
	to[tot]=y;
	w[tot]=z;
}

inline bool isok1(int kk)
{
	int num=0,zhi=0,ll=1,rr=n-1;
	while(ll<=rr)
	{
		if(num>=m)return true;
		zhi=a[rr].val,rr--;
		if(zhi>=kk)
		{
			zhi=0;
			num++;
			continue;
		}
		else
		{
			while(zhi+a[ll].val<kk&&ll<=rr+1)ll++;
			if(ll>rr)return false;
			ll++;
			zhi=0;
			num++;
			
		}
	}
	if(num>=m)return true;
	return false;
}

inline bool isok2(int kk)
{
	int num=0,zhi=0;
	for(re i=1;i<n;i++)
	{
		if(zhi+b[i]>=kk)
		{
			zhi=0;
			num++;
		}
		else zhi+=b[i];
		if(num>=m)return true;
	}
	return false;
}

inline void Dfs(int u,int fa)//树形DP核心代码
{
	for(re i=fi[u];i;i=nxt[i])
	{
		int v=to[i];
		if(v==fa)continue;
		Dfs(v,u);
		f[u]+=f[v];
	}
	for(re i=fi[u];i;i=nxt[i])
	{
		int v=to[i];
		if(v==fa)continue;
		s.insert(g[v]+w[i]);
	}
	while(!s.empty())
	{
		int sum=*s.rbegin();
		if(sum>=mi)
		{
			tt=s.find(sum);
			f[u]++;
			s.erase(tt);
		}
		else break;
	}
	while(!s.empty())
	{
		int sum=*s.begin();
		s.erase(s.begin());
		tt=s.lower_bound(mi-sum);
		if(tt==s.end())g[u]=sum;
		else
		{
			f[u]++;
			s.erase(tt);
		}
	}
}

int main()
{
	scanf("%d%d",&n,&m);
	for(re i=1;i<n;i++)
	{
		scanf("%d%d%d",&a[i].x,&a[i].y,&a[i].val);
		b[a[i].x]=a[i].val;
		if(a[i].x!=a[i-1].x&&i>1)tal1=0;
		if(a[i].x!=a[i].y-1)tal2=0;
	}
	if(tal1==1)//保留特判是为了加速
	{
		sort(a+1,a+n,cmp1);
		l=1,r=1e9+7;
		while(l<r)
		{
			mi=(l+r+1)>>1;
			if(isok1(mi))l=mi,ans=mi;
			else r=mi-1;
		}
	}
	else if(tal2==1)
	{
		l=1,r=1e9+7;
		while(l<r)
		{
			mi=(l+r+1)>>1;
			if(isok2(mi))l=mi,ans=mi;
			else r=mi-1;
		}
	}
	if(!tal1&&!tal2)
	{
		l=1e9+5,r=1;
		for(re i=1;i<n;i++)
		{
			lian(a[i].x,a[i].y,a[i].val);
			lian(a[i].y,a[i].x,a[i].val);
			l=min(l,a[i].val);
			r=r+a[i].val;
		}
		while(l<r)//核心二分
		{
			mi=(l+r+1)>>1;
			memset(f,0,sizeof(f));
			memset(g,0,sizeof(g));
			Dfs(1,0);
			if(f[1]>=m)
			{
				l=mi;
				ans=max(ans,mi);
			}
			else r=mi-1;
		}
	}
	printf("%d\n",ans);
	return 0;
}

完结撒花!!!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

liaoxiyan123

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值