0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

二分法とニュートン法の典型的サンプル

0
Last updated at Posted at 2026-03-24

ちょっとむかしを思い出しつつ、AIに書いてもらった。1
典型的な√aを計算する。

二分法は、二分検索と同じように、lowとhighの中央を詰めていく。
ニュートン法は、$y=x^2-a$を微分して、$y'=2x$を使って、$x-\frac{y}{y'}=\frac{1}{2}(\frac{a}{x}+x)$を次の値にする。

初期値が、a>1のとき二分法は[1, a]、ニュートン法はaからスタートすればよいが、a<1のときは累乗すると小さくなるので二分法は[a, 1]、ニュートン法は1からスタートになる。

public class SquareRootUniversal {

    private static final double EPSILON = 1e-10;

    public static void main(String[] args) {
        // a < 1 のケース (0.01) と a > 1 のケース (1e20) 両方でテスト
        double[] testValues = {0.0001, 0.5, 2.0, 1e20};

        for (double a : testValues) {
            System.out.println("=== Target: sqrt(" + a + ") ===");
            solveNewton(a);
            solveBisection(a);
            System.out.println();
        }
    }

    /**
     * 汎用ニュートン法
     */
    public static void solveNewton(double a) {
        if (a < 0) return;
        if (a == 0) { System.out.println("Newton: 0.0"); return; }

        // ポイント: a < 1 のときは 1.0 を初期値にすることで、必ず「解より大きい側」からスタートできる
        double x = Math.max(1.0, a); 
        int steps = 0;

        while (true) {
            steps++;
            double nextX = 0.5 * (x + a / x);
            System.out.printf("Step %d: x = %.10e, nextX = %.10e\n", steps, x, nextX);
            if (Math.abs(nextX - x) < EPSILON) {
                x = nextX;
                break;
            }
            x = nextX;
        }
        System.out.printf("Newton Result: %.15f (Steps: %d)\n", x, steps);
    }

    /**
     * 汎用二分法
     */
    public static void solveBisection(double a) {
        if (a < 0) return;
        if (a == 0) { System.out.println("Bisection: 0.0"); return; }

        // a < 1 のときは範囲を [a, 1]、a >= 1 のときは [1, a] に設定する
        double low = Math.min(a, 1.0);
        double high = Math.max(a, 1.0);
        int steps = 0;

        while ((high - low) / high > EPSILON) {
            steps++;
            double mid = (low + high) / 2.0;
            System.out.printf("Step %d: mid = %.10e, low = %.10e, high = %.10e\n", steps, mid, low, high);
            if (mid * mid > a) {
                high = mid;
            } else {
                low = mid;
            }
        }
        System.out.printf("Bisection Result: %.15f (Steps: %d)\n", (low + high) / 2.0, steps);
    }
}

ニュートン法の方が収束が早い。
ただし、1e20のとき、半分づつしか詰めていけない。

=== Target: sqrt(1.0E-4) ===
Step 1: x = 1.0000000000e+00, nextX = 5.0005000000e-01
Step 2: x = 5.0005000000e-01, nextX = 2.5012499000e-01
Step 3: x = 2.5012499000e-01, nextX = 1.2526239506e-01
Step 4: x = 1.2526239506e-01, nextX = 6.3030359624e-02
Step 5: x = 6.3030359624e-02, nextX = 3.2308448330e-02
Step 6: x = 3.2308448330e-02, nextX = 1.7701806998e-02
Step 7: x = 1.7701806998e-02, nextX = 1.1675473895e-02
Step 8: x = 1.1675473895e-02, nextX = 1.0120218365e-02
Step 9: x = 1.0120218365e-02, nextX = 1.0000714039e-02
Step 10: x = 1.0000714039e-02, nextX = 1.0000000025e-02
Step 11: x = 1.0000000025e-02, nextX = 1.0000000000e-02
Newton Result: 0.010000000000000 (Steps: 11)
Step 1: mid = 5.0005000000e-01, low = 1.0000000000e-04, high = 1.0000000000e+00
Step 2: mid = 2.5007500000e-01, low = 1.0000000000e-04, high = 5.0005000000e-01
Step 3: mid = 1.2508750000e-01, low = 1.0000000000e-04, high = 2.5007500000e-01
Step 4: mid = 6.2593750000e-02, low = 1.0000000000e-04, high = 1.2508750000e-01
Step 5: mid = 3.1346875000e-02, low = 1.0000000000e-04, high = 6.2593750000e-02
:
Step 36: mid = 1.0000000003e-02, low = 9.9999999888e-03, high = 1.0000000018e-02
Step 37: mid = 9.9999999960e-03, low = 9.9999999888e-03, high = 1.0000000003e-02
Step 38: mid = 9.9999999997e-03, low = 9.9999999960e-03, high = 1.0000000003e-02
Step 39: mid = 1.0000000001e-02, low = 9.9999999997e-03, high = 1.0000000003e-02
Step 40: mid = 1.0000000001e-02, low = 9.9999999997e-03, high = 1.0000000001e-02
Bisection Result: 0.010000000000131 (Steps: 40)

