Как получить все возможные уникальные комбинации заданной длины из исходного массива элементов в 1С?

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

Нам часто встречаются задачи, требующие генерации всех возможных подмножеств или комбинаций из заданного набора элементов. Например, у нас есть массив символов ABCDEF, и нам нужно получить все возможные массивы длиной в 5 символов, удаляя по одному символу, или длиной в 4 символа, удаляя по два. Ключевое условие – порядок элементов в полученных массивах учитывать не нужно, то есть ABCDE и BACDE считаются одной и той же комбинацией.

Рассмотрим различные подходы к решению этой головоломки на платформе 1С, анализируя их реализацию и особенности.

Основы комбинаторики: Сочетания

Прежде чем мы перейдем к коду, давайте вспомним основы. Задача, которую мы решаем, называется генерацией сочетаний без повторений. Сочетание из N элементов по K — это выбор K элементов из множества N, где порядок выбранных элементов не имеет значения. Число таких сочетаний обозначается как C(N,K) и вычисляется по формуле: C(N,K) = N! / (K! * (N-K)!).

Для нашего примера:

  1. Массив из 6 символов, нужно 5: C(6,5) = 6! / (5! * (6-5)!) = 6! / (5! * 1!) = 6 комбинаций. Это: ABCDE, ABCDF, ABCEF, ABDEF, ACDEF, BCDEF.
  2. Массив из 6 символов, нужно 4: C(6,4) = 6! / (4! * (6-4)!) = 6! / (4! * 2!) = (6*5) / (2*1) = 15 комбинаций. Это: ABCD, ABCE, ABCF, ABDE, ABDF, ABEF, ACDE, ACDH, ACEF, ADEF, BCDE, BCDF, BCEF, BDEF, CDEF.

Задача подразумевает, что мы выбираем K элементов для оставшегося массива, что эквивалентно удалению N-K элементов. Это одно и то же.

Рекурсивный подход с имитацией удаления элементов (на основе сообщения 25)

Один из самых интуитивных и распространенных способов решения этой задачи — использование рекурсии (перебор с возвратом). Мы будем строить комбинации, последовательно "удаляя" элементы из исходного массива до достижения нужной длины.

Суть алгоритма:

  1. Мы начинаем с исходного массива.
  2. На каждом шаге рекурсии мы пытаемся "удалить" один элемент из текущего массива. Для этого мы создаем новый массив, копируем в него все элементы, кроме одного, который мы хотим удалить.
  3. Если длина нового массива становится равной требуемой, мы добавляем его в список результатов. Чтобы избежать дубликатов, мы приводим массив к каноническому виду (например, сортируем, если элементы исходного массива не отсортированы, а затем преобразуем в строку) и проверяем, не был ли он уже добавлен.
  4. Если длина нового массива еще больше требуемой, мы рекурсивно вызываем ту же функцию для этого нового массива.
  5. Этот процесс продолжается до тех пор, пока мы не переберем все возможные варианты удаления элементов.

Давайте посмотрим на пример реализации в 1С:


&НаСервереБезКонтекста
Функция ПолучитьНовыйМассивБезЭлементаПоИндексу(ИсхМассив, Индекс)
    Массив_ = Новый Массив;
    Для Каждого Элем Из ИсхМассив Цикл
        Массив_.Добавить(Элем);
    КонецЦикла;
    Массив_.Удалить(Индекс);
    Возврат Массив_;
КонецФункции

&НаСервереБезКонтекста
Функция ПреобразоватьВСтроку(МассивСимволов)
    // Для обеспечения уникальности комбинаций независимо от порядка,
    // если исходные данные не отсортированы,
    // необходимо сначала отсортировать МассивСимволов перед преобразованием в строку.
    // Для данного примера с "A, B, C, D, E, F" и удалением, результат будет уже отсортирован,
    // но в общем случае это важно.
    // Пример сортировки (если требуется):
    // МассивСимволов.Сортировать(); // Метод Сортировать() есть у некоторых коллекций, но не у всех.
    // Для универсальности можно скопировать в список значений, отсортировать и затем преобразовать.
    
    Стр = "";
    Для Каждого Элем Из МассивСимволов Цикл
        Стр = Стр + Элем;    
    КонецЦикла;
    Возврат Стр;
