В процессе разработки бизнес-логики в 1С программисты нередко сталкиваются с необходимостью распределения числовых величин. Это может быть распределение сумм взаиморасчетов, округление копеек при расчете налогов, генерация тестовых данных или планирование партионного учета. Зачастую стандартного деления с остатком недостаточно, и требуется разделить число на несколько неравных или случайных частей так, чтобы их сумма строго равнялась исходному числу.
В этой статье мы подробно разберем несколько математических подходов к решению этой задачи на платформе 1С:Предприятие, проанализируем неочевидные ошибки распределения и напишем оптимальные алгоритмы для каждого случая.
Рассмотрим базовый алгоритм, основанный на последовательном вычитании случайных чисел. Суть метода заключается в том, что для каждой части мы генерируем случайное число в диапазоне от единицы до текущего остатка за вычетом резерва на оставшиеся незаполненными части.
Давайте посмотрим на реализацию этого метода в 1С:
Функция РазделитьЧислоСлучайно(ИсходноеЧисло, КоличествоЧастей) Экспорт
ГСЧ = Новый ГенераторСлучайныхЧисел;
Результат = Новый Массив;
ТекущийОстаток = ИсходноеЧисло;
ОставшеесяКоличествоЧастей = КоличествоЧастей - 1;
Пока ОставшеесяКоличествоЧастей > 0 Цикл
// Резервируем как минимум по 1 для каждой оставшейся части
МаксимальнаяГраница = ТекущийОстаток - ОставшеесяКоличествоЧастей;
Если МаксимальнаяГраница < 1 Тогда
// Если остатка не хватает даже на единицы, заполняем единицами
СлучайноеЧисло = 1;
Иначе
СлучайноеЧисло = ГСЧ.СлучайноеЧисло(1, МаксимальнаяГраница);
КонецЕсли;
ТекущийОстаток = ТекущийОстаток - СлучайноеЧисло;
ОставшеесяКоличествоЧастей = ОставшеесяКоличествоЧастей - 1;
Результат.Добавить(СлучайноеЧисло);
КонецЦикла;
// Последней части отдаем весь оставшийся «хвост»
Результат.Добавить(ТекущийОстаток);
Возврат Результат;
КонецФункции
Важно проанализировать проблему математического смещения (bias) в данном методе. Этот алгоритм работает отлично с точки зрения итоговой суммы (она всегда в точности равна ИсходноеЧисло), однако он страдает от системной неравномерности распределения. Поскольку на первых итерациях цикла доступный диапазон для генератора случайных чисел намного больше, первые элементы массива в среднем будут значительно крупнее последующих. При разбиении 1000 на 5 частей первое число будет выбираться из диапазона от 1 до 996 (среднее значение ~498). Если выпадет крупное число, на долю остальных останутся крошечные значения. Данный метод нельзя назвать математически «честным» распределением.
Если нам не требуется случайность, а нужно получить максимально равные доли с правильным распределением копеек или остатка от деления, мы можем применить классический детерминированный подход. Разберем по шагам, как равномерно распределить число, добавив остаток к последнему элементу:
Функция РазделитьЧислоРавномерно(ИсходноеЧисло, КоличествоЧастей) Экспорт
БазоваяЧасть = Цел(ИсходноеЧисло / КоличествоЧастей);
ОстатокОтДеления = ИсходноеЧисло % КоличествоЧастей;
Результат = Новый Массив;
Для Индекс = 1 По КоличествоЧастей Цикл
Результат.Добавить(БазоваяЧасть);
КонецЦикла;
// Добавляем остаток к последнему элементу
Если ОстатокОтДеления > 0 Тогда
Результат[КоличествоЧастей - 1] = Результат[КоличествоЧастей - 1] + ОстатокОтДеления;
КонецЕсли;
Возврат Результат;
КонецФункции
Этот способ незаменим при распределении сумм в бухгалтерских проводках или при разбиении платежей. Все элементы получаются одинаковыми (за исключением последнего, который забирает копейки остатка).
Для генерации истинно случайных, несмещенных неравных частей в теории вероятностей применяется элегантный метод «сломанной палки». Представим наше число как отрезок прямой. Чтобы разделить его на 5 частей, нам нужно поставить на этом отрезке 4 случайные точки разреза, а затем измерить получившиеся фрагменты.
Проанализируем алгоритм реализации этого метода в 1С:
КоличествоЧастей - 1 случайных уникальных чисел в диапазоне от 1 до ИсходноеЧисло - 1.ИсходноеЧисло.Посмотрим на программную реализацию:
Функция РазделитьЧислоБезСмещения(ИсходноеЧисло, КоличествоЧастей) Экспорт
ГСЧ = Новый ГенераторСлучайныхЧисел;
ТочкиРазреза = Новый Массив;
// Находим уникальные точки разреза
Пока ТочкиРазреза.Количество() < КоличествоЧастей - 1 Цикл
СлучайнаяТочка = ГСЧ.СлучайноеЧисло(1, ИсходноеЧисло - 1);
Если ТочкиРазреза.Найти(СлучайнаяТочка) = Неопределено Тогда
ТочкиРазреза.Добавить(СлучайнаяТочка);
КонецЕсли;
КонецЦикла;
// Добавляем границы отрезка
ТочкиРазреза.Добавить(0);
ТочкиРазреза.Добавить(ИсходноеЧисло);
// Для сортировки чисел в 1С удобно использовать СписокЗначений
СписокСортировки = Новый СписокЗначений;
Для Каждого Точка Из ТочкиРазреза Цикл
СписокСортировки.Добавить(Точка);
КонецЦикла;
СписокСортировки.СортироватьПоЗначению(НаправлениеСортировки.Возр);
// Вычисляем длину каждого отрезка
Результат = Новый Массив;
Для Индекс = 1 По СписокСортировки.Количество() - 1 Цикл
Разность = СписокСортировки[Индекс].Значение - СписокСортировки[Индекс - 1].Значение;
Результат.Добавить(Разность);
КонецЦикла;
Возврат Результат;
КонецФункции
Этот метод гарантирует, что каждая возможная комбинация слагаемых имеет абсолютно одинаковую вероятность выпадения. Здесь нет смещения в пользу первого или последнего элемента.
Рассмотрим сложную, но часто встречающуюся бизнес-задачу: нужно разбить число на несколько частей, которые должны быть примерно равны, но иметь небольшое случайное отклонение в пределах заданного процента (например, 5% или 10%).
Обычный перебор с генерацией заново (метод отбора) крайне неэффективен и может привести к бесконечному циклу, если случайные числа будут постоянно выходить за рамки лимита. Поэтому мы напишем оптимизированный алгоритм с последующей мультипликативной компенсацией погрешности:
Функция РазделитьСКонтролируемымОтклонением(ИсходноеЧисло, КоличествоЧастей, ПроцентОтклонения) Экспорт
ГСЧ = Новый ГенераторСлучайныхЧисел;
Результат = Новый Массив;
БазовоеЗначение = Цел(ИсходноеЧисло / КоличествоЧастей);
Дельта = Цел(БазовоеЗначение * (ПроцентОтклонения / 100));
Минимум = БазовоеЗначение - Дельта;
Максимум = БазовоеЗначение + Дельта;
Сумма = 0;
Для Шаг = 1 По КоличествоЧастей - 1 Цикл
СлучайноеЧисло = ГСЧ.СлучайноеЧисло(Минимум, Максимум);
Результат.Добавить(СлучайноеЧисло);
Сумма = Сумма + СлучайноеЧисло;
КонецЦикла;
// Вычисляем значение последней части методом вычитания накопленной суммы
ПоследняяЧасть = ИсходноеЧисло - Сумма;
Результат.Добавить(ПоследняяЧасть);
// Корректируем значения, если последняя часть вышла за рамки допустимого диапазона
Пока ПоследняяЧасть < Минимум Или ПоследняяЧасть > Максимум Цикл
Если ПоследняяЧасть < Минимум Тогда
Недостача = Минимум - ПоследняяЧасть;
ПоследняяЧасть = Минимум;
Результат[КоличествоЧастей - 1] = ПоследняяЧасть;
// Распределяем недостачу по другим элементам
Для Индекс = 0 По КоличествоЧастей - 2 Цикл
Если Недостача = 0 Тогда
Прервать;
КонецЕсли;
ТекущееЗначение = Результат[Индекс];
ДоступноДляВычета = ТекущееЗначение - Минимум;
Если ДоступноДляВычета > 0 Тогда
Вычет = Мин(ДоступноДляВычета, Недостача);
Результат[Индекс] = ТекущееЗначение - Вычет;
Недостача = Недостача - Вычет;
КонецЕсли;
КонецЦикла;
ИначеЕсли ПоследняяЧасть > Максимум Тогда
Избыток = ПоследняяЧасть - Максимум;
ПоследняяЧасть = Максимум;
Результат[КоличествоЧастей - 1] = ПоследняяЧасть;
// Распределяем избыток по другим элементам
Для Индекс = 0 По КоличествоЧастей - 2 Цикл
Если Избыток = 0 Тогда
Прервать;
КонецЕсли;
ТекущееЗначение = Результат[Индекс];
ДоступноДляДобавления = Максимум - ТекущееЗначение;
Если ДоступноДляДобавления > 0 Тогда
Добавка = Мин(ДоступноДляДобавления, Избыток);
Результат[Индекс] = ТекущееЗначение + Добавка;
Избыток = Избыток - Добавка;
КонецЕсли;
КонецЦикла;
КонецЕсли;
КонецЦикла;
Возврат Результат;
КонецФункции
Этот алгоритм работает за линейное время и гарантированно возвращает результат, в котором каждое число лежит в жестко заданных границах отклонения, а их общая сумма в точности совпадает с исходной величиной.
Иногда требуется получить максимально возможное количество уникальных, неравных между собой слагаемых (строгие разбиения Леонарда Эйлера). В этом случае мы последовательно берем числа 1, 2, 3, 4 и так далее, пока их сумма укладывается в исходное число, а весь остаток прибавляем к последнему элементу:
Функция РазделитьНаМаксимальноНеравныеУникальныеЧасти(ИсходноеЧисло) Экспорт
Результат = Новый Массив;
ТекущееЧисло = 1;
Остаток = ИсходноеЧисло;
Пока Остаток >= ТекущееЧисло Цикл
Результат.Добавить(ТекущееЧисло);
Остаток = Остаток - ТекущееЧисло;
ТекущееЧисло = ТекущееЧисло + 1;
КонецЦикла;
// Добавляем оставшуюся разность к последнему элементу
Если Остаток > 0 Тогда
ПоследнийИндекс = Результат.Количество() - 1;
Результат[ПоследнийИндекс] = Результат[ПоследнийИндекс] + Остаток;
КонецЕсли;
Возврат Результат;
КонецФункции
Например, для числа 100 этот метод вернет последовательность из 12 уникальных слагаемых: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 и последнее число 34. Это математический предел уникальности для такого числа.
Если же нам нужно фиксированное количество КоличествоЧастей, но при этом слагаемые должны быть максимально неравными, мы можем использовать следующую простую схему:
Функция РазделитьНаФиксированныеНеравныеЧасти(ИсходноеЧисло, КоличествоЧастей) Экспорт
Результат = Новый Массив;
Остаток = ИсходноеЧисло;
Для Шаг = 1 По КоличествоЧастей - 1 Цикл
Результат.Добавить(Шаг);
Остаток = Остаток - Шаг;
КонецЦикла;
Результат.Добавить(Остаток);
Возврат Результат;
КонецФункции
Для числа 1000 и 5 частей мы получим массив: 1, 2, 3, 4 и 990. Это классический пример граничного неравномерного распределения.
Мы проанализировали пять различных подходов к разбиению чисел в 1С. Выбор конкретного метода зависит от ваших бизнес-требований: для финансовых расчетов идеально подойдет Метод 2, для игровых механик или честного распределения нагрузок — Метод 3, а для контролируемого случайного отклонения в рамках бюджета — Метод 4.