=== Target: sqrt(0.5) ===
Step 1: x = 1.0000000000e+00, nextX = 7.5000000000e-01
Step 2: x = 7.5000000000e-01, nextX = 7.0833333333e-01
Step 3: x = 7.0833333333e-01, nextX = 7.0710784314e-01
Step 4: x = 7.0710784314e-01, nextX = 7.0710678119e-01
Step 5: x = 7.0710678119e-01, nextX = 7.0710678119e-01
Newton Result: 0.707106781186548 (Steps: 5)
Bisection Result: 0.707106781163020 (Steps: 33)

=== Target: sqrt(2.0) ===
Step 1: x = 2.0000000000e+00, nextX = 1.5000000000e+00
Step 2: x = 1.5000000000e+00, nextX = 1.4166666667e+00
Step 3: x = 1.4166666667e+00, nextX = 1.4142156863e+00
Step 4: x = 1.4142156863e+00, nextX = 1.4142135624e+00
Step 5: x = 1.4142135624e+00, nextX = 1.4142135624e+00
Newton Result: 1.414213562373095 (Steps: 5)
Bisection Result: 1.414213562326040 (Steps: 33)

=== Target: sqrt(1.0E20) ===
Step 1: x = 1.0000000000e+20, nextX = 5.0000000000e+19
Step 2: x = 5.0000000000e+19, nextX = 2.5000000000e+19
Step 3: x = 2.5000000000e+19, nextX = 1.2500000000e+19
Step 4: x = 1.2500000000e+19, nextX = 6.2500000000e+18
Step 5: x = 6.2500000000e+18, nextX = 3.1250000000e+18
Step 6: x = 3.1250000000e+18, nextX = 1.5625000000e+18
Step 7: x = 1.5625000000e+18, nextX = 7.8125000000e+17
Step 8: x = 7.8125000000e+17, nextX = 3.9062500000e+17
Step 9: x = 3.9062500000e+17, nextX = 1.9531250000e+17
Step 10: x = 1.9531250000e+17, nextX = 9.7656250000e+16
Step 11: x = 9.7656250000e+16, nextX = 4.8828125000e+16
Step 12: x = 4.8828125000e+16, nextX = 2.4414062500e+16
Step 13: x = 2.4414062500e+16, nextX = 1.2207031250e+16
Step 14: x = 1.2207031250e+16, nextX = 6.1035156250e+15
Step 15: x = 6.1035156250e+15, nextX = 3.0517578125e+15
Step 16: x = 3.0517578125e+15, nextX = 1.5258789063e+15
Step 17: x = 1.5258789063e+15, nextX = 7.6293945317e+14
Step 18: x = 7.6293945317e+14, nextX = 3.8146972665e+14
Step 19: x = 3.8146972665e+14, nextX = 1.9073486346e+14
Step 20: x = 1.9073486346e+14, nextX = 9.5367431990e+13
Step 21: x = 9.5367431990e+13, nextX = 4.7683716519e+13
Step 22: x = 4.7683716519e+13, nextX = 2.3841859308e+13
Step 23: x = 2.3841859308e+13, nextX = 1.1920931751e+13
Step 24: x = 1.1920931751e+13, nextX = 5.9604700699e+12
Step 25: x = 5.9604700699e+12, nextX = 2.9802434236e+12
Step 26: x = 2.9802434236e+12, nextX = 1.4901384889e+12
Step 27: x = 1.4901384889e+12, nextX = 7.4510279840e+11
Step 28: x = 7.4510279840e+11, nextX = 3.7261850403e+11
Step 29: x = 3.7261850403e+11, nextX = 1.8644343752e+11
Step 30: x = 1.8644343752e+11, nextX = 9.3489896607e+10
Step 31: x = 9.3489896607e+10, nextX = 4.7279765453e+10
Step 32: x = 4.7279765453e+10, nextX = 2.4697417583e+10
Step 33: x = 2.4697417583e+10, nextX = 1.4373211954e+10
Step 34: x = 1.4373211954e+10, nextX = 1.0665299547e+10
Step 35: x = 1.0665299547e+10, nextX = 1.0020750636e+10
Step 36: x = 1.0020750636e+10, nextX = 1.0000021485e+10
Step 37: x = 1.0000021485e+10, nextX = 1.0000000000e+10
Step 38: x = 1.0000000000e+10, nextX = 1.0000000000e+10
Step 39: x = 1.0000000000e+10, nextX = 1.0000000000e+10
Newton Result: 10000000000.000000000000000 (Steps: 39)
Bisection Result: 10000000000.005482000000000 (Steps: 67)

