Как в 1С найти набор чисел, которые в сумме дают заданное значение (Задача о сумме подмножеств)?

Программист 1С v7.7 Управленческий учет
← На главную

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

Эта задача известна в информатике как «Задача о сумме подмножеств» (Subset Sum Problem). Она является частным случаем более общей «Задачи о рюкзаке». Важно сразу понимать, что эта задача относится к классу NP-полных. Говоря простым языком, это означает, что не существует известного алгоритма, который бы гарантированно находил точное решение за короткое время для больших объемов данных. Полный перебор всех возможных комбинаций (а их 2N, где N — количество слагаемых) становится невозможным уже при нескольких десятках элементов. Для 5000 слагаемых, как в исходной проблеме, это число просто астрономическое.

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

Подход 1: Простой и быстрый, но неточный — "Жадный" алгоритм

Это самый очевидный и легко реализуемый метод, который часто приходит в голову первым. Он не гарантирует нахождения точного решения, но может дать приемлемый результат, особенно если допустима некоторая погрешность.

Разберем по шагам, как его реализовать:

  1. Сортировка. Берем наш исходный список слагаемых и сортируем его по убыванию. В 1С 7.7 для этого удобно использовать ТаблицуЗначений или СписокЗначений и его метод Сортировать().
  2. Последовательный набор. Создаем переменную для хранения текущей набранной суммы (изначально 0) и пустой список для хранения выбранных слагаемых.
  3. Итерация. Начинаем перебирать отсортированный список с самого большого элемента.
  4. Проверка условия. Для каждого очередного слагаемого проверяем: если мы добавим его к уже набранной сумме, не превысит ли результат искомую сумму?
  5. Добавление в результат. Если итоговая сумма не превышается, то добавляем это слагаемое в наш результирующий список, а его значение — к набранной сумме. Если превышается — просто пропускаем это слагаемое и переходим к следующему.
  6. Окончание. Продолжаем процесс, пока не пройдем по всему списку.

Преимущества:

Недостатки:

Пример. Допустим, нам нужно набрать сумму 20 из набора чисел: [12, 11, 9, 8, 5, 2, 1].

  1. Берем 12. Набранная сумма = 12. Осталось набрать 8.
  2. 11 не подходит (12 + 11 > 20).
  3. 9 не подходит (12 + 9 > 20).
  4. Берем 8. Набранная сумма = 12 + 8 = 20. Решение найдено!

А теперь другой пример. Сумма та же 20, набор: [12, 11, 9, 8, 5, 2, 1]. Нужно набрать 19.

  1. Берем 12. Набрано 12. Осталось набрать 7.
  2. 11, 9, 8 — не подходят.
  3. Берем 5. Набрано 12 + 5 = 17. Осталось 2.
  4. Берем 2. Набрано 17 + 2 = 19. Решение найдено!

Кажется, что все хорошо. Но посмотрим на такой случай. Сумма 16, набор: [10, 8, 8, 5, 3].

  1. Берем 10. Набрано 10. Осталось 6.
  2. 8, 8 — не подходят.
  3. Берем 5. Набрано 10 + 5 = 15. Осталось 1.
  4. 3 — не подходит.

Результат "жадного" алгоритма — 15, погрешность 1. А ведь точное решение существует: 8 + 8 = 16. "Жадный" алгоритм его не нашел.

Подход 2: Точное решение через рекурсивный перебор с отсечениями

Этот метод представляет собой "умный" перебор. Вместо того чтобы проверять все 2N комбинаций, мы будем строить дерево решений и отсекать заведомо проигрышные ветви. Этот подход гораздо медленнее "жадного", но способен найти точное решение.

Основная идея:

