每日算法 — 使用java实现欧拉方法:常微分方程初值问题的数值积分

引言

欧拉方法(Euler Method)是求解常微分方程(ODE)初值问题最基础、最直观的数值方法。由莱昂哈德·欧拉在18世纪提出,该方法通过用切线近似代替曲线,将连续的微分方程转化为离散的递推公式。虽然其精度较低(一阶收敛),但欧拉方法是理解更高级数值方法(如龙格-库塔法、亚当斯法)的基石。本文将用Java实现前向欧拉、后向欧拉和改进欧拉三种变体,并分析其稳定性与误差。

数学背景

初值问题定义

给定常微分方程:y'(t) = f(t, y(t)),初始条件 y(t₀) = y₀

目标是求y(t)在区间[t₀, t_end]上的近似数值解。

前向欧拉法

从微分中值定理出发:y(t+h) ≈ y(t) + h·y'(t) = y(t) + h·f(t, y(t))

其中h为步长。该方法显式、计算简单,但条件稳定。

后向欧拉法

y_{n+1} = y_n + h·f(t_{n+1}, y_{n+1})

隐式格式,每步需解方程,但无条件稳定(A-稳定)。

改进欧拉法(梯形法则/Heun法)

结合前后欧拉:y_{n+1} = y_n + h/2 · [f(t_n, y_n) + f(t_{n+1}, y*_{n+1})]

其中 y*_{n+1} = y_n + h·f(t_n, y_n) 为预测值。二阶精度。

Java实现

import java.util.function.BiFunction;
import java.util.ArrayList;
import java.util.List;

/**
 * 欧拉方法家族:求解常微分方程初值问题
 * y' = f(t, y), y(t0) = y0
 */
public class EulerMethod {

    /**
     * 前向欧拉法(显式欧拉)
     * y_{n+1} = y_n + h * f(t_n, y_n)
     *
     * @param f     右端函数 f(t, y)
     * @param t0    初始时间
     * @param y0    初始值
     * @param h     步长
     * @param steps 迭代步数
     * @return 每一步的 (t, y) 列表
     */
    public static List<Point> forwardEuler(
            BiFunction<Double, Double, Double> f,
            double t0, double y0, double h, int steps) {
        List<Point> result = new ArrayList<>();
        double t = t0;
        double y = y0;
        result.add(new Point(t, y));

        for (int i = 0; i < steps; i++) {
            double slope = f.apply(t, y);  // 当前点的斜率
            y = y + h * slope;             // 沿切线前进
            t = t + h;
            result.add(new Point(t, y));
        }
        return result;
    }

    /**
     * 后向欧拉法(隐式欧拉)
     * y_{n+1} = y_n + h * f(t_{n+1}, y_{n+1})
     * 使用不动点迭代求解隐式方程
     */
    public static List<Point> backwardEuler(
            BiFunction<Double, Double, Double> f,
            double t0, double y0, double h, int steps,
            double tol, int maxInnerIter) {
        List<Point> result = new ArrayList<>();
        double t = t0;
        double y = y0;
        result.add(new Point(t, y));

        for (int i = 0; i < steps; i++) {
            double tNext = t + h;
            // 用前向欧拉的结果作为初值猜测
            double yGuess = y + h * f.apply(t, y);

            // 不动点迭代求解隐式方程
            double yNext = yGuess;
            for (int j = 0; j < maxInnerIter; j++) {
                double yNew = y + h * f.apply(tNext, yNext);
                if (Math.abs(yNew - yNext) < tol) {
                    yNext = yNew;
                    break;
                }
                yNext = yNew;
            }

            y = yNext;
            t = tNext;
            result.add(new Point(t, y));
        }
        return result;
    }

    /**
     * 改进欧拉法(Heun法 / 预测-校正法)
     * 预测: y* = y_n + h * f(t_n, y_n)
     * 校正: y_{n+1} = y_n + h/2 * [f(t_n, y_n) + f(t_{n+1}, y*)]
     *
     * 精度:O(h^2)
     */
    public static List<Point> improvedEuler(
            BiFunction<Double, Double, Double> f,
            double t0, double y0, double h, int steps) {
        List<Point> result = new ArrayList<>();
        double t = t0;
        double y = y0;
        result.add(new Point(t, y));

        for (int i = 0; i < steps; i++) {
            double k1 = f.apply(t, y);                 // 起点斜率
            double yPredict = y + h * k1;              // 预测值(前向欧拉)
            double k2 = f.apply(t + h, yPredict);      // 预测点斜率
            double yCorrect = y + (h / 2.0) * (k1 + k2); // 梯形校正

            y = yCorrect;
            t = t + h;
            result.add(new Point(t, y));
        }
        return result;
    }

    static class Point {
        double t;
        double y;
        Point(double t, double y) { this.t = t; this.y = y; }
        @Override
        public String toString() {
            return String.format("(%.4f, %.6f)", t, y);
        }
    }

    /**
     * 计算数值解与精确解的最大绝对误差
     */
    public static double maxError(List<Point> numerical,
                                   BiFunction<Double, Double, Double> exact,
                                   double t0, double h) {
        double maxErr = 0;
        for (int i = 0; i < numerical.size(); i++) {
            double t = t0 + i * h;
            double exactVal = exact.apply(t, 0.0); // 忽略第二个参数
            double err = Math.abs(numerical.get(i).y - exactVal);
            maxErr = Math.max(maxErr, err);
        }
        return maxErr;
    }