指数部を半分にする

そこで出してきたのが、double型の指数部を半分にした初期値にしよう。
ソース見ても分からないので、デバッグを入れまくった。

        if (a <= 0) return;

        // --- 1. ビット演算による初期値の推定 ---
        // IEEE 754 形式のビットパターンを取得
        long bits = Double.doubleToLongBits(a);
        System.out.printf("%d %d %.10e\n", (bits >> 52)-1023, (bits & 0x000FFFFFFFFFFFFFL), a);
        
        // 指数部を半分にする魔法の操作
        // 指数部はバイアス(1023)がかかっているので、それを考慮してビットをずらす
        long initialBits;
        double x;
        initialBits = (bits >> 1) + (511L << 52); 
        x = Double.longBitsToDouble(initialBits);
        System.out.printf("%d %d %.10e\n", (initialBits >> 52)-1023, (initialBits & 0x000FFFFFFFFFFFFFL), x);
        initialBits = (bits >> 1) + (512L << 52); 
        x = Double.longBitsToDouble(initialBits);
        System.out.printf("%d %d %.10e\n", (initialBits >> 52)-1023, (initialBits & 0x000FFFFFFFFFFFFFL), x);
        initialBits = (((bits >> 52) - 1023) / 2 + 1023) << 52 | (bits & 0x000FFFFFFFFFFFFFL); 
        x = Double.longBitsToDouble(initialBits);
        System.out.printf("%d %d %.10e\n", (initialBits >> 52)-1023, (initialBits & 0x000FFFFFFFFFFFFFL), x);
        
        System.out.printf("Estimated Initial x0: %.10e\n", x);

初期値を1.16e10からスタートすると、39ステップが5ステップで収束する。

Target: sqrt(1.0E20) = 1.0E10
66 1599915997629504 1.0000000000e+20
32 3051757812500000 7.2053503417e+09
33 3051757812500000 1.4410700683e+10
33 1599915997629504 1.1641532183e+10
Estimated Initial x0: 1.1641532183e+10
Step 1: x = 1.1641532183e+10, nextX = 1.0115733387e+10
Step 2: x = 1.0115733387e+10, nextX = 1.0000662049e+10
Step 3: x = 1.0000662049e+10, nextX = 1.0000000022e+10
Step 4: x = 1.0000000022e+10, nextX = 1.0000000000e+10
Step 5: x = 1.0000000000e+10, nextX = 1.0000000000e+10
Fast Newton Result: 10000000000.000000000000000 (Steps: 5)

66 1599915997629504 1.0000000000e+20

