Какой алгоритм является более точным для вычисления суммы отсортированного массива чисел?

22

Дана возрастающая конечная последовательность положительных чисел . Какой из следующих двух алгоритмов лучше для вычисления суммы чисел?z1,z2,.....zn

s=0; 
for \ i=1:n 
    s=s + z_{i} ; 
end

Или:

s=0; 
for \ i=1:n 
s=s + z_{n-i+1} ; 
end

По моему мнению, было бы лучше начать добавлять числа от самого большого до наименьшего числа, потому что ошибка становится все меньше и меньше. Мы также знаем, что когда мы добавляем очень большое число к очень маленькому, приближенным результатом может быть большое число.

Это верно? Что еще можно сказать?

CryptoBeginner
источник

Ответы:

18

Добавление произвольных чисел с плавающей точкой обычно дает некоторую ошибку округления, а ошибка округления будет пропорциональна размеру результата. Если вы вычислите одну сумму и начнете с добавления первых чисел, средний результат будет больше. Таким образом, вы начали бы добавлять с наименьшими числами.

Но вы получите лучший результат (и он будет работать быстрее), если вы создадите четыре суммы, например: начните с sum1, sum2, sum3, sum4 и добавьте по очереди четыре элемента массива к sum1, sum2, sum3, sum4. Поскольку каждый результат составляет в среднем только 1/4 от первоначальной суммы, ваша ошибка в четыре раза меньше.

Еще лучше: добавьте числа в парах. Затем добавьте результаты в парах. Снова добавляйте эти результаты в парах и так далее, пока у вас не останется два числа для добавления.

Очень просто: используйте более высокую точность. Используйте long double, чтобы вычислить сумму двойников. Используйте double, чтобы вычислить сумму чисел с плавающей точкой.

Близко к совершенству: посмотрите алгоритм Кахана, описанный ранее. Лучше всего использовать, добавляя, начиная с наименьшего числа.

gnasher729
источник
26

Это целые числа или числа с плавающей точкой? Предполагая, что это с плавающей точкой, я бы выбрал первый вариант. Лучше добавлять меньшие числа друг к другу, а затем добавлять большие числа позже. Со вторым вариантом вы в конечном итоге добавляете небольшое число к большому числу, когда я увеличивается, что может привести к проблемам. Вот хороший ресурс по арифметике с плавающей запятой: что должен знать каждый компьютерщик об арифметике с плавающей запятой

Kurt
источник
24

Ответ animal_magic верен: вы должны добавлять числа от наименьшего к наибольшему, однако я хочу привести пример, чтобы показать, почему.

Предположим, мы работаем в формате с плавающей запятой, который дает нам ошеломляющие 3 цифры точности. Теперь мы хотим добавить десять чисел:

[1000, 1, 1, 1, 1, 1, 1, 1, 1, 1]

Конечно, точный ответ - 1009, но мы не можем получить его в нашем 3-значном формате. Округляя до 3 цифр, мы получаем самый точный ответ - 1010. Если мы добавим наименьшее к наибольшему, то в каждом цикле получим:

Loop Index        s
1                 1
2                 2
3                 3
4                 4
5                 5
6                 6
7                 7
8                 8
9                 9
10                1009 -> 1010

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

Loop Index        s
1                 1000
2                 1001 -> 1000
3                 1001 -> 1000
4                 1001 -> 1000
5                 1001 -> 1000
6                 1001 -> 1000
7                 1001 -> 1000
8                 1001 -> 1000
9                 1001 -> 1000
10                1001 -> 1000

Так как числа с плавающей запятой округляются после каждой операции, все сложения округляются, увеличивая нашу ошибку от 1 до 9 от точной. Теперь представьте, что ваш набор чисел для добавления имел 1000, а затем сто 1 или миллион. Обратите внимание, что, чтобы быть по-настоящему точным, вам нужно сложить наименьшие два числа, а затем преобразовать результат в набор чисел.

Годрик Провидец
источник
15

В общем случае я бы использовал компенсационное суммирование (или суммирование Кахана). Если числа не отсортированы, их сортировка будет намного дороже, чем их добавление . Компенсированное суммирование также более точно, чем отсортированное суммирование или наивное суммирование (см. Предыдущую ссылку).

Что касается ссылок, то, что каждый программист должен знать об арифметике с плавающей запятой, охватывает основные моменты достаточно подробно, чтобы кто-то мог прочитать это за 20 (+/- 10) минут и понять основы. «То, что должен знать каждый компьютерщик об арифметике с плавающей точкой» Гольдберга, является классическим справочником, но большинство людей, которых я знаю, которые рекомендуют, чтобы бумага не читала это подробно сами, потому что это около 50 страниц (более того, в некоторых печатные издания), и написано в плотной прозе, поэтому я с трудом рекомендую это в качестве ссылки на первую линию для людей. Это хорошо для второго взгляда на предмет. Энциклопедическим справочником является точность и устойчивость численных алгоритмов Хайама., который охватывает этот материал, а также накопление числовых ошибок во многих других алгоритмах; это также 680 страниц, поэтому я бы не стал сначала смотреть на эту ссылку.

