每日算法 — 使用java实现网球:空气阻力抛物线轨迹与最优击球策略

引言

网球是一项对球路预判要求极高的运动。与理想抛物线不同,真实网球在空中飞行时受到空气阻力、马格努斯效应(旋转)和重力三重作用,轨迹呈现复杂的非线性特征。本文将用Java实现一个考虑空气阻力的网球轨迹模拟系统,并基于对手位置设计贪心最优击球点决策算法。

物理模型与算法原理

1. 空气阻力下的运动方程

设网球质量为 ( m ),速度为 ( \vec{v} ),空气阻力与速度平方成正比:

[ \vec{F}_d = -\frac{1}{2} \rho C_d A |\vec{v}| \vec{v} ]

其中 ( \rho ) 为空气密度,( C_d ) 为阻力系数,( A ) 为迎风面积。运动方程为:

[ m\frac{d\vec{v}}{dt} = \vec{F}_g + \vec{F}_d ]

2. 数值积分:四阶龙格-库塔法(RK4)

由于空气阻力使方程非线性,解析解难以求得,采用RK4数值积分逐步计算位置和速度。

3. 最优击球策略

给定对手位置,通过模拟不同初始速度和角度的轨迹,选择使对手跑动距离最大化的击球点。

Java完整实现

import java.util.ArrayList;
import java.util.List;

/**
 * 网球轨迹模拟与最优击球策略系统
 * 核心算法:RK4数值积分 + 贪心最优击球点搜索
 */
public class TennisSimulator {

    // 物理常量
    private static final double BALL_MASS = 0.057; // kg
    private static final double AIR_DENSITY = 1.225; // kg/m^3
    private static final double DRAG_COEFFICIENT = 0.507;
    private static final double BALL_RADIUS = 0.033; // m
    private static final double CROSS_SECTIONAL_AREA = Math.PI * BALL_RADIUS * BALL_RADIUS;
    private static final double GRAVITY = 9.81;
    private static final double DRAG_FACTOR = 0.5 * AIR_DENSITY * DRAG_COEFFICIENT * CROSS_SECTIONAL_AREA / BALL_MASS;

    // 球场尺寸 (m)
    private static final double COURT_LENGTH = 23.77;
    private static final double COURT_WIDTH = 10.97;
    private static final double NET_HEIGHT = 0.914;

    /**
     * 三维向量
     */
    static class Vector3D {
        double x, y, z;
        Vector3D(double x, double y, double z) { this.x = x; this.y = y; this.z = z; }
        Vector3D add(Vector3D v) { return new Vector3D(x + v.x, y + v.y, z + v.z); }
        Vector3D scale(double s) { return new Vector3D(x * s, y * s, z * s); }
        double magnitude() { return Math.sqrt(x * x + y * y + z * z); }
        Vector3D copy() { return new Vector3D(x, y, z); }
        @Override
        public String toString() { return String.format("(%.3f, %.3f, %.3f)", x, y, z); }
    }

    /**
     * 轨迹点记录
     */
    static class TrajectoryPoint {
        double time;
        Vector3D position;
        Vector3D velocity;
        TrajectoryPoint(double t, Vector3D p, Vector3D v) { time = t; position = p; velocity = v; }
    }

    /**
     * 计算加速度(含重力和空气阻力)
     */
    private Vector3D computeAcceleration(Vector3D velocity) {
        double speed = velocity.magnitude();
        // 重力向下
        Vector3D gravity = new Vector3D(0, 0, -GRAVITY);
        // 空气阻力与速度方向相反,大小与速度平方成正比
        if (speed < 1e-6) return gravity;
        double dragMagnitude = DRAG_FACTOR * speed * speed;
        Vector3D drag = velocity.scale(-dragMagnitude / speed);
        return gravity.add(drag);
    }

