Жадные алгоритмы и DP: когда что использовать
Научитесь определять признаки задач, решаемых жадным методом, и задач, требующих DP, используя свойство жадного выбора и аргумент обмена
«Жадные алгоритмы и DP: когда что использовать» — бесплатный урок Coding Interview Prep на CoddyKit. Это урок 1 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Coding Interview Prep, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Coding Interview Prep содержит 4 уроков всего.
Обзор жадных алгоритмов и DP
И жадные алгоритмы, и динамическое программирование решают задачи оптимизации — поиска максимального или минимального значения либо оптимальной конфигурации. Жадный алгоритм на каждом шаге делает локально оптимальный выбор, не пересматривая предыдущие решения. DP рассматривает все варианты, но использует мемоизацию, чтобы не выполнять одни и те же вычисления повторно. Знание того, какой подход применить, может сэкономить часы отладки неверного жадного алгоритма или неоправданно сложной таблицы DP.
# Greedy: always take the locally best option
# Example: coin change with coins [1, 5, 10, 25]
# Greedy: take as many 25s as possible, then 10s, etc.
# This works for standard denominations but NOT all coin sets!
# DP: explore all possibilities via memoisation
# Example: coin change with coins [1, 3, 4] and target 6
# Greedy would pick 4, then 1, 1 → 3 coins
# DP finds: 3 + 3 → 2 coins (optimal!)
print('Greedy can fail when local optimum != global optimum')Свойство жадного выбора
Задача обладает свойством жадного выбора, если глобально оптимальное решение всегда можно построить, делая локально оптимальные (жадные) выборы. Формально: существует оптимальное решение, которое начинается с жадного выбора, поэтому нам никогда не приходится выполнять backtrack. Обычно это доказывают с помощью аргумента обмена: предположите, что некоторое оптимальное решение не содержит жадный выбор, а затем покажите, что его можно заменить на жадный выбор без ухудшения результата.
# Exchange argument example: Activity Selection
# Greedy: always pick the activity that ends earliest
# Proof: suppose optimal solution starts with activity A (not earliest-ending)
# Let G be the earliest-ending activity.
# Replace A with G in the solution:
# - G ends no later than A, so G does not conflict with any activity A allowed
# - The solution remains valid with at least as many activities
# Therefore greedy choice (earliest end) is always safe.
activities = [(1,4), (3,5), (0,6), (5,7), (3,9), (5,9), (6,10), (8,11), (8,12), (2,14)]
activities.sort(key=lambda x: x[1]) # sort by end time
print('Sorted by end:', activities[:4], '...')Оптимальная структура подзадач
И жадные алгоритмы, и DP требуют оптимальной структуры подзадач: оптимальное решение полной задачи содержит оптимальные решения подзадач. Различие заключается в том, можно ли определить оптимальные решения подзадач жадным способом (не перебирая все варианты) или необходимо сравнить несколько выборов. Если после выбора оставшаяся подзадача имеет ту же структуру, жадный подход работает. Если нужно сравнить несколько вариантов, используйте DP.
# Greedy works: activity selection
# Making the greedy choice (earliest-ending) leaves a sub-problem
# that is structurally identical (activity selection on remaining activities)
# and the greedy choice for the sub-problem is still valid.
# DP needed: 0/1 knapsack
# After choosing to include/exclude item i, the remaining sub-problem
# depends on WHICH item we chose — different choices yield different sub-problems.
# No single greedy rule works for all inputs.
print('Greedy: sub-problem is unique after each choice')
print('DP: sub-problem depends on which choice was made')Перекрывающиеся подзадачи как признак DP
Если одна и та же подзадача решается несколько раз при рекурсивном разбиении, необходимо использовать DP с мемоизацией. Нарисуйте дерево рекурсии и найдите повторяющиеся узлы. Для чисел Фибоначчи fib(3) вычисляется дважды в дереве для fib(5). В задаче о размене монет с монетами [1,3,4] и целевой суммой 6 подзадачи для сумм 3, 2 и 1 появляются несколько раз. Перекрывающиеся подзадачи плюс оптимальная структура подзадач = DP.
# Recursion tree for coin change [1,3,4], target=6
# bt(6) → bt(5) → bt(4) → bt(3) (repeated!)
# → bt(2) → bt(1) (repeated!)
# → bt(3) (repeated!)
# → bt(2) (repeated!)
# Without memoisation: exponential time
# With DP table: O(target * len(coins)) time
def coin_change_dp(coins, amount):
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for a in range(1, amount + 1):
for c in coins:
if c <= a:
dp[a] = min(dp[a], dp[a - c] + 1)
return dp[amount] if dp[amount] != float('inf') else -1
print(coin_change_dp([1, 3, 4], 6)) # 2 (3+3)
print(coin_change_dp([2], 3)) # -1 (impossible)Классические задачи для жадных алгоритмов
Задачи, для которых жадный подход доказуемо корректен: (1) Планирование действий/интервалов — жадный выбор по самому раннему времени окончания. (2) Минимальное остовное дерево — алгоритмы Прима и Краскала. (3) Кодирование Хаффмана — всегда объединяйте два узла с наименьшей частотой. (4) Рюкзак с дробным заполнением — выбирайте предметы по убыванию отношения ценности к весу. (5) Задача о прыжках — отслеживайте максимально достижимый индекс. Все эти задачи обоснованы доказательством с помощью аргумента обмена.
# Fractional Knapsack: greedy works
def fractional_knapsack(items, capacity):
# Sort by value/weight ratio descending
items.sort(key=lambda x: x[1]/x[0], reverse=True)
total = 0
for weight, value in items:
if capacity <= 0: break
take = min(weight, capacity)
total += take * (value / weight)
capacity -= take
return total
items = [(10, 60), (20, 100), (30, 120)] # (weight, value)
print(fractional_knapsack(items, 50)) # 240.0
# 0/1 Knapsack: greedy FAILS
# Must use DP (can't take fractions)Когда жадный подход не работает: контрпримеры
Поиск контрпримера — самый быстрый способ опровергнуть жадную гипотезу. В задаче о размене монет с монетами [1, 3, 4] и целевой суммой 6 жадный подход (сначала берём самую крупную монету) выбирает 4, затем 1+1 — всего 3 монеты. DP находит вариант 3+3 — всего 2 монеты. В задаче о рюкзаке 0/1 жадный выбор по отношению ценности к весу берёт предмет с лучшим отношением, но может пропустить комбинации, которые лучше заполняют вместимость. Если вы можете построить контрпример менее чем за минуту, переходите к DP.
# Counterexample: coin change with non-standard coins
def greedy_coins(coins, amount):
coins.sort(reverse=True)
count = 0
for c in coins:
while amount >= c:
amount -= c
count += 1
return count if amount == 0 else -1
def dp_coins(coins, amount):
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for a in range(1, amount + 1):
for c in coins:
if c <= a: dp[a] = min(dp[a], dp[a-c] + 1)
return dp[amount] if dp[amount] < float('inf') else -1
coins, target = [1, 3, 4], 6
print('Greedy:', greedy_coins(coins[:], target)) # 3 (4+1+1)
print('DP: ', dp_coins(coins, target)) # 2 (3+3)Сравнительная таблица: жадные алгоритмы и DP
Ключевые различия: Временная сложность — для жадного алгоритма обычно O(n log n) (определяется сортировкой); для DP — O(n × states). Пространственная сложность — у жадного алгоритма O(1) дополнительной памяти; у DP — O(states). Корректность — жадному алгоритму требуется доказательство; DP всегда корректно, если состояния и рекуррентное соотношение заданы правильно. Применимость — жадные алгоритмы подходят для планирования, остовных деревьев и кодирования Хаффмана; DP — для рюкзака, выравнивания последовательностей и поиска кратчайшего пути с отрицательными весами.
# Performance comparison
import time
def time_it(func, *args):
start = time.time()
result = func(*args)
return result, time.time() - start
# Large coin change test
coins = [1, 5, 10, 25, 100]
amount = 10000
def dp_coins(coins, amount):
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for a in range(1, amount + 1):
for c in coins:
if c <= a: dp[a] = min(dp[a], dp[a-c]+1)
return dp[amount]
result, elapsed = time_it(dp_coins, coins, amount)
print(f'DP coin change(amount={amount}): {result} coins in {elapsed:.4f}s')Схема принятия решения
Схема принятия решения на собеседовании: (1) Можете ли вы доказать свойство жадного выбора с помощью аргумента обмена? Если да → жадный алгоритм. (2) Перекрываются ли подзадачи (одно и то же состояние достигается несколькими способами)? Если да → DP. (3) Требуется ли в задаче подсчитать или перечислить все решения? → DP или поиск с возвратом. (4) Требуется ли найти одно оптимальное значение при естественном порядке элементов? Рассмотрите жадный подход. (5) Если сомневаетесь, напишите решение на DP — оно всегда корректно, если рекуррентное соотношение задано правильно, даже если работает медленнее.
# Decision questions to ask:
questions = [
'1. Is there a natural ordering (by time, ratio, size)?',
'2. Does making the greedy choice leave a smaller same-type problem?',
'3. Can I construct a counterexample quickly?',
'4. Are sub-problems reused across different choice sequences?',
'5. Does the problem involve counting or listing (not just optimising)?',
]
for q in questions:
print(q)
print()
print('Greedy signals: scheduling, spanning tree, Huffman, jump game')
print('DP signals: knapsack, edit distance, LCS, coin change (general)')Задачи на интервалы: жадные алгоритмы и DP
Задачи на интервалы делятся между жадными алгоритмами и DP. Непересекающиеся интервалы (удалить минимальное число): выполните sort по end time и жадно выбирайте интервалы — жадный подход доказуемо оптимален. Взвешенное планирование интервалов (максимизировать суммарный вес): необходим DP, поскольку интервалы с большим весом могут пересекаться со множеством лёгких интервалов, поэтому требуется сравнивать все допустимые подмножества. Определяющий фактор — имеют ли все интервалы одинаковый вес (жадный алгоритм) или разный вес (DP).
# Non-overlapping intervals: greedy works
def erase_overlap_intervals(intervals):
if not intervals: return 0
intervals.sort(key=lambda x: x[1])
count = 0
last_end = float('-inf')
for start, end in intervals:
if start >= last_end:
last_end = end # keep this interval
else:
count += 1 # remove this interval
return count
print(erase_overlap_intervals([[1,2],[2,3],[3,4],[1,3]])) # 1
print(erase_overlap_intervals([[1,2],[1,2],[1,2]])) # 2Распознавание признаков задачи
Распространённые признаки в формулировках задач: «минимальное число операций», «максимальная прибыль», «оптимальный выбор» → это может быть жадный алгоритм или DP, проверьте наличие перекрывающихся подзадач. «подсчитать число способов» → всегда DP. «найти любое допустимое расписание» → возможно, жадный алгоритм. «все возможные варианты» → поиск с возвратом. «нельзя брать соседние элементы» → DP (задача о грабителе домов). «встречи, интервалы, задачи» → вероятно, жадный алгоритм. Сопоставление признаков с семействами алгоритмов ускоряет определение типа задачи на собеседовании.
# Signal-to-algorithm mapping
signals = {
'minimum steps/coins/operations': 'DP (unless trivially greedy)',
'maximum profit/value with constraint': 'DP (knapsack family)',
'count ways to reach/achieve': 'DP (always)',
'all combinations/permutations': 'Backtracking',
'schedule tasks within time': 'Greedy (sort by deadline/end)',
'cannot pick adjacent': 'DP (house robber pattern)',
'free to pick any subset': 'DP or Greedy (check overlap)',
'interval merging/selecting': 'Greedy (sort by end time)',
}
for signal, algo in signals.items():
print(f'{signal!r}: → {algo}')Доказательство корректности жадного алгоритма
Чтобы доказать корректность жадного алгоритма, используйте аргумент обмена: (1) Предположите, что существует оптимальное решение OPT, которое отличается от жадного решения G уже на первом выборе. (2) Покажите, что жадный выбор можно подставить в OPT, не увеличив значение целевой функции. (3) По индукции жадное решение оказывается не хуже любого оптимального решения. На собеседовании не обязательно приводить полное доказательство, но объяснение сути аргумента обмена показывает глубокое понимание.
# Exchange argument demo: earliest-finish-time activity selection
# Suppose OPT starts with activity A (not earliest-ending)
# Let G = earliest-ending activity available
# A.end >= G.end (G ends earlier or same time)
# Swap A for G in OPT:
# - G.end <= A.end, so G does not conflict with anything A allowed after it
# - OPT remains valid with the same number of activities
# - Repeat: after swap, OPT begins with G, matching greedy first choice
# By induction, OPT can be transformed to match G activity by activity
# without losing activities → greedy is optimal
print('Exchange argument: any OPT can be modified to match Greedy without loss')
print('This proves Greedy >= OPT in objective value')Быстрая проверка
Проверьте, насколько хорошо вы поняли концепции курса «Структуры данных & алгоритмы — подготовка к собеседованию по программированию», рассмотренные в этом уроке.
Итоги урока
В этом уроке вы узнали: жадный алгоритм корректен, если выполняется свойство жадного выбора, которое можно доказать с помощью аргумента обмена; DP необходим, когда подзадачи перекрываются (одна и та же подзадача достигается несколькими способами) и не могут быть решены одним жадным правилом; самый быстрый способ опровергнуть жадную гипотезу — построить контрпример на нестандартных входных данных. Далее мы решим задачи на планирование и объединение интервалов с помощью жадного подхода, сортируя по времени окончания.
Часто задаваемые вопросы
Урок «Жадные алгоритмы и DP: когда что использовать» бесплатный?
Да — полный текст урока «Жадные алгоритмы и DP: когда что использовать» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Coding Interview Prep, подпишись на CoddyKit PRO. Курс Coding Interview Prep содержит 4 уроков всего.
Чему я научусь в уроке «Жадные алгоритмы и DP: когда что использовать»?
Научитесь определять признаки задач, решаемых жадным методом, и задач, требующих DP, используя свойство жадного выбора и аргумент обмена Ты практикуешь Coding Interview Prep с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать Coding Interview Prep?
Предыдущий опыт не требуется. Coding Interview Prep на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 1 из 4.
Сколько времени занимает урок «Жадные алгоритмы и DP: когда что использовать»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке Coding Interview Prep?
Да. Каждый урок Coding Interview Prep включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Жадные алгоритмы и DP: когда что использовать
- Планирование и объединение интервалов
- Игра с прыжками I и II
- Планировщик задач и заправочная станция