КонецФункции

&НаСервереБезКонтекста
Процедура ЗаполнитьМассивМассивов(МассивСимволов, ТребуемоеКоличествоЭлементов, МассивМассивов, Добавленные)
    // Итерация от последнего элемента к первому помогает избежать дубликатов
    // и более предсказуемо формировать комбинации при рекурсивном удалении.
    Инд = МассивСимволов.Количество() - 1;
    Пока Инд >=0 Цикл
        // Создаем новый массив, удаляя элемент по текущему индексу
        НовыйМассивСимволов = ПолучитьНовыйМассивБезЭлементаПоИндексу(МассивСимволов, Инд);
        
        Если НовыйМассивСимволов.Количество() > ТребуемоеКоличествоЭлементов Тогда
            // Если массив все еще длиннее требуемого, продолжаем рекурсивно удалять
            ЗаполнитьМассивМассивов(НовыйМассивСимволов, ТребуемоеКоличествоЭлементов, МассивМассивов, Добавленные);
        Иначе
            // Если достигнута требуемая длина, добавляем комбинацию
            МассивСимволовСтрокой = ПреобразоватьВСтроку(НовыйМассивСимволов);
            // Проверяем, не была ли эта комбинация уже добавлена, чтобы избежать дубликатов.
            // Это важно, так как "порядок элементов не учитывать"
            // и разные пути удаления могут привести к одной и той же комбинации.
            Если Добавленные.Найти(МассивСимволовСтрокой) = Неопределено Тогда 
                МассивМассивов.Добавить(НовыйМассивСимволов); 
                Добавленные.Добавить(МассивСимволовСтрокой); 
            КонецЕсли;
        КонецЕсли;
        Инд = Инд - 1;
    КонецЦикла;
КонецПроцедуры

&НаКлиенте
Процедура Выполнить_(Команда)//кнопка на форме
    мСимволы = Новый Массив;
    мСимволы.Добавить("A");
    мСимволы.Добавить("B");            
    мСимволы.Добавить("C");
    мСимволы.Добавить("D");
    мСимволы.Добавить("E");
    мСимволы.Добавить("F");
    
    МассивМассивов = Новый Массив;//результат массив массивов
    Добавленные = Новый Массив;//результат массив строк (для проверки уникальности)
    
    Сообщить("--- Комбинации из 5 элементов ---");
    ЗаполнитьМассивМассивов(мСимволы, 5, МассивМассивов, Добавленные);//для 5 элементов в выходных массивах    
    Для Каждого Комбинация Из МассивМассивов Цикл
        Сообщить(ПреобразоватьВСтроку(Комбинация));
    КонецЦикла;
    Сообщить("Всего комбинаций (5): " + МассивМассивов.Количество());
    
    МассивМассивов = Новый Массив; // Очищаем для нового расчета
    Добавленные = Новый Массив;    // Очищаем для нового расчета
    
    Сообщить("--- Комбинации из 4 элементов ---");
    ЗаполнитьМассивМассивов(мСимволы, 4, МассивМассивов, Добавленные);//для 4 элементов в выходных массивах
    Для Каждого Комбинация Из МассивМассивов Цикл
        Сообщить(ПреобразоватьВСтроку(Комбинация));
    КонецЦикла;
    Сообщить("Всего комбинаций (4): " + МассивМассивов.Количество());
КонецПроцедуры

В этом коде:

  1. Функция ПолучитьНовыйМассивБезЭлементаПоИндексу создает копию исходного массива и удаляет из нее элемент по заданному индексу. Это имитирует "удаление" символа.
  2. Функция ПреобразоватьВСтроку нужна для конвертации массива символов в строку. Это позволяет нам использовать массив Добавленные как набор уже найденных уникальных комбинаций. Важно отметить, что если исходный массив не отсортирован, и мы хотим избежать комбинаций типа {A,B} и {B,A} как разных, то перед преобразованием в строку необходимо будет отсортировать каждый формируемый НовыйМассивСимволов. В данном случае, поскольку мы работаем с отсортированными символами A,B,C,D,E,F и последовательно удаляем элементы, получаемые подмножества уже будут "отсортированы" по порядку своих элементов, поэтому дополнительная сортировка не требуется.
  3. Процедура ЗаполнитьМассивМассивов является основной рекурсивной частью. Она перебирает элементы текущего массива, удаляет их по очереди и рекурсивно вызывает саму себя до тех пор, пока длина формируемого массива не достигнет ТребуемоеКоличествоЭлементов.
  4. Массив Добавленные служит для хранения уже найденных комбинаций в строковом виде, чтобы избежать повторного добавления одинаковых наборов элементов. Это критично для выполнения условия "порядок элементов не учитывать".