    /**
     * RK4单步积分
     * @param state 当前状态 [pos, vel]
     * @param dt 时间步长
     * @return 下一时刻状态
     */
    private Vector3D[] rk4Step(Vector3D[] state, double dt) {
        Vector3D pos = state[0], vel = state[1];

        // k1
        Vector3D k1v = computeAcceleration(vel).scale(dt);
        Vector3D k1p = vel.scale(dt);

        // k2
        Vector3D v2 = vel.add(k1v.scale(0.5));
        Vector3D k2v = computeAcceleration(v2).scale(dt);
        Vector3D k2p = v2.scale(dt);

        // k3
        Vector3D v3 = vel.add(k2v.scale(0.5));
        Vector3D k3v = computeAcceleration(v3).scale(dt);
        Vector3D k3p = v3.scale(dt);

        // k4
        Vector3D v4 = vel.add(k3v);
        Vector3D k4v = computeAcceleration(v4).scale(dt);
        Vector3D k4p = v4.scale(dt);

        Vector3D newPos = pos.add(
            k1p.add(k2p.scale(2)).add(k3p.scale(2)).add(k4p).scale(1.0 / 6.0)
        );
        Vector3D newVel = vel.add(
            k1v.add(k2v.scale(2)).add(k3v.scale(2)).add(k4v).scale(1.0 / 6.0)
        );

        return new Vector3D[]{newPos, newVel};
    }

    /**
     * 模拟网球完整轨迹
     * @param initialPos 初始位置
     * @param initialVel 初始速度
     * @param dt 时间步长
     * @param maxTime 最大模拟时间
     * @return 轨迹点列表
     */
    public List<TrajectoryPoint> simulateTrajectory(Vector3D initialPos, Vector3D initialVel, double dt, double maxTime) {
        List<TrajectoryPoint> trajectory = new ArrayList<>();
        Vector3D pos = initialPos.copy();
        Vector3D vel = initialVel.copy();
        double t = 0;

        trajectory.add(new TrajectoryPoint(t, pos.copy(), vel.copy()));

        while (t < maxTime && pos.z >= 0) {
            Vector3D[] state = rk4Step(new Vector3D[]{pos, vel}, dt);
            pos = state[0];
            vel = state[1];
            t += dt;
            trajectory.add(new TrajectoryPoint(t, pos.copy(), vel.copy()));

            // 过网检测:当球在网附近且高度低于网高时判定触网
            if (Math.abs(pos.x - COURT_LENGTH / 2) < 0.5 && pos.z < NET_HEIGHT && vel.z < 0) {
                break; // 触网,停止模拟
            }
        }

        return trajectory;
    }

    /**
     * 计算击球落点
     */
    public Vector3D getLandingPoint(List<TrajectoryPoint> trajectory) {
        for (int i = 1; i < trajectory.size(); i++) {
            TrajectoryPoint prev = trajectory.get(i - 1);
            TrajectoryPoint curr = trajectory.get(i);
            if (prev.position.z >= 0 && curr.position.z <= 0) {
                // 线性插值估计落地点
                double alpha = prev.position.z / (prev.position.z - curr.position.z);
                double lx = prev.position.x + alpha * (curr.position.x - prev.position.x);
                double ly = prev.position.y + alpha * (curr.position.y - prev.position.y);
                return new Vector3D(lx, ly, 0);
            }
        }
        return null;
    }

