Как реализовать динамическую генерацию сочетаний из N элементов по M в 1С?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

При решении различных прикладных задач в 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(Команда)
    мДлина = Новый Массив(мДлина);
    Для мКоличество1 = 0 По мДлина.ВГраница - 1 Цикл
        // ...
    КонецЦикла;
КонецПроцедуры

В данном примере переменная мДлина изначально является реквизитом управляемой формы с типом Число. Попытка переопределить её внутри процедуры как Новый Массив(мДлина) приводит к ошибке выполнения. Управляемая форма строго следит за типами своих реквизитов. Мы не можем записать объект типа Массив в реквизит, предназначенный для хранения чисел. Платформа либо выдаст ошибку несоответствия типов, либо попытается неявно преобразовать массив обратно к числу, что приведет к краху алгоритма.

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

Для корректной работы мы должны объявить локальную переменную (например, РабочийМассив), инициализировать её и заполнить элементами на основании введенного пользователем числа мДлина:


РабочийМассив = Новый Массив;
Для Счетчик = 1 По мДлина Цикл
    РабочийМассив.Добавить("А" + Счетчик);
КонецЦикла;

---

Способ №1. Рекурсивный алгоритм (бэк treks/backtracking)

Рекурсия — это наиболее естественный способ симуляции динамической вложенности циклов. Функция вызывает саму себя, углубляясь на следующий уровень выборки, пока не наберет необходимое количество элементов $M$. Как только сочетание сформировано, функция возвращает управление на предыдущий уровень, делает «шаг назад» (удаляет последний добавленный элемент) и пробует другие варианты.

Давайте подробно разберем программную реализацию рекурсивного метода в 1С. Создайте на управляемой форме два реквизита: мДлина (Число) и мКоличество (Число), а также команду ВыполнитьРасчетРекурсивно.

Посмотрим на код обработчика команды и самой рекурсивной процедуры:


&НаКлиенте
Процедура ВыполнитьРасчетРекурсивно(Команда)
    
    // Проверим корректность введенных пользователем данных
    Если мКоличество > мДлина Или мКоличество <= 0 Тогда
        ПоказатьПредупреждение(, "Размер выборки (М) должен быть больше 0 и не превышать общую длину массива (N)!");
        Возврат;
    КонецЕсли;
    
    // Шаг 1. Инициализируем и заполняем исходный массив элементов (N)
    ИсходныйМассив = Новый Массив;
    Для Счетчик = 1 По мДлина Цикл
        ИсходныйМассив.Добавить("А" + Счетчик);
    КонецЦикла;
    
    // Шаг 2. Подготовим коллекции для хранения результатов
    Результат = Новый Массив;
    ТекущийБуфер = Новый Массив;
    
    // Шаг 3. Запускаем рекурсивный поиск сочетаний
    ГенерироватьСочетанияРекурсивно(ИсходныйМассив, мКоличество, 0, ТекущийБуфер, Результат);
    
    // Шаг 4. Выводим полученный результат пользователю
    Для Каждого Сочетание Из Результат Цикл
        СтрокаВывода = "";
        Для Каждого Элемент Из Сочетание Цикл
            СтрокаВывода = СтрокаВывода + Элемент + " ";
        КонецЦикла;
        Сообщить(СтрокаВывода);
    КонецЦикла;
    
КонецПроцедуры

&НаКлиенте
Процедура ГенерироватьСочетанияРекурсивно(ИсходныйМассив, РазмерВыборки, СтартовыйИндекс, ТекущийБуфер, Результат)
    
    // Если буфер заполнен до нужного размера M, мы нашли одно из сочетаний
    Если ТекущийБуфер.Количество() = РазмерВыборки Тогда
        
        // ВНИМАНИЕ: в 1С массивы передаются по ссылке!
        // Нельзя просто добавить ТекущийБуфер в Результат.
        // Нужно обязательно создать его независимую копию.
        КопияСочетания = Новый Массив;
        Для Каждого Элемент Из ТекущийБуфер Цикл
            КопияСочетания.Добавить(Элемент);
        КонецЦикла;
        
        Результат.Добавить(КопияСочетания);
        Возврат;
        
    КонецЕсли;
    
    // Перебираем элементы исходного массива, начиная со СтартовогоИндекса
    Для Индекс = СтартовыйИндекс По ИсходныйМассив.ВГраница() Цикл
        
        // Добавляем текущий элемент в буфер комбинации
        ТекущийБуфер.Добавить(ИсходныйМассив[Индекс]);
        
        // Рекурсивно переходим к выбору следующего элемента.
        // Передаем Индекс + 1, чтобы исключить повторения и гарантировать движение только вперед
        ГенерироватьСочетанияРекурсивно(ИсходныйМассив, РазмерВыборки, Индекс + 1, ТекущийБуфер, Результат);
        
        // Важнейший шаг бэктрекинга: удаляем последний элемент, освобождая место для других вариантов
        ТекущийБуфер.Удалить(ТекущийБуфер.ВГраница());
        
    КонецЦикла;
    
КонецПроцедуры

Важная техническая тонкость: Обратите внимание на копирование массива перед добавлением в общий результат. В языке 1С все коллекционные объекты (массивы, структуры, таблицы значений) передаются по ссылке. Если бы мы просто написали Результат.Добавить(ТекущийБуфер), то при последующем вызове метода ТекущийБуфер.Удалить() данные стерлись бы и внутри уже сохраненного результата. В итоге мы получили бы массив, заполненный пустыми массивами. Создание копии через цикл решает эту проблему.

---

Способ №2. Итеративный алгоритм (Лексикографический порядок)

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

Этот алгоритм базируется на работе с массивом индексов текущего сочетания. Пусть мы выбираем сочетания по $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$).

  1. На старте индексы равны [0, 1] (вывод: А1 А2).
  2. Проверяем индекс на позиции 1 (значение 1). Максимальное значение для него: $4 - 2 + 1 = 3$. Так как $1 < 3$, увеличиваем его. Индексы становятся [0, 2] (вывод: А1 А3).
  3. Снова проверяем позицию 1 (значение 2). $2 < 3$, увеличиваем. Индексы: [0, 3] (вывод: А1 А4).
  4. Проверяем позицию 1 (значение 3). Оно равно максимуму ($3$). Переходим к позиции 0 (значение 0). Максимум для позиции 0: $4 - 2 + 0 = 2$. Так как $0 < 2$, увеличиваем позицию 0 на 1. Она становится равной $1$. Все последующие индексы сбрасываем: позиция 1 становится равной $позиция 0 + 1 = 2$. Получили индексы [1, 2] (вывод: А2 А3).
  5. Проверяем позицию 1 (значение 2). Увеличиваем. Получаем [1, 3] (вывод: А2 А4).
  6. Позиция 1 равна 3 (максимум). Позиция 0 равна 1. Увеличиваем позицию 0 до 2. Позицию 1 сбрасываем до $2 + 1 = 3$. Индексы: [2, 3] (вывод: А3 А4).
  7. Все индексы достигли своих максимальных значений. Работа алгоритма завершена.

---

Как заранее узнать количество комбинаций?

В реальной практике количество сочетаний может исчисляться сотнями тысяч или миллионами. Если запустить генерацию элементов без предварительной проверки, сеанс пользователя 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С:Предприятие.

← На главную