Генерация всех подмножеств с последующей фильтрацией (на основе сообщения 30)

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

Суть алгоритма:

  1. Начинаем с пустого множества как единственного подмножества.
  2. Для каждого элемента исходного массива мы берем все уже сгенерированные подмножества и для каждого из них создаем новое подмножество, добавляя текущий элемент.
  3. Таким образом, если у нас есть N элементов, на каждом шаге количество подмножеств удваивается, пока мы не получим 2^N подмножеств.
  4. После генерации всех подмножеств мы отфильтровываем те, чья длина соответствует требуемой.

Пример реализации в 1С:


Функция ВсеПодмножества(Множество)
    Ответ = Новый Массив;
    // Начинаем с пустого списка значений, представляющего пустое подмножество
    Ответ.Добавить(Новый СписокЗначений);
    
    // Для каждого элемента исходного множества
    Для ИндексЭлемента = 0 По Множество.Количество() - 1 Цикл
        // Создаем новые подмножества, добавляя текущий элемент ко всем существующим
        // Важно итерировать от конца к началу, чтобы не изменять коллекцию во время обхода
        // или использовать временное хранилище для новых подмножеств
        КоличествоСуществующих = Ответ.Количество();
        Для ИндексВарианта = 0 По КоличествоСуществующих - 1 Цикл
            // Копируем существующее подмножество
            НовоеПодмножество = Ответ[ИндексВарианта].Скопировать();
            // Добавляем к нему текущий элемент
            НовоеПодмножество.Добавить(Множество[ИндексЭлемента]);
            // Добавляем новое подмножество в общий список
            Ответ.Добавить(НовоеПодмножество);
        КонецЦикла;
    КонецЦикла;
    Возврат Ответ;
КонецФункции

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

В этом примере:

  1. Функция ВсеПодмножества эффективно генерирует все возможные подмножества исходного массива Множество. Она начинает с пустого множества и для каждого элемента последовательно добавляет его ко всем уже сгенерированным подмножествам, удваивая их количество на каждом шаге.
  2. Для хранения подмножеств используется СписокЗначений.
  3. После получения всех подмножеств мы итерируем по ним и отбираем только те, чья длина (элмРез.Количество()) равна ТребуемоеКоличествоЭлементов.
  4. Как и в предыдущем методе, для соблюдения условия "порядок элементов не учитывать" мы преобразуем каждое найденное подмножество в строковое представление. В случае, если элементы в исходном Множество добавлялись в отсортированном порядке, и сам алгоритм добавления элементов в подмножества сохраняет этот порядок, то результирующие подмножества уже будут "отсортированы" и не потребуют дополнительной сортировки перед преобразованием в строку для проверки уникальности.

Итеративный перебор с возвратом (на основе сообщения 42)

Итеративные алгоритмы с возвратом (backtracking) также являются популярным способом для генерации комбинаций или перестановок. Они могут быть более производительными, чем рекурсивные, за счет отсутствия накладных расходов на вызовы функций, но часто сложнее для понимания и реализации.

Суть алгоритма:

  1. Мы используем массив для хранения текущей формируемой комбинации и массив для отслеживания уже использованных элементов.
  2. Начинаем с первого уровня (глубины) и выбираем первый доступный элемент.
  3. Переходим на следующий уровень. Если достигнута требуемая глубина, текущая комбинация фиксируется.
  4. Если на текущем уровне больше нет доступных элементов, возвращаемся на предыдущий уровень и продолжаем поиск.
  5. Этот процесс повторяется до тех пор, пока не будут перебраны все возможные варианты.

