Share This
Связаться со мной
Крути в низ
Categories
//Сумма трех, четырех и так далее чисел — на Python

Сумма трех, четырех и так далее чисел — на Python

26.03.2021Category : Python

Давайте рассмотрим довольно классическую задачку на программирование под названием «Сумма трех чисел» (и производную от нее — «Сумму четырех чисел»). Решать будем брутфорс-методом, а затем усовершенствуем решение при помощи рекурсии так, чтобы оно подходило для любой задачи «Сумма K чисел». Вы увидите, что хотя брутфорс-решение не очень хорошо масштабируется, оно все равно не бесполезно.

summa treh chetyreh i tak dalee chisel na python bd1ff1a - Сумма трех, четырех и так далее чисел - на Python

Давайте начнем с простого варианта задачи — суммы трех чисел. Вот условие:

# Дан массив nums, содержащий n целых чисел,  # и дано целевое число - target. # Напишите функцию, которая будет определять,  # есть ли в nums три элемента a, b и c,  # сумма которых равна целевому числу # (т. е. a + b + c = target).  # Пример:  # Input: # nums = [-1,0,1,2,-1,-4] # target = 0  # Output: [[-1,-1,2],[-1,0,1]]  # Обратите внимание, что в выводе программы # не должно быть повторяющихся троек.

Нам дается список чисел, в котором нужно найти три числа, в сумме дающих нуль (или любое другое указанное число). Как бы вы это решили?

1. Подход

Брутфорс-решение будет построено на итерациях. Представьте, что мы берем каждое число и сопоставляем его со всеми остальными числами, а затем сопоставляем каждую пару со всем другими числами. На что это похоже? Правильно, на вложенные циклы for. Вероятно, где-то здесь можно применить рекурсию. Если видите способ — поделитесь в комментариях!

Но есть одна заковыка: в выводе не должно быть дубликатов троек. Как нам это учесть? Вероятно, вы подумали об использовании множеств, и — да, именно их мы и используем. Мы превратим каждую найденную тройку в строку и поместим во множество, таким образом гарантируя отсутствие повторов.

2. Подготовка

Наш метод sum_of_three будет принимать два аргумента: список nums и число-сумму — target.

def sum_of_three(nums, target):

Далее нам нужно учесть крайний случай. Что, если в nums будет меньше 3 чисел? Тогда мы точно не сможем найти тройку чисел, дающих в сумме target. В этом случае мы вернем пустой список.

def sum_of_three(nums, target):     if len(nums) < 3:         return []

Теперь нам нужно инициализировать пару переменных, которые мы будем использовать. Во-первых, список output, в котором будут содержаться списки с найденными тройками чисел. Также мы создадим один из этих списков троек и назовем его current. Как только мы заполним current подходящей тройкой чисел, мы сможем добавить его в output.

output = [] current = []

И, как мы и говорили, нам нужно создать множество. Кроме того нам нужно будет сортировать списки чисел, чтобы не включить в множество один набор дважды. А такое может быть, если числа будут идти в разном порядке. Например, если у нас есть [1, -2, 1] и [1, 1, 2], нам нужно добавить во множество только какой-то один из этих списков.

checked = set() nums.sort()

3. Итерации

Итак, мы готовы к созданию циклов for. Всего нам нужно будет сделать три таких цикла. При этом самый важный момент — продумывание верхних и нижних границ для индексов.

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

for i in range(len(nums) - 2):

В качестве напоминания: функция range() принимает два аргумента. Синтаксис:

range(start, stop, step)

Первые два аргумента задают диапазон, причем первый (start) включается в него, а второй (end) — не включается. Третий аргумент (step) опционален и задает шаг. Если он не указан, то по умолчанию шаг равен 1.

Но первый аргумент тоже опционален. Если его не указывать, по умолчанию стартовая позиция — 0.

Итак, что мы будем делать в нашем первом цикле for? План следующий:

  1. Добавить первое число в current.
  2. Вычесть это число из суммы.
  3. Сверить с другими числами.
  4. «Откатить» шаги 1 и 2, вернувшись в исходное положение, чтобы перейти к следующему числу.

