При решении различных прикладных задач в 1С (например, при анализе возможных комбинаций скидок, подборе оптимальных комплектов номенклатуры, распределении грузов или анализе совместимости характеристик) разработчики сталкиваются с классической задачей комбинаторики — генерацией сочетаний без повторений из N элементов по M.
Начинающие программисты часто совершают одну и ту же ошибку: пытаются решить эту задачу с помощью жестко заданных вложенных циклов Для. Такой подход работает только тогда, когда число элементов в выборке (длина сочетания $M$) заранее строго определено и неизменно (например, строго по 2 элемента). Но что делать, если пользователь сам вводит параметры $N$ (общее число элементов) и $M$ (размер выборки) на форме? Написать динамическое количество вложенных циклов «в лоб» в статичном коде невозможно.
В этой статье мы подробно разберем, почему вложенные циклы не подходят для динамических параметров, рассмотрим типичные ошибки работы с типами данных в 1С и реализуем два универсальных программных решения: рекурсивный обход (backtracking) и итеративный алгоритм в лексикографическом порядке.
---
Рассмотрим код, с которого начиналось обсуждение на форуме. Автор пытался получить все сочетания по 2 элемента из массива длиной 4:
Массив = Новый Массив(4);
Массив[0] = "А1";
Массив[1] = "А2";
Массив[2] = "А3";
Массив[3] = "А4";
Для Индекс1 = 0 По Массив.ВГраница - 1 Цикл
Для Индекс2 = Индекс1 + 1 По Массив.ВГраница Цикл
Сообщить(Массив[Индекс1] + Массив[Индекс2]);
КонецЦикла;
КонецЦикла;
Этот код отлично работает для конкретного случая $M = 2$. Мы получаем ожидаемый результат: А1А2, А1А3, А1А4, А2А3, А2А4, А3А4. Но если нам потребуется выбрать сочетания по 3 элемента, нам придется вручную дописывать третий вложенный цикл Для Индекс3 = Индекс2 + 1 По.... Если выборка будет состоять из 5 элементов — потребуется пять вложенных циклов. Когда число элементов $M$ вводится пользователем на форме, этот подход полностью ломается, так как мы не можем динамически добавлять новые циклы в исходный код во время работы программы.
---
При попытке сделать параметры динамическими, новички часто путают типы данных. Давайте разберем характерную ошибку из обсуждения на форуме:
&НаКлиенте
Процедура Команда1(Команда)
мДлина = Новый Массив(мДлина);
Для мКоличество1 = 0 По мДлина.ВГраница - 1 Цикл
// ...
КонецЦикла;
КонецПроцедуры
В данном примере переменная мДлина изначально является реквизитом управляемой формы с типом Число. Попытка переопределить её внутри процедуры как Новый Массив(мДлина) приводит к ошибке выполнения. Управляемая форма строго следит за типами своих реквизитов. Мы не можем записать объект типа Массив в реквизит, предназначенный для хранения чисел. Платформа либо выдаст ошибку несоответствия типов, либо попытается неявно преобразовать массив обратно к числу, что приведет к краху алгоритма.
Правило: Всегда разделяйте реквизиты формы, хранящие введенные пользователем числовые параметры, и локальные переменные кода, в которых создаются и обрабатываются массивы данных.
Для корректной работы мы должны объявить локальную переменную (например, РабочийМассив), инициализировать её и заполнить элементами на основании введенного пользователем числа мДлина:
РабочийМассив = Новый Массив;
Для Счетчик = 1 По мДлина Цикл
РабочийМассив.Добавить("А" + Счетчик);
КонецЦикла;
---
Рекурсия — это наиболее естественный способ симуляции динамической вложенности циклов. Функция вызывает саму себя, углубляясь на следующий уровень выборки, пока не наберет необходимое количество элементов $M$. Как только сочетание сформировано, функция возвращает управление на предыдущий уровень, делает «шаг назад» (удаляет последний добавленный элемент) и пробует другие варианты.
Давайте подробно разберем программную реализацию рекурсивного метода в 1С. Создайте на управляемой форме два реквизита: мДлина (Число) и мКоличество (Число), а также команду ВыполнитьРасчетРекурсивно.
Посмотрим на код обработчика команды и самой рекурсивной процедуры:
&НаКлиенте
Процедура ВыполнитьРасчетРекурсивно(Команда)
// Проверим корректность введенных пользователем данных
Если мКоличество > мДлина Или мКоличество <= 0 Тогда
ПоказатьПредупреждение(, "Размер выборки (М) должен быть больше 0 и не превышать общую длину массива (N)!");
Возврат;
КонецЕсли;
// Шаг 1. Инициализируем и заполняем исходный массив элементов (N)
ИсходныйМассив = Новый Массив;
Для Счетчик = 1 По мДлина Цикл
ИсходныйМассив.Добавить("А" + Счетчик);
КонецЦикла;
// Шаг 2. Подготовим коллекции для хранения результатов
Результат = Новый Массив;
ТекущийБуфер = Новый Массив;
// Шаг 3. Запускаем рекурсивный поиск сочетаний
ГенерироватьСочетанияРекурсивно(ИсходныйМассив, мКоличество, 0, ТекущийБуфер, Результат);
// Шаг 4. Выводим полученный результат пользователю
Для Каждого Сочетание Из Результат Цикл
СтрокаВывода = "";
Для Каждого Элемент Из Сочетание Цикл
СтрокаВывода = СтрокаВывода + Элемент + " ";
КонецЦикла;
Сообщить(СтрокаВывода);
КонецЦикла;
КонецПроцедуры
&НаКлиенте
Процедура ГенерироватьСочетанияРекурсивно(ИсходныйМассив, РазмерВыборки, СтартовыйИндекс, ТекущийБуфер, Результат)
// Если буфер заполнен до нужного размера M, мы нашли одно из сочетаний
Если ТекущийБуфер.Количество() = РазмерВыборки Тогда
// ВНИМАНИЕ: в 1С массивы передаются по ссылке!
// Нельзя просто добавить ТекущийБуфер в Результат.
// Нужно обязательно создать его независимую копию.
КопияСочетания = Новый Массив;
Для Каждого Элемент Из ТекущийБуфер Цикл
КопияСочетания.Добавить(Элемент);
КонецЦикла;
Результат.Добавить(КопияСочетания);
Возврат;
КонецЕсли;
// Перебираем элементы исходного массива, начиная со СтартовогоИндекса
Для Индекс = СтартовыйИндекс По ИсходныйМассив.ВГраница() Цикл
// Добавляем текущий элемент в буфер комбинации
ТекущийБуфер.Добавить(ИсходныйМассив[Индекс]);
// Рекурсивно переходим к выбору следующего элемента.
// Передаем Индекс + 1, чтобы исключить повторения и гарантировать движение только вперед
ГенерироватьСочетанияРекурсивно(ИсходныйМассив, РазмерВыборки, Индекс + 1, ТекущийБуфер, Результат);
// Важнейший шаг бэктрекинга: удаляем последний элемент, освобождая место для других вариантов
ТекущийБуфер.Удалить(ТекущийБуфер.ВГраница());
КонецЦикла;
КонецПроцедуры
Важная техническая тонкость: Обратите внимание на копирование массива перед добавлением в общий результат. В языке 1С все коллекционные объекты (массивы, структуры, таблицы значений) передаются по ссылке. Если бы мы просто написали Результат.Добавить(ТекущийБуфер), то при последующем вызове метода ТекущийБуфер.Удалить() данные стерлись бы и внутри уже сохраненного результата. В итоге мы получили бы массив, заполненный пустыми массивами. Создание копии через цикл решает эту проблему.
---
Рекурсия — это мощный инструмент, но у нее есть два минуса: она расходует стек вызовов операционной системы и может быть сложна для понимания при отладке. Существует альтернативный, полностью линейный (итеративный) алгоритм, который генерирует сочетания одно за другим в строгом лексикографическом (алфавитном) порядке индексов.
Этот алгоритм базируется на работе с массивом индексов текущего сочетания. Пусть мы выбираем сочетания по $M$ элементов из массива размером $N$.
Мы начинаем с первого возможного сочетания индексов: [0, 1, 2, ... M-1].
На каждой итерации мы ищем с конца массива индексов первый такой индекс, который еще можно увеличить (его текущее значение строго меньше максимально возможного значения для данной позиции). Мы увеличиваем его на 1, а все последующие индексы сбрасываем в возрастающую последовательность.
Рассмотрим по шагам реализацию этого быстрого и элегантного алгоритма на встроенном языке 1С:
&НаКлиенте
Процедура ВыполнитьРасчетИтеративно(Команда)
Если мКоличество > мДлина Или мКоличество <= 0 Тогда
ПоказатьПредупреждение(, "Некорректно заданы параметры N и M!");
Возврат;
КонецЕсли;
// Заполняем базовый массив данных
ИсходныйМассив = Новый Массив;
Для Счетчик = 1 По мДлина Цикл
ИсходныйМассив.Добавить("А" + Счетчик);
КонецЦикла;
// Инициализируем рабочий массив индексов первыми элементами: [0, 1, 2, ..., M-1]
Индексы = Новый Массив(мКоличество);
Для i = 0 По мКоличество - 1 Цикл
Индексы[i] = i;
КонецЦикла;
Пока Истина Цикл
// Шаг 1. Формируем и выводим сочетание по текущим индексам
СтрокаВывода = "";
Для i = 0 По мКоличество - 1 Цикл
СтрокаВывода = СтрокаВывода + ИсходныйМассив[Индексы[i]] + " ";
КонецЦикла;
Сообщить(СтрокаВывода);
// Шаг 2. Ищем с конца индекс, который можно увеличить.
// Максимальное значение индекса на позиции i равно: N - M + i
ИндексДляИзменения = -1;
Для i = 1 По мКоличество Цикл
ТекущаяПозиция = мКоличество - i;
Если Индексы[ТекущаяПозиция] < мДлина - мКоличество + ТекущаяПозиция Тогда
ИндексДляИзменения = ТекущаяПозиция;
Прервать;
КонецЕсли;
КонецЦикла;
// Если подходящего индекса не нашли — все сочетания сгенерированы!
Если ИндексДляИзменения = -1 Тогда
Прервать;
КонецЕсли;
// Шаг 3. Увеличиваем найденный индекс на единицу
Индексы[ИндексДляИзменения] = Индексы[ИндексДляИзменения] + 1;
// Шаг 4. Все последующие индексы выстраиваем по порядку (+1 от соседа слева)
Для i = ИндексДляИзменения + 1 По мКоличество - 1 Цикл
Индексы[i] = Индексы[i - 1] + 1;
КонецЦикла;
КонецЦикла;
КонецПроцедуры
Проанализируем работу этого алгоритма на примере сочетания 2 элементов из 4 ($N=4, M=2$).
[0, 1] (вывод: А1 А2).[0, 2] (вывод: А1 А3).[0, 3] (вывод: А1 А4).[1, 2] (вывод: А2 А3).[1, 3] (вывод: А2 А4).[2, 3] (вывод: А3 А4).---
В реальной практике количество сочетаний может исчисляться сотнями тысяч или миллионами. Если запустить генерацию элементов без предварительной проверки, сеанс пользователя 1С может просто зависнуть из-за перегрузки интерфейса или нехватки оперативной памяти.
Для предотвращения таких ситуаций перед выводом данных всегда полезно рассчитать общее число сочетаний по классической формуле:
$$C_n^m = \frac{n!}{m!(n-m)!}$$
Чтобы избежать переполнения разрядности чисел при расчете факториала в цикле, применим оптимизированную функцию расчета без прямого вычисления факториалов больших чисел:
&НаКлиентеБезКонтекста
Функция РассчитатьКоличествоСочетаний(Знач N, Знач M)
Если M > N Тогда Возврат 0; КонецЕсли;
Если M = 0 Или M = N Тогда Возврат 1; КонецЕсли;
// Используем свойство симметрии сочетаний C(n, m) = C(n, n - m)
Если M > N - M Тогда
M = N - M;
КонецЕсли;
Результат = 1;
Для i = 1 По M Цикл
Результат = Результат * (N - M + i) / i;
КонецЦикла;
Возврат Результат;
КонецФункции
Используя эту функцию, мы можем легко добавить проверку перед основным расчетом:
КолВо = РассчитатьКоличествоСочетаний(мДлина, мКоличество);
Если КолВо > 50000 Тогда
ПоказатьПредупреждение(, "Слишком много комбинаций (" + КолВо + "). Пожалуйста, уменьшите параметры расчета!");
Возврат;
КонецЕсли;
Разработанные нами алгоритмы полностью решают проблему динамической вложенности циклов. Рекурсивный подход проще воспринимается логически, а итеративный алгоритм обеспечивает максимальное быстродействие и стабильность работы в системе 1С:Предприятие.