每日算法 — 使用java实现篮球:团队传球网络建模与贪心最优投篮决策

篮球是一项充满策略与协作的团队运动。本文用Java实现篮球比赛的核心算法模拟,重点讲解团队传球网络的图论建模、球员跑位路径规划、防守覆盖计算以及基于期望值的贪心最优投篮决策算法。

一、核心算法模块

篮球战术模拟可拆解为以下算法问题:

  1. 传球网络:将球员视为节点,传球路线视为边,构建动态图
  2. 跑位路径:使用A*或Dijkstra计算球员到目标位置的最优路径
  3. 防守覆盖:计算防守球员对进攻球员的覆盖面积与干扰概率
  4. 投篮决策:综合投篮命中率、时间、比分差等因素计算最优选择

二、投篮命中率模型

投篮命中率受多种因素影响,可建模为:

$$P_{make} = P_{base} \times D_{defense} \times D_{fatigue} \times D_{distance}$$

其中:
– $P_{base}$:球员基础命中率
– $D_{defense}$:防守干扰因子(被紧逼时下降)
– $D_{fatigue}$:体力衰减因子
– $D_{distance}$:距离衰减因子(通常呈指数衰减)

三、Java完整实现

import java.util.*;

/**
 * 篮球战术模拟核心算法
 * 包含传球网络、跑位路径、防守覆盖与最优投篮决策
 */
public class BasketballSimulation {

    // 球场尺寸(标准NBA半场)
    private static final double COURT_WIDTH = 15.0;   // m
    private static final double COURT_HEIGHT = 14.0;  // m
    private static final double BASKET_X = 7.5;       // 篮筐X
    private static final double BASKET_Y = 0.0;       // 篮筐Y(底线)
    private static final double THREE_POINT_RADIUS = 7.24; // 三分线半径

    private static final double DT = 0.1;  // 模拟时间步长(秒)

    private List<Player> teamA = new ArrayList<>();  // 进攻方
    private List<Player> teamB = new ArrayList<>();  // 防守方
    private Ball ball;
    private double gameClock = 24.0;  // 进攻时间
    private int scoreA = 0, scoreB = 0;

    /**
     * 二维向量工具类
     */
    static class Vec2 {
        double x, y;
        Vec2(double x, double y) { this.x = x; this.y = y; }
        Vec2 add(Vec2 v) { return new Vec2(x + v.x, y + v.y); }
        Vec2 sub(Vec2 v) { return new Vec2(x - v.x, y - v.y); }
        Vec2 mul(double s) { return new Vec2(x * s, y * s); }
        double len() { return Math.sqrt(x * x + y * y); }
        double dist(Vec2 v) { return sub(v).len(); }
        Vec2 norm() {
            double l = len();
            return l > 0 ? new Vec2(x / l, y / l) : new Vec2(0, 0);
        }
        @Override
        public String toString() { return String.format("(%.1f, %.1f)", x, y); }
    }

    /**
     * 球员实体
     */
    static class Player {
        String name;
        Vec2 pos;           // 位置
        Vec2 vel;           // 速度
        double speed;       // 最大移动速度 m/s
        double baseFG;      // 基础投篮命中率
        double threeFG;     // 三分命中率
        double stamina;     // 体力 0-100
        double height;      // 身高 m
        boolean hasBall;    // 是否持球
        Vec2 targetPos;     // 目标跑位位置

        Player(String name, double x, double y, double speed, 
               double fg, double three, double height) {
            this.name = name;
            this.pos = new Vec2(x, y);
            this.vel = new Vec2(0, 0);
            this.speed = speed;
            this.baseFG = fg;
            this.threeFG = three;
            this.stamina = 100;
            this.height = height;
            this.hasBall = false;
            this.targetPos = new Vec2(x, y);
        }