Давайте реализуем это в коде. Первый шаг простой, мы лишь добавляем число под индексом i в current при помощи метода append.

current.append(nums[i])

Следующий шаг тоже довольно элементарный. Причем мы можем просто уменьшать target на текущее число (т. е. target -= nums[i]), но для лучшей читаемости давайте создадим новую переменную — working_target.

working_target = target - nums[i]

Третий шаг уже сложнее. Под «Сверить с другими числами» следует понимать вложенные циклы for, которыми мы займемся далее. Пока мы это просто закомментируем.

Четвертый шаг тоже простой. Мы удаляем последний элемент из current при помощи current.pop() и возвращаем target в исходный вид. Впрочем, здесь нам не придется ничего делать с target, потому что мы создали отдельную переменную. Наш цикл будет выглядеть так:

for i in range(len(nums) - 2):     current.append(nums[i])     working_target = target - nums[i]     # сделать вложенные циклы for     current.pop()

Отлично, теперь переходим ко второму, вложенному циклу.

Каков будет наш диапазон индексов? Мы начинаем с индекса, идущего следом за i, и доходим до предпоследней позиции в nums.

for j in range(i + 1, len(nums) - 1):

И повторяем шаги 1-4 из первого цикла. То есть мы добавляем число под индексом j в current, уменьшаем working_target на это число, оставляем место для третьего цикла, а затем откатываем назад шаги 1 и 2.

for j in range(i _ 1, len(nums) - 1):     working_target -= nums[j]     current.append(nums[j])     # сделать вложенный цикл for     current.pop()     working_target +=nums[j]

Наконец, давайте напишем самый внутренний цикл. С индексами уже должно быть понятно. Мы начинаем с j + 1 и идем до конца.

for k in range(j + 1, len(nums)):

Число под индексом k мы добавляем в current — тут никаких проблем нет. Но при вычитании k из working_target нам нужно проверить, равна ли разница нулю. Если все три числа в сумме дают target, то target — a — b — c = 0.

for k in range(j + 1, len(nums)):     current.append(nums[k])     if working_target - nums[k]  == 0:

Что у нас будет в блоке if? Пришла пора применить наше множество и проверить, нет ли в нем уже такой тройки чисел.

Мы не можем поместить списки во множество, так что нам нужно преобразовать нашу тройку чисел в строку. Следующий код создаст строку, в которой между числами будут проставлены знаки &. Например, [1, 1, -2] превратится в 1&1&-2. Для разделения чисел можно использовать любой символ, но он нужен обязательно — чтобы не спутать 1, 11 с 11, 1.

# добавление тройки чисел во множество для исключения дубликатов code = str(current[0]) for n in range(1, len(current)):     code += "&" + str(current[n])

Теперь мы можем проверить, есть ли code во множестве. Если его там нет, мы можем добавить его во множество и добавить current в наш итоговый output. Не забудьте, что добавить нужно копию current, а не просто ссылку. В противном случае тройка может измениться после добавления в output!

if code not in checked:     output.append(current.copy())     checked.add(code)

Наконец, мы удаляем третье число из current. В итоге третий цикл for выглядит следующим образом:

for k in range(j + 1, len(nums)):     current.append(nums[k])     if working_target - nums[k] == 0:         # добавление тройки чисел во множество для исключения дубликатов         code = str(current[0])         for n in range(1, len(current)):             code += "&" + str(current[n])         if code not in checked:             output.append(current.copy())             checked.add(code)     current.pop()

4. Заканчиваем работать над задачей «Сумма трех чисел»

Все, что нам остается, это вернуть список output. Наш метод в итоге выглядит так (следите внимательно за отступами во всех этих циклах for!):

def sum_of_three(nums, target):     if len(nums) < 3:         return []     output = []     current = []     checked = set()     nums.sort()      for i in range(len(nums) - 2):         current.append(nums[i])         working_target = target - nums[i]         for j in range(i + 1, len(nums) - 1):             working_target -= nums[j]             current.append(nums[j])             for k in range(j + 1, len(nums)):                 current.append(nums[k])                 if working_target - nums[k] == 0:                 # добавление тройки чисел во множество для исключения дубликатов                     code = str(current[0])                     for n in range(1, len(current)):                         code += "&" + str(current[n])                     if code not in checked:                         output.append(current.copy())                         checked.add(code)                 current.pop()             current.pop()             working_target += nums[j]         current.pop()         working_target += nums[i]     return output