Важно отметить, что приведенный в сообщении 42 код, по своей логике, генерирует перестановки без повторений, а не сочетания. Перестановки отличаются от сочетаний тем, что порядок элементов в них важен (AB и BA - разные перестановки, но одно сочетание). Для получения сочетаний из перестановок требуется дополнительный шаг: отсортировать каждый полученный результат и затем дедуплицировать его (например, с помощью массива Добавленные, как мы делали в предыдущих примерах).

Пример реализации в 1С:


Функция Факториал(чЗначение)     
    чРезульт = 1;
    
    Для чНом = 1 по чЗначение Цикл
        чРезульт = чРезульт * чНом;
    КонецЦикла;
    
    Возврат чРезульт; 
КонецФункции
 
&НаКлиенте
Процедура Команда1(Команда)
    чКолво = 6; // Общее количество элементов в исходном массиве
    чМаксимальнаяГлубина = 4; // Требуемое количество элементов в комбинации
    
    // Плановое количество результатов для ПЕРЕСТАНОВОК (N! / (N-K)!)
    // Для сочетаний это будет N! / (K! * (N-K)!)
    Сообщить("Плановое количество перестановок = " + Факториал(чКолво) / ( Факториал(чКолво - чМаксимальнаяГлубина))); 
    Сообщить("Плановое количество сочетаний = " + Факториал(чКолво) / (Факториал(чМаксимальнаяГлубина) * Факториал(чКолво - чМаксимальнаяГлубина))); 
    
    мсМножество = Новый Массив(чКолво); // Исходные элементы (индексы от 0 до чКолво-1)
    // Символ(65 + чНПП) для A, B, C... (65 это ASCII код для 'A')
    Для  чНПП = 0 по чКолво - 1 Цикл // Изменено с чКолво+1 на чКолво-1 для корректного индексирования
        мсМножество[чНПП] = Символ(65 + чНПП);
    КонецЦикла;
    
    мсТекРезульт = Новый Массив(чМаксимальнаяГлубина + 1); // Текущая формируемая комбинация
    мсИспользовано = Новый Массив(чКолво + 1); // Отслеживание использованных индексов элементов
    
    чУровень = 1; // Текущий уровень рекурсии (глубина)
    чНомерТекЭлемента = 0; // Индекс элемента, с которого начинаем перебор на текущем уровне
    чНППРезультат = 1; // Счетчик результатов
    
    // Массив для хранения уникальных комбинаций (для соблюдения "порядок не учитывать")
    МассивУникальныхКомбинаций = Новый Массив();
    СоответствиеУникальныхСтрок = Новый Соответствие(); // Для быстрой проверки уникальности
    
    Пока чУровень > 0 Цикл    // Пока не перебрали все варианты на первом уровне
        чНомерТекЭлемента = чНомерТекЭлемента + 1; 
        
        Если чНомерТекЭлемента > чКолво Тогда  
            // На этой глубине перебрали все элементы. 
            // Очищаем использованный элемент и возвращаемся на предыдущий уровень.
            Если мсТекРезульт[чУровень] <> Неопределено И мсТекРезульт[чУровень] <> 0 Тогда
                мсИспользовано[мсТекРезульт[чУровень]] = 0;
            КонецЕсли;
            мсТекРезульт[чУровень] = 0;
            
            чУровень = чУровень - 1;       
            // Чтобы продолжить с следующего элемента на предыдущем уровне, 
            // нужно взять индекс элемента, который был выбран на этом уровне.
            Если чУровень > 0 Тогда
                чНомерТекЭлемента = мсТекРезульт[чУровень];
            Иначе
                Продолжить; // Завершение, если вернулись за пределы первого уровня
            КонецЕсли;
            Продолжить;
        Иначе           
            Если мсИспользовано[чНомерТекЭлемента] <> 0 И мсИспользовано[чНомерТекЭлемента] <> Неопределено Тогда 
                // Текущий элемент уже был использован на одном из предыдущих уровней
                Продолжить;  // Пропускаем этот элемент
            Иначе      
                // Найден новый элемент для этого уровня
                // Сначала "отпускаем" элемент, который был выбран на текущем уровне ранее, если он был
                Если мсТекРезульт[чУровень] <> Неопределено И мсТекРезульт[чУровень] <> 0 Тогда
                    мсИспользовано[мсТекРезульт[чУровень]] = 0;
                КонецЕсли;
                
                мсИспользовано[чНомерТекЭлемента] = чУровень; // Отмечаем элемент как использованный на текущем уровне
                мсТекРезульт[чУровень] = чНомерТекЭлемента; // Записываем индекс элемента в текущий результат
            КонецЕсли;
        КонецЕсли;
        
        Если  чУровень < чМаксимальнаяГлубина Тогда  
            // Если нужный уровень не достигнут, идем на следующий
            чНомерТекЭлемента = 0; // Начинаем перебор с первого элемента на новом уровне
            чУровень = чУровень  + 1; 
        Иначе              
            ///////////////////// Вывод результата ////////////////////////////////   
            сРезульт = "";
            МассивКомбинации = Новый Массив();
            Для  чНомРез = 1 по чУровень Цикл
                сРезульт = сРезульт + мсМножество[мсТекРезульт[чНомРез] - 1]; // -1 так как мсМножество индексируется с 0
                МассивКомбинации.Добавить(мсМножество[мсТекРезульт[чНомРез] - 1]);
            КонецЦикла; 
            
            // Чтобы получить сочетания из перестановок, нужно отсортировать и дедуплицировать
            МассивКомбинации.Сортировать(); // Если метод сортировки не доступен, нужно реализовать свой или использовать СписокЗначений
            СтрокаКомбинации = "";
            Для Каждого ЭлементКомбинации Из МассивКомбинации Цикл
                СтрокаКомбинации = СтрокаКомбинации + ЭлементКомбинации;
            КонецЦикла;

            Если СоответствиеУникальныхСтрок.Получить(СтрокаКомбинации) = Неопределено Тогда
                СоответствиеУникальныхСтрок.Вставить(СтрокаКомбинации, Истина);
                МассивУникальныхКомбинаций.Добавить(МассивКомбинации);
                Сообщить("№" + чНППРезультат +" (сочетание) " + СтрокаКомбинации);
                чНППРезультат = чНППРезультат + 1;
            КонецЕсли;
            
            // Сообщить("№" + чНППРезультат +" (перестановка) " + сРезульт); // Оригинальный вывод перестановок
            // чНППРезультат = чНППРезультат + 1;
        КонецЕсли;
    КонецЦикла;
    Сообщить("Итого уникальных сочетаний найдено: " + МассивУникальныхКомбинаций.Количество());
