Задача на сложение: предложите и сравните алгоритмы вычисления суммы больших целых чисел (миллионы цифр) по памяти и времени и объясните, почему классические школьные приёмы могут быть неэффективны

11 Ноя в 09:35
4 +1
0
Ответы
1
Ниже — краткое сравнение практичных алгоритмов сложения больших целых (миллионы цифр) по времени и памяти и объяснение, почему «школьный» по-символьный приём часто неэффективен.
Алгоритмы (описание, асимптотика, требования по памяти)
- Школьный посимвольный (символ за символом, base 10):
- Время: T=Θ(n)T=\Theta(n)T=Θ(n) операций над символами, но константа велика (конвертация символ→число, ветвления, обработка ASCII).
- Память: хранение строк ~ O(n)O(n)O(n) байт на число.
- Минус: тяжёлые константы, плохое использование машинного слова и кэша.
- Лимбовое (word-based) последовательное (рекомендуемая базовая реализация):
- Представление: разбить число на лимбы длиной www бит (или kkk десятичных цифр; база B=2wB=2^wB=2w или 10k10^k10k).
- Число лимбов: с десятичных цифр nnn можно взять m=⌈n/k⌉m=\left\lceil n/k\right\rceilm=n/k (или битово m=⌈nlog⁡210w⌉m=\left\lceil \frac{n\log_2 10}{w}\right\rceilm=wnlog2 10 ).
- Время: T=C⋅m=Θ(m)T=C\cdot m=\Theta(m)T=Cm=Θ(m) (один сложение+перенос на лимб), где CCC — небольшая константа (исп. инструкция ADC).
- Память: O(m)O(m)O(m) машинных слов на операнд и O(m)O(m)O(m) на результат (итого ≈ 3m3m3m слов если хранить всё).
- Плюс: малые константы, хорошая кэш-локальность.
- SIMD/векторизация:
- Выполняем параллельно несколько лимбов за инструкцию; требуется дополнительно решать переносы.
- Время практическое: уменьшение константы на лимб; полное разрешение переносов через параллельный префикс (см. ниже).
- Параллельный (разделяй-и-властвуй + префикс для переносов):
- Идея: разбить на блоки, в каждом блоке посчитать частичную сумму и флаги переполнения, затем вычислить переносы префикс-алгоритмом.
- Время на ppp процессорах: T=O(m/p+log⁡m)T=O(m/p+\log m)T=O(m/p+logm). На PRAM с p=mp=mp=m: T=O(log⁡m)T=O(\log m)T=O(logm).
- Память: O(m)O(m)O(m) + дополнительный буфер для флагов/сумм блоков.
- Полезно для многопроцессорных/графических ускорителей, но реализация сложнее из‑за сложности переносов.
- Carry-save для суммы многих чисел (когда суммируете ttt больших чисел):
- Использовать дерево carry-save аддеров, откладывая переносы, затем один завершающий проход с переносами.
- Время: глубина дерева O(log⁡t)O(\log t)O(logt) для сложения ttt чисел; итоговый проход O(m)O(m)O(m).
- Память: временные суммарные буферы O(m)O(m)O(m).
- Внешняя память (out-of-core, числа не помещаются в ОЗУ):
- Разбить на большие блоки, которые загружаются/записываются на диск; обрабатывать блоки от младших к старшим, хранить и передавать один перенос между блоками.
- I/O сложность доминирует: число блоков ⌈m/Bblk⌉\lceil m/B_{blk}\rceilm/Bblk где BblkB_{blk}Bblk — размер блока в лимбах.
- Память: O(Bblk)O(B_{blk})O(Bblk ).
Почему классический школьный приём неэффективен на больших данных
- Константы: школьный метод посимвольно использует операции над байтами/символами и конвертацию, тогда как лимбовый использует одну машинную инструкцию на www бит.
- Плохое использование машинного слова: при w=64w=64w=64 вы упаковываете 646464 бита в одно слово — это даёт ~64× выигрыш против по-символьной обработки.
- Кэш и локальность: обработка побайтно часто приводит к большему числу обращений и к худшей локальности, увеличивая время.
- Переносы: при очень большом числе цифр последовательное побитовое/посимвольное распространение переноса остаётся по сути линейным, но с большими накладными расходами; для многопотоков требуется префиксный алгоритм.
- Память и формат: хранение как ASCII строк потребляет ~10× больше места, чем плотное представление лимбами; это увеличивает I/O и замедляет работу.
Рекомендации (практика)
- Использовать лимбовую репрезентацию с базой B=232B=2^{32}B=232 или 2642^{64}264 (в зависимости от архитектуры).
- Для двух чисел: последовательное добавление по лимбам с инструкцией ADC — оптимально по простоте и скорости.
- Для многопроцессорных реализаций: блокировать данные, считать локальные суммы, затем префикс для переносов (время O(m/p+log⁡m)O(m/p+\log m)O(m/p+logm)).
- Для сумм многих чисел: применять carry-save дерево, затем один завершающий проход.
- Для данных, не помещающихся в ОЗУ: блоковое внешнее сложение, стриминг младших блоков и передача переноса между блоками.
- Для практических нужд используйте готовые библиотеки (GMP, libbf), они уже оптимизированы и используют описанные подходы.
Короткая сводка сложностей
- Последовательное лимбовое: T=Θ(m)T=\Theta(m)T=Θ(m), память O(m)O(m)O(m).
- Параллельное (p ядер): T=O(m/p+log⁡m)T=O(m/p+\log m)T=O(m/p+logm).
- Внешняя память: I/O ≈ O(⌈m/Bblk⌉)O(\lceil m/B_{blk}\rceil)O(⌈m/Bblk ⌉) блоков; CPU ≈ O(m)O(m)O(m).
Если нужно, могу привести пример структуры представления и псевдокод для эффективной лимбовой реализации.
11 Ноя в 10:35
Не можешь разобраться в этой теме?
Обратись за помощью к экспертам
Гарантированные бесплатные доработки в течение 1 года
Быстрое выполнение от 2 часов
Проверка работы на плагиат
Поможем написать учебную работу
Прямой эфир