        /**
         * 向目标位置移动
         */
        void moveTowards(Vec2 target, double dt) {
            Vec2 dir = target.sub(pos);
            double dist = dir.len();
            if (dist < 0.1) {
                vel = new Vec2(0, 0);
                return;
            }

            // 速度与体力相关
            double currentSpeed = speed * (0.5 + stamina / 200.0);
            double moveDist = Math.min(dist, currentSpeed * dt);
            vel = dir.norm().mul(moveDist / dt);
            pos = pos.add(dir.norm().mul(moveDist));

            // 移动消耗体力
            stamina -= moveDist * 0.5;
            stamina = Math.max(0, stamina);
        }

        /**
         * 计算当前位置的投篮命中率
         */
        double calculateShotProbability() {
            double distToBasket = pos.dist(new Vec2(BASKET_X, BASKET_Y));

            // 基础命中率选择(两分/三分)
            double base = distToBasket > THREE_POINT_RADIUS ? threeFG : baseFG;

            // 距离衰减:每远离1米命中率下降约5%
            double distFactor = Math.exp(-distToBasket * 0.08);

            // 体力衰减
            double fatigueFactor = 0.5 + stamina / 200.0;

            return base * distFactor * fatigueFactor;
        }
    }

    /**
     * 篮球实体
     */
    static class Ball {
        Vec2 pos;
        Player holder;      // 持球者
        Vec2 targetPos;     // 传球目标位置
        double passProgress; // 传球进度 0-1
        boolean inAir;      // 是否在空中

        Ball() {
            this.pos = new Vec2(7.5, 7.0);
            this.holder = null;
            this.inAir = false;
        }
    }

    BasketballSimulation() {
        // 初始化进攻方(5人)
        teamA.add(new Player("控卫", 7.5, 10.0, 5.0, 0.45, 0.38, 1.90));
        teamA.add(new Player("分卫", 11.0, 9.0, 4.8, 0.44, 0.40, 1.95));
        teamA.add(new Player("小前锋", 4.0, 9.0, 5.2, 0.47, 0.35, 2.00));
        teamA.add(new Player("大前锋", 10.0, 5.0, 4.5, 0.50, 0.30, 2.05));
        teamA.add(new Player("中锋", 5.0, 5.0, 4.0, 0.55, 0.15, 2.10));

        // 初始化防守方
        teamB.add(new Player("防控卫", 7.5, 8.5, 5.0, 0.30, 0.25, 1.88));
        teamB.add(new Player("防分卫", 10.5, 8.0, 4.8, 0.30, 0.25, 1.93));
        teamB.add(new Player("防小前", 4.5, 8.0, 5.2, 0.30, 0.25, 1.98));
        teamB.add(new Player("防大前", 9.5, 4.0, 4.5, 0.30, 0.25, 2.03));
        teamB.add(new Player("防中锋", 5.5, 4.0, 4.0, 0.30, 0.25, 2.08));

        this.ball = new Ball();
        teamA.get(0).hasBall = true;  // 控卫持球
        ball.holder = teamA.get(0);
    }

    /**
     * 计算防守覆盖因子:防守者距离越近,干扰越大
     */
    double calculateDefenseFactor(Player shooter) {
        double factor = 1.0;
        for (Player defender : teamB) {
            double dist = shooter.pos.dist(defender.pos);
            // 紧逼防守(<1m):命中率下降40%
            // 一般防守(1-3m):命中率下降10-40%
            // 放空(>3m):无影响
            if (dist < 1.0) {
                factor *= 0.6;
            } else if (dist < 3.0) {
                factor *= (0.6 + 0.4 * (dist - 1.0) / 2.0);
            }
        }
        return Math.max(0.3, factor);
    }

