NOIP2018·赛道修建

初见安~本狸参加了2018年的NOIP,然后到现在【看题解】才能过Day1 T3……tcl……QwQ

本篇题解及代码有参考洛谷题解。

本篇夹带了对于二分深切的痛恨。请自行忽略。

传送门:洛谷 P5021

题目描述

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

C 城一共有 nn 个路口,这些路口编号为 1,2,…,n1,2,…,n,有 n-1n−1 条适合于修建赛道的双向通行的道路,每条道路连接着两个路口。其中,第 ii 条道路连接的两个路口编号为 a_iai​ 和 b_ibi​,该道路的长度为 l_ili​。借助这 n-1n−1 条道路,从任何一个路口出发都能到达其他所有的路口。

一条赛道是一组互不相同的道路 e_1,e_2,…,e_ke1​,e2​,…,ek​,满足可以从某个路口出发,依次经过 道路 e_1,e_2,…,e_ke1​,e2​,…,ek​(每条道路经过一次,不允许调头)到达另一个路口。一条赛道的长度等于经过的各道路的长度之和。为保证安全,要求每条道路至多被一条赛道经过。

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

输入格式:

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

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

输出格式:

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

输入样例#1: 

7 1 
1 2 10 
1 3 5 
2 4 9 
2 5 8 
3 6 6 
3 7 7

输出样例#1: 

31

输入样例#2: 

9 3 
1 2 6 
2 3 3 
3 4 5 
4 5 10 
6 2 4 
7 2 9 
8 4 7 
9 4 4

输出样例#2: 

15

说明

【输入输出样例 1 说明】

所有路口及适合于修建赛道的道路如下图所示:

道路旁括号内的数字表示道路的编号,非括号内的数字表示道路长度。 需要修建 11 条赛道。可以修建经过第 3,1,2,63,1,2,6 条道路的赛道(从路口 44 到路口 77), 则该赛道的长度为 9 + 10 + 5 + 7 = 319+10+5+7=31,为所有方案中的最大值。

【输入输出样例 2 说明】

所有路口及适合于修建赛道的道路如下图所示:

需要修建 33条赛道。可以修建如下 33条赛道:

  1. 经过第 1,61,6条道路的赛道(从路口 11 到路口77),长度为 6 + 9 = 156+9=15;
  2. 经过第5,2,3,85,2,3,8 条道路的赛道(从路口66 到路口 99),长度为 4 + 3 + 5 + 4 = 164+3+5+4=16;
  3. 经过第 7,47,4 条道路的赛道(从路口 88 到路口55),长度为 7 + 10 = 177+10=17。 长度最小的赛道长度为 1515,为所有方案中的最大值。

【数据规模与约定】

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

其中,“分支不超过 33”的含义为:每个路口至多有 33 条道路与其相连。 对于所有的数据, 2\leqslant n\leqslant50000,1 \leqslant m \leqslant n - 1..........【全是数学写法就不打一遍了QAQ。

题解:

题意就是——给你一棵树,让你找出m条路径使没有边重合的前提下让最短的路径长度尽量长,并输出那个值。

我记住了,看到这种东西让最小值最达化,让最大值最小化的就想到二分答案……

二分枚举最短边的最大长度,条件为能找到m条合法路径。所谓合法路径,就是路径长度\geqslant二分枚举的长度

那么问题就从让二分的最大长度尽量大 变成了 怎么让合法路径数量尽量多

因为路径都有两种形态——链状 和 对折。对应的也可以说成是——链 和 两条链拼在一起。归根到底都是链。所以——我们就可以维护以u为LCA的最优链的长度。这里的最优是迎合“合法路径数尽量多”的。所谓最多——我们类似于树形dp地dfs下去,回溯时处理:如果有u的某个子节点的f[v]到u这里的路径长度满足条件了,那么我们就算是找到一条路径了,直接累加;否则,会剩下很多条半链,我们再考虑拼合到一起。

对于这些不能独立的半链,我们用贪心的思路,先按长度排个序,再头尾搭配得到尽量多的合法折链。也就是“尽量小的和尽量大的合并”。合并完了过后还没完,因为就算是这样合并了,也还是会剩下一些半链的,所以就要让f[u]来继承一条最优的半链了。怎么最优呢?当然是长度最长啊!!!所以我们找到一条不影响两两拼合的数量的最长的半链过渡给f[u]即可。注意,是不影响答案,如果记录下选的边的编号的话,但凡存在多解的情况就有可能继承过去的不是最优的长度。所以怎么判断不影响呢?继续二分吧……【因为枚举每个儿子的话有可能复杂度会爆掉的,而且因为有独链的存在会导致判定很麻烦】二分选哪个儿子的f值继承过来。

