Seluruh peralatan section ini kini sudah di tangan: representasi bit, presisi, galat penjumlahan, numeric_limits, bilangan acak, complex, matriks, dan <cmath>. Chapter penutup ini merangkainya menjadi disiplin kerja yang membedakan kode numerik yang bisa dipercaya dari kode yang hanya “tampak jalan”: memilih algoritma yang stabil, menguji hasil terhadap nilai acuan yang diketahui, dan menetapkan toleransi yang jujur terhadap domain masalah. Istilah kuncinya adalah stability — algoritma stabil tidak memperbesar galat kecil yang tak terhindarkan dari floating-point, sedangkan algoritma tidak stabil membiarkan satu pengurangan hampir-sama menghapus sebagian besar digit signifikan. Kita buktikan dengan studi kasus dua rumus untuk masalah yang sama, uji acuan ala litmus test, dan kasus uang yang mengembalikan kita ke integer sen.

Dua Rumus untuk Masalah yang Sama #

Persamaan kuadrat x² - 100000000x + 1 = 0 punya dua akar: satu besar (≈ 10⁸) dan satu kecil (eksaknya 10⁻⁸ dari relasi Vieta). Rumus kuadrat standar menghitung akar kecil sebagai (-b - √disc)/2a — pengurangan dua bilangan hampir sama besar yang memicu catastrophic cancellation. Program berikut membandingkan rumus naif dengan rumus stabil yang menghitung akar besar dulu, lalu memperoleh akar kecil lewat relasi x1 · x2 = c/a:

cpp
#include <cmath>
#include <iomanip>
#include <iostream>
#include <limits>

// Kembalikan akar kecil dengan rumus yang stabil secara numerik:
// hitung akar besar dulu, lalu dapatkan akar kecil dari c/q.
double akarStabil(double a, double b, double c) {
    double disc = b * b - 4.0 * a * c;
    double q = -(b + std::copysign(std::sqrt(disc), b)) / 2.0;
    return c / q;   // akar kecil via relasi Vieta: x1 * x2 = c / a
}

int main() {
    std::cout << std::setprecision(std::numeric_limits<double>::max_digits10);
    std::cout << std::boolalpha;

    // Studi 1: persamaan kuadrat a=1, b=-1e8, c=1
    double a = 1.0, b = -1e8, c = 1.0;
    double disc = b * b - 4.0 * a * c;
    double xNaif = (-b - std::sqrt(disc)) / (2.0 * a);
    double xStabil = akarStabil(a, b, c);
    std::cout << "x naif   = " << xNaif << "\n";
    std::cout << "x stabil = " << xStabil << "\n";

    // Uji terhadap nilai acuan analitik: 1e-8
    double acuan = 1e-8;
    std::cout << "galat naif   = " << std::fabs(xNaif - acuan) << "\n";
    std::cout << "galat stabil = " << std::fabs(xStabil - acuan) << "\n";

    // Studi 2: uji acuan (litmus) — fungsi yang kita tahu jawabannya
    double s = std::sin(std::acos(-1.0) / 6.0);
    std::cout << "sin(pi/6)    = " << s << "\n";
    std::cout << "galat vs 0.5 = " << std::fabs(s - 0.5) << "\n";
    std::cout << "lulus tol 1e-15? "
              << (std::fabs(s - 0.5) <= 1e-15) << "\n";

    // Studi 3: uang — floating vs integer sen
    double totalD = 0.0;
    for (int i = 0; i < 100; ++i) totalD += 0.1;
    long totalSen = 0;
    for (int i = 0; i < 100; ++i) totalSen += 10;
    std::cout << "total double : " << totalD << "\n";
    std::cout << "total sen    : " << totalSen << " (= Rp10,00 persis)\n";

    return 0;
}

Output yang Dihasilkan:

text
x naif   = 7.4505805969238281e-09
x stabil = 1e-08
galat naif   = 2.5494194030761721e-09
galat stabil = 0
sin(pi/6)    = 0.49999999999999994
galat vs 0.5 = 5.5511151231257827e-17
lulus tol 1e-15? true
total double : 9.9999999999999805
total sen    : 1000 (= Rp10,00 persis)

