每日算法 — 使用java实现足球:图论传球网络与贪心最优路径规划

足球作为全球最受欢迎的运动之一,其战术体系背后蕴含着丰富的算法思想。本文将使用Java实现一个足球传球网络分析系统,重点讲解图论建模、最短路径算法(Dijkstra)以及贪心策略在团队配合中的算法应用。

足球战术的图论抽象

将足球场上的球员和传球关系抽象为图结构:

  • 节点(Vertex):场上球员,包含位置、能力值等属性
  • 边(Edge):传球路线,权重由传球距离、防守压力、视野遮挡等因素决定
  • 目标:寻找从持球球员到射门位置的最优传球路径

传球成功率模型

传球边的权重由以下因素综合计算:

weight = distanceFactor * pressureFactor * visibilityFactor / accuracyFactor

权重越低表示传球路线越优。

核心数据结构

球员节点

/**
 * 球员节点类
 * 表示图中的一个顶点,包含位置和能力属性
 */
public class Player {

    private final String id;
    private final String name;
    private double x;           // 场上X坐标(0-100,横向)
    private double y;           // 场上Y坐标(0-100,纵向)
    private double passing;     // 传球能力值 0-100
    private double vision;      // 视野能力值 0-100
    private double speed;       // 速度能力值 0-100
    private boolean hasBall;    // 是否持球
    private boolean isGoalkeeper; // 是否门将

    public Player(String id, String name, double x, double y) {
        this.id = id;
        this.name = name;
        this.x = x;
        this.y = y;
        this.passing = 70.0;
        this.vision = 70.0;
        this.speed = 70.0;
        this.hasBall = false;
        this.isGoalkeeper = false;
    }

    /**
     * 计算与另一球员的欧几里得距离
     */
    public double distanceTo(Player other) {
        double dx = this.x - other.x;
        double dy = this.y - other.y;
        return Math.sqrt(dx * dx + dy * dy);
    }

    /**
     * 判断球员是否在对方半场(进攻方向)
     */
    public boolean isInOpponentHalf(boolean attackingRight) {
        return attackingRight ? this.x > 50 : this.x < 50;
    }

    // Getters and setters
    public String getId() { return id; }
    public String getName() { return name; }
    public double getX() { return x; }
    public double getY() { return y; }
    public void setPosition(double x, double y) { this.x = x; this.y = y; }
    public double getPassing() { return passing; }
    public void setPassing(double passing) { this.passing = passing; }
    public double getVision() { return vision; }
    public void setVision(double vision) { this.vision = vision; }
    public boolean hasBall() { return hasBall; }
    public void setHasBall(boolean hasBall) { this.hasBall = hasBall; }
    public boolean isGoalkeeper() { return isGoalkeeper; }
    public void setGoalkeeper(boolean isGoalkeeper) { this.isGoalkeeper = isGoalkeeper; }

    @Override
    public String toString() {
        return String.format("%s(%s, %.0f, %.0f)", name, id, x, y);
    }
}

传球边

/**
 * 传球路线(图的边)
 * 包含权重计算和传球质量评估
 */
public class Pass {

    private final Player from;
    private final Player to;
    private double weight;           // 路径权重(越低越好)
    private double successRate;      // 传球成功率估计

    // 权重计算参数
    private static final double MAX_PASS_DISTANCE = 40.0;  // 最大有效传球距离
    private static final double PRESSURE_RADIUS = 15.0;     // 防守压力影响半径

    public Pass(Player from, Player to) {
        this.from = from;
        this.to = to;
        calculateWeight();
    }

    /**
     * 计算传球边的权重
     * 综合考虑距离、传球能力和几何角度
     */
    private void calculateWeight() {
        double distance = from.distanceTo(to);

        // 距离因子:超过最大距离时权重急剧上升
        double distanceFactor;
        if (distance > MAX_PASS_DISTANCE) {
            distanceFactor = 1000.0;  // 惩罚过远距离
        } else {
            distanceFactor = distance / MAX_PASS_DISTANCE * 50.0;
        }

        // 传球能力因子:传球能力越高,权重越低
        double abilityFactor = 100.0 - from.getPassing();

        // 视野因子:考虑传球角度(简化模型)
        double visionFactor = 100.0 - from.getVision();

        // 综合权重
        this.weight = distanceFactor * 0.5 + abilityFactor * 0.3 + visionFactor * 0.2;

        // 估算成功率
        this.successRate = Math.max(0.1, 
            (from.getPassing() / 100.0) * (1.0 - distance / MAX_PASS_DISTANCE));
    }