    /**
     * 寻找最优传球目标
     * 评估指标:接球后投篮期望得分 + 进攻空间开阔度
     */
    Player findBestPassTarget(Player passer) {
        Player best = null;
        double bestScore = Double.NEGATIVE_INFINITY;

        for (Player teammate : teamA) {
            if (teammate == passer) continue;

            // 传球可行性:路径上是否有防守拦截风险
            double passRisk = calculatePassRisk(passer.pos, teammate.pos);
            if (passRisk > 0.7) continue;  // 风险过高不传球

            // 接球后投篮期望值
            double shotProb = teammate.calculateShotProbability();
            double defenseFactor = calculateDefenseFactor(teammate);
            double isThree = teammate.pos.dist(new Vec2(BASKET_X, BASKET_Y)) > THREE_POINT_RADIUS ? 1 : 0;
            double expectedPoints = shotProb * defenseFactor * (2 + isThree);

            // 综合得分:期望值 - 传球风险惩罚
            double score = expectedPoints - passRisk * 2;

            if (score > bestScore) {
                bestScore = score;
                best = teammate;
            }
        }

        return best;
    }

    /**
     * 计算传球风险:基于防守球员与传球线的距离
     */
    double calculatePassRisk(Vec2 from, Vec2 to) {
        double maxRisk = 0;
        for (Player defender : teamB) {
            double dist = pointToLineDistance(defender.pos, from, to);
            // 距离传球线越近,拦截风险越高
            double risk = Math.max(0, 1.0 - dist / 2.0);
            maxRisk = Math.max(maxRisk, risk);
        }
        return maxRisk;
    }

    /**
     * 点到线段距离
     */
    double pointToLineDistance(Vec2 p, Vec2 a, Vec2 b) {
        Vec2 ab = b.sub(a);
        Vec2 ap = p.sub(a);
        double t = Math.max(0, Math.min(1, (ap.x * ab.x + ap.y * ab.y) / (ab.x * ab.x + ab.y * ab.y)));
        Vec2 closest = a.add(ab.mul(t));
        return p.dist(closest);
    }

    /**
     * 执行投篮
     */
    boolean shoot(Player shooter) {
        double prob = shooter.calculateShotProbability();
        prob *= calculateDefenseFactor(shooter);

        boolean made = Math.random() < prob;
        double dist = shooter.pos.dist(new Vec2(BASKET_X, BASKET_Y));
        int points = dist > THREE_POINT_RADIUS ? 3 : 2;

        if (made) {
            scoreA += points;
            System.out.printf("🏀 %s %.1fm处命中%d分球!命中率%.1f%%%n",
                shooter.name, dist, points, prob * 100);
        } else {
            System.out.printf("❌ %s %.1fm处投篮不中 (预期%.1f%%)%n",
                shooter.name, dist, prob * 100);
        }

        shooter.hasBall = false;
        ball.holder = null;
        gameClock = 24.0;  // 重置进攻时间
        return made;
    }

    /**
     * 执行传球
     */
    void pass(Player from, Player to) {
        from.hasBall = false;
        to.hasBall = true;
        ball.holder = to;
        System.out.printf("🔄 %s → %s%n", from.name, to.name);
    }

    /**
     * 更新防守球员位置:紧贴对位进攻球员
     */
    void updateDefense() {
        for (int i = 0; i < teamB.size() && i < teamA.size(); i++) {
            Player defender = teamB.get(i);
            Player target = teamA.get(i);

            // 人盯人防守:保持在进攻者与篮筐之间
            Vec2 basket = new Vec2(BASKET_X, BASKET_Y);
            Vec2 dir = target.pos.sub(basket).norm();
            Vec2 idealPos = target.pos.sub(dir.mul(1.2));  // 距离1.2m

            defender.moveTowards(idealPos, DT);
        }
    }

