Как возвести в квадрат число с плавающей точкой , чтобы программа не выходила за пределы double, но и работала верно?

В этой программе вводим сначал n-количество чисел, которые будем вводить , а затем n чисел вводим. Суть в том , что при вводе чисел таких как 3.е165 программа выводит inf т.к. выходим за пределы double~e308. А преподаватель просит , чтобы программа работала без никаких проблем на таких значениях. Что делать ?

#include <stdio.h>
#include <math.h>
#define N 10000
double norm2(double a[],int n);
int main(void)
{
    double a[N]; int n;
    scanf("%d",&n);
    norm2(a,n);
    
    return 0;
}
double norm2(double a[],int n)
{
    int i=0; double sum=0;
    for(i=0;i<n;i++)
    {
        scanf("%le",&a[i]);
        if(a[i]<0.0) a[i]=-a[i];
        sum+=exp(log(a[i])+log(a[i]));}
    printf("%le",sqrt(sum));
    return 0;
    } 

Ответы (2 шт):

Автор решения: Harry

Нате, сдайте и не мучьтесь. В конце концов, чем меньше нынешние студенты будут работать мозгами, тем выше будут зарплаты у тех, кто таки работает...

double norm2(double a[],int n)
{
    double sum = 0, maxa = fabs(a[0]);
    for(int i = 1; i < n; i++)
        if (maxa < fabs(a[i])) maxa = fabs(a[i]);
    for(int i = 0; i < n; i++)
    {
        double x = a[i]/maxa;
        sum += x*x;
    }
    return sqrt(sum)*maxa;
}

int main(int argc, char * argv[])
{
    int n;
    scanf("%d",&n);
    double * a = malloc(n*sizeof(double));
    for(int i = 0; i < n; ++i)
        scanf("%le",&a[i]);
    printf("%le\n",norm2(a,n));
}

Вот полный код - https://ideone.com/qFxpez - это чтоб не началось "у меня то или это не компилируется"...

→ Ссылка
Автор решения: Stanislav Volodarskiy

У этой задачи есть очень точное решение, которое не требует дополнительной памяти. Когда я говорю точное, это значит что нет дополнительных делений (для нормализации по максимальному числу) и корень извлекается с максимальной точностью. И при всём этом программа работает в всём диапазоне конечных вещественных чисел, включая денормализованые.

Конечное ненулевое вещественное число x будет храниться в виде x = m·2e, где |m| ∈ [0.5, 1). Ноль будет храниться в виде 0 = 0·20.

Возведение x = m·2e в квадрат: x2 = m2·22e. Величина m2 может оказаться меньше 0.5, тогда x2 = (2·m2)·2(2e-1).

Извлечение корня из x = m·2e:
если e чётное, то √x = √m·2e/2.
если e нечётное, то √x = √(2m)·2e-1/2.

Если после извлечения корня новая мантисса не попадает в диапазон [0.5, 1), то она домножается на степень двойки, экспонента подправляется соответственно.

Сложение чуть сложнее. x1 = m1·2e1, x2 = m2·2e2. Если e1 ≥ e2 и m1, m2 ≠ 0 , то
x1 + x2 = (m1 + m2·2e2-e1)·2e1. Новая мантисса подправляется как в предыдущих операциях. Остальные случаи вычисляются аналогично.

Этих операций достаточно чтобы вычислить норму (корень из суммы квадратов). Функция frexp используется для разложения вещественного x в пару (m, e) – мантисса и экспонента. Функция ldexp собирает x из (m, e). Обе функции используют двоичное представление x и только меняют в нём поле экспоненты. Никакой сложной арифметики.

Ниже приведена программа, про которую можно доказать такое утверждение:

  1. если набор чисел xi не приводит к переполнению или потере значимости, программа вычислит √∑i xi2 в точности равный результату "наивной" программы, которая накапливает сумму квадратов s += x * x; и затем извлекает из этой суммы корень;

  2. если промежуточные результаты в "наивной" программе переполняются или теряют значимость, но результат (в математическом смысле) представим конечным вещественным числом, то программа ниже вычислит этот результат.

Другими словами наша программа нигде не хуже "наивной" и использует для промежуточных результатов бесконечный диапазон экспонент, выдавая правильные результаты там где "наивная" программа свалится в ноль или бесконечность.

#include <math.h>
#include <stdio.h>

typedef struct {
    double mantissa;
    int exponent;
} number_t;

number_t n_init(double x) {
    number_t n;
    n.mantissa = frexp(x, &n.exponent);
    return n;
}

number_t n_sqr(const number_t n) {
    number_t nn;
    nn.mantissa = frexp(n.mantissa * n.mantissa, &nn.exponent);
    nn.exponent += 2 * n.exponent;
    return nn;
}

number_t n_add(const number_t n1, const number_t n2) {
    if (n1.mantissa == 0) {
        return n2;
    }
    if (n2.mantissa == 0) {
        return n1;
    }
    double m;
    int e;
    if (n1.exponent >= n2.exponent) {
        m = n1.mantissa + ldexp(n2.mantissa, n2.exponent - n1.exponent);
        e = n1.exponent;
    } else {
        m = n2.mantissa + ldexp(n1.mantissa, n1.exponent - n2.exponent);
        e = n2.exponent;
    }
    number_t n = n_init(m);
    n.exponent += e;
    return n;
}

number_t n_sqrt(const number_t n) {
    double m = n.mantissa;
    int e = n.exponent;
    if (e % 2 != 0) {
        m = ldexp(m, 1);
        --e;
    }
    number_t nn = n_init(sqrt(m));
    nn.exponent += e / 2;
    return nn;
}


int main() {
    int n;
    if (scanf("%d", &n) != 1) {
        return 1;
    }

    number_t s = n_init(0);
    for (int i = 0; i < n; ++i) {
        double x;
        if (scanf("%lf", &x) != 1) {
            return 1;
        }
        s = n_add(s, n_sqr(n_init(x)));
    }
    number_t ss = n_sqrt(s);
    printf("%lg\n", ldexp(ss.mantissa, ss.exponent));
}
$ gcc norm.c -lm

$ echo 2 3e-307 4e-307 | ./a.out
5e-307

$ echo 2 3 4 | ./a.out
5

$ echo 2 3e307 4e307 | ./a.out
5e+307
→ Ссылка