Жадные алгоритмы с доказательствами. Интервальные задачи, выбор оптимального.
Жадные алгоритмы делают локально оптимальный выбор на каждом шаге. Требуют доказательства корректности.
Greedy Algorithm (Жадный алгоритм) — это алгоритм, который на каждом шаге делает локально оптимальный выбор в надежде, что это приведёт к глобально оптимальному решению.
Ассоциативность — свойство операции, при котором результат не зависит от расстановки скобок.
Коммутативность — свойство операции, при котором результат не зависит от порядка операндов.
Монотонная функция — функция, которая только возрастает или только убывает.
Простое число — натуральное число больше 1, которое делится только на 1 и на само себя.
НОД (Наибольший общий делитель) — наибольшее число, на которое делятся оба данных числа.
НОК (Наименьшее общее кратное) — наименьшее число, которое делится на оба данных числа.
| Задача | Greedy | Dynamic Programming | Brute Force |
|---|---|---|---|
| Interval Scheduling | ✅ O(n log n) | ⚠️ O(n²) | ❌ O(2^n) |
| Coin Change (US coins) | ✅ O(n) | ⚠️ O(n × amount) | ❌ O(2^n) |
| Knapsack (fractional) | ✅ O(n log n) | ❌ Не применимо | ❌ O(2^n) |
| Knapsack (0/1) | ❌ Не работает | ✅ O(n × W) | ❌ O(2^n) |
Вывод: Greedy быстрее DP, но работает не для всех задач. Всегда проверяйте на контрпримерах!
# Interval Scheduling: сортировать по окончанию
intervals.sort(key=lambda x: x[1])
# Coin Change (жадный): брать наибольшие номиналы
coins.sort(reverse=True)
# Maximum Subarray: max(num, current + num)
current = max(num, current + num)# Общая структура
def greedy_solution(input_data):
sort_or_prepare(input_data)
result = 0
for item in input_data:
if can_take(item):
take(item)
result += contribution(item)
return resultУсловие:
Дан массив nums, где nums[i] — максимальная длина прыжка из позиции i. Определите, можно ли достичь последнего индекса, начиная с первого.
Примеры:
Ввод: nums = [2,3,1,1,4]
Вывод: true
Объяснение: Прыжок на 1 шаг от индекса 0 к 1, затем на 3 шага к последнему индексу.
Ввод: nums = [3,2,1,0,4]
Вывод: false
Объяснение: Всегда упрётесь в индекс 3, где длина прыжка = 0.
Ограничения:
💡 Подсказка: Отслеживайте максимальную достижимую позицию (max_reach). Если текущая позиция > max_reach — недостижимо.
Решение:
def canJump(nums):
max_reach = 0
for i, num in enumerate(nums):
if i > max_reach:
return False
max_reach = max(max_reach, i + num)
return TrueОбъяснение:
Сложность:
Условие:
Дан массив nums, где nums[i] — максимальная длина прыжка из позиции i. Верните минимальное количество прыжков для достижения последнего индекса.
Примеры:
Ввод: nums = [2,3,1,1,4]
Вывод: 2
Объяснение: 0→1 (1 прыжок), 1→4 (3 прыжка) = 2 прыжка
Ввод: nums = [2,3,0,1,4]
Вывод: 2
Ограничения:
💡 Подсказка: Отслеживайте current_end (конец текущего диапазона) и farthest (максимальная достижимая). Увеличивайте jumps при достижении current_end.
Решение:
def jump(nums):
jumps = 0
current_end = 0
farthest = 0
for i in range(len(nums) - 1):
farthest = max(farthest, i + nums[i])
if i == current_end:
jumps += 1
current_end = farthest
return jumpsОбъяснение:
Сложность:
Условие: Есть n газовых станций по кругу. gas[i] — количество газа на станции i, cost[i] — газ для поездки до следующей станции.
Верните индекс начальной станции, с которой можно объехать круг. Если невозможно, верните -1.
Примеры:
Ввод: gas = [1,2,3,4,5], cost = [3,4,5,1,2]
Вывод: 3
Объяснение: Начать с станции 3, набрать 1, доехать до 4,5,0,1,2 — полный круг.
Ввод: gas = [2,3,4], cost = [3,4,3]
Вывод: -1
Ограничения:
💡 Подсказка: Если sum(gas) < sum(cost) — невозможно. Иначе решение существует. Отслеживайте tank: если tank < 0, начинаем с следующей станции.
Решение:
def canCompleteCircuit(gas, cost):
if sum(gas) < sum(cost):
return -1
tank = 0
start = 0
for i in range(len(gas)):
tank += gas[i] - cost[i]
if tank < 0:
start = i + 1
tank = 0
return startОбъяснение:
Сложность:
Условие: n детей стоят в ряд. ratings[i] — рейтинг i-го ребёнка. Раздайте конфеты по правилам:
Верните минимальное количество конфет.
Примеры:
Ввод: ratings = [1,0,2]
Вывод: 5
Объяснение: [2,1,2] конфеты
Ввод: ratings = [1,2,2]
Вывод: 4
Объяснение: [1,2,1] конфеты
Ограничения:
💡 Подсказка: Два прохода: слева направо для возрастающих, справа налево для убывающих. candies[i] = max(left, right).
Решение:
def candy(ratings):
n = len(ratings)
candies = [1] * n
# Слева направо
for i in range(1, n):
if ratings[i] > ratings[i-1]:
candies[i] = candies[i-1] + 1
# Справа налево
for i in range(n-2, -1, -1):
if ratings[i] > ratings[i+1]:
candies[i] = max(candies[i], candies[i+1] + 1)
return sum(candies)Объяснение:
Сложность:
Условие:
Дан массив интервалов intervals. Верните минимальное количество интервалов, которые нужно удалить, чтобы оставшиеся не пересекались.
Примеры:
Ввод: intervals = [[1,2],[2,3],[3,4],[1,3]]
Вывод: 1
Объяснение: Удалить [1,3]
Ввод: intervals = [[1,2],[1,2],[1,2]]
Вывод: 2
Ввод: intervals = [[1,2],[2,3]]
Вывод: 0
Ограничения:
💡 Подсказка: Сортировка по концу интервала. Жадный выбор: всегда выбираем интервал с earliest finish time.
Решение:
def eraseOverlapIntervals(intervals):
intervals.sort(key=lambda x: x[1]) # Сортировка по концу
count = 0
end = float('-inf')
for start, finish in intervals:
if start >= end:
end = finish # Не пересекается
else:
count += 1 # Пересекается, удаляем
return countОбъяснение:
Сложность:
Условие:
Дан массив интервалов intervals. Объедините все пересекающиеся интервалы.
Примеры:
Ввод: intervals = [[1,3],[2,6],[8,10],[15,18]]
Вывод: [[1,6],[8,10],[15,18]]
Объяснение: [1,3] и [2,6] пересекаются → [1,6]
Ввод: intervals = [[1,4],[4,5]]
Вывод: [[1,5]]
Ограничения:
💡 Подсказка: Сортировка по началу. Если start <= result[-1][1] — объединяем: result[-1][1] = max(end, result[-1][1]).
Решение:
def merge(intervals):
intervals.sort() # Сортировка по началу
result = []
for start, end in intervals:
if not result or start > result[-1][1]:
result.append([start, end])
else:
result[-1][1] = max(result[-1][1], end)
return resultОбъяснение:
Сложность:
Условие:
Дан массив непересекающихся интервалов intervals (отсортирован по началу) и новый интервал newInterval. Вставьте newInterval и объедините пересекающиеся.
Примеры:
Ввод: intervals = [[1,3],[6,9]], newInterval = [2,5]
Вывод: [[1,5],[6,9]]
Ввод: intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]], newInterval = [4,8]
Вывод: [[1,2],[3,10],[12,16]]
Ограничения:
💡 Подсказка: Три этапа: 1) Интервалы до newInterval. 2) Объединение пересекающихся. 3) Интервалы после.
Решение:
def insert(intervals, newInterval):
result = []
i = 0
n = len(intervals)
# Интервалы до newInterval
while i < n and intervals[i][1] < newInterval[0]:
result.append(intervals[i])
i += 1
# Объединение пересекающихся
while i < n and intervals[i][0] <= newInterval[1]:
newInterval[0] = min(newInterval[0], intervals[i][0])
newInterval[1] = max(newInterval[1], intervals[i][1])
i += 1
result.append(newInterval)
# Интервалы после
while i < n:
result.append(intervals[i])
i += 1
return resultОбъяснение:
Сложность:
Условие:
Дан целочисленный массив nums. Найдите подмассив с максимальной суммой и верните эту сумму.
Примеры:
Ввод: nums = [-2,1,-3,4,-1,2,1,-5,4]
Вывод: 6
Объяснение: [4,-1,2,1] имеет сумму 6
Ввод: nums = [1]
Вывод: 1
Ввод: nums = [5,4,-1,7,8]
Вывод: 23
Ограничения:
💡 Подсказка: Алгоритм Кадане: current = max(num, current + num). Либо начинаем новый подмассив, либо продолжаем.
Решение:
def maxSubArray(nums):
max_sum = current = nums[0]
for num in nums[1:]:
current = max(num, current + num)
max_sum = max(max_sum, current)
return max_sumОбъяснение:
Сложность:
Условие:
Дан массив prices, где prices[i] — цена акции в день i. Вы можете совершать много транзакций (купить/продать). Верните максимальную прибыль.
Примеры:
Ввод: prices = [7,1,5,3,6,4]
Вывод: 7
Объяснение: Купить день 2 (1), продать день 3 (5) = 4. Купить день 4 (3), продать день 5 (6) = 3. Итого 7.
Ввод: prices = [1,2,3,4,5]
Вывод: 4
Ввод: prices = [7,6,4,3,1]
Вывод: 0
Ограничения:
💡 Подсказка: Возьмите все восходящие отрезки: profit += prices[i] - prices[i-1] для всех i, где prices[i] > prices[i-1].
Решение:
def maxProfit(prices):
profit = 0
for i in range(1, len(prices)):
if prices[i] > prices[i-1]:
profit += prices[i] - prices[i-1]
return profitОбъяснение:
Сложность:
Условие:
Дана строка s. Разбейте её на максимальное количество частей так, чтобы каждая буква встречалась только в одной части.
Верните список размеров частей.
Примеры:
Ввод: s = "ababcbacadefegdehijhklij"
Вывод: [9,7,8]
Объяснение: "ababcbaca", "defegde", "hijhklij"
Ввод: s = "eccbbbbdec"
Вывод: [10]
Ограничения:
💡 Подсказка: last[c] — последняя позиция символа c. При проходе: end = max(end, last[c]). Когда i == end — конец партиции.
Решение:
def partitionLabels(s):
last = {c: i for i, c in enumerate(s)}
result = []
start = end = 0
for i, c in enumerate(s):
end = max(end, last[c])
if i == end:
result.append(end - start + 1)
start = i + 1
return resultОбъяснение:
Сложность:
Условие:
Дана строка s с '(', ')' и ''. '' может быть '(', ')' или пустой строкой. Проверьте валидность строки.
Примеры:
Ввод: s = "()"
Вывод: true
Ввод: s = "(*)"
Вывод: true
Ввод: s = "(*))"
Вывод: true
Ограничения:
💡 Подсказка: lo и hi — минимальное и максимальное количество открытых скобок. lo считаем '*' как ')', hi как '('.
Решение:
def checkValidString(s):
lo = hi = 0
for c in s:
lo += 1 if c == '(' else -1
hi += 1 if c != ')' else -1
if hi < 0:
return False
lo = max(lo, 0)
return lo == 0Объяснение:
Сложность:
Условие:
Лимонад стоит $5. Покупатели платят $5, $10 или $20. У вас нет сдачи изначально. Верните true, если можете дать сдачу каждому.
Примеры:
Ввод: bills = [5,5,5,10,20]
Вывод: true
Объяснение: Три $5, затем $10 → сдача $5, затем $20 → сдача $10+$5
Ввод: bills = [5,5,10,10,20]
Вывод: false
Объяснение: После двух $5 и двух $10, для $20 нужна сдача $15, но есть только две $10
Ограничения:
💡 Подсказка: Для $15 сдачи приоритет: $10 + $5 (сохраняем $5), иначе три $5.
Решение:
def lemonadeChange(bills):
five = ten = 0
for bill in bills:
if bill == 5:
five += 1
elif bill == 10:
if not five:
return False
five -= 1
ten += 1
else: # bill == 20
if ten and five:
ten -= 1
five -= 1
elif five >= 3:
five -= 3
else:
return False
return TrueОбъяснение:
Сложность:
| Задача | Жадный выбор |
|---|---|
| Jump Game | Максимальная досягаемость |
| Jump Game II | Границы диапазона прыжка |
| Gas Station | Если tank < 0 — новая стартовая |
| Candy | Два прохода: left→right, right→left |
| Interval Scheduling | Раннее окончание |
| Merge Intervals | start <= prev.end |
| Maximum Subarray | max(num, current + num) |
| Buy Stock II | Все восходящие отрезки |
| Partition Labels | last[c], end = max |
| Valid Parenthesis | lo, hi диапазон |
| Lemonade Change | Приоритет $10 + $5 |
Проблема: Предполагают, что жадность работает, без доказательства.
# ❌ Неправильно — жадность не всегда работает
# Задача: 0/1 Knapsack
# Жадный выбор по весу или ценности НЕ даёт оптимальное решение!
# ✅ Правильно — DP для 0/1 Knapsack
# Жадность работает только для fractional knapsackПочему это неправильно: Жадность требует доказательства корректности.
Как обнаружить: Контрпримеры дают лучший результат.
Проблема: Сортируют не по тому ключу.
# ❌ Неправильно для Interval Scheduling
intervals.sort(key=lambda x: x[0]) # По началу!
# ✅ Правильно
intervals.sort(key=lambda x: x[1]) # По окончанию!Почему это неправильно: Раннее окончание оставляет больше места.
Проблема: Не проверяют необходимую условие для Gas Station.
# ❌ Неправильно
def canCompleteCircuit(gas, cost):
start = 0
tank = 0
# Нет проверки sum(gas) >= sum(cost)!
# ✅ Правильно
def canCompleteCircuit(gas, cost):
if sum(gas) < sum(cost):
return -1 # Невозможно!Почему это неправильно: Если газа меньше, чем нужно, решение невозможно.
Проблема: Не учитывают оба направления.
# ❌ Неправильно
def candy(ratings):
candies = [1] * n
for i in range(1, n): # Только слева направо!
if ratings[i] > ratings[i-1]:
candies[i] = candies[i-1] + 1
# ✅ Правильно
def candy(ratings):
candies = [1] * n
for i in range(1, n): # Слева направо
if ratings[i] > ratings[i-1]:
candies[i] = candies[i-1] + 1
for i in range(n-2, -1, -1): # Справа налево
if ratings[i] > ratings[i+1]:
candies[i] = max(candies[i], candies[i+1] + 1)Почему это неправильно: Нужно учесть оба направления.
Проблема: Увеличивают jumps на каждой позиции.
# ❌ Неправильно
for i in range(n):
jumps += 1 # На каждой позиции!
# ✅ Правильно
for i in range(n - 1):
farthest = max(farthest, i + nums[i])
if i == current_end: # Только на границе
jumps += 1
current_end = farthestПочему это неправильно: jumps должен увеличиваться только при смене диапазона.
Условие: Вы планируете прыжки по массиву. Каждый элемент — максимальная длина прыжка. Найдите минимальное количество прыжков для достижения конца.
Пример:
Ввод: nums = [2,3,1,1,4]
Вывод: 2
Объяснение: 0→1 (прыжок 2), 1→4 (прыжок 3)
def jump(nums):
if len(nums) <= 1:
return 0
jumps = 0
current_end = 0
farthest = 0
for i in range(len(nums) - 1):
farthest = max(farthest, i + nums[i])
if i == current_end:
jumps += 1
current_end = farthest
return jumps
# Пример:
# nums = [2,3,1,1,4]
# i=0: farthest=2, i==current_end → jumps=1, current_end=2
# i=1: farthest=4
# i=2: farthest=4, i==current_end → jumps=2, current_end=4
# return 2Объяснение:
Сложность:
Greedy требует доказательства, что локальный оптимум ведёт к глобальному.
Ключевые принципы:
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.