Bedah baris-per-baris:

  1. Baris 8-12 (double akarStabil(double a, double b, double c)): rumus stabil memilih akar yang tidak melibatkan pengurangan bilangan hampir sama. std::copysign(√disc, b) menandatangani akar diskriminan sama dengan b, sehingga q dihitung dari penjumlahan dua nilai setanda — bebas cancellation — dan akar kecil diperoleh dari c/q (relasi Vieta), sebuah pembagian yang aman.
  2. Baris 21 (double xNaif = (-b - std::sqrt(disc)) / (2.0 * a);): inilah jalur bermasalah. √disc ≈ 10⁸ dan -b = 10⁸, jadi pengurangannya membatalkan hampir semua digit; sisa digit yang benar tinggal satu-dua, terlihat dari hasil 7.45e-09 yang meleset 25% dari jawaban sejatinya.
  3. Baris 22-24 (double xStabil = akarStabil(...); ...): metode stabil mengembalikan tepat 1e-08. Perhatikan bahwa selisih kedua metode tidak berasal dari compiler atau bug — kedua rumus sama-sama benar secara aljabar; yang berbeda adalah urutan operasi floating-point-nya.
  4. Baris 27-29 (double acuan = 1e-8; ... galat ...): mengukur galat terhadap nilai acuan analitik mengubah perdebatan “sepertinya benar” menjadi angka: galat naif ≈ 2,5 × 10⁻⁹, galat stabil = 0. Tanpa acuan, kedua hasil tercetak sama-sama “angka kecil yang masuk akal”.
  5. Baris 32-36 (std::sin(std::acos(-1.0) / 6.0) ...): uji acuan (litmus) — fungsi dengan jawaban yang kita tahu. sin(π/6) tercetak 0.49999999999999994, galat 5,6 × 10⁻¹⁷, dan lulus toleransi 1e-15. Program uji seperti ini murah, deterministik, dan menjadi alarm ketika pustaka/platform berubah.
  6. Baris 39-44 (totalD ... totalSen): seratus kali menambah 0,1 di double menghasilkan 9.9999999999999805 — bukan 10 — sementara penghitungan yang sama dalam sen (long) menghasilkan 1000 persis. Untuk nilai yang wajib eksak, integer satuan terkecil menutup masalah dari akarnya.

Disiplin Kerja yang Dibawa Pulang #

Tiga studi di atas menghasilkan checklist praktis. Pertama, pilih bentuk rumus yang menghindari peniadaan: hindari mengurangkan bilangan hampir sama besar, hindari membagi dengan bilangan yang sangat kecil, dan cari identitas matematika (Vieta, trigonometri, logaritma) yang mengganti operasi berbahaya dengan yang aman. Kedua, bandingkan hasil antar metode: bila dua algoritma independen untuk masalah yang sama berbeda jauh, itu bukan noise — itu sinyal adanya instabilitas, dan metode yang lebih stabil biasanya bisa diidentifikasi lewat analisis galat seperti di atas. Ketiga, uji dengan nilai acuan dan toleransi yang beralasan: nilai analitik, kasus batas (nol, satu, nilai maksimum numeric_limits), dan hasil dari literatur; simpan sebagai unit test dengan toleransi relatif dari Chapter Floating Point Precision, bukan ==.

Toleransi itu sendiri adalah keputusan domain: simulasi fisika mungkin puas di 10⁻¹², laporan keuangan menuntut eksak, dan render grafis rela meleset di piksel. Yang tidak boleh dilakukan adalah membiarkan toleransi ditentukan oleh kebetulan — misalnya menyisakan presisi default cout memutuskan apakah dua hasil “terlihat sama”.

Penutup Section Pemrograman Numerik #

Sepanjang sepuluh chapter kita bergerak dari bit ke disiplin: overflow dan aturan modular-nya, anatomi IEEE 754, presisi dan epsilon, penumpukan galat dan Kahan summation, peta numeric_limits, bilangan acak yang dapat direproduksi, bilangan complex, representasi vektor dan matriks, pustaka <cmath>, hingga checklist akurasi di halaman ini. Benang merahnya satu: di dunia biner, hasil numerik yang bisa dipercaya lahir dari kesadaran terhadap batas representasi — bukan dari keberuntungan. Dengan bekal hitung yang jujur ini, kita siap melangkah ke Section Struktur Data dan Algoritma, tempat organisasi data dan efisiensi langkah menjadi bintang utamanya — dan Anda akan menemukan bahwa pertimbangan performa di sana berdiri di atas fondasi aritmetika yang baru saja kita rapikan.

Rujukan resmi untuk topik Chapter ini: fungsi matematika umum <cmath> dan pustaka numerik C++ di cppreference.