二分最短赛道长度 limit,树上 DFS 自底向上用「救生艇」双指针贪心拼接子树伸上来的单链,判定能否修出 m 条长度都 ≥ limit 的赛道。
OJ: roj
题目 ID: 3660
难度:提高+/省选-
标签:二分答案树形贪心贪心树python
创建: 2026-10-02 13:44
更新: 2026-10-10 11:30
形式化题目
给定一棵
样例 2 推演(
flowchart TD
A(("1")) ---|"6"| B(("2"))
B ---|"3"| C(("3"))
C ---|"5"| D(("4"))
D ---|"10"| E(("5"))
B ---|"4"| F(("6"))
B ---|"9"| G(("7"))
D ---|"7"| H(("8"))
D ---|"4"| I(("9"))
把边长加起来容易验证:路径
正解
思路
第一步:把"最短的最长"变成判定问题。 直接决定
能否在树上选出
条边互不重叠的简单路径,每条长度都 ?
这个判定有一个关键的单调性:
第二步:树形贪心求最大条数。 任取一个结点为根,自底向上处理。对每个结点
- 完全落在子树内的赛道,在处理
之前就结算掉; - 一端在
的子树里、另一端向父亲延伸的"半成品":从某个子孙出发走到 的一条单链,它是该子树中某点跳到 、再到祖先的路径前缀。
于是每个结点只需向父亲汇报一个信息:子树伸上来的单链。这些单链两两之间可以在
- 某条链长度已经
:它自己就是一条完整赛道,直接结算。不用再管它——留一条链去配它,纯属浪费(对方被白吃掉)。 - 其余的链长度都
,单干不行,只能两两拼。把 排序,双指针从两端向中间扫:最短 + 最长若能凑到 ,就配对成一条赛道;否则最短的那条谁也配不上(它和最长的都不行,和别的更不行),只能弃掉。这就是经典的救生艇贪心,能证明配出的是最多的对数。
未配对、未被弃掉的链里,最长的那条作为
两个易错的实现细节(都踩过坑):
- 弃掉不等于永远消失。 若
处所有链恰好两两配满,双指针结束时刻意不剩链,但一个"最长被弃链"其实值得一记:比如单链 、 ,贪心配出 后什么都不传,可 本身留在 里若改配 ,就能把 传上去。正确写法是:配对数取最大 ,在此前提下"弃掉"的链里取最长的;由于交换论证可证"弃掉较大的链、留下较小的"只会让配对更难,能弃的最大值随被弃链的下标单调,所以对弃谁再套一个二分,找到删掉后仍配得出 对的最长链即可(全配满时记 )。
| 结点 | 收到单链 | 剩余链 | |
|---|---|---|---|
| 配 |
|||
| 根 | 配 |
||
| 根 | 单干 |
的链直接单干结算,不能参与配对后再被"剩下"——这是第一版贪心漏掉的情况,导致长链被无辜配对、答案偏小。
整个流程用一张图总结:
flowchart TD
A["二分 limit"] --> B["树上判定:>= limit 的不重叠路径最多几条?"]
B --> C{"条数 >= m ?"}
C -->|"是,越大也行"| D["lo = mid"]
C -->|"否,太大了"| E["hi = mid - 1"]
D --> F["输出 lo"]
E --> A
树形判定的执行顺序:先用一次 BFS 求出"父结点先于子结点"的访问序,再倒序遍历——保证计算某结点时它的所有子结点都已经把单链汇报上来了。
代码
代码中 max_pairs 是救生艇双指针,pair_chains 负责单个结点的分配并返回 (结算条数, 传给父亲的剩余链),max_tracks(limit) 就是树形判定本身,主流程是一段标准二分。
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-10 11:30
* update_at: 2026-10-10 11:30
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
struct Edge{int v; ll w;};
int max_pairs(vector<ll> rest,ll limit){int i=0,j=(int)rest.size()-1,pairs=0; while(i<j){if(rest[i]+rest[j]>=limit){pairs++;i++;j--;} else i++;} return pairs;}
pair<int,ll> pair_chains(vector<ll> rests,ll limit){int singles=0; vector<ll> rest; for(int i=0;i<(int)rests.size();i++){if(rests[i]>=limit) singles++; else rest.push_back(rests[i]);} sort(rest.begin(),rest.end()); int p=max_pairs(rest,limit); ll leftover=0; int lo=0,hi=(int)rest.size()-1; while(lo<hi){int mid=(lo+hi+1)/2; vector<ll> keep; for(int i=0;i<(int)rest.size();i++) if(i!=mid) keep.push_back(rest[i]); if(max_pairs(keep,limit)>=p) lo=mid; else hi=mid-1;} if(p*2<(int)rest.size() && !rest.empty()) leftover=rest[lo]; return {singles+p,leftover};}
int main(){ios::sync_with_stdio(false);cin.tie(nullptr); int n,m; if(!(cin>>n>>m)) return 0; vector<vector<Edge> > adj(n+1); ll total_len=0; for(int i=0;i<n-1;i++){int a,b; ll l;cin>>a>>b>>l; adj[a].push_back({b,l}); adj[b].push_back({a,l}); total_len+=l;} vector<int> parent(n+1), order; order.push_back(1); for(int idx=0;idx<(int)order.size();idx++){int u=order[idx]; for(int i=0;i<(int)adj[u].size();i++){int v=adj[u][i].v; if(v!=parent[u]){parent[v]=u; order.push_back(v);}}} ll lo=1,hi=total_len; while(lo<hi){ll mid=(lo+hi+1)/2; int total=0; vector<ll> up(n+1); for(int idx=(int)order.size()-1;idx>=0;idx--){int u=order[idx]; vector<ll> rests; for(int i=0;i<(int)adj[u].size();i++){int v=adj[u][i].v; if(v!=parent[u]) rests.push_back(up[v]+adj[u][i].w);} pair<int,ll> pr=pair_chains(rests,mid); total+=pr.first; up[u]=pr.second;} if(total>=m) lo=mid; else hi=mid-1;} cout<<lo<<'\n'; return 0;}#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-02 13:44
# update_at: 2026-10-02 13:44
import sys
def max_pairs(rest: list[int], limit: int) -> int:
"""排序单链里最多能拼出多少对(每对长度和 ≥ limit):双指针,最短 + 最长能拼就拼。"""
i, j, pairs = 0, len(rest) - 1, 0
while i < j:
if rest[i] + rest[j] >= limit: # 最短 + 最长能拼就拼,省着用长链
pairs += 1
i += 1
j -= 1
else: # 最短的链已无配对可能
i += 1
return pairs
def pair_chains(rests: list[int], limit: int) -> tuple[int, int]:
"""把子树伸上来的单链分配成赛道:≥ limit 的单链独立成赛道,其余两两拼接。
返回 (本结点确定的赛道条数, 剩余可向父结点延伸的最长单链)。
配对数取最大 p;在这个前提下传上去的链越长越好,
而「弃掉较大的链、留下较小的」只会让配对更难(交换论证可证单调),
所以二分出「删掉它后仍能配出 p 对」的最长链当作剩余。
"""
singles = sum(c >= limit for c in rests) # ≥ limit 的链各自独立成赛道
rest = sorted(c for c in rests if c < limit) # 全部 < limit,拼不出也不够单干
p = max_pairs(rest, limit)
leftover = 0 # 恰好全部配满时无剩余链
lo, hi = 0, len(rest) - 1 # 谓词随下标单调:删得越大越难配
while lo < hi:
mid = (lo + hi + 1) // 2
keeps = max_pairs(rest[:mid] + rest[mid + 1:], limit) >= p # 删掉 rest[mid] 还能配 p 对吗
if keeps:
lo = mid
else:
hi = mid - 1
if p * 2 < len(rest): # 有配不出去的弃链,取能弃的最长的那条
leftover = rest[lo]
return singles + p, leftover
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n, m = next(data), next(data)
adj: list[list[tuple[int, int]]] = [[] for _ in range(n + 1)] # 邻接表:(邻居, 道路长)
total_len = 0
for _ in range(n - 1):
a, b, l = next(data), next(data), next(data)
adj[a].append((b, l))
adj[b].append((a, l))
total_len += l
# 迭代求「父结点先于子结点」的访问序:n 达 5e4,链形树会把递归爆栈
parent = [0] * (n + 1)
order = [1]
for u in order: # order 边遍历边增长,父结点必先入序
for v, _ in adj[u]:
if v != parent[u]:
parent[v] = u
order.append(v)
def max_tracks(limit: int) -> int:
"""每条赛道长度都 ≥ limit 时,整棵树最多能修出多少条赛道。"""
total = 0
up = [0] * (n + 1) # 结点处拼不出赛道、还能向父结点继续延伸的最长链
for u in reversed(order): # 逆序处理,保证算 u 时所有子结点已汇报
rests = [up[v] + l for v, l in adj[u] if v != parent[u]]
pairs, longest = pair_chains(rests, limit)
total += pairs
up[u] = longest
return total # 根结点的剩余链无处延伸,自然作废
# 二分答案:limit 越大能修的赛道越少,找能满足 m 条的最大 limit
lo, hi = 1, total_len
while lo < hi:
mid = (lo + hi + 1) // 2
if max_tracks(mid) >= m:
lo = mid
else:
hi = mid - 1
print(lo)
if __name__ == "__main__":
solve()复杂度
- 单次判定:每个结点进队一次,
排序的总代价是 ;结点内"弃谁"的二分多乘一层 ,但同样只作用在 条链上,总计仍 。 - 二分次数:
次。 - 总复杂度
,与 C++ 正解同阶,Python 常数偏大但真实数据全部通过;空间 。
总结
- “最短的最长” → 二分答案
,把最优化变成树上判定: 的不重叠路径最多能选出几条。 - 树形贪心自底向上:每条子树单链要么自成一赛道(
),要么按救生艇双指针两两拼接;弃链时在配对数不变的前提下二分出能弃的最长链传给父亲。 - 两个坑:
的链要直接单干;全配满时别把本可传上去的链弄丢。 - 复杂度
。
图示解析
单链在结点处的四种归宿(树形贪心的核心动作):
| 归宿 | 条件 | 动作 | 对答案的贡献 |
|---|---|---|---|
| 单干 | 链长 |
直接结算,不再上报 | |
| 配对 | 两条链长之和 |
结算这一对 | |
| 上传 | 未配对、未被弃、且最长 | 长度传给父亲 | |
| 弃掉 | 未配对但已确定不划算 | 只在上传候选里登记长度 |
以样例 2 走一遍
| 结点 | 收到的单链 | 分配 | 结算 | 剩余链 |
|---|---|---|---|---|
| 都不够,上传 |
||||
| 上传 | ||||
| 上传 | ||||
| — |
三处结算共