Маленький пример для тестирования метода:

nums = [-1,0,1,2,-1,-4] target = 0 print(sum_of_three(nums, target))

Мы получим в итоговом output [[-1,-1,2],[-1,0,1]]. Обратите внимание, что у нас нет дубликатов троек, хотя во втором списке тоже есть две -1.

5. Наращиваем сложность: сумма четырех чисел

summa treh chetyreh i tak dalee chisel na python 569b328 - Сумма трех, четырех и так далее чисел - на Python

Теперь, когда вы счастливо перевели дух, покончив с суммой трех чисел, как насчет суммы четырех?

Хотя кажется, что эта задача сложнее, по сути, мы можем использовать все тот же брутфорс-метод. Я знаю, что временная сложность и так была ужасной — O(N3), а теперь станет еще хуже — O(N4). Но, как говорится, «зато работает».

Как мы подойдем к решению? Мы можем просто немного изменить код нашего метода sum_of_three, добавив еще один цикл. Каждый цикл начинается с предыдущего индекса, увеличенного на единицу, и заканчивается на единицу ближе к концу списка. Вот как это выглядит:

for i in range(len(nums) - 3):     for j in range(i + 1, len(nums) - 2):         for k in range(j + 1, len(nums) - 1):             for m in range(k + 1, len(nums)):

В каждом цикле мы проходим одинаковые шаги: добавляем число в current, вычитаем его из target, что-то проверяем, а затем откатываем назад предыдущие действия. Чтобы вам не пришлось лишний раз скроллить, вот вам наш готовый метод:

def sum_of_four(nums, target):     if len(nums) < 4:         return []     output = []     current = []     nums.sort()     checked = set()     for i in range(len(nums) - 3):         working_target = target - nums[i]         current.append(nums[i])         for j in range(i + 1, len(nums) - 2):             working_target -= nums[j]             current.append(nums[j])             for k in range(j + 1, len(nums) - 1):                 working_target -= nums[k]                 current.append(nums[k])                 for m in range(k + 1, len(nums)):                     current.append(nums[m])                     if working_target - nums[m] == 0:                         # добавление чисел во множество для исключения дубликатов                         code = str(current[0])                         for n in range(1, len(current)):                             code += "&" + str(current[n])                         if code not in checked:                             output.append(current.copy())                             checked.add(code)                     current.pop()                 current.pop()                 working_target += nums[k]             current.pop()             working_target += nums[j]         current.pop()     return output

Он точно такой же, как для «Суммы трех чисел», только с дополнительным циклом. Мы можем проверить его работу:

nums = [1,0,-1,0,-2,2, 0] target = 0 print(sum_of_four(nums, target))

В результате получим [[-2, -1, 1, 2], [-2, 0, 0, 2], [-1, 0, 0, 1]]. Работает!

6. Сумма пяти чисел и так далее

summa treh chetyreh i tak dalee chisel na python de784e9 - Сумма трех, четырех и так далее чисел - на Python

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

Да никаких проблем! Просто добавим еще один цикл for. Но сейчас вы можете заметить, что вырисовывается паттерн:

for j in range(i + 1, len(nums) - 2):     working_target -= nums[j]     current.append(nums[j])     for k in range(j + 1, len(nums) - 1):         working_target -= nums[k]         current.append(nums[k])         for m in range(k + 1, len(nums)):             current.append(nums[m])

А когда у нас есть повторяющийся код, хороший тон — написать для него отдельный метод. Почему бы нам не написать рекурсивный метод для запуска каждого цикла for? Тогда мы сможем просто указывать ему, сколько чисел мы хотим суммировать, а он сам определит, сколько вложенных циклов сделать.

Итак, давайте сделаем такой же метод, как раньше, только теперь он будет принимать еще и аргумент k — количество чисел, сумма которых должна быть равна target (три, четыре, пять и т. д. чисел).

