На этом шаге мы рассмотрим реализацию этой задачи.
Цель этой задачи - в том, чтобы для заданного множества S из n положительных целых чисел и заданного целого числа х найти в S такое подмножество, сумма элементов которого равна х. Формально, если si - i-е целое число множества S, то задача состоит в том, чтобы найти такое подмножество T ⊆ S, что:
∑ si = x (12.1) si∈T
В данном случае мы создадим алгоритм перебора с возвратами, печатающий все такие подмножества. Например, если S = {1, 2, 3, 5, 6, 7, 9} и х = 13, то метод должен вывести на экран следующие пять подмножеств: {6, 7}, {2, 5, 6}, {1, 5, 7}, {1, 3, 9} и {1, 2, 3, 7}.
Простейшее решение задачи "в лоб" - это генерация всех возможных подмножеств множества S с проверкой истинности требования (12.1). Такой полный перебор можно осуществить процедурой, подобной generate_subsets() из примера 12.1. Множество S было бы входным списком элементов, а метод имел бы дополнительный параметр, в котором хранится значение х. Кроме этого, процедура в начальном условии перед выводом подмножества на экран должна будет проверить ограничение (12.1). Иными словами, она должна проверять ограничение (12.1) в каждом листе дерева рекурсии.
Однако даже при использовании перебора с возвратами существует возможность ускорить поиск. В примере 12.7 приводится одна из реализаций, где частичное решение (sol) представляет собой подмножество T.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
def print_subset_sum(i, sol, psum, elements, x): # Базовый вариант if psum == x: print_subset_binary(sol, elements) elif i < len(elements): # Генерация элементов-кандидатов for k in range(0, 2): # Проверить, можно ли обрезать дерево рекурсии if psum + k * elements[i] <= x: # Расширить частичное решение sol[i] = k # Обновить сумму, связанную с частичным решением psum = psum + k * elements[i] # Попробовать расширить частичное решение print_subset_sum(i + 1, sol, psum, elements, x) # в этом нет необходимости: # psum = psum + k * elements[i] # Убедиться, что 0 указывает на отсутствие элемента sol[i] = 0 def print_subset_sum_wrapper(elements, x): sol = [0] * (len(elements)) print_subset_sum(0, sol, 0, elements, x) def print_subset_binary(sol, elements): no_elements = True print('{', end='') for i in range(0, len(sol)): if sol[i] == 1: if no_elements: print(elements[i], sep='', end='') no_elements = False else: print(', ', elements[i], sep='', end='') print('}') |
Чтобы избежать вычисления суммы элементов частичного решения в каждом рекурсивном вызове, используется дополнительный параметр - переменная-накопитель суммы psum, которой, очевидно, в методе-оболочке нужно присвоить начальное значение 0. Ключевая идея алгоритма состоит в том, что дерево рекурсии можно сократить, если сумма элементов частичного решения равна х, когда выполнено требование (12.1), или больше х, когда добавление нового положительного целого числа из множества S к частичному решению только увеличивает его сумму (12.1).
Прежде всего метод проверяет начальное условие (строки 3 и 4), когда на экран выводится подмножество (частичное решение), удовлетворяющее ограничению (12.1). Это позволяет сокращать дерево рекурсии в его внутренних узлах глубины i. В начальном условии в T могут входить только первые i элементов S. Другими словами, значимыми являются лишь первые i элементов частичного решения. Если n - i последних элементов могут принимать произвольные значения, то методу печати следовало бы передать значение i и указать "истинный" размер частичного решения. Но вместо этого алгоритм вызывает метод print_subset_binary() из примера 12.1, который требует, чтобы эти последние значения были всегда равны нулю, указывая на то, что они не относятся к частичному решению T. Это требование обеспечивается в методе-оболочке print_subset_sum_wrapper() инициализацией частичного решения n нулями, что равносильно пустому множеству, и обновлением частичного решения таким образом, чтобы последние его n - i элементов всегда оставались нулевыми при достижении начального условия (это реализуется присваиванием в строке 25).
Если частичное решение из n элементов не отвечает требованию (12.1), метод просто завершается, не выполняя никаких действий. В противном случае при выполнении условия i < n (строка 5) он продолжает пополнять частичное решение. В этом случае он использует цикл, где проверяет два варианта - не включать (k = 0) или включать (k = 1) элемент st в частичное решение. Так как k - число, его можно использовать для вычисления суммы psum + k * elements[i] частичного решения. Обратите внимание, что при k = 0 она не меняется, тогда как при k = 1 она увеличивается на si. Поэтому перед включением элемента в частичное решение и очередным рекурсивным вызовом в строке 10 проверяется, не превысила ли новая сумма значения х. Если это так, то в частичное решение включается новый кандидат (строка 13), psum обновляется (строка 16) и выполняется рекурсивный вызов (строка 19). После всех этих действий нет необходимости восстанавливать значение psum (в строке 22), так как в первой итерации цикла при k = 0 значение psum не меняется. В завершение алгоритм обнуляет значение позиции i частичного решения, чтобы правильно вывести на экран частичное решение в начальном условии (последние его n - i элементов всегда должны быть нулями).
На рисунке 1 приведено дерево рекурсии метода print_subset_sum() для множества S = {2, 6, 3, 5} и х = 8.
Рис.1. Дерево рекурсии процедуры, решающей задачу суммы подмножества для множества S = {2, 6, 3, 5} и x = 8
Частичные решения создаются на каждом уровне, как в методах, изложенных, начиная с 189 шага. Спускаясь по левой ветви дерева, алгоритм не включает элемент в T, но включает его, спускаясь по правой ветви. Здесь числа рядом с узлами обозначают сумму включенных в частичные решения элементов (psum) при вызове метода. Обратите внимание, что дерево сокращается, как только найдено решение, удовлетворяющее условию (12.1), или сумма элементов в T превысила х.
В итоге вызов print_subset_sum_wrapper([2,6,3,5],8) приводит к правильному результату:
{3, 5} {2, 6}
В заключение отметим, что без обнуления в строке 25 частичное решение {2, 6} попадёт в начальное условие с единицей в своей третьей позиции, и метод выведет на экран {2, 6, 3}.
На следующем шаге мы рассмотрим путь в лабиринте.