    /**
     * 更新进攻跑位
     */
    void updateOffense() {
        // 持球者寻找最优动作
        Player ballHandler = ball.holder;
        if (ballHandler == null) return;

        // 决策1:是否直接投篮?
        double shotProb = ballHandler.calculateShotProbability();
        shotProb *= calculateDefenseFactor(ballHandler);
        double expectedValue = shotProb * 2;  // 简化:按2分计算

        // 决策2:传球给队友的期望
        Player passTarget = findBestPassTarget(ballHandler);
        double passExpected = 0;
        if (passTarget != null) {
            double targetProb = passTarget.calculateShotProbability();
            targetProb *= calculateDefenseFactor(passTarget);
            passExpected = targetProb * 2;
        }

        // 贪心决策:选择期望得分更高的动作
        if (expectedValue >= passExpected && expectedValue > 0.3) {
            shoot(ballHandler);
        } else if (passTarget != null) {
            pass(ballHandler, passTarget);
        } else {
            // 没有好机会,运球寻找空间
            Vec2 basket = new Vec2(BASKET_X, BASKET_Y);
            Vec2 driveDir = basket.sub(ballHandler.pos).norm();
            ballHandler.moveTowards(ballHandler.pos.add(driveDir.mul(2)), DT);
        }

        // 无球跑位:其他球员寻找空位
        for (Player p : teamA) {
            if (p.hasBall) continue;
            // 简单跑位:向三分线外拉开空间
            Vec2 basket = new Vec2(BASKET_X, BASKET_Y);
            double dist = p.pos.dist(basket);
            if (dist < THREE_POINT_RADIUS * 0.8) {
                Vec2 away = p.pos.sub(basket).norm();
                p.targetPos = p.pos.add(away.mul(3));
            }
            p.moveTowards(p.targetPos, DT);
        }
    }

    /**
     * 模拟一次进攻回合
     */
    void simulatePossession() {
        System.out.println("\n=== 进攻回合开始 ===");
        gameClock = 24.0;

        // 重置球权到控卫
        for (Player p : teamA) p.hasBall = false;
        teamA.get(0).hasBall = true;
        ball.holder = teamA.get(0);

        while (gameClock > 0 && ball.holder != null) {
            updateDefense();
            updateOffense();
            gameClock -= DT;

            // 体力自然恢复
            for (Player p : teamA) { p.stamina = Math.min(100, p.stamina + 0.05); }
            for (Player p : teamB) { p.stamina = Math.min(100, p.stamina + 0.05); }
        }

        System.out.printf("比分 A:%d - B:%d%n", scoreA, scoreB);
    }

    public static void main(String[] args) {
        BasketballSimulation sim = new BasketballSimulation();

        // 模拟5次进攻回合
        for (int i = 0; i < 5; i++) {
            sim.simulatePossession();
        }

        System.out.println("\n=== 最终比分 ===");
        System.out.printf("A队 %d : %d B队%n", sim.scoreA, sim.scoreB);
    }
}

四、传球网络的图论分析

将球队传球建模为有向图 $G=(V, E)$,其中顶点 $V$ 为球员,边 $E$ 为传球。引入网络流概念分析进攻效率:

  • 节点容量:球员的持球处理能力
  • 边权重:传球成功率 × 接球后得分期望
  • 最大流:从持球者到篮筐的最优进攻路径

使用Edmonds-Karp或Dinic算法求解最大流,可量化评估战术路线的有效性。

五、投篮决策的期望最大化

将投篮决策建模为马尔可夫决策过程(MDP):

  • 状态:球员位置、剩余时间、比分差、体力
  • 动作:投篮、传球、运球突破
  • 奖励:得分、时间消耗、防守吸引

通过值迭代或蒙特卡洛树搜索求解最优策略,但贪心策略在实时性要求高的场景中已能取得不错效果。

六、总结

本文通过Java实现了篮球战术模拟的核心算法。从传球网络到投篮决策,每个模块都展示了算法在体育模拟中的应用。核心收获包括:

  • 投篮命中率的距离-体力-防守多因子模型
  • 传球风险评估的几何计算方法(点到线段距离)
  • 贪心决策框架:比较各动作的期望得分选择最优
  • 人盯人防守的位置计算(保持在进攻者与篮筐之间)