0Pricing
DSA Interview Prep · Урок

Максимальный подмассив и подмассив с максимальным произведением

Применяйте алгоритм Кадане к задаче maximum-sum-subarray и расширяйте его, отслеживая максимум и минимум для варианта с произведением

«Максимальный подмассив и подмассив с максимальным произведением» — бесплатный урок DSA Interview Prep на CoddyKit. Это урок 2 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения DSA Interview Prep, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс DSA Interview Prep содержит 4 уроков всего.

Задача о подмассиве с максимальной суммой

В задаче о подмассиве с максимальной суммой требуется найти непрерывный подмассив в одномерном массиве чисел, сумма элементов которого максимальна. Например, в массиве [-2, 1, -3, 4, -1, 2, 1, -5, 4] подмассив [4, -1, 2, 1] даёт максимальную сумму, равную 6. При переборе всех подмассивов методом полного перебора выполняется O(n²) операций, а алгоритм Кадане решает эту задачу за O(n).

nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
# Brute force: O(n^2)
max_sum = float('-inf')
for i in range(len(nums)):
    curr = 0
    for j in range(i, len(nums)):
        curr += nums[j]
        max_sum = max(max_sum, curr)
print(max_sum)  # 6

Интуиция алгоритма Кадане

Алгоритм Кадане выполняет один проход по массиву, поддерживая текущую сумму в current_sum. На каждом элементе нужно решить: лучше ли продолжить существующий подмассив или начать новый с этого элемента? Если current_sum становится отрицательной, она только ухудшит любой будущий подмассив, поэтому следует начать заново. Рекуррентное соотношение имеет вид current_sum = max(num, current_sum + num).

def max_subarray(nums):
    max_sum = current_sum = nums[0]
    for num in nums[1:]:
        # Extend or start fresh?
        current_sum = max(num, current_sum + num)
        max_sum = max(max_sum, current_sum)
    return max_sum

nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
print(max_subarray(nums))  # 6

Трассировка алгоритма Кадане

Проследим работу алгоритма Кадане на массиве [-2, 1, -3, 4, -1, 2, 1, -5, 4]: начальные значения curr=-2, max=-2. При 1: curr=max(1,-2+1)=1, max=1. При -3: curr=max(-3,1-3)=-2, max=1. При 4: curr=max(4,-2+4)=4, max=4. При -1: curr=3, max=4. При 2: curr=5, max=5. При 1: curr=6, max=6. При -5: curr=1. При 4: curr=5, max=6. Алгоритм правильно определяет, что оптимальным является подмассив, заканчивающийся на индексе 6.

def max_subarray_trace(nums):
    curr = max_sum = nums[0]
    for i, num in enumerate(nums[1:], 1):
        new_curr = max(num, curr + num)
        max_sum = max(max_sum, new_curr)
        print(f'i={i}, num={num}, curr: {curr}->{new_curr}, max={max_sum}')
        curr = new_curr
    return max_sum

max_subarray_trace([-2, 1, -3, 4, -1, 2, 1, -5, 4])

Возврат самого подмассива

Если на собеседовании Вас попросят вернуть сам подмассив, а не только его сумму, необходимо отслеживать начальный и конечный индексы. При начале нового подмассива (если num > current_sum + num) обновляйте temp_start. При обновлении max_sum сохраните temp_start в start, а текущий индекс — в end. Это добавляет O(1) накладных расходов к тому же алгоритму со сложностью O(n).

def max_subarray_indices(nums):
    max_sum = curr = nums[0]
    start = end = temp_start = 0
    for i in range(1, len(nums)):
        if nums[i] > curr + nums[i]:
            curr = nums[i]
            temp_start = i
        else:
            curr += nums[i]
        if curr > max_sum:
            max_sum = curr
            start, end = temp_start, i
    return max_sum, nums[start:end+1]

print(max_subarray_indices([-2, 1, -3, 4, -1, 2, 1, -5, 4]))
# (6, [4, -1, 2, 1])

Задача о подмассиве с максимальным произведением

Задача о подмассиве с максимальным произведением сложнее варианта с суммой из-за отрицательных чисел. Два отрицательных числа при умножении дают положительный результат, поэтому очень маленькое отрицательное произведение после умножения на другое отрицательное число может стать максимальным. Для [2, 3, -2, 4] ответ равен 6 ([2, 3]). Для [-2, 0, -1] ответ равен 0. На каждом шаге необходимо отслеживать как максимальное, так и минимальное произведение.

nums = [2, 3, -2, 4]
# [2,3,-2,4]: products [2, 6, -12, -48]
# subarrays: [2]=2, [2,3]=6, [3]=3, etc.
# max is 6 from subarray [2,3]

nums2 = [-2, 3, -4]
# [-2]*3*[-4] = 24
# negative*negative=positive!
print('Expected:', 24)

Отслеживание максимального и минимального произведений

Главная идея такова: в каждой позиции текущее максимальное произведение является одним из значений num, max_so_far * num или min_so_far * num (последний вариант полезен, когда отрицательное число превращает минимум в максимум). Аналогично вычисляется минимум. Обновляйте одновременно cur_max и cur_min, используя предыдущие значения, чтобы на этом же шаге не использовать уже обновлённые значения.

def max_product(nums):
    max_prod = min_prod = result = nums[0]
    for num in nums[1:]:
        # All three candidates for new max
        candidates = (num, max_prod * num, min_prod * num)
        max_prod, min_prod = max(candidates), min(candidates)
        result = max(result, max_prod)
    return result

print(max_product([2, 3, -2, 4]))    # 6
print(max_product([-2, 3, -4]))      # 24
print(max_product([-2, 0, -1]))      # 0
print(max_product([-2]))             # -2