再者就是如何判断不影响?除了你准备继承过去的那个值,在剩下的所有无法独立的半链里再跑一次看看是否能得到和上一段合并半链得到的最大的结果一样的结果,一样则行,否则不行。

至此——这个题就完啦。

【其实我就是看题解代码看懂的QwQ】

#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
#include<vector>
#define maxn 50005
using namespace std;
const int mod = 1e6;
typedef long long ll;
int read() {
	int x = 0, f = 1, ch = getchar();
	while(!isdigit(ch)) {if(ch == '-') f = -1; ch = getchar();}
	while(isdigit(ch)) x = (x << 1) + (x << 3) + ch - '0', ch = getchar();
	return x * f;
}

struct edge {
	int to, w, nxt;
	edge() {}
	edge(int t, int ww, int nn) {to = t, w = ww, nxt = nn;}
}e[maxn << 1];

int head[maxn], k = 0;
void add(int u, int v, int w) {e[k] = edge(v, w, head[u]); head[u] = k++;}

vector<int> son[maxn];
ll f[maxn], ans;
int recheck(int u, int pos, ll x) {
	register int l = 0, r = son[u].size() - 1, tot = 0;
	for(; r >= 0; r--) {
		if(r == pos) r--;//l和r都避开pos这个位置
		while(l < r && son[u][l] + son[u][r] < x) l++; if(l == pos) l++;
		if(l >= r) break;
		tot++; l++;
	}
	return tot;
}

void dfs(int u, int fa, ll x) {
	son[u].clear();
	for(register int v, i = head[u]; ~i; i = e[i].nxt) {
		v = e[i].to; if(v == fa) continue;
		dfs(v, u, x);
		f[v] += e[i].w;
		if(f[v] >= x) ans++;
		else son[u].push_back(f[v]);
	}
	
	sort(son[u].begin(), son[u].end());//son里直接存各个非独立半链的长度
	register int l = 0, r = son[u].size() - 1, tmp = 0, mid, pos;
	for(; r >= 0; r--) {//贪心找最大匹配数中
		while(l < r && son[u][l] + son[u][r] < x) l++;
		if(l >= r) break; tmp++; l++;//l这里必须++,否则可能会重复加
	}
	
	ans += tmp;//累加答案
	if((tmp << 1) == son[u].size()) return;//这就说明没有继承的余地了……
	l = 0; r = son[u].size();//这里r不是size-1了,也是因为我二分的方式原因……
	while(l < r) {//这样写就挂掉了呢。改成另一种就过了 
		mid = l + r >> 1;
		if(recheck(u, mid, x) == tmp) l = mid + 1;//这里tmp还要用的
		else r = mid;
	}
	f[u] = son[u][l - 1];
}

int n, m;
bool check(ll mid) {
	ans = 0; memset(f, 0, sizeof f); dfs(1, 0, mid);//初始化,dfs
	if(ans >= m) return true; return false;//如果找到了>=m条合法路径,那么这个答案就是可行的
}

signed main() {
	memset(head, -1, sizeof head);
	n = read(), m = read();
	register ll l = 0, r = 0, mid;
	for(register int i = 1, u, v, w; i < n; i++) 
		u = read(), v = read(), w = read(), add(u, v, w), add(v, u, w), r += w;
	r /= 1ll * m; r++;//这里r++是因为我的二分方式是左闭右开的……所以就被坑害了好久……
	register long long out;
	while(l < r) {
		mid = l + r >> 1;
		if(check(mid)) l = mid + 1;
		else r = mid;
	}
	
	printf("%lld\n", l - 1);
	return 0;
}

控诉一下……二分是真的毒瘤好吧……我真的真的真的记住了这个卡了我一个下午的bug:

前者,区间是左闭右开的;后者,是左右均闭的。所以要这么写效果才能一样啊……

int l = 1; r = 11;
while(l < r) {
	mid = l + r >> 1;
	if() l = mid + 1;
	else r = mid;
} out:l - 1

//=============================================================================

int pos, l = 1, r = 10;
while(l <= r) {
	mid = l + r >> 1;
	if() l = mid + 1, pos = mid;
	else r = mid - 1;
} out:pos

迎评:)
——End——

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值