def sum_of_k(nums, target, k):     if len(nums) < k:         return []     output = []     current = []     nums.sort()     checked = set()

А теперь мы создадим рекурсивный метод sum_helper(), который будет принимать… да практически все переменные, созданные нам ранее. Кроме того, нам нужно знать предыдущий индекс, стартующий с 0 (чтобы начинать цикл с позиции i + 1). Этот метод будет изменять массивы, поэтому нам нужно вызвать его, а затем вернуть output.

sum_helper(nums, target, k, output, checked, current, 0) return output

7. Итерация с рекурсивным методом

Давайте подумаем, как нам создать наш рекурсивный вспомогательный метод. Мы каждый раз будем вычитать из k. В конечном итоге, «сумма четырех» — это просто «сумма трех» с дополнительным числом.

Нашему рекурсивному методы нужен базовый случай. Если k равна 0, нам не нужно ничего делать. В этом случае мы просто выходим из функции (return).

if k == 0:     return

Если k > 0, нам нужно перебрать в цикле nums. Мы начинаем с индекса prev и идем до «конец списка — k + 1» (к этому мы пришли, рассматривая паттерн в предыдущем решении).

for i in range(prev, len(nums) - k + 1):

Что дальше? Смотрим на наш список: мы добавляем текущее число в current.

current.append(nums[i])

Возможно, вы думаете, что следующий шаг — вычесть это число из target, но сначала нам нужно кое-что проверить. Этот цикл for должен быть универсальным для каждой итерации k, так что мы должны учесть и случай, если данный цикл — самый вложенный.

То есть нам нужно проверить, дает ли нуль вычитание числа из target. Затем мы делаем то же, что и раньше (можете скопировать с минимальными правками) — добавляем code в виде строки во множество и, если значение уникально, добавляем копию current в output.

if k == 1 and target - nums[i] == 0:     # добавление чисел во множество для исключения дубликатов     code = str(current[0])     for n in range(1, len(current)):         code += "&" + str(current[n])     if code not in checked:         output.append(current.copy())         checked.add(code)

Теперь нам нужно вычесть число из рабочей target и написать наш рекурсивный случай. Но погодите, мы же можем сделать это за один шаг! Вот рекурсивный вызов:

sum_helper(nums, target - nums[i], k - 1, output, checked, current, i + 1)

Здесь у нас меняется target, k уменьшается на 1, а текущий индекс + 1 становится предыдущим индексом prev.

Наконец, нам нужно откатить назад добавление числа в current:

current.pop()

Вот и все! Вместе наши методы выглядят следующим образом:

def sum_of_k(nums, target, k):     if len(nums) < k:         return []     output = []     current = []     nums.sort()     checked = set()     sum_helper(nums, target, k, output, checked, current, 0)     return output  def sum_helper(nums, target, k, output, checked, current, prev):     if k == 0:         return     for i in range(prev, len(nums) - k + 1):         current.append(nums[i])         if k == 1 and target - nums[i] == 0:             # добавление чисел во множество для исключения дубликатов             code = str(current[0])             for n in range(1, len(current)):                 code += "&" + str(current[n])             if code not in checked:                 output.append(current.copy())                 checked.add(code)         sum_helper(nums, target - nums[i], k - 1, output, checked, current, i + 1)         current.pop()

Наш метод по-прежнему очень громоздкий, но нам хотя бы не приходится изменять его вручную при каждом изменении k. Следующий код будет работать независимо от того, равна k 3, 4 или другим числам!

nums = [-1,0,1,2,-1,-4, -2] target = 0 k = 4 print(sum_of_k(nums, target, k)) # -> [[-2, -1, 1, 2], [-1, -1, 0, 2]]

Не стоит забывать, что это — брутфорс-решение. Его временная сложность — O(Nk), а это очень плохо, если k оказывается большим числом. Вероятно, эту задачу можно решить более элегантно. Если вы знаете, как, — добро пожаловать в комментарии!

  • 11 views
  • 0 Comment

Leave a Reply

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.

Связаться со мной
Close