КонецПроцедуры

В этой реализации:

  1. Переменные чКолво и чМаксимальнаяГлубина определяют общий размер исходного массива и желаемую длину комбинаций.
  2. мсМножество хранит исходные элементы. Обратите внимание, что индексация в 1С-массивах начинается с 0, поэтому Символ(65 + чНПП) и мсМножество[мсТекРезульт[чНомРез] - 1] используются для корректного доступа.
  3. мсТекРезульт — это массив, где хранятся индексы выбранных элементов для текущей перестановки/комбинации.
  4. мсИспользовано — это вспомогательный массив, который помогает отслеживать, какие элементы уже были выбраны на предыдущих уровнях, чтобы избежать дублирования элементов в одной перестановке.
  5. Цикл Пока чУровень > 0 реализует итеративный перебор с возвратом. Он симулирует рекурсивные вызовы, управляя переменными чУровень (глубина) и чНомерТекЭлемента (текущий элемент на данном уровне).
  6. Когда достигается чМаксимальнаяГлубина, формируется строка сРезульт, представляющая текущую перестановку.
  7. Критично для задачи: чтобы превратить генерируемые перестановки в уникальные сочетания (где порядок не важен), мы должны создать МассивКомбинации, отсортировать его (чтобы получить каноническое представление) и затем использовать СоответствиеУникальныхСтрок для проверки уникальности строкового представления отсортированного массива. Только уникальные отсортированные комбинации добавляются в финальный список.

