二分答案 + 倍增上跳 + 贪心匹配:到不了首都的军队停在最高可达点,到得了首都的按剩余时间匹配未被覆盖的根之子。
OJ: roj
题目 ID: 3603
难度:NOI/NOI+/CTSC
标签:
创建: 2026-10-02 10:05
更新: 2026-10-10 11:30
形式化题目
给定一棵
样例中
graph TD
R["1 首都"] ---|"1"| A["2<br/>边境"]
R ---|"2"| B["3"]
B ---|"3"| C["4<br/>边境"]
观察这棵树:叶子是 2 和 4,根到它们的路径分别是
正解
思路
朴素做法:枚举每支军队的最终驻扎点与检查点位置组合,再逐一验证叶子路径,是指数级的,无法承受
观察一:能上跳就上跳(支配关系)。 检查点越靠近根,覆盖的叶子越多。一支从
- 走不到首都(
):唯一最优解是停在可达的最高祖先 ,用倍增在 内求出; - 走得到首都(
):走到首都本身不产生覆盖,但获得了机动性——既可以在上跳途中顺路停在原孩子 (代价 ,恒可行),也可以继续走到首都、再下到其他孩子 (要求剩余时间 )。
观察二:答案单调,二分时间。 若
观察三:需求总在根的孩子这一层。 根的孩子
于是
- 上跳分流:走不到首都的军队停在最高可达点并打标记;走得到首都的按剩余时间
升序收集(按到根时长降序排序即得)。 - 覆盖统计:自底向上求出每个根的孩子是否已被标记的军队覆盖。
- "回不去"特判:对走到首都的军队,若原孩子
还没被覆盖、且 (从首都下不回去了),就直接让它原路停在 处,把 的需求消掉——这是"决策包容性":这种军队留在原地只会比把它投到别处更优,因为它离开原孩子后就再也无法解决 了。反之若 ,它既能解决 也能解决别人,先进匹配池不亏。 - 双指针匹配:剩下的需求按边权升序,匹配池(剩余时间升序)里每次取"第一个够用"的军队,能全部配上则
可行。
无解判定:一个根的孩子必须有至少一支军队进入其子树,且一支军队只能覆盖一个孩子,因此
以样例演示
| 二分值 |
分流 | 覆盖统计与特判 | 双指针匹配 | 结论 |
|---|---|---|---|---|
| 两军都能到首都,剩余各 |
无标记,根之子 2、3 均为需求; |
需求 2( |
不可行 | |
| 两军都能到首都,剩余各 |
同上,进池 |
需求 2 配 |
可行 |
二分最终落在最小可行值 3,与题解样例一致。注意若把"走到首都的军队只能从首都往下走"作为模型,样例之外会出现把顺路停在原孩子的情况误判为不可行的漏洞,特判第 3 步正是为此补上。
代码
/**
* 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;
vector<ll> DIST, WTOP; vector<vector<int> > UP, CH; vector<int> ORDER, TOP, RC, ARMY; vector<ll> NEG_D; int LOGN, M;
int lift_node(int v, ll x){ ll threshold=DIST[v]-x; int u=v; for(int j=LOGN-1;j>=0;j--){int a=UP[j][u]; if(a && DIST[a]>=threshold) u=a;} return u; }
vector<char> subtree_covered(vector<char> &vis){ vector<char> cov(DIST.size()); for(int idx=(int)ORDER.size()-1; idx>=0; idx--){int u=ORDER[idx]; if(vis[u]){cov[u]=1; continue;} if(!CH[u].empty()){char ok=1; for(int i=0;i<(int)CH[u].size();i++) if(!cov[CH[u][i]]){ok=0;break;} cov[u]=ok;}} return cov; }
bool feasible_time(ll x){ vector<char> vis(DIST.size()); int split=lower_bound(NEG_D.begin(),NEG_D.end(),-x)-NEG_D.begin(); for(int i=0;i<split;i++) vis[lift_node(ARMY[i],x)]=1; vector<char> cov=subtree_covered(vis); vector<ll> pool; for(int i=split;i<M;i++){int v=ARMY[i]; ll rest=x-DIST[v]; int son=TOP[v]; if(!cov[son] && rest<WTOP[son]) cov[son]=1; else pool.push_back(rest);} int j=0; for(int idx=0;idx<(int)RC.size();idx++){int c=RC[idx]; if(cov[c]) continue; ll w=WTOP[c]; while(j<(int)pool.size() && pool[j]<w) j++; if(j==(int)pool.size()) return false; j++;} return true; }
struct Edge{int v; ll w;};
bool cmp_army(int a,int b){ return DIST[a] > DIST[b]; }
bool cmp_root_child(int a,int b){ return WTOP[a] < WTOP[b]; }
int main(){ ios::sync_with_stdio(false); cin.tie(nullptr); int n; if(!(cin>>n)) return 0; vector<vector<Edge> > adj(n+1); ll total=0; for(int i=0;i<n-1;i++){int u,v;ll w;cin>>u>>v>>w; adj[u].push_back({v,w}); adj[v].push_back({u,w}); total+=w;} DIST.assign(n+1,0); CH.assign(n+1,vector<int>()); TOP.assign(n+1,0); WTOP.assign(n+1,0); vector<int> fa(n+1); vector<char> seen(n+1); queue<int> q; seen[1]=1; q.push(1); while(!q.empty()){int u=q.front(); q.pop(); for(int i=0;i<(int)adj[u].size();i++){int v=adj[u][i].v; ll w=adj[u][i].w; if(seen[v]) continue; seen[v]=1; DIST[v]=DIST[u]+w; CH[u].push_back(v); fa[v]=u; TOP[v]=(u==1?v:TOP[u]); if(u==1) WTOP[v]=w; ORDER.push_back(v); q.push(v);}} LOGN=1; while((1<<LOGN)<=n) LOGN++; UP.assign(LOGN, vector<int>(n+1)); for(int v=0;v<=n;v++) UP[0][v]=fa[v]; for(int j=1;j<LOGN;j++) for(int v=0;v<=n;v++) UP[j][v]=UP[j-1][UP[j-1][v]]; cin>>M; ARMY.resize(M); for(int i=0;i<M;i++) cin>>ARMY[i]; sort(ARMY.begin(),ARMY.end(),cmp_army); NEG_D.resize(M); for(int i=0;i<M;i++) NEG_D[i]=-DIST[ARMY[i]]; RC=CH[1]; sort(RC.begin(),RC.end(),cmp_root_child); if(M<(int)CH[1].size()){cout<<-1<<'\n';return 0;} ll lo=0,hi=total; while(lo<hi){ll mid=(lo+hi)/2; if(feasible_time(mid)) hi=mid; else lo=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 10:34
# update_at: 2026-10-02 10:34
import sys
from bisect import bisect_left
from collections import deque
DIST: list[int] = [] # 每个点到根的总时长
UP: list[list[int]] = [] # UP[j][v]:v 的 2^j 级祖先,0 表示不存在
CH: list[list[int]] = [] # 以 1 为根的孩子表
ORDER: list[int] = [] # BFS 层序(不含根),倒序即自底向上
TOP: list[int] = [] # 每个点正上方的根之子,军队"原驻地"看它
WTOP: list[int] = [] # 根之子到根的边权(仅深度 1 有点义)
RC: list[int] = [] # 根的孩子,按边权升序
ARMY: list[int] = [] # 军队所在点,按到根时长降序
NEG_D: list[int] = [] # 对应的 -DIST,升序,供 bisect 判断能否走到首都
LOG = 0
M = 0
def lift(v: int, x: int) -> int:
"""倍增上跳:返回 x 小时内能到达的最高祖先(到不了首都)。"""
threshold = DIST[v] - x # 目标 dist 至少要这么大;为正数 ⇒ 不会跳到首都
u = v
for j in range(LOG - 1, -1, -1):
a = UP[j][u]
if a and DIST[a] >= threshold:
u = a
return u
def subtree_covered(vis: bytearray) -> bytearray:
"""自底向上标记:每个点的子树是否已被驻扎军队完整覆盖。"""
cov = bytearray(len(DIST))
for u in reversed(ORDER):
if vis[u]:
cov[u] = 1
continue
children = CH[u]
if children: # 叶子没有孩子:没军队驻扎就是没覆盖
covered = 1
for c in children:
if not cov[c]:
covered = 0
break
cov[u] = covered
return cov
def feasible(x: int) -> bool:
"""判断 x 小时内能否控制疫情:上跳分流 → 覆盖统计 → 贪心匹配。"""
vis = bytearray(len(DIST))
split = bisect_left(NEG_D, -x) # 前缀到不了首都,后缀可以
for i in range(split):
vis[lift(ARMY[i], x)] = 1
cov = subtree_covered(vis)
# 能走到首都的军队按剩余时间升序(到根时长降序),剩余时间不够回到原孩子的
# 先留在原地把原孩子盖掉,剩下的进匹配池——这一步是"决策包容性"的特判
pool: list[int] = []
for i in range(split, M):
v = ARMY[i]
rest = x - DIST[v]
son = TOP[v]
if not cov[son] and rest < WTOP[son]:
cov[son] = 1 # 原孩子还没盖住,且回不去:直接留在原地
else:
pool.append(rest)
# 未覆盖的根之子按边权升序,剩余时间取"刚刚够用"的军队(双指针)
j = 0
pool_len = len(pool)
for c in RC:
if cov[c]:
continue
w = WTOP[c]
while j < pool_len and pool[j] < w:
j += 1
if j == pool_len:
return False
j += 1
return True
def solve() -> None:
global LOG, M, RC, ARMY, NEG_D
data = iter(map(int, sys.stdin.buffer.read().split()))
n = next(data)
adj: list[list[tuple[int, int]]] = [[] for _ in range(n + 1)]
total = 0 # 全部边权之和,充当二分上界
for _ in range(n - 1):
u, v, w = next(data), next(data), next(data)
adj[u].append((v, w))
adj[v].append((u, w))
total += w
# 从首都 BFS 定根:父亲、层序、到根时长、根之子
DIST.extend([0] * (n + 1))
CH.extend([] for _ in range(n + 1))
TOP.extend([0] * (n + 1))
WTOP.extend([0] * (n + 1))
fa = [0] * (n + 1)
seen = bytearray(n + 1)
seen[1] = 1
queue = deque([1])
while queue:
u = queue.popleft()
for v, w in adj[u]:
if seen[v]:
continue
seen[v] = 1
DIST[v] = DIST[u] + w
CH[u].append(v)
fa[v] = u
TOP[v] = v if u == 1 else TOP[u]
if u == 1:
WTOP[v] = w
ORDER.append(v)
queue.append(v)
del adj, seen
LOG = (n).bit_length()
UP.extend([[0] * (n + 1) for _ in range(LOG)])
for v in ORDER:
UP[0][v] = fa[v]
for j in range(1, LOG):
row, prev = UP[j], UP[j - 1]
for v in ORDER:
row[v] = prev[prev[v]]
M = next(data)
ARMY = sorted((next(data) for _ in range(M)), key=lambda v: DIST[v], reverse=True)
NEG_D = [-DIST[v] for v in ARMY]
RC = sorted(CH[1], key=lambda c: WTOP[c])
if M < len(CH[1]): # 军队数少于根之子数,一个子树都分不到
print(-1)
return
lo, hi = 0, total # 边权全取,所有军队都能走到首都,必可行
while lo < hi:
mid = (lo + hi) // 2
if feasible(mid):
hi = mid
else:
lo = mid + 1
print(lo)
if __name__ == "__main__":
solve()复杂度
设二分轮数为
总结
本题是"二分答案 + 倍增 + 贪心匹配"的组合:二分把最优化问题转为判定问题;"上跳支配"把每支军队的走法收敛为唯一的最高停点或一份剩余时间;"需求只在根的孩子层"把覆盖验证压成一次自底向上统计;最后用"回不去原孩子就留在原地"的特判加双指针匹配完成判定。