求めたい数aの、バイアスを戻した指数($2^{exponent}$)、仮数($1+fraction×2^{-52}$)、double値($2^{66}×(1+\frac{1599915997629504}{2^{52}})=10^{20}$

initialBits = (bits >> 1) + (511L << 52);
32 3051757812500000 7.2053503417e+09

AIが出してきたやつ。まるごと1ビット右シフトして、指数部のバイアスが1023なので、半分の511を(指数部の52ビット目に合わせて)足してやれ。
仮数部も変わっている。指数部が切り捨てた511なので、平方根の値よりも小さい数になっている。

initialBits = (bits >> 1) + (512L << 52);
33 3051757812500000 1.4410700683e+10

そこで、511でなく、512の方が、大きくなるのではと、指数部がさっきより1大きい。2

initialBits = (((bits >> 52) - 1023) / 2 + 1023) << 52 | (bits & 0x000FFFFFFFFFFFFFL);
33 1599915997629504 1.1641532183e+10

仮数部を変えずに、指数部のみ半分にしてみた。
2乗根以外のk乗根のときにkで割ればよい。

なお、符号ビットもシフトされるのでは?と思ったら、冒頭で弾いている。


ちなみに三乗根の初期値はこれだと出してきた。
もう知らんがな。

public static double initialCubeRoot(double a) {
    long bits = Double.doubleToLongBits(a);
    // 指数部をだいたい1/3にするマジックナンバー
    // 0x4000000000000000L 付近の調整値が必要
    long initialBits = bits / 3 + 0x2A51000000000000L; 
    return Double.longBitsToDouble(initialBits);
}

ちなみにk乗根は、大きく離れているとざっくり$\frac{k-1}{k}$になるので、4乗根ならば、2乗根を2回の方が早いとな。
まあ100乗根なんて計算する日は来ないだろう。

セカント法

直近の2つの近似値 $(x_{n-1}, x_n)$ を結ぶ直線を引き、その直線が $x$ 軸と交わる点を次の近似値 $x_{n+1}$ にする。

public static void solveSecant(double x0, double x1, java.util.function.DoubleUnaryOperator f) {
    double xPrev = x0;
    double xCurr = x1;

    for (int i = 0; i < 100; i++) {
        double fPrev = f.applyAsDouble(xPrev);
        double fCurr = f.applyAsDouble(xCurr);

        // 分母が0(または極小)にならないかチェック
        if (Math.abs(fCurr - fPrev) < 1e-18) break;

        // セカント法の更新式
        double xNext = xCurr - fCurr * (xCurr - xPrev) / (fCurr - fPrev);

        if (Math.abs(xNext - xCurr) < Math.abs(xCurr) * 1e-15) {
            System.out.println("Converged at: " + xNext);
            return;
        }
        xPrev = xCurr;
        xCurr = xNext;
    }
}

逆2次補間法

直近の3点を通る放物線(2次式)を使って解を探す。

$x$ と $y$ の役割を入れ替えて、$x = g(y)$ という「横向きの放物線」として近似するのが「逆」2次補間法の特徴です。

  • 通常の2次補間: $y = ax^2 + bx + c$
  • 逆2次補間: $x = ay^2 + by + c$

3つの点 $(x_{n-2}, y_{n-2}), (x_{n-1}, y_{n-1}), (x_n, y_n)$ があるとき、次の近似値 $x_{n+1}$ は以下の式で計算される。
$$x_{n+1} = \frac{y_{n-1}y_n}{(y_{n-2}-y_{n-1})(y_{n-2}-y_n)}x_{n-2} + \frac{y_{n-2}y_n}{(y_{n-1}-y_{n-2})(y_{n-1}-y_n)}x_{n-1} + \frac{y_{n-2}y_{n-1}}{(y_n-y_{n-2})(y_n-y_{n-1})}x_n$$

ブレント法(Brent's method)

3つのハイブリッド

  • 逆2次補間法 (Inverse Quadratic Interpolation)
  • セカント法 (Secant Method)
  • 二分法 (Bisection Method)

Commons Mathにあるのか。

// Apache Commons Math を使った例
UnivariateSolver solver = new BrentSolver();
double result = solver.solve(100, x -> Math.cos(x) - x, 0, 1);

ハレー法

ハレー彗星でお馴染みのおっさんが提案した方法。って、1700年ころ?

ニュートン法の式に、$f(x)$ の2階微分($f''(x)$)の情報を加えることで、グラフの「曲がり具合」を計算に取り入れる。

$$x_{n+1} = x_n - \frac{2 f(x_n) f'(x_n)}{2 (f'(x_n))^2 - f(x_n) f''(x_n)}$$

高速逆平方根(Fast Inverse Square Root)

$1/\sqrt{x}$ を計算するための関数。
threehalfsって、3次元ベクトルの長さの逆数を計算したいためか。

float Q_rsqrt( float number )
{
    long i;
    float x2, y;
    const float threehalfs = 1.5F;

    x2 = number * 0.5F;
    y  = number;
    i  = * ( long * ) &y;                       // floatをlongとして解釈(ビットの強引な取り出し)
    i  = 0x5f3759df - ( i >> 1 );               // ★謎のマジックナンバー★
    y  = * ( float * ) &i;                      // 再びfloatに戻す
    y  = y * ( threehalfs - ( x2 * y * y ) );   // ニュートン法(1回だけ)
    // y  = y * ( threehalfs - ( x2 * y * y ) );   // 2回目は不要なのでコメントアウトされている

    return y;
}

現在のCPU(x86やARM)には、この「逆平方根」をハードウェアレベルで高速に行う専用命令(RSQRTSSなど)が搭載されています。そのため、現代のプログラミングでこのトリッキーなコードを自前で書く必要はなくなりました。
って、ライブラリに関数がないと使えないわな。

  1. 早速動かしたら二分法が終わらない。最初、絶対誤差で判定していて、1e20なんか仮数部の精度を考えたら無理だわな。相対誤差でないとダメと言われたが、作ったのはそっちですがな。

  2. ニュートン法は大きい数から攻めた方がよいと書いてあったような。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?