Анализ подхода "удаления" элементов (на основе сообщения 43)

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

Пример реализации в 1С (с учетом исправления парсера):


&НаКлиенте
Функция Комбинации(масИсходный,СколькоОставить,НужныПерестановки=Ложь)
    массивМассивов = Новый Массив;
    Если СколькоОставить>=масИсходный.Количество() ИЛИ СколькоОставить<=0 Тогда
        //нечего тут хулиганить
        Возврат массивМассивов;
    КонецЕсли;
    
    // ДопСмещение = Количество элементов, которые нужно пропустить в начале или конце
    ДопСмещение = масИсходный.Количество() - СколькоОставить - 1; 
    индВнеш = масИсходный.Количество()-1; // Начинаем с последнего элемента
    
    Пока индВнеш>=ДопСмещение Цикл
        времМассив = Новый Массив;
        
        Для индВнутр=0 По масИсходный.Количество() - 1 Цикл
            // Логика пропуска элементов:
            // Пропускаем элементы, если их индекс меньше, чем (индВнеш - ДопСмещение)
            // ИЛИ если их индекс больше, чем индВнеш
            Если индВнутр < индВнеш - ДопСмещение 
                ИЛИ индВнутр > индВнеш Тогда
                // Добавляем элемент, если он не пропускается
                времМассив.Добавить(масИсходный[индВнутр]);
            КонецЕсли;
        КонецЦикла;
        индВнеш = индВнеш - 1; // Переходим к предыдущему элементу для следующей итерации
        
        // В оригинальной задаче "порядок элементов не учитывать", 
        // поэтому перестановки обычно не нужны для финального набора комбинаций.
        // Если бы были нужны перестановки, то здесь вызвали бы функцию Перестановки.
        // Для сочетаний: просто добавляем сформированный временный массив.
        // Необходимо также обеспечить уникальность, если 'времМассив' может быть сформирован несколько раз.
        // (в данном коде не реализовано, но для корректности должно быть)
        Если НужныПерестановки Тогда
            // массивПерестановок = Перестановки(времМассив,времМассив.Количество(),Истина);
            // Для каждого массив Из массивПерестановок Цикл
            //    массивМассивов.Добавить(массив);
            // КонецЦикла;
            // Имитация добавления для примера
            массивМассивов.Добавить(времМассив); 
        Иначе
            массивМассивов.Добавить(времМассив);
        КонецЕсли;
        
    КонецЦикла;
    Возврат массивМассивов;
КонецФункции

&НаКлиенте
Процедура ABCDEF(Команда)
    Массив = Новый Массив;
    массив.Добавить("A");
    массив.Добавить("B");
    массив.Добавить("C");
    массив.Добавить("D");
    массив.Добавить("E");
    массив.Добавить("F");
                                            
    // Тестируем с 2 элементами из 6. Ожидаем 15 комбинаций.
    массивМассивов = Комбинации(массив,2); 
    
    // Вывод результатов
    Сообщение = Новый СообщениеПользователю;
    Для каждого массив Из массивМассивов Цикл
        Сообщение.Текст = "";
        Для каждого символ Из массив Цикл
            Сообщение.Текст = Сообщение.Текст + символ;
        КонецЦикла;                  
        Сообщение.Сообщить();        
    КонецЦикла;    
    Сообщение.Текст = "Всего результатов найдено и выведено: "+ Строка(массивМассивов.Количество());
    Сообщение.Сообщить();
КонецПроцедуры

Анализ и критика:

Данный алгоритм пытается сформировать подмножества, исключая элементы по определенной логике. Однако, при внимательном рассмотрении, он не генерирует все возможные комбинации. Давайте проследим его работу для Комбинации(ABCDEF, 2):

В итоге, для Комбинации(ABCDEF, 2) данный код выдаст только AB, AF, EF. Ожидалось же 15 комбинаций (C(6,2)). Следовательно, этот алгоритм не решает задачу генерации всех возможных сочетаний в общем виде, а лишь находит очень специфический поднабор.