    public Player getFrom() { return from; }
    public Player getTo() { return to; }
    public double getWeight() { return weight; }
    public double getSuccessRate() { return successRate; }
    public double getDistance() { return from.distanceTo(to); }

    @Override
    public String toString() {
        return String.format("Pass(%s -> %s, weight=%.1f, rate=%.1f%%)", 
                            from.getName(), to.getName(), weight, successRate * 100);
    }
}

传球网络图

import java.util.*;

/**
 * 传球网络图
 * 基于邻接表实现的有权图
 */
public class PassingNetwork {

    private final Map<String, Player> players;           // 球员ID -> 球员
    private final Map<String, List<Pass>> adjacencyList; // 球员ID -> 出边列表
    private final boolean attackingRight;                // 进攻方向

    public PassingNetwork(boolean attackingRight) {
        this.players = new HashMap<>();
        this.adjacencyList = new HashMap<>();
        this.attackingRight = attackingRight;
    }

    public void addPlayer(Player player) {
        players.put(player.getId(), player);
        adjacencyList.putIfAbsent(player.getId(), new ArrayList<>());
    }

    /**
     * 添加双向传球路线(无向边)
     */
    public void addPassRoute(Player from, Player to) {
        Pass pass1 = new Pass(from, to);
        Pass pass2 = new Pass(to, from);

        adjacencyList.computeIfAbsent(from.getId(), k -> new ArrayList<>()).add(pass1);
        adjacencyList.computeIfAbsent(to.getId(), k -> new ArrayList<>()).add(pass2);
    }

    /**
     * 获取球员的所有传球选择
     */
    public List<Pass> getPassOptions(String playerId) {
        return adjacencyList.getOrDefault(playerId, Collections.emptyList());
    }

    public Player getPlayer(String id) {
        return players.get(id);
    }

    public Collection<Player> getAllPlayers() {
        return players.values();
    }

    public boolean isAttackingRight() {
        return attackingRight;
    }
}

Dijkstra最短路径算法

import java.util.*;

/**
 * Dijkstra最短路径实现
 * 用于寻找最优传球路线
 */
public class PathFinder {

    /**
     * Dijkstra算法节点状态
     */
    private static class NodeState implements Comparable<NodeState> {
        String playerId;
        double distance;
        String previous;  // 前驱节点

        NodeState(String playerId, double distance, String previous) {
            this.playerId = playerId;
            this.distance = distance;
            this.previous = previous;
        }

        @Override
        public int compareTo(NodeState other) {
            return Double.compare(this.distance, other.distance);
        }
    }

    /**
     * 寻找从持球球员到目标区域的最优传球路径
     * 
     * @param network 传球网络
     * @param startId 持球球员ID
     * @param targetX 目标区域X坐标(射门位置)
     * @param targetY 目标区域Y坐标
     * @return 最优传球路径(球员ID列表),若无路径返回空列表
     */
    public List<Player> findOptimalPassingRoute(PassingNetwork network, 
                                                  String startId, 
                                                  double targetX, double targetY) {

        // Dijkstra标准实现
        Map<String, Double> distances = new HashMap<>();
        Map<String, String> previous = new HashMap<>();
        Set<String> visited = new HashSet<>();
        PriorityQueue<NodeState> pq = new PriorityQueue<>();

        // 初始化
        for (Player p : network.getAllPlayers()) {
            distances.put(p.getId(), Double.MAX_VALUE);
        }
        distances.put(startId, 0.0);
        pq.offer(new NodeState(startId, 0.0, null));

        String closestToTarget = null;
        double minTargetDistance = Double.MAX_VALUE;

        while (!pq.isEmpty()) {
            NodeState current = pq.poll();

            if (visited.contains(current.playerId)) {
                continue;
            }
            visited.add(current.playerId);

            Player currentPlayer = network.getPlayer(current.playerId);

            // 检查是否到达目标区域附近
            double distToTarget = Math.sqrt(
                Math.pow(currentPlayer.getX() - targetX, 2) + 
                Math.pow(currentPlayer.getY() - targetY, 2)
            );

            if (distToTarget < minTargetDistance && 
                currentPlayer.isInOpponentHalf(network.isAttackingRight())) {
                minTargetDistance = distToTarget;
                closestToTarget = current.playerId;
            }

            // 松弛操作
            for (Pass pass : network.getPassOptions(current.playerId)) {
                String neighborId = pass.getTo().getId();
                if (visited.contains(neighborId)) {
                    continue;
                }

                double newDist = current.distance + pass.getWeight();
                if (newDist < distances.get(neighborId)) {
                    distances.put(neighborId, newDist);
                    previous.put(neighborId, current.playerId);
                    pq.offer(new NodeState(neighborId, newDist, current.playerId));
                }
            }
        }

        // 重构路径
        if (closestToTarget == null) {
            return Collections.emptyList();
        }

        return reconstructPath(network, previous, startId, closestToTarget);
    }

