Вычисление факториала во встроенном языке 1С

· 9 мин

Содержание

Факториал числа Н — это произведение всех целых чисел от 1 до Н, обозначается Н!. Во встроенном языке 1С штатной функции для него нет, поэтому факториал пишут руками: либо циклом, либо через рекурсию. Для значений до 33! результат ещё помещается в обычный тип Число, дальше нужна длинная арифметика через строки.

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

Что произведение чисел от 1 до N даёт на выходе

Факториал растёт очень быстро. Несколько опорных значений, которые полезно держать в голове при отладке:

НН!Разрядов
011
111
51203
103 628 8007
136 227 020 80010
202 432 902 008 176 640 00019

Два правила, которые задают всю логику функции:

  • 🔹 0! = 1 по определению. Это не ошибка и не частный случай — пустое произведение равно единице.
  • 🔹 Факториал определён только для целых неотрицательных чисел. Для дробного или отрицательного аргумента результата нет, функция должна такой вызов отклонять.

Начальное значение накопителя в функции — именно 1, а не 0. Если стартовать с нуля, любое умножение обнулит результат, и функция всегда вернёт 0.

Функция факториала через цикл

Самый надёжный вариант — обычный цикл с накоплением произведения. Такую функцию логично вынести в общий модуль (например, ОбщегоНазначенияСервер в своей обработке или собственный общий модуль): тогда её можно вызывать из любого места конфигурации.

Функция Факториал(Н) Экспорт
    Результат = 1;
    Для Сч = 2 По Н Цикл
        Результат = Результат * Сч;
    КонецЦикла;
    Возврат Результат;
КонецФункции

Цикл начинается с 2, потому что умножение на 1 ничего не меняет. При Н = 0 и Н = 1 цикл просто не выполнится, и функция корректно вернёт единицу — отдельная ветка для нуля не нужна.

Проверить результат можно прямо в консоли запросов или во внешней обработке:

Сообщить("5! = " + Строка(Факториал(5)));   // 5! = 120
Сообщить("0! = " + Строка(Факториал(0)));   // 0! = 1

Обратите внимание: число склеивается со строкой только через Строка(). Прямая конкатенация "5! = " + Факториал(5) вызовет ошибку преобразования типов.

Рекурсивный вариант и когда он уместен

Рекурсия выражает определение факториала буквально: Н! = Н × (Н−1)!. Код выходит короче, но у него есть цена.

Функция ФакториалРекурсивно(Н) Экспорт
    Если Н <= 1 Тогда
        Возврат 1;
    КонецЕсли;
    Возврат Н * ФакториалРекурсивно(Н - 1);
КонецФункции

Ключевая строка — базовый случай Если Н <= 1. Без него функция уйдёт в бесконечный вызов самой себя и упадёт с переполнением стека. Условие <= 1 (а не = 1) заодно защищает от нуля: при Н = 0 функция тоже вернёт единицу.

Когда что выбирать:

  • ➕ Цикл — рабочий выбор по умолчанию. Нет накладных расходов на вызовы, предсказуемое потребление памяти.
  • ➖ Рекурсия — нагляднее в учебных примерах, но каждый вложенный вызов занимает место в стеке. Для факториала это обычно не проблема (аргументы маленькие), однако на очень больших Н глубокая рекурсия рискует упереться в лимит вложенности.

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

Большие числа: до какого N результат точный

Тип Число в 1С хранит ограниченное количество значащих разрядов — порядка 38 цифр. Пока результат в этот лимит укладывается, он точный:

  • 33! — это 37 цифр, ещё помещается полностью.
  • 34! — уже 39 цифр, в обычное число не влезает: получите либо погрешность в младших разрядах, либо ошибку.

То есть для Н больше 33 обычная функция даёт неверный результат без видимого предупреждения. Это самая коварная ошибка: код не падает, а молча возвращает «почти правильное» число.

Если по задаче нужны значения больше 33!, число приходится хранить как строку и умножать «в столбик» — поразрядно, как на бумаге. Накапливаем цифры в массиве от младшего разряда к старшему:

Функция ФакториалБольшой(Н) Экспорт
    Цифры = Новый Массив;
    Цифры.Добавить(1);
    Для Множитель = 2 По Н Цикл
        Перенос = 0;
        Для Индекс = 0 По Цифры.ВГраница() Цикл
            Произведение = Цифры[Индекс] * Множитель + Перенос;
            Цифры[Индекс] = Произведение % 10;
            Перенос = Цел(Произведение / 10);
        КонецЦикла;
        Пока Перенос > 0 Цикл
            Цифры.Добавить(Перенос % 10);
            Перенос = Цел(Перенос / 10);
        КонецЦикла;
    КонецЦикла;
    // Собираем строку от старшего разряда к младшему
    Строка = "";
    Индекс = Цифры.ВГраница();
    Пока Индекс >= 0 Цикл
        Строка = Строка + Строка(Цифры[Индекс]);
        Индекс = Индекс - 1;
    КонецЦикла;
    Возврат Строка;
КонецФункции

Здесь % — остаток от деления, Цел() — целая часть, ВГраница() — индекс последнего элемента массива. Результат — строка произвольной длины, арифметических ограничений у неё нет. Проверка:

Сообщить(ФакториалБольшой(25));  // 15511210043330985984000000

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

Ещё одна ловушка — вывод. Даже если результат посчитан строковой функцией, попытка сначала превратить его обратно в Число (например, для вывода через Сообщить) снова упрётся в тот же предел разрядности. Большой факториал нужно и хранить, и показывать как строку — не конвертируя в число ни на одном шаге. То же касается записи такого значения в реквизит: строковый реквизит подойдёт, числовой обрежет разряды.

Как не считать лишнего: оптимизация и кэш

Сам по себе факториал считается за один проход — алгоритм линейный, ускорять в нём особо нечего. Оптимизация нужна в двух ситуациях.

Повторные вызовы с одними и теми же аргументами. Если функцию дёргают в цикле по тысяче раз подряд, результаты имеет смысл кэшировать. Простой кэш на соответствии:

Перем КэшФакториалов;

Функция ФакториалСКэшем(Н) Экспорт
    Если КэшФакториалов = Неопределено Тогда
        КэшФакториалов = Новый Соответствие;
    КонецЕсли;
    Значение = КэшФакториалов.Получить(Н);
    Если Значение <> Неопределено Тогда
        Возврат Значение;
    КонецЕсли;
    Значение = Факториал(Н);
    КэшФакториалов.Вставить(Н, Значение);
    Возврат Значение;
КонецФункции

Главный приём — часто факториал вообще не нужен. В прикладных расчётах он почти всегда появляется внутри формулы числа сочетаний или размещений. И там считать два отдельных факториала — ошибка: промежуточные значения разрастаются до огромных чисел, хотя итог помещается в обычное. Правильно сокращать дробь сразу (об этом в разделе про комбинаторику).

Переменную модуля КэшФакториалов нельзя делить между сеансами — на сервере у каждого вызова своё окружение. Кэш живёт в пределах одного исполнения, этого для циклов достаточно.

Проверка аргумента и типичные ошибки

Боевая функция должна отклонять бессмысленный ввод, иначе получите либо вечный цикл, либо тихо неверный результат. Минимальная защита:

Функция ФакториалБезопасный(Н) Экспорт
    Если ТипЗнч(Н) <> Тип("Число") Тогда
        ВызватьИсключение "Аргумент факториала должен быть числом";
    КонецЕсли;
    Если Н < 0 Тогда
        ВызватьИсключение "Факториал определён только для неотрицательных чисел";
    КонецЕсли;
    Если Н <> Цел(Н) Тогда
        ВызватьИсключение "Факториал определён только для целых чисел";
    КонецЕсли;
    Результат = 1;
    Для Сч = 2 По Н Цикл
        Результат = Результат * Сч;
    КонецЦикла;
    Возврат Результат;
КонецФункции

Что чаще всего ломает факториал на практике:

  • 🔸 Накопитель стартует с нуля. Результат всегда 0. Лечится заменой Результат = 0 на Результат = 1.
  • 🔸 Нет базового случая в рекурсии. Бесконечные вызовы и переполнение стека.
  • 🔸 Передают дробь. Цел(Н) отсекает дробную часть, и 5,7! молча считается как 5!. Поэтому дробный аргумент лучше отклонять явно.
  • 🔸 Считают Н > 33 обычным числом. Результат выглядит правдоподобно, но неверен в младших разрядах. Если значения большие — только строковая функция.
  • 🔸 Отрицательный аргумент. Цикл Для Сч = 2 По -3 просто не выполнится и вернёт 1 — внешне «работает», по сути неправильно.