Для корректной генерации всех сочетаний с использованием принципа "удаления" элементов, лучше придерживаться рекурсивных методов, которые систематически перебирают все варианты исключения. Приведенный код в сообщении 43 может быть полезен только для очень специфических задач, где требуется именно такой ограниченный набор подмножеств, но не для общей задачи генерации комбинаций.

Выбор оптимального алгоритма и производительность

Когда речь заходит об "оптимальном алгоритме хитрого перебора", мы должны учитывать несколько факторов:

  1. Размер исходного множества (N) и длина комбинаций (K): Для небольших значений N (например, до 15-20), разница в производительности между рекурсивными и итеративными алгоритмами будет незначительной. Все будет работать достаточно быстро. Однако, когда N становится большим (например, 30 и выше), количество сочетаний C(N,K) может стать астрономическим, и даже самый "оптимальный" алгоритм будет работать долго просто из-за огромного количества генерируемых результатов.
  2. Сложность алгоритма: Теоретически, нижняя граница сложности для генерации всех сочетаний составляет O(C(N,K)), умноженное на сложность операций по формированию каждого сочетания (например, копирование массива, сортировка, преобразование в строку). Это означает, что мы не можем избежать выполнения работы, пропорциональной количеству результатов.
  3. Накладные расходы:
    • Рекурсивные алгоритмы (как в сообщении 25): Могут быть более читаемыми и простыми в написании, но имеют накладные расходы на вызовы функций и управление стеком. Создание нового массива на каждой итерации (как в ПолучитьНовыйМассивБезЭлементаПоИндексу) также требует дополнительных ресурсов памяти и времени.
    • Итеративные алгоритмы с возвратом (как в сообщении 42): Избегают рекурсивных вызовов, но могут быть сложнее для понимания. Они часто управляют стеком вручную с помощью переменных, что может дать небольшой выигрыш в производительности.
    • Генерация всех подмножеств с фильтрацией (как в сообщении 30): Этот метод генерирует 2^N подмножеств, что может быть значительно больше, чем C(N,K), если K далеко от N/2. Например, для N=6, K=4, C(6,4)=15. Но 2^6=64. Если K очень маленькое или очень большое, этот подход может быть менее эффективным, чем прямой генератор сочетаний.
    • Алгоритмы на основе битовых масок: Если исходные элементы можно легко представить индексами, битовые маски являются очень эффективным способом представления и генерации сочетаний. Каждая комбинация представляется числом, где установленные биты указывают на включенные элементы. Перебор чисел с определенным количеством установленных битов позволяет получить все сочетания. На 1С напрямую битовые операции не так удобно использовать как в языках низкого уровня, но концепцию можно адаптировать.
  4. Обработка уникальности ("порядок не учитывать"): Для обеспечения того, что ABCDE и BACDE рассматриваются как одно и то же сочетание, крайне важно приводить каждую комбинацию к каноническому виду (например, сортировать элементы внутри комбинации), а затем использовать механизм дедупликации (например, Соответствие или Массив.Найти со строковым представлением) для хранения только уникальных результатов. Это добавляет накладные расходы на сортировку и поиск, но необходимо для правильного ответа на задачу. Все приведенные нами корректные алгоритмы так или иначе учитывают этот аспект.

В целом, для типовых задач на 1С с относительно небольшими N (до 10-15 элементов) любой из представленных рекурсивных или итеративных методов (сообщение 25, 30, 42 с доработкой) будет работать достаточно быстро и эффективно. Выбор конкретного метода может зависеть от личных предпочтений в читаемости кода и легкости отладки.

Если же мы сталкиваемся с очень большими N, то стоит рассмотреть следующие аспекты:

  1. Ограничение по памяти: Хранение всех сочетаний может потребовать огромного объема памяти. Возможно, потребуется генерировать их "на лету" и обрабатывать по одному.
  2. Использование внешних компонент или JavaScript: Для интенсивных вычислений на стороне клиента или с использованием платформенных возможностей, которые более эффективны для числовых операций, можно передать задачу на выполнение в HTML-поле с JavaScript или воспользоваться внешней компонентой.

Для данной конкретной задачи, где N=6, любой из предложенных корректных алгоритмов (сообщения 25, 30, и 42 с учетом доработки для сочетаний) будет работать мгновенно.

← На главную