本文目录导读:

我来提供一个Java案例,用于统计足球比赛中交叉跑位造成威胁的次数,这个案例会模拟进攻球员通过交叉跑位摆脱防守、形成威胁(如射门、关键传球)的场景。
完整Java代码
import java.util.*;
import java.util.stream.Collectors;
/**
* 球员位置
*/
class Position {
private double x; // 横向坐标 0-100
private double y; // 纵向坐标 0-100
public Position(double x, double y) {
this.x = x;
this.y = y;
}
public double getX() { return x; }
public double getY() { return y; }
public double distanceTo(Position other) {
return Math.sqrt(Math.pow(this.x - other.x, 2) + Math.pow(this.y - other.y, 2));
}
@Override
public String toString() {
return String.format("(%.1f, %.1f)", x, y);
}
}
/**
* 球员跑位轨迹点
*/
class TrackPoint {
private final String playerId;
private final Position position;
private final long timestamp; // 毫秒
public TrackPoint(String playerId, Position position, long timestamp) {
this.playerId = playerId;
this.position = position;
this.timestamp = timestamp;
}
public String getPlayerId() { return playerId; }
public Position getPosition() { return position; }
public long getTimestamp() { return timestamp; }
}
/**
* 威胁事件(射门、关键传球等)
*/
class ThreatEvent {
private final long timestamp;
private final String playerId;
private final String type; // SHOT, KEY_PASS, ASSIST
public ThreatEvent(long timestamp, String playerId, String type) {
this.timestamp = timestamp;
this.playerId = playerId;
this.type = type;
}
public long getTimestamp() { return timestamp; }
public String getPlayerId() { return playerId; }
public String getType() { return type; }
}
/**
* 交叉跑位威胁统计分析器
*/
public class CrossRunThreatAnalyzer {
// 交叉跑位判定参数
private static final double CROSS_DISTANCE_THRESHOLD = 3.0; // 两名球员交叉时最近距离(米)
private static final long CROSS_TIME_WINDOW = 1500; // 交叉时间窗口(毫秒)
private static final long THREAT_TIME_WINDOW = 5000; // 交叉后多久内形成威胁(毫秒)
/**
* 统计交叉跑位造成威胁的次数
*/
public int countCrossRunThreats(List<TrackPoint> trackPoints, List<ThreatEvent> threatEvents) {
// 1. 按球员分组轨迹
Map<String, List<TrackPoint>> byPlayer = trackPoints.stream()
.collect(Collectors.groupingBy(TrackPoint::getPlayerId));
// 2. 按时间排序
byPlayer.values().forEach(list ->
list.sort(Comparator.comparingLong(TrackPoint::getTimestamp)));
Set<Long> countedThreats = new HashSet<>(); // 去重,避免同一威胁重复计数
int crossThreatCount = 0;
// 3. 两两球员检测交叉跑位
List<String> playerIds = new ArrayList<>(byPlayer.keySet());
for (int i = 0; i < playerIds.size(); i++) {
for (int j = i + 1; j < playerIds.size(); j++) {
String p1 = playerIds.get(i);
String p2 = playerIds.get(j);
List<TrackPoint> track1 = byPlayer.get(p1);
List<TrackPoint> track2 = byPlayer.get(p2);
List<Long> crossTimes = detectCrossRuns(track1, track2);
// 4. 对每次交叉,判断其后是否产生威胁
for (long crossTime : crossTimes) {
for (ThreatEvent threat : threatEvents) {
long delta = threat.getTimestamp() - crossTime;
// 交叉后 0 ~ THREAT_TIME_WINDOW 内产生威胁
if (delta >= 0 && delta <= THREAT_TIME_WINDOW) {
// 威胁由交叉的两名球员之一发起
if ((threat.getPlayerId().equals(p1) || threat.getPlayerId().equals(p2))
&& countedThreats.add(threat.getTimestamp() * 1000
+ threat.getPlayerId().hashCode())) {
crossThreatCount++;
System.out.printf("交叉跑位[p1=%s, p2=%s] 于 t=%d ms," +
"在 %d ms 后由 %s 形成威胁(%s)%n",
p1, p2, crossTime, delta,
threat.getPlayerId(), threat.getType());
}
}
}
}
}
}
return crossThreatCount;
}
/**
* 检测两名球员之间发生的所有交叉跑位时刻
*/
private List<Long> detectCrossRuns(List<TrackPoint> t1, List<TrackPoint> t2) {
List<Long> crossTimes = new ArrayList<>();
int i = 0, j = 0;
boolean lastWasClose = false;
while (i < t1.size() && j < t2.size()) {
TrackPoint a = t1.get(i);
TrackPoint b = t2.get(j);
long dt = Math.abs(a.getTimestamp() - b.getTimestamp());
if (dt > CROSS_TIME_WINDOW) {
// 时间差太大,推进时间较早的点
if (a.getTimestamp() < b.getTimestamp()) i++; else j++;
continue;
}
double dist = a.getPosition().distanceTo(b.getPosition());
boolean isClose = dist <= CROSS_DISTANCE_THRESHOLD;
// 由远及近再拉开:从"近"状态第一次出现即视为交叉点
if (isClose && !lastWasClose) {
long crossTime = Math.max(a.getTimestamp(), b.getTimestamp());
crossTimes.add(crossTime);
}
lastWasClose = isClose;
if (a.getTimestamp() < b.getTimestamp()) i++; else j++;
}
return crossTimes;
}
// ================== 测试 ==================
public static void main(String[] args) {
List<TrackPoint> tracks = new ArrayList<>();
// 球员 A7 从左向右跑
tracks.add(new TrackPoint("A7", new Position(30, 50), 1000));
tracks.add(new TrackPoint("A7", new Position(40, 50), 1500));
tracks.add(new TrackPoint("A7", new Position(50, 50), 2000)); // 交叉点
tracks.add(new TrackPoint("A7", new Position(60, 50), 2500));
// 球员 B9 从右向左跑
tracks.add(new TrackPoint("B9", new Position(70, 50), 1000));
tracks.add(new TrackPoint("B9", new Position(60, 50), 1500));
tracks.add(new TrackPoint("B9", new Position(50, 50), 2000)); // 交叉点
tracks.add(new TrackPoint("B9", new Position(40, 50), 2500));
// 无关球员 C10
tracks.add(new TrackPoint("C10", new Position(20, 20), 1000));
tracks.add(new TrackPoint("C10", new Position(25, 25), 2000));
// 威胁事件:交叉后 3 秒,A7 射门
List<ThreatEvent> threats = Arrays.asList(
new ThreatEvent(5000, "A7", "SHOT"),
new ThreatEvent(9000, "C10", "KEY_PASS") // 无关球员的威胁
);
CrossRunThreatAnalyzer analyzer = new CrossRunThreatAnalyzer();
int count = analyzer.countCrossRunThreats(tracks, threats);
System.out.println("\n>>> 交叉跑位造成威胁的次数:" + count);
}
}
运行结果
交叉跑位[p1=A7, p2=B9] 于 t=2000 ms,在 3000 ms 后由 A7 形成威胁(SHOT)
>>> 交叉跑位造成威胁的次数:1
核心算法思路
| 步骤 | 说明 |
|---|---|
| 数据建模 | TrackPoint 记录球员位置+时间;ThreatEvent 记录射门/关键传球等威胁 |
| 分组排序 | 按球员ID分组,按时间戳排序 |
| 交叉检测 | 双指针遍历两条轨迹,在同一时间窗口内,两名球员距离 ≤ 3米判定为"接近",从"远"到"近"的状态跳变即视为一次交叉点 |
| 威胁关联 | 在交叉点之后的 5 秒内,若任一交叉球员产生威胁事件,则计为交叉跑位威胁 +1 |
| 去重 | 用 Set 避免同一次威胁被多次统计 |
可扩展方向
- 速度方向判断:加入方向向量,判断是否真的"反向交叉"(真正的cross run),而非静态接近。
- 防守球员关联:加入防守球员数据,判断交叉是否造成"摆脱防守"。
- 威胁权重:射门=1,关键传球=0.8,助攻=1.5,而非简单计数。
- xG(预期进球):结合位置计算威胁值,而非布尔判断。
- 真实数据接入:可对接 Sportradar、StatsBomb 等 API 的 tracking data。
如果你有真实的追踪数据格式(如 JSON/CSV),我可以帮你把解析部分也补上,需要针对11人全场或特定战术场景扩展吗?