    // ==================== 测试与演示 ====================

    public static void main(String[] args) {
        System.out.println("=== 欧拉方法求解ODE演示 ===\n");

        // 示例1: y' = y, y(0)=1,精确解 y = e^t
        System.out.println("【示例1】y' = y, y(0) = 1,精确解 y(t) = e^t");
        BiFunction<Double, Double, Double> f1 = (t, y) -> y;
        double t0 = 0, y0 = 1, h = 0.1;
        int steps = 20;  // t从0到2

        List<Point> fwd1 = forwardEuler(f1, t0, y0, h, steps);
        List<Point> imp1 = improvedEuler(f1, t0, y0, h, steps);

        System.out.println("  t      前向欧拉      改进欧拉      精确值(e^t)");
        for (int i = 0; i <= steps; i += 4) {
            double t = t0 + i * h;
            double exact = Math.exp(t);
            System.out.printf("%.2f   %.6f   %.6f   %.6f%n",
                              t, fwd1.get(i).y, imp1.get(i).y, exact);
        }

        // 误差分析
        double errFwd1 = 0, errImp1 = 0;
        for (int i = 0; i <= steps; i++) {
            double t = t0 + i * h;
            double exact = Math.exp(t);
            errFwd1 = Math.max(errFwd1, Math.abs(fwd1.get(i).y - exact));
            errImp1 = Math.max(errImp1, Math.abs(imp1.get(i).y - exact));
        }
        System.out.printf("最大误差 - 前向欧拉: %.6f, 改进欧拉: %.6f%n%n",
                          errFwd1, errImp1);

        // 示例2: y' = -2y + t, y(0)=1
        System.out.println("【示例2】y' = -2y + t, y(0) = 1");
        BiFunction<Double, Double, Double> f2 = (t, y) -> -2 * y + t;
        List<Point> fwd2 = forwardEuler(f2, 0, 1, 0.1, 20);
        List<Point> imp2 = improvedEuler(f2, 0, 1, 0.1, 20);
        System.out.println("  t      前向欧拉      改进欧拉");
        for (int i = 0; i <= 20; i += 5) {
            System.out.printf("%.2f   %.6f   %.6f%n",
                              i * 0.1, fwd2.get(i).y, imp2.get(i).y);
        }
        System.out.println();

        // 示例3: 步长收敛性分析
        System.out.println("【示例3】步长收敛性分析 (y'=y, t∈[0,1])");
        System.out.println("  步长h      前向欧拉误差    改进欧拉误差    误差比");
        double[] hs = {0.2, 0.1, 0.05, 0.025, 0.0125};
        for (double hh : hs) {
            int n = (int) Math.round(1.0 / hh);
            List<Point> fwe = forwardEuler(f1, 0, 1, hh, n);
            List<Point> ime = improvedEuler(f1, 0, 1, hh, n);
            double ef = Math.abs(fwe.get(n).y - Math.E);
            double ei = Math.abs(ime.get(n).y - Math.E);
            System.out.printf("%.4f      %.6e    %.6e    %.2f%n", hh, ef, ei, ef / ei);
        }

        // 示例4: 后向欧拉(刚性方程测试)
        System.out.println("\n【示例4】后向欧拉法稳定性测试");
        // y' = -15y, y(0)=1,精确解 y = e^(-15t)
        // 前向欧拉需要 h < 2/15 ≈ 0.133 才稳定
        BiFunction<Double, Double, Double> f4 = (t, y) -> -15 * y;
        double h4 = 0.2;  // 超过稳定性阈值,前向欧拉将发散
        int steps4 = 10;
        List<Point> fwd4 = forwardEuler(f4, 0, 1, h4, steps4);
        List<Point> bwd4 = backwardEuler(f4, 0, 1, h4, steps4, 1e-10, 100);
        System.out.println("方程: y' = -15y (刚性), 步长h=0.2");
        System.out.println("  t      前向欧拉(不稳定)   后向欧拉(稳定)    精确值");
        for (int i = 0; i <= steps4; i += 2) {
            double t = i * h4;
            System.out.printf("%.2f   %12.6f   %12.6f   %.6f%n",
                              t, fwd4.get(i).y, bwd4.get(i).y, Math.exp(-15 * t));
        }
    }
}

三种欧拉方法对比

特性 前向欧拉 后向欧拉 改进欧拉
类型 显式 隐式 显式(预测-校正)
精度阶 O(h) O(h) O(h²)
稳定性 条件稳定 A-稳定(无条件) 条件稳定(比前向好)
计算量 最小 大(需迭代) 中等
适用场景 非刚性方程 刚性方程 平衡精度与效率

误差分析

局部截断误差:单步产生的误差
– 前向/后向欧拉:O(h²)
– 改进欧拉:O(h³)

全局误差:累积到终点的误差
– 前向/后向欧拉:O(h)
– 改进欧拉:O(h²)

刚性方程:当方程包含多个时间尺度差异极大的分量时,需要A-稳定的方法(如后向欧拉)。

总结

欧拉方法是数值ODE求解的入门砖石。前向欧拉简单直观但稳定性受限;后向欧拉无条件稳定但计算成本高;改进欧拉在两者之间取得平衡,精度提升至二阶。理解这三种变体的特性差异,是学习和选择更高级方法(如四阶龙格-库塔)的必经之路。