Algoritmos gulosos vs DP: quando usar cada um
Identifique as características de problemas solucionáveis por algoritmos gulosos em contraste com aqueles que exigem DP, usando a propriedade da escolha gulosa e o argumento de troca.
Algoritmos gulosos vs DP: quando usar cada um é uma aula grátis de Coding Interview Prep no CoddyKit. Esta é a aula 1 de 4. Você pode ler a aula completa abaixo gratuitamente — depois pratica ao vivo no navegador com um editor de código integrado e um tutor de IA 24/7. Faz parte do caminho de aprendizado de Coding Interview Prep, e seu progresso é sincronizado entre a web e o app CoddyKit. O curso de Coding Interview Prep inclui 4 aulas no total.
Visão geral de algoritmos gulosos e DP
Tanto os algoritmos gulosos quanto a programação dinâmica resolvem problemas de otimização — encontrando um máximo, um mínimo ou uma disposição ideal. Um algoritmo guloso faz a escolha localmente ideal em cada etapa, sem reconsiderar decisões anteriores. A DP explora todas as possibilidades, mas usa memorização para evitar recomputações. Saber qual abordagem aplicar pode poupar horas de depuração de um algoritmo guloso incorreto ou de uma tabela de DP desnecessariamente complexa.
# 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')A propriedade da escolha gulosa
Um problema tem a propriedade da escolha gulosa quando uma solução globalmente ótima pode sempre ser construída fazendo escolhas localmente ótimas (gulosas). Formalmente: existe uma solução ótima que começa com a escolha gulosa, portanto nunca precisamos fazer backtrack. Para provar isso, normalmente usa-se um argumento de troca: suponha que uma solução ótima qualquer não inclua a escolha gulosa e mostre que é possível trocá-la pela escolha gulosa sem piorar o resultado.
# 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], '...')Subestrutura ótima
Tanto os algoritmos gulosos quanto a DP exigem uma subestrutura ótima: a solução ótima do problema completo contém soluções ótimas para os subproblemas. A diferença está em saber se as soluções ótimas dos subproblemas podem ser determinadas de forma gulosa (sem explorar todas as opções) ou se é necessário comparar várias escolhas. Se você fizer uma escolha e o subproblema restante tiver a mesma estrutura, a abordagem gulosa funciona. Se precisar comparar várias escolhas, use 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')Subproblemas sobrepostos indicam DP
Se o mesmo subproblema for resolvido várias vezes em uma decomposição recursiva, será necessária DP com memorização. Desenhe a árvore de recursão e procure nós repetidos. Para Fibonacci, fib(3) é calculado duas vezes na árvore de fib(5). Na troca de moedas com moedas [1,3,4] e alvo 6, os subproblemas para os alvos 3, 2 e 1 aparecem várias vezes. Subproblemas sobrepostos + subestrutura ótima = 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)Problemas clássicos de algoritmos gulosos
Problemas nos quais a abordagem gulosa é comprovadamente correta: (1) Escalonamento de atividades/intervalos — escolha gulosa pelo menor tempo de término. (2) Árvore geradora mínima — algoritmos de Prim e Kruskal. (3) Codificação de Huffman — sempre mescle os dois nós de menor frequência. (4) Mochila fracionária — escolha os itens pela maior razão valor/peso. (5) Jogo de saltos — acompanhe o maior índice alcançável. Todos esses problemas têm justificativas baseadas em argumentos de troca.
# 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)Quando a abordagem gulosa falha: contraexemplos
Encontrar um contraexemplo é a maneira mais rápida de refutar uma hipótese gulosa. Na troca de moedas com moedas [1, 3, 4] e alvo 6, a abordagem gulosa (maior primeiro) escolhe 4 e depois 1+1, totalizando 3 moedas. A DP encontra 3+3, totalizando 2 moedas. Para a mochila 0/1, a abordagem gulosa pela razão escolhe o item com a melhor razão, mas pode perder combinações que preencham melhor a capacidade. Se você conseguir construir um contraexemplo em menos de um minuto, mude para 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)Tabela comparativa: guloso vs DP
Principais diferenças lado a lado: Complexidade de tempo — algoritmos gulosos normalmente têm complexidade O(n log n) (dominada pela ordenação); DP tem complexidade O(n × estados). Complexidade de espaço — algoritmo guloso usa O(1) de espaço auxiliar; DP usa O(estados). Correção — a abordagem gulosa exige uma prova; DP é sempre correta se os estados e a recorrência estiverem certos. Aplicabilidade — algoritmos gulosos são usados em escalonamento, árvores geradoras e Huffman; DP é usada em mochila, alinhamento de sequências e caminhos mínimos com pesos negativos.
# 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')Estrutura de decisão
Fluxograma de decisão para entrevistas: (1) Você consegue provar a propriedade da escolha gulosa com um argumento de troca? Se sim → algoritmo guloso. (2) Os subproblemas se sobrepõem (o mesmo estado é alcançado de várias maneiras)? Se sim → DP. (3) O problema pede para contar ou enumerar todas as soluções? → DP ou retrocesso. (4) O problema pede um único valor ótimo com uma ordenação natural? Suspeite de uma abordagem gulosa. (5) Em caso de dúvida, implemente a DP — ela sempre estará correta se a recorrência estiver certa, mesmo que seja mais lenta.
# 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)')Problemas de intervalos: guloso vs DP
Os problemas de intervalos dividem-se entre abordagens gulosas e DP. Intervalos não sobrepostos (remover o menor número): ordene pelo tempo de término e escolha os intervalos de forma gulosa — a abordagem gulosa é comprovadamente ótima. Escalonamento ponderado de intervalos (maximizar o peso total): é necessário usar DP, pois intervalos pesados podem se sobrepor a muitos intervalos leves, exigindo a comparação de todos os subconjuntos válidos. O fator decisivo é saber se todos os intervalos têm peso igual (guloso) ou peso variável (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]])) # 2Reconhecendo sinais do problema
Sinais comuns nos enunciados: 'número mínimo de operações', 'lucro máximo', 'seleção ótima' → pode ser um algoritmo guloso ou DP; verifique a sobreposição. 'conte o número de maneiras' → sempre DP. 'encontre qualquer escalonamento válido' → pode ser guloso. 'todas as possibilidades' → retrocesso. 'não pode escolher elementos adjacentes' → DP (ladrão de casas). 'reuniões, intervalos, tarefas' → provavelmente guloso. Associar os sinais às famílias de algoritmos acelera o diagnóstico de problemas em entrevistas.
# 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}')Provando a correção de algoritmos gulosos
Para provar que um algoritmo guloso está correto, use o argumento de troca: (1) Suponha que exista uma solução ótima OPT diferente da solução gulosa G na primeira escolha. (2) Mostre que é possível trocar a escolha de OPT pela escolha gulosa sem aumentar o valor da função objetivo. (3) Por indução, a solução gulosa é tão boa quanto qualquer solução ótima. Em entrevistas, não é necessário apresentar uma prova completa, mas explicar a intuição do argumento de troca demonstra um entendimento profundo.
# 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')Verificação rápida
Verifique sua compreensão dos conceitos de Estruturas de Dados & Algoritmos — Preparação para entrevistas de programação desta lição.
Recapitulação da lição
Nesta lição, você aprendeu: um algoritmo guloso está correto quando a propriedade da escolha gulosa é válida — algo que pode ser provado por meio de um argumento de troca; DP é necessária quando os subproblemas se sobrepõem (o mesmo subproblema é alcançado de várias maneiras) e não podem ser resolvidos por uma única regra gulosa; e a maneira mais rápida de refutar uma hipótese gulosa é construir um contraexemplo com entradas não convencionais. A seguir, resolveremos problemas de escalonamento e mesclagem de intervalos usando a abordagem gulosa de sort por tempo de término.
Perguntas Frequentes
A aula “Algoritmos gulosos vs DP: quando usar cada um” é grátis?
Sim — o texto completo de “Algoritmos gulosos vs DP: quando usar cada um” é grátis para ler aqui na web. Para praticá-la interativamente (um editor de código integrado e um tutor de IA 24/7) e desbloquear o restante do curso de Coding Interview Prep, atualize para CoddyKit PRO. O curso de Coding Interview Prep inclui 4 aulas no total.
O que vou aprender em “Algoritmos gulosos vs DP: quando usar cada um”?
Identifique as características de problemas solucionáveis por algoritmos gulosos em contraste com aqueles que exigem DP, usando a propriedade da escolha gulosa e o argumento de troca. Você pratica Coding Interview Prep com código prático que executa diretamente no navegador, e um tutor de IA 24/7 responde suas dúvidas enquanto trabalha na aula.
Preciso ter experiência prévia para começar Coding Interview Prep?
Nenhuma experiência prévia é necessária. Coding Interview Prep no CoddyKit é estruturado para alunos iniciantes até avançados, então você pode começar aqui ou desde o início e aprender no seu ritmo. Esta é a aula 1 de 4.
Quanto tempo leva a aula “Algoritmos gulosos vs DP: quando usar cada um”?
A maioria das aulas CoddyKit leva cerca de 5–10 minutos. Cada uma é compacta e interativa, então você faz progresso constante e retoma exatamente de onde parou entre web e app.
Posso escrever e executar código nesta aula de Coding Interview Prep?
Sim. Cada aula de Coding Interview Prep inclui um editor de código integrado, então você escreve e executa código real direto no navegador e recebe feedback de IA instantaneamente — nenhuma configuração local necessária.
Todas as aulas deste curso
- Algoritmos gulosos vs DP: quando usar cada um
- Escalonamento e fusão de intervalos
- Jogo dos saltos I e II
- Escalonador de tarefas e posto de combustível