Где факториал реально нужен в задачах

В чистом учёте факториал почти не встречается — он всплывает в комбинаторике и вероятностных расчётах: сколько способов распределить, сколько уникальных комбинаций возможно, какова вероятность совпадения. Типовой случай — число сочетаний C(n, k): сколькими способами выбрать k элементов из n без учёта порядка.

Через формулу n! / (k! · (n−k)!) считать её напролом нельзя — факториалы в числителе разрастаются до чисел за пределом точности, хотя сам результат маленький. Правильный способ — умножать и делить по шагам, сокращая дробь на ходу:

Функция ЧислоСочетаний(Н, К) Экспорт
    Если К < 0 ИЛИ К > Н Тогда
        Возврат 0;
    КонецЕсли;
    Если К > Н - К Тогда
        К = Н - К;  // симметрия: C(n,k) = C(n,n-k)
    КонецЕсли;
    Результат = 1;
    Для Сч = 1 По К Цикл
        Результат = Результат * (Н - К + Сч) / Сч;
    КонецЦикла;
    Возврат Результат;
КонецФункции

Проверка: ЧислоСочетаний(5, 2) даёт 10, ЧислоСочетаний(49, 6) — 13 983 816 (столько комбинаций в лотерее «6 из 49»). Числа здесь остаются небольшими, предел точности не задевается — именно потому что факториалы напрямую не вычисляются.

Рядом с сочетаниями в задачах всплывают ещё два родственника факториала:

  • Перестановки — сколькими способами упорядочить все Н элементов. Это и есть чистый факториал: P(Н) = Н!. Пример: 5 товаров в витрине можно расставить Факториал(5) = 120 способами.
  • Размещения — сколькими способами выбрать K элементов из Н с учётом порядка: A(Н, К) = Н! / (Н−К)!. Как и сочетания, её безопаснее считать произведением последних K множителей, не вычисляя полные факториалы:
Функция ЧислоРазмещений(Н, К) Экспорт
    Если К < 0 ИЛИ К > Н Тогда
        Возврат 0;
    КонецЕсли;
    Результат = 1;
    Для Сч = 0 По К - 1 Цикл
        Результат = Результат * (Н - Сч);
    КонецЦикла;
    Возврат Результат;
КонецФункции

Для ЧислоРазмещений(5, 2) это 5 × 4 = 20. Разница с сочетаниями ровно в К!: A(n,k) = C(n,k) · k! — порядок внутри выборки учитывается.

В языке запросов факториал и эти формулы недоступны — умножений в произвольном диапазоне там нет. Считайте значения в коде (функцией общего модуля), а в запрос передавайте уже готовый результат параметром.

Проверьте себя
Чему равен 0! (факториал нуля)?
С какого начального значения должен стартовать накопитель в цикле?
До какого примерно N обычный тип Число даёт точный факториал?

Частые вопросы

Есть ли в 1С встроенная функция факториала?
Нет. Ни в глобальном контексте, ни в языке запросов готовой функции факториала нет — её пишут самостоятельно циклом или рекурсией и выносят в общий модуль.
Можно ли посчитать факториал дробного числа?
Через обычную формулу — нет, факториал определён только для целых неотрицательных чисел. Для дробных значений существует гамма-функция, но штатных средств для неё в 1С тоже нет, её пришлось бы реализовывать численно.
Почему факториал большого числа считается неправильно?
Тип Число хранит ограниченное число разрядов (порядка 38 цифр). Начиная примерно с 34! результат перестаёт в них помещаться, и младшие разряды теряются. Для таких значений нужна строковая функция с умножением «в столбик».
Где разместить функцию, чтобы вызывать её отовсюду?
В общем модуле с установленным флагом Сервер и ключевым словом Экспорт у функции. Тогда она доступна из документов, обработок, отчётов и форм.
Факториал или число сочетаний — что считать?
Если в итоге нужны сочетания или размещения, считайте их отдельной функцией с сокращением дроби по шагам. Отдельные факториалы в числителе и знаменателе лишь раздувают промежуточные значения и бьют по точности.