В практике программирования на платформе 1С:Предприятие часто возникают задачи, связанные с комбинаторикой. Одной из классических задач является генерация всех возможных комбинаций для набора элементов, что часто называют моделью «кодового замка». Представим, что у нас есть определенное количество барабанов, на каждом из которых нанесены цифры в заданном диапазоне (от минимального до максимального значения). Нам необходимо вывести абсолютно все возможные состояния такого замка.
Рассмотрим ситуацию подробнее: если у нас 3 барабана и на каждом могут быть цифры от 3 до 7, то программа должна выдать последовательность типа 333, 334, 335... до 777. Разберем по шагам различные методы реализации этой задачи, проанализируем их производительность и выберем наиболее оптимальные подходы.
Рекурсия в 1С — это механизм, при котором процедура вызывает саму себя. Для задач перебора это один из самых элегантных способов, так как он позволяет легко обрабатывать произвольное количество «барабанов» без написания вложенных циклов. Проанализируем пример кода, который считается одним из самых быстрых в рамках обсуждения.
Алгоритм работы: мы запускаем процедуру для первого барабана. Внутри она циклом перебирает все свои значения и для каждого значения вызывает саму себя для следующего барабана. Когда мы достигаем последнего барабана, мы просто выводим накопленную строку комбинации.
&НаКлиенте
Процедура СгенерироватьВсеКомбинации(Команда)
// Задаем исходные параметры
КоличествоБарабанов = 3;
МинимальноеЗначение = 3;
МаксимальноеЗначение = 7;
// Структура для передачи ограничений, чтобы не дублировать переменные
ПараметрыЗамка = Новый Структура;
ПараметрыЗамка.Вставить("КолВо", КоличествоБарабанов);
ПараметрыЗамка.Вставить("Мин", МинимальноеЗначение);
ПараметрыЗамка.Вставить("Макс", МаксимальноеЗначение);
Если КоличествоБарабанов = 0 Тогда
Возврат;
КонецЕсли;
ТекущаяСтрока = "";
КрутитьБарабанРекурсивно(1, ТекущаяСтрока, ПараметрыЗамка);
КонецПроцедуры
&НаКлиенте
Процедура КрутитьБарабанРекурсивно(НомерБарабана, НакопленнаяКомбинация, ПараметрыЗамка)
// Важно: добавляем возможность прервать процесс пользователем
ОбработкаПрерыванияПользователя();
Для Значение = ПараметрыЗамка.Мин По ПараметрыЗамка.Макс Цикл
// Формируем строковое представление текущего шага
НоваяСтрока = НакопленнаяКомбинация + ?(ПустаяСтрока(НакопленнаяКомбинация), "", "-") + Значение;
Если НомерБарабана = ПараметрыЗамка.КолВо Тогда
// Мы дошли до последнего барабана — выводим результат
Сообщить(НоваяСтрока);
Иначе
// Переходим к следующему барабану
КрутитьБарабанРекурсивно(НомерБарабана + 1, НоваяСтрока, ПараметрыЗамка);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Рассмотрим важный нюанс: рекурсия в 1С имеет ограничение по глубине стека (обычно около 1000–2000 вызовов). Для задачи с замком это означает, что количество барабанов не должно превышать этого числа, что в реальных задачах встречается крайне редко. Преимущество метода — минимальное количество обращений к памяти и высокая скорость работы интерпретатора.
Если мы хотим избежать рекурсии (например, из-за боязни переполнения стека или для упрощения отладки), можно применить метод «счетчика». Представим наш замок как массив, где каждый элемент — это текущее значение барабана. Мы будем увеличивать значение последнего элемента, и если он достигает максимума, «сбрасывать» его в минимум и увеличивать предыдущий разряд. Разберем этот алгоритм.
&НаКлиенте
Процедура ГенерацияЦиклом(Команда)
МинЗначение = 3;
МаксЗначение = 7;
КоличествоРазрядов = 3;
// Инициализируем массив начальными значениями
МассивБарабанов = Новый Массив;
Для Сч = 1 По КоличествоРазрядов Цикл
МассивБарабанов.Добавить(МинЗначение);
КонецЦикла;
ПереборЗавершен = Ложь;
Пока НЕ ПереборЗавершен Цикл
// 1. Выводим текущую комбинацию
ТекстКомбинации = "";
Для Каждого Значение Из МассивБарабанов Цикл
ТекстКомбинации = ТекстКомбинации + Значение;
КонецЦикла;
Сообщить(ТекстКомбинации);
// 2. Инкремент (логика счетчика)
Для Индекс = (КоличествоРазрядов - 1) По 0 Шаг -1 Цикл
МассивБарабанов[Индекс] = МассивБарабанов[Индекс] + 1;
Если МассивБарабанов[Индекс] > МаксЗначение Тогда
// Если вышли за предел — обнуляем текущий разряд и идем к следующему
Если Индекс = 0 Тогда
ПереборЗавершен = Истина;
Прервать;
Иначе
МассивБарабанов[Индекс] = МинЗначение;
КонецЕсли;
Иначе
// Увеличение прошло успешно, прерываем цикл инкремента
Прервать;
КонецЕсли;
КонецЦикла;
ОбработкаПрерыванияПользователя();
КонецЦикла;
КонецПроцедуры
Этот метод работает медленнее рекурсии из-за частого обращения к элементам массива и ручного управления индексами, но он абсолютно безопасен с точки зрения ресурсов системы.
Интересный способ решения задачи — использование теории систем счисления. Если количество цифр на одном барабане равно N, то общее количество комбинаций равно N в степени КоличествоБарабанов. Мы можем запустить один цикл от 0 до этого общего числа и переводить каждое число в систему счисления с основанием N. Проанализируем ситуацию на примере кода.
&НаКлиенте
Процедура СистемыСчисления(Команда)
КолБарабанов = 3;
Мин = 3;
Макс = 7;
Основание = Макс - Мин + 1;
ВсегоВариантов = Pow(Основание, КолБарабанов);
Для НомерВарианта = 0 По ВсегоВариантов - 1 Цикл
ТекЧисло = НомерВарианта;
Результат = "";
// Перевод в систему счисления
Для Сч = 1 По КолБарабанов Цикл
Остаток = ТекЧисло % Основание;
Результат = Строка(Остаток + Мин) + " " + Результат;
ТекЧисло = Цел(ТекЧисло / Основание);
КонецЦикла;
Сообщить(Результат);
КонецЦикла;
КонецПроцедуры
Преимущества: код выглядит очень лаконично. Недостатки: операции деления (/) и взятия остатка (%) являются более тяжелыми для процессора, чем простое сложение, поэтому на миллионах итераций этот метод уступит рекурсии.
Выясним, можно ли решить задачу средствами СУБД. В 1С запросы отлично справляются с генерацией декартова произведения множеств. Если мы соединим таблицу саму с собой без условий соединения, мы получим все возможные комбинации. Рассмотрим, как это реализовать динамически.
Процедура ГенерацияЗапросом(Количество, Минимум, Максимум)
Запрос = Новый Запрос;
// Подготавливаем текст выбора цифр для одного барабана
ТекстБарабана = "ВЫБРАТЬ " + Минимум + " КАК Цифра";
Для Сч = (Минимум + 1) По Максимум Цикл
ТекстБарабана = ТекстБарабана + " ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ " + Сч;
КонецЦикла;
// Динамически формируем текст запроса с соединениями
ТекстПолей = "";
ТекстТаблиц = "";
Для к = 1 По Количество Цикл
ТекстПолей = ТекстПолей + ", Т" + к + ".Цифра";
ТекстТаблиц = ТекстТаблиц + " , (" + ТекстБарабана + ") КАК Т" + к;
КонецЦикла;
Запрос.Текст = "ВЫБРАТЬ " + Сред(ТекстПолей, 2) + " ИЗ " + Сред(ТекстТаблиц, 3);
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
// Обработка результата
КонецЦикла;
КонецПроцедуры
Этот метод чрезвычайно эффективен, если вам нужно не просто вывести строки, а записать их в базу данных или обработать внутри запроса. Однако помните о лимитах СУБД на количество таблиц в одном запросе.
Проанализируем результаты тестов, проведенных участниками сообщества. При генерации 100 000 комбинаций:
Сообщить).Важные рекомендации по работе с большими данными в 1С:
Сообщить(): Основное время при генерации комбинаций тратится не на расчеты, а на отрисовку строк в окне сообщений. Если вам нужно сохранить миллион комбинаций, используйте объект ЗаписьТекста для выгрузки в файл.С = С + "1" создает новую строку в памяти. Для формирования длинных списков лучше добавлять элементы в Массив, а затем один раз использовать СтрСоединить().&НаСервере. Передача управления между клиентом и сервером внутри цикла мгновенно уничтожит производительность.Подводя итог, можно сказать, что выбор метода зависит от конкретных условий. Для учебных задач и небольших объемов идеально подходит рекурсия. Для промышленной генерации данных (например, формирование всех возможных артикулов товара на основе его характеристик) лучше использовать механизм запросов.