Мы пишем рекурсивную процедуру, которая для каждого слагаемого из списка решает, брать его в итоговую сумму или не брать.

  1. Оптимизация: Как и в "жадном" алгоритме, первым делом стоит отсортировать список слагаемых по убыванию. Это позволит быстрее находить решения и эффективнее отсекать ветви перебора.
  2. Рекурсивная функция. Создаем функцию, которая принимает на вход:
    • индекс текущего рассматриваемого элемента в списке;
    • сумму, которую еще осталось набрать.
  3. Базовые случаи (выход из рекурсии):
    • Если оставшаяся сумма равна нулю — Ура! Решение найдено. Мы можем завершить работу и вернуть найденный набор.
    • Если мы дошли до конца списка, а сумма не равна нулю, или если оставшаяся сумма стала отрицательной — эта ветвь поиска неудачная, нужно вернуться на шаг назад.
  4. Рекурсивный шаг:
    • Вариант 1 (Берем элемент): Пробуем включить текущий элемент в решение. Вызываем эту же функцию для следующего элемента (индекс + 1), а в качестве оставшейся суммы передаем текущая_оставшаяся_сумма - значение_текущего_элемента.
    • Вариант 2 (Не берем элемент): Пробуем не включать текущий элемент. Вызываем функцию для следующего элемента (индекс + 1) с той же самой оставшейся суммой.

Производительность:

В худшем случае этот алгоритм все равно переберет все варианты. Но на практике, благодаря отсечениям (например, если оставшаяся сумма меньше нуля), он работает значительно быстрее. Для набора из 20-30 элементов он найдет решение за секунды. Для 200-300 элементов (как упоминалось в обсуждении на форуме) он также может отработать за приемлемое время. Однако для 5000 элементов этот метод неприменим.

Подход 3: Классическое решение — Динамическое программирование

Это классический академический метод решения данной задачи. Он гарантирует нахождение точного решения, но имеет серьезные ограничения по памяти и времени, связанные с величиной искомой суммы.

Идея:

Строится таблица (двумерный массив), где по одной оси отложены слагаемые (от 1 до N), а по другой — все возможные суммы (от 1 до S, где S — искомая сумма). Каждая ячейка [i][j] в этой таблице будет хранить ответ на вопрос: "Можно ли набрать сумму j, используя только первые i слагаемых?"

Ограничения:

Сложность этого алгоритма O(N * S). Это означает, что время его работы и требуемая память прямо пропорциональны произведению количества слагаемых на величину искомой суммы. Если в нашей задаче N = 5000, а S = 10 000 000, то нам понадобится таблица размером 5000 * 10 000 000, что невозможно реализовать ни на одной современной системе, тем более в 1С 7.7.

Вывод: данный метод подходит только для задач с небольшим количеством элементов и небольшой искомой суммой.

Практические выводы и рекомендации

Проанализировав ситуацию, можно дать следующие рекомендации для решения задачи в 1С:

  1. Определите требования к точности. Самый главный вопрос: вам нужно абсолютно точное решение или подойдет максимально близкое? Если бизнес-процесс допускает погрешность, то задача сильно упрощается.
  2. Для N ~ 5000 и больших сумм.
    • Если нужно быстрое и приблизительное решение, используйте "жадный" алгоритм. Это ваш лучший выбор. Реализуйте его и оцените, насколько велика получающаяся погрешность. Возможно, она окажется приемлемой для ваших целей.
    • Если нужно точное решение, то стандартными алгоритмическими методами в рамках 1С для такого объема данных его не найти. Здесь нужно пересматривать постановку задачи:
      • Может, в данных есть какая-то специфика? Например, все числа кратны 1000?
      • Может, можно разбить большой набор на несколько маленьких и решать задачу для них?
      • Может, можно привлечь внешние библиотеки или специализированные решатели (solvers) для целочисленного программирования, вызывая их из 1С?
  3. Для небольшого количества элементов (N < 30-40).
    • Смело используйте рекурсивный перебор с отсечениями. Он найдет точное решение за вменяемое время.

В конечном итоге, для задачи с 5000 слагаемых и необходимостью набрать сумму в несколько миллионов, наиболее реалистичным путем является использование простого и быстрого "жадного" алгоритма, с пониманием его ограничений и возможной неточности.

← На главную