    /**
     * 贪心最优击球策略:寻找使对手跑动距离最大的击球参数
     * @param myPosition 我方击球位置
     * @param opponentPosition 对手当前位置
     * @return 最优初始速度
     */
    public Vector3D findOptimalShot(Vector3D myPosition, Vector3D opponentPosition) {
        double bestScore = -1;
        Vector3D bestVelocity = null;

        // 枚举速度大小和角度(简化搜索空间)
        for (double speed = 20; speed <= 50; speed += 2) {
            for (double angleZ = Math.toRadians(5); angleZ <= Math.toRadians(30); angleZ += Math.toRadians(2)) {
                for (double angleY = Math.toRadians(-10); angleY <= Math.toRadians(10); angleY += Math.toRadians(2)) {
                    double vx = speed * Math.cos(angleZ) * Math.cos(angleY);
                    double vy = speed * Math.cos(angleZ) * Math.sin(angleY);
                    double vz = speed * Math.sin(angleZ);

                    Vector3D vel = new Vector3D(vx, vy, vz);
                    List<TrajectoryPoint> traj = simulateTrajectory(myPosition, vel, 0.01, 5.0);
                    Vector3D landing = getLandingPoint(traj);

                    if (landing == null) continue;

                    // 落点必须在对方场地内
                    if (landing.x <= COURT_LENGTH / 2 || landing.x >= COURT_LENGTH ||
                        Math.abs(landing.y) > COURT_WIDTH / 2) {
                        continue;
                    }

                    // 评分:对手到落点的距离 + 落点远离对手中心线的程度
                    double distToOpponent = Math.sqrt(
                        Math.pow(landing.x - opponentPosition.x, 2) +
                        Math.pow(landing.y - opponentPosition.y, 2)
                    );
                    double crossCourtBonus = Math.abs(landing.y);
                    double score = distToOpponent + 0.5 * crossCourtBonus;

                    if (score > bestScore) {
                        bestScore = score;
                        bestVelocity = vel;
                    }
                }
            }
        }

        return bestVelocity;
    }

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

        // 我方在底线发球
        Vector3D myPos = new Vector3D(0, 0, 2.5);
        // 对手在对面底线中央
        Vector3D opponentPos = new Vector3D(COURT_LENGTH, 0, 0);

        System.out.println("=== 网球轨迹模拟与最优击球策略 ===\n");

        // 测试不同初始速度的轨迹
        Vector3D[] testVelocities = {
            new Vector3D(35, 0, 8),
            new Vector3D(35, 3, 8),
            new Vector3D(35, -3, 8)
        };

        for (Vector3D vel : testVelocities) {
            List<TrajectoryPoint> traj = sim.simulateTrajectory(myPos, vel, 0.01, 5.0);
            Vector3D landing = sim.getLandingPoint(traj);
            System.out.printf("初始速度: %s%n", vel);
            System.out.printf("落点: %s%n", landing);
            System.out.printf("飞行时间: %.3f s%n", traj.get(traj.size() - 1).time);
            System.out.println();
        }

        // 寻找最优击球
        System.out.println("=== 贪心最优击球搜索 ===");
        Vector3D optimalVel = sim.findOptimalShot(myPos, opponentPos);
        if (optimalVel != null) {
            List<TrajectoryPoint> optimalTraj = sim.simulateTrajectory(myPos, optimalVel, 0.01, 5.0);
            Vector3D optimalLanding = sim.getLandingPoint(optimalTraj);
            System.out.printf("最优初始速度: %s%n", optimalVel);
            System.out.printf("最优落点: %s%n", optimalLanding);
            System.out.printf("对手跑动距离: %.3f m%n",
                Math.sqrt(Math.pow(optimalLanding.x - opponentPos.x, 2) +
                          Math.pow(optimalLanding.y - opponentPos.y, 2)));
        }
    }
}

复杂度分析

操作 时间复杂度 空间复杂度
RK4单步积分 ( O(1) ) ( O(1) )
完整轨迹模拟 ( O(T/dt) ),( T )为飞行时间 ( O(T/dt) )
贪心最优击球搜索 ( O(N_s \cdot N_a \cdot T/dt) ) ( O(T/dt) )

总结

本文实现了考虑空气阻力的网球轨迹模拟系统,核心贡献包括:
1. RK4数值积分:以高精度求解非线性运动方程
2. 物理建模:综合考虑重力与速度平方成正比的空气阻力
3. 贪心最优策略:通过枚举搜索找到使对手跑动距离最大化的击球参数

该模型可进一步扩展加入马格努斯效应(旋转)和风向影响,使模拟更加逼真。