把「某座塔的第 k 次发射」当成一个发射位,命中时刻 = 飞行时间 + 射出时刻;二分答案 D 后建二分图,用匈牙利算法判断 M 个入侵者能否各自分到时刻不超过 D 且互不重复的发射位。
OJ: roj
题目 ID: 3202
难度:提高+/省选-
标签:图论二分答案二分图最大匹配匈牙利算法python
创建: 2026-10-02 01:38
更新: 2026-10-10 13:00
形式化题目
有
每座塔都有无限多枚导弹,但同一时刻最多发射一枚,且每次发射后的
导弹只算水平距离
要求给每个入侵者指派一次命中
约束:
正解
思路
第一步:先把「发射位」这个概念提出来。
题面里的时间单位混用(
- 发射本身耗
秒,所以编号 的发射在第 秒射出; - 每次发射后冷却
分钟 秒,编号 之前已有 次冷却,累计 秒; - 飞行时间是
分钟 秒。
合起来就是
注意一个关键性质:
发射位是本题唯一的稀缺资源:每座塔最多用
第二步:问题变成「指派」。
于是所求就是:给每个入侵者
直接枚举指派是阶乘级别(约
第三步:可行性单调
问「能不能在
二分区间也不需要连续:最优解一定等于某个
第四步:判定用二分图最大匹配。
固定
- 左部
个点:入侵者; - 右部
个点:发射位 ,展平成一维编号 ; 连边当且仅当 。
「每个入侵者都能分到一个发射位」
下图是样例取
graph LR
subgraph L["左部:入侵者"]
A0["入侵者 (0,0)"]
A1["入侵者 (0,50)"]
A2["入侵者 (50,0)"]
end
subgraph R["右部:塔 (50,50) 的发射位"]
S0["发射位 k=0<br/>射出 30 秒"]
S1["发射位 k=1<br/>射出 1260 秒"]
S2["发射位 k=2<br/>射出 2490 秒"]
end
A0 -->|"4272.641"| S0
A1 -->|"3030.000"| S0
A1 -->|"4260.000"| S1
A2 -->|"3030.000"| S0
A2 -->|"4260.000"| S1
边上的数字是这次命中的时刻(秒);只有时刻不超过
观察重点有两个:左部只有 3 个点、右部有 3 个发射位,但
第五步:两个实现细节。
- 前缀截边:固定
时不必重扫全部 条边。对每个目标把 个发射位按 排序,用 bisect_right一次就能截出「时刻」的前缀作为邻接表。 - 候选时刻去重:二分在排序去重后的候选表上做,保证最终输出的正是某次命中的时刻。
代码
main.py 分四层,覆盖上面五步:slot_times() 是时间公式,max_matching() 是判定用的
匈牙利算法,minimum_time() 是二分答案,solve() 只负责读入、调用和输出。
全程统一用秒计时,只有最后输出时除以 SECOND = 60 换回分钟。
/**
* 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 13:00
* update_at: 2026-10-10 13:00
*/
// 导弹防御:二分最小可行截止时刻 + 匈牙利算法(入侵者 对 发射位)
#include <cstdio>
#include <vector>
#include <cmath>
#include <algorithm>
using namespace std;
const double SECOND = 60.0; // 题面 T1 用秒、T2 用分钟、飞行时间也用分钟
// 匈牙利:从入侵者 u 出发找增广路
bool augment(int u, vector<char>& seen, vector<int>& mt, const vector<vector<int> >& adj) {
for (int i = 0; i < (int)adj[u].size(); i++) {
int x = adj[u][i];
if (!seen[x]) {
seen[x] = 1;
if (mt[x] < 0 || augment(mt[x], seen, mt, adj)) {
mt[x] = u;
return true;
}
}
}
return false;
}
// 每个入侵者只需抢到一个发射位;有一个抢不到就判定不可行
int max_matching(const vector<vector<int> >& adj, int size) {
vector<int> mt(size, -1);
int matched = 0;
for (int u = 0; u < (int)adj.size(); u++) {
vector<char> seen(size, 0);
if (augment(u, seen, mt, adj)) {
matched++;
} else {
break;
}
}
return matched;
}
int main() {
int n, m, t1, t2, v;
if (scanf("%d %d %d %d %d", &n, &m, &t1, &t2, &v) != 5) {
return 0;
}
vector<double> ix(m), iy(m), tx(n), ty(n);
for (int j = 0; j < m; j++) {
scanf("%lf %lf", &ix[j], &iy[j]);
}
for (int i = 0; i < n; i++) {
scanf("%lf %lf", &tx[i], &ty[i]);
}
// slot_time[j][i*m+k]:第 i 座塔第 k 次发射命中第 j 个入侵者的时刻(秒)
vector<double> shoot(m);
for (int k = 0; k < m; k++) {
shoot[k] = (double)(k + 1) * t1 + (double)k * SECOND * t2; // 射出时刻
}
int size = n * m;
vector<vector<double> > slot(m, vector<double>(size, 0));
for (int j = 0; j < m; j++) {
for (int i = 0; i < n; i++) {
double dx = tx[i] - ix[j], dy = ty[i] - iy[j];
double ready = SECOND * sqrt(dx * dx + dy * dy) / v; // 纯飞行时间(秒)
for (int k = 0; k < m; k++) {
slot[j][i * m + k] = ready + shoot[k];
}
}
}
// 每个入侵者按时刻升序排出全部发射位
vector<vector<int> > order(m);
vector<vector<double> > by_time(m);
for (int j = 0; j < m; j++) {
order[j].resize(size);
for (int s = 0; s < size; s++) {
order[j][s] = s;
}
for (int a = 0; a < size; a++) { // 简单选择排序,规模小
int best = a;
for (int b = a + 1; b < size; b++) {
if (slot[j][order[j][b]] < slot[j][order[j][best]]) {
best = b;
}
}
int t = order[j][a];
order[j][a] = order[j][best];
order[j][best] = t;
}
by_time[j].resize(size);
for (int s = 0; s < size; s++) {
by_time[j][s] = slot[j][order[j][s]];
}
}
vector<double> timeline;
for (int j = 0; j < m; j++) {
for (int s = 0; s < size; s++) {
timeline.push_back(slot[j][s]);
}
}
sort(timeline.begin(), timeline.end());
timeline.erase(unique(timeline.begin(), timeline.end()), timeline.end());
// 在 [0, len-1] 上二分出最小的可行截止时刻下标
int lo = 0, hi = (int)timeline.size() - 1;
while (lo < hi) {
int mid = (lo + hi) / 2;
double deadline = timeline[mid];
vector<vector<int> > adj(m);
for (int j = 0; j < m; j++) {
int cnt = (int)(upper_bound(by_time[j].begin(), by_time[j].end(), deadline) -
by_time[j].begin());
for (int s = 0; s < cnt; s++) {
adj[j].push_back(order[j][s]);
}
}
if (max_matching(adj, size) == m) {
hi = mid;
} else {
lo = mid + 1;
}
}
printf("%.6f\n", timeline[lo] / SECOND);
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 01:38
# update_at: 2026-10-02 01:38
import sys
from bisect import bisect_right
from math import dist
SECOND = 60 # 题面 T1 用秒、T2 用分钟、飞行时间也用分钟,这里统一折算成秒再比较
def slot_times(
towers: list[tuple[int, int]],
intruders: list[tuple[int, int]],
t1: int,
t2: int,
v: int,
) -> list[list[float]]:
"""slot[j][i*m+k]:第 i 座塔第 k 次发射命中第 j 个入侵者的时刻(秒)。
一次发射就是一个「发射位」,编码 i*m+k 表示「第 i 座塔的第 k 次发射」(k 从 0 起)。
塔 i 第 k 次发射在第 (k+1)*T1 秒射出,此前每轮还要冷却 60*T2 秒;
飞行时间为水平距离/V 分钟,即 60*距离/V 秒。
"""
n, m = len(towers), len(intruders)
ready = [[SECOND * dist(w, q) / v for q in intruders] for w in towers] # 塔 i → 目标 j 的纯飞行时间
shoot = [(k + 1) * t1 + k * SECOND * t2 for k in range(m)] # 第 k 次发射的射出时刻
return [[ready[i][j] + s for i in range(n) for s in shoot] for j in range(m)]
def max_matching(adj: list[list[int]], size: int) -> int:
"""匈牙利算法:左部是 m 个入侵者,右部是 size 个发射位,返回最大匹配数。
每个入侵者只需被命中一次,所以左部每个点只要抢到一个发射位就算匹配成功。
"""
match = [-1] * size
def augment(u: int, seen: list[bool]) -> bool:
for x in adj[u]:
if not seen[x]:
seen[x] = True
if match[x] < 0 or augment(match[x], seen):
match[x] = u
return True
return False
matched = 0
for u in range(len(adj)):
if augment(u, [False] * size):
matched += 1
else:
break # 这个入侵者抢不到位,后面也补不上,直接判定不可行
return matched
def minimum_time(slot_time: list[list[float]], m: int) -> float:
"""二分最小的可行截止时刻(秒):答案一定是某个发射位的命中时刻。"""
size = len(slot_time[0])
order = [sorted(range(size), key=lambda s, j=j: slot_time[j][s]) for j in range(m)]
by_time = [[slot_time[j][s] for s in order[j]] for j in range(m)] # 与 order 同步的升序时刻表
def feasible(deadline: float) -> bool:
"""在 deadline 之前(含)每个入侵者都能占到一个互不冲突的发射位。"""
adj = [order[j][: bisect_right(by_time[j], deadline)] for j in range(m)]
return max_matching(adj, size) == m
timeline = sorted({t for row in slot_time for t in row})
lo, hi = 0, len(timeline) - 1
while lo < hi:
mid = (lo + hi) // 2
if feasible(timeline[mid]):
hi = mid
else:
lo = mid + 1
return timeline[lo]
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n, m, t1, t2, v = next(data), next(data), next(data), next(data), next(data)
intruders = [(next(data), next(data)) for _ in range(m)]
towers = [(next(data), next(data)) for _ in range(n)]
answer = minimum_time(slot_times(towers, intruders, t1, t2, v), m)
print(f"{answer / SECOND:.6f}")
if __name__ == "__main__":
solve()复杂度
- 时间复杂度:
。预处理时刻表 ,一次匈牙利最坏 , 外层二分 次。 时实测最慢一组约 s。 - 空间复杂度:
,即 的时刻表与邻接表,峰值内存约 MB。
总结
本题的关键不是「模拟」,而是把连续的发射过程离散成一个一个「发射位」:
第
三个容易踩的坑:单位要把
图示解析
下表是样例中塔
| 发射编号 |
射出时刻(秒) | 打 |
打 |
打 |
|---|---|---|---|---|
| 0 | 30 | 4272.641 | 3030.000 | 3030.000 |
| 1 | 1260 | 5502.641 | 4260.000 | 4260.000 |
| 2 | 2490 | 6732.641 | 5490.000 | 5490.000 |
从列方向看,同一列的时刻随 91.500000。
反过来验证为什么不能更早:下一个更小的候选值是