Джефф Оксберри
источник
2
Для полноты, в книге Хайама вы найдете ответ на оригинальный вопрос на странице 82 : растущий порядок является лучшим. Также есть раздел (4.6), в котором обсуждается выбор метода.
Федерико Полони,
7

Предыдущие ответы уже обсуждают этот вопрос в целом и дают здравый совет, но есть еще одна особенность, о которой я хотел бы упомянуть. На большинстве современных архитектур forописанный вами цикл в любом случае будет выполняться с 80-битной расширенной точностью , что гарантирует дополнительную точность, поскольку все временные переменные будут занесены в регистры. Таким образом, у вас уже есть какая-то форма защиты от числовых ошибок. Однако в более сложных циклах промежуточные значения будут храниться в памяти между операциями и, следовательно, урезаться до 64 бит. я думаю что

s=0; 
for \ i=1:n 
    printf("Hello World");
    s=s + z_{i} ; 
end

достаточно, чтобы получить более низкую точность суммирования (!!). Так что будьте очень осторожны, если вы хотите printf-отладить ваш код при проверке на точность.

Для интересующихся эта статья описывает проблему в широко используемой числовой программе (ранжирующая QR-факторизация Лапака), чья отладка и анализ были очень сложными именно из-за этой проблемы.

Федерико Полони
источник
1
Большинство современных машин являются 64-битными и используют модули SSE или AVX даже для скалярных операций. Эти единицы не поддерживают 80-битную арифметику и используют ту же внутреннюю точность, что и аргументы операции. Использование x87 FPU в настоящее время, как правило, не рекомендуется, и большинству 64-битных компиляторов требуются специальные опции, чтобы использовать его.
Христо Илиев,
1
@HristoIliev Спасибо за комментарий, я этого не знал!
Федерико Полони
4

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

Однако> 20 лет назад в моем классе «Численные методы» преподаватель заявил об этом, и мне пришло в голову, что это все еще вносит больше ошибок, чем необходимо из-за относительной разницы в значении между аккумулятором и добавляемыми значениями.

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

Чтобы продемонстрировать это, я разработал алгоритм, который мог бы сделать это эффективно (в пространстве и времени), используя пространство, высвобождаемое по мере удаления элементов из первичного массива, для создания вторичного массива суммированных значений, которые были изначально упорядочены с момента добавления были из сумм значений, которые всегда увеличивались. На каждой итерации проверяются «подсказки» обоих массивов, чтобы найти 2 наименьших значения.

user3054301
источник
2

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

SingleStepper
источник
0

Используйте сложение двоичного дерева, т. Е. Выберите среднее значение распределения (ближайшего числа) в качестве корня двоичного дерева и создайте отсортированное двоичное дерево, добавив меньшие значения слева от графика и большие значения справа и т. Д. , Добавить все дочерние узлы одного родителя рекурсивно в подходе снизу вверх. Это будет эффективно, так как ошибка avg возрастает с увеличением количества суммирований, а в подходе с двоичным деревом число суммирований имеет порядок log n в базе 2. Следовательно, ошибка avg будет меньше.

Уллас Кашьяп
источник
Это то же самое, что добавить соседние пары в исходный массив (так как он отсортирован). Нет причин помещать все значения в дерево.
Годрик Провидец
0

То, что Христо Илиев сказал выше о 64-битных компиляторах, предпочитающих инструкции SSE и AVX по сравнению с FPU (AKA NDP), абсолютно верно, по крайней мере для Microsoft Visual Studio 2013. Однако для операций с плавающей запятой двойной точности, которые я использовал, я обнаружил, что это на самом деле быстрее, а теоретически более точно, использовать FPU. Если это важно для вас, я бы предложил сначала протестировать различные решения, прежде чем выбрать окончательный подход.

При работе в Java я очень часто использую тип данных BigDecimal произвольной точности. Это слишком просто, и обычно не замечают снижения скорости. Вычисление трансцендентных функций с бесконечными рядами и sqrt с использованием метода Ньютона может занять миллисекунду или более, но это выполнимо и довольно точно.

CElliott
источник
0

Я только оставил это здесь /programming//a/58006104/860099 (когда вы идете туда, нажмите «показать фрагмент кода» и запустить его по кнопке

Это пример JavaScript, который ясно показывает, что сумма, начиная с наибольшей, дает большую ошибку

arr=[9,.6,.1,.1,.1,.1];

sum     =             arr.reduce((a,c)=>a+c,0);  // =  9.999999999999998
sortSum = [...arr].sort().reduce((a,c)=>a+c,0);  // = 10

console.log('sum:     ',sum);
console.log('sortSum:',sortSum);
Камил Келчевски
источник
Ответы только на ссылки не приветствуются на этом сайте. Можете ли вы объяснить, что предусмотрено в ссылке?
Никогуаро
@nicoguaro Я обновляю ответ - все ответы очень хорошие, но вот конкретный пример
Камил Келчевски