Почему важен min_prod

Рассмотрим [-3, -10, 5]. После обработки -3: максимум=-3, минимум=-3. После -10: кандидаты — (-10, 30, 30) → максимум=30, минимум=-10. После 5: кандидаты — (5, 150, -50) → максимум=150. Без отслеживания min_prod Вы пропустили бы изменение знака, которое происходит, когда очень большое по модулю отрицательное минимальное произведение умножается на другое отрицательное число. Всегда вычисляйте max и min на основе одних и тех же предыдущих значений, чтобы избежать ошибки чтения устаревшего значения.

def max_product_traced(nums):
    max_p = min_p = result = nums[0]
    for num in nums[1:]:
        prev_max, prev_min = max_p, min_p
        max_p = max(num, prev_max * num, prev_min * num)
        min_p = min(num, prev_max * num, prev_min * num)
        result = max(result, max_p)
        print(f'num={num}: max_p={max_p}, min_p={min_p}')
    return result

max_product_traced([-3, -10, 5])
# max_p after -10: 30 (flip!)
# max_p after 5: 150

Нули обнуляют произведение

Ноль в массиве сбрасывает оба текущих произведения в ноль, фактически разделяя массив на независимые подмассивы. При num = 0 и max_prod * 0 = 0, и min_prod * 0 = 0, поэтому все три кандидата становятся равны 0, а предыдущий максимальный результат сохраняется. Специальный код не требуется — общая формула естественным образом обрабатывает нули.

def max_product(nums):
    max_p = min_p = result = nums[0]
    for num in nums[1:]:
        cands = (num, max_p * num, min_p * num)
        max_p, min_p = max(cands), min(cands)
        result = max(result, max_p)
    return result

# Zero splits array into independent subarrays
print(max_product([3, -1, 4, 0, 2, 5, -1]))   # 10 (2*5)
print(max_product([0, 2]))                       # 2
print(max_product([-1, 0, -2]))                  # 0

Альтернатива: проход по произведению слева направо и справа налево

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

def max_product_sweep(nums):
    result = max(nums)
    left = right = 1
    n = len(nums)
    for i in range(n):
        left *= nums[i]
        right *= nums[n - 1 - i]
        result = max(result, left, right)
        if left == 0: left = 1
        if right == 0: right = 1
    return result

print(max_product_sweep([2, 3, -2, 4]))   # 6
print(max_product_sweep([-2, 3, -4]))     # 24
print(max_product_sweep([-2, 0, -1]))     # 0

Алгоритм Кадане и произведение: ключевые различия

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

# Max Sum Subarray: O(n) time, O(1) space
def max_sum(nums):
    curr = result = nums[0]
    for n in nums[1:]:
        curr = max(n, curr + n)  # restart or extend
        result = max(result, curr)
    return result

# Max Product Subarray: O(n) time, O(1) space
def max_prod(nums):
    lo = hi = result = nums[0]
    for n in nums[1:]:
        lo, hi = min(n, lo*n, hi*n), max(n, lo*n, hi*n)
        result = max(result, hi)
    return result

print(max_sum([-2, 1, -3, 4, -1, 2, 1]))   # 6
print(max_prod([-2, 3, -4]))               # 24

Сложность и советы для собеседования

И алгоритм Кадане (максимальная сумма), и отслеживание минимума и максимума (максимальное произведение) работают за O(n) времени и используют O(1) памяти. Советы для собеседования: (1) Для максимальной суммы упомяните альтернативный подход «разделяй и властвуй» со сложностью O(n log n), чтобы показать широту знаний. (2) Для максимального произведения подчеркните, что min_prod и max_prod нужно одновременно обновлять на основе предыдущих значений, чтобы не использовать устаревшие данные. (3) Всегда уточняйте: может ли массив быть пустым? Должен ли подмассив быть непустым? (Да, по принятому соглашению он должен быть непустым.)

# Both run O(n) time, O(1) space
# Kadane handles: all negative (returns least negative)
# Product handles: zeros (resets naturally), negatives (tracks both extremes)

nums_all_neg = [-5, -2, -8]
print('Max sum (all neg):', max(max(nums_all_neg[0:1]),
      max(x for x in nums_all_neg)))  # -2
# Correct: return the maximum element when all are negative

Быстрая проверка

Проверьте, насколько Вы понимаете концепции курса «Структуры данных и алгоритмы — подготовка к собеседованию по программированию», рассмотренные в этом уроке.

Итоги урока

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

Часто задаваемые вопросы

Урок «Максимальный подмассив и подмассив с максимальным произведением» бесплатный?

Да — полный текст урока «Максимальный подмассив и подмассив с максимальным произведением» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс DSA Interview Prep, подпишись на CoddyKit PRO. Курс DSA Interview Prep содержит 4 уроков всего.

Чему я научусь в уроке «Максимальный подмассив и подмассив с максимальным произведением»?

Применяйте алгоритм Кадане к задаче maximum-sum-subarray и расширяйте его, отслеживая максимум и минимум для варианта с произведением Ты практикуешь DSA Interview Prep с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать DSA Interview Prep?

Предыдущий опыт не требуется. DSA Interview Prep на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 2 из 4.

Сколько времени занимает урок «Максимальный подмассив и подмассив с максимальным произведением»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке DSA Interview Prep?

Да. Каждый урок DSA Interview Prep включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Грабитель домов: рекуррентное решение «взять или пропустить»
  2. Максимальный подмассив и подмассив с максимальным произведением
  3. Разбиение слов и сегментация строки
  4. Декодирование способов и подсчёт путей
← Назад к DSA Interview Prep