    /**
     * 重构路径
     */
    private List<Player> reconstructPath(PassingNetwork network, 
                                          Map<String, String> previous,
                                          String startId, String endId) {
        LinkedList<Player> path = new LinkedList<>();
        String current = endId;

        while (current != null) {
            path.addFirst(network.getPlayer(current));
            current = previous.get(current);
        }

        return path;
    }
}

贪心最优决策

import java.util.*;

/**
 * 贪心传球决策器
 * 每一步选择当前最优的传球目标
 */
public class GreedyPassingStrategy {

    /**
     * 贪心策略:选择当前位置最优的直接传球目标
     * 综合考虑:成功率、推进距离、防守压力
     * 
     * @param network 传球网络
     * @param from 当前持球球员
     * @return 最优传球选择
     */
    public Pass selectBestPass(PassingNetwork network, Player from) {
        List<Pass> options = network.getPassOptions(from.getId());
        if (options.isEmpty()) {
            return null;
        }

        Pass bestPass = null;
        double bestScore = Double.MIN_VALUE;

        for (Pass pass : options) {
            double score = evaluatePass(pass, network);
            if (score > bestScore) {
                bestScore = score;
                bestPass = pass;
            }
        }

        return bestPass;
    }

    /**
     * 评估传球价值
     * 综合评分函数
     */
    private double evaluatePass(Pass pass, PassingNetwork network) {
        Player to = pass.getTo();

        // 因子1:传球成功率(越高越好)
        double successScore = pass.getSuccessRate() * 40;

        // 因子2:推进距离(向进攻方向的位移)
        double progressScore = calculateProgressScore(pass, network) * 30;

        // 因子3:接球球员的危险位置(是否处于射门有利位置)
        double threatScore = calculateThreatScore(to, network) * 20;

        // 因子4:传球距离惩罚(过短传球的负面分)
        double distancePenalty = -pass.getDistance() * 0.1;

        return successScore + progressScore + threatScore + distancePenalty;
    }

    /**
     * 计算推进得分
     * 向球门方向移动的距离
     */
    private double calculateProgressScore(Pass pass, PassingNetwork network) {
        Player from = pass.getFrom();
        Player to = pass.getTo();

        double progress;
        if (network.isAttackingRight()) {
            progress = to.getX() - from.getX();
        } else {
            progress = from.getX() - to.getX();
        }

        return Math.max(0, progress);
    }

    /**
     * 计算威胁得分
     * 基于接球球员与球门的距离和角度
     */
    private double calculateThreatScore(Player player, PassingNetwork network) {
        double goalX = network.isAttackingRight() ? 100.0 : 0.0;
        double goalY = 50.0;  // 球门中心

        double distToGoal = Math.sqrt(
            Math.pow(player.getX() - goalX, 2) + 
            Math.pow(player.getY() - goalY, 2)
        );

        // 距离球门越近威胁越高
        return Math.max(0, 100 - distToGoal);
    }

    /**
     * 模拟一次进攻回合
     * 使用贪心策略连续传球直到射门或丢球
     */
    public List<Pass> simulateAttack(PassingNetwork network, Player startPlayer) {
        List<Pass> attackSequence = new ArrayList<>();
        Player current = startPlayer;
        int maxPasses = 10;  // 最多10脚传球

        System.out.println("\n=== 进攻模拟 ===");
        System.out.println("起始: " + current);

        for (int i = 0; i < maxPasses; i++) {
            Pass bestPass = selectBestPass(network, current);

            if (bestPass == null) {
                System.out.println("无传球选择,进攻结束");
                break;
            }

            // 模拟传球结果
            double roll = Math.random();
            if (roll > bestPass.getSuccessRate()) {
                System.out.println("传球失败!" + bestPass);
                break;
            }

            attackSequence.add(bestPass);
            System.out.println("第" + (i + 1) + "传: " + bestPass);

            current = bestPass.getTo();

            // 检查是否到达射门位置
            double goalX = network.isAttackingRight() ? 100.0 : 0.0;
            double distToGoal = Math.abs(current.getX() - goalX);
            if (distToGoal < 20 && !current.isGoalkeeper()) {
                System.out.println("到达射门位置!距离: " + String.format("%.1f", distToGoal));
                break;
            }
        }

        return attackSequence;
    }
}

完整演示

/**
 * 足球传球网络演示
 */
public class FootballDemo {

    public static void main(String[] args) {
        // 创建传球网络(向右进攻)
        PassingNetwork network = new PassingNetwork(true);

        // 创建球员(简化4-3-3阵型)
        Player gk = new Player("GK", "门将", 5, 50);
        gk.setGoalkeeper(true);

        Player lb = new Player("LB", "左后卫", 20, 20);
        Player cb1 = new Player("CB1", "中后卫1", 20, 40);
        Player cb2 = new Player("CB2", "中后卫2", 20, 60);
        Player rb = new Player("RB", "右后卫", 20, 80);

        Player cm1 = new Player("CM1", "中场1", 40, 30);
        cm1.setPassing(85); cm1.setVision(80);
        Player cm2 = new Player("CM2", "中场2", 40, 50);
        cm2.setPassing(90); cm2.setVision(85);
        Player cm3 = new Player("CM3", "中场3", 40, 70);
        cm3.setPassing(80); cm3.setVision(75);

        Player lw = new Player("LW", "左边锋", 60, 20);
        lw.setSpeed(90);
        Player st = new Player("ST", "前锋", 65, 50);
        st.setPassing(75); st.setSpeed(85);
        Player rw = new Player("RW", "右边锋", 60, 80);
        rw.setSpeed(88);

        // 添加球员
        for (Player p : Arrays.asList(gk, lb, cb1, cb2, rb, cm1, cm2, cm3, lw, st, rw)) {
            network.addPlayer(p);
        }

        // 建立传球路线(相邻位置可传球)
        network.addPassRoute(gk, cb1);
        network.addPassRoute(gk, cb2);
        network.addPassRoute(lb, cb1);
        network.addPassRoute(cb1, cb2);
        network.addPassRoute(cb2, rb);
        network.addPassRoute(lb, cm1);
        network.addPassRoute(cb1, cm2);
        network.addPassRoute(cb2, cm3);
        network.addPassRoute(rb, cm3);
        network.addPassRoute(cm1, cm2);
        network.addPassRoute(cm2, cm3);
        network.addPassRoute(cm1, lw);
        network.addPassRoute(cm2, st);
        network.addPassRoute(cm3, rw);
        network.addPassRoute(lw, st);
        network.addPassRoute(st, rw);

        // 设置CM2持球
        cm2.setHasBall(true);

        System.out.println("===== 足球传球网络分析 =====\n");

        // 1. Dijkstra最短路径
        PathFinder pathFinder = new PathFinder();
        List<Player> path = pathFinder.findOptimalPassingRoute(
            network, "CM2", 100, 50
        );

        System.out.println("Dijkstra最优路径到球门附近:");
        for (int i = 0; i < path.size(); i++) {
            System.out.print(path.get(i).getName());
            if (i < path.size() - 1) System.out.print(" -> ");
        }
        System.out.println("\n");

        // 2. 贪心策略模拟
        GreedyPassingStrategy greedy = new GreedyPassingStrategy();
        List<Pass> attack = greedy.simulateAttack(network, cm2);

        System.out.println("\n本次进攻共 " + attack.size() + " 脚传球");
    }
}

复杂度分析

操作 时间复杂度 空间复杂度 说明
图构建 O(V + E) O(V + E) V球员数,E传球路线数
Dijkstra O(E log V) O(V) 优先队列优化
贪心选择 O(E) O(1) 遍历所有出边
进攻模拟 O(k * E) O(k) k为传球次数

扩展方向

  1. 实时防守压力计算:引入对方球员位置,动态调整边权重
  2. A*寻路优化:使用球门位置作为启发函数,加速路径搜索
  3. 蒙特卡洛模拟:模拟大量进攻回合,评估不同阵型的期望得分
  4. PageRank中心性:分析球员在传球网络中的重要程度

总结

本文将足球战术抽象为图论问题,通过Dijkstra算法寻找最优传球路径,利用贪心策略模拟实时决策。读者可以在此基础上扩展更复杂的战术模型,将经典算法应用于体育数据分析领域。