Voraces frente a PD: cuándo usar cada enfoque
Identifique las características de los problemas resolubles con un algoritmo voraz frente a los que requieren programación dinámica, usando la propiedad de elección voraz y el argumento de intercambio.
Voraces frente a PD: cuándo usar cada enfoque es una lección gratuita de Coding Interview Prep en CoddyKit. Esta es la lección 1 de 4. Puedes leer la lección completa abajo gratuitamente — luego la practicas en el navegador con un editor de código integrado y un tutor de IA 24/7. Forma parte de la ruta de aprendizaje de Coding Interview Prep, y tu progreso se sincroniza en la web y la app de CoddyKit. El curso de Coding Interview Prep incluye 4 lecciones en total.
Introducción a los enfoques voraz y de programación dinámica
Tanto los algoritmos voraces como la programación dinámica resuelven problemas de optimización: buscan un máximo, un mínimo o una disposición óptima. El enfoque voraz toma la decisión localmente óptima en cada paso sin reconsiderar las decisiones anteriores. La programación dinámica explora todas las posibilidades, pero utiliza memoización para evitar volver a calcular resultados. Saber cuál aplicar puede ahorrarle horas de depuración de un algoritmo voraz incorrecto o de una tabla de programación dinámica innecesariamente compleja.
# 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')La propiedad de elección voraz
Un problema tiene la propiedad de elección voraz cuando siempre se puede construir una solución globalmente óptima tomando decisiones locales óptimas (voraces). Formalmente: existe una solución óptima que comienza con la elección voraz, por lo que nunca es necesario retroceder. Para demostrarlo normalmente se utiliza un argumento de intercambio: se supone que una solución óptima cualquiera no incluye la elección voraz y, después, se demuestra que se puede intercambiar por ella sin empeorar el 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], '...')Subestructura óptima
Tanto los algoritmos voraces como la programación dinámica requieren una subestructura óptima: la solución óptima del problema completo contiene soluciones óptimas para los subproblemas. La diferencia está en si las soluciones óptimas de los subproblemas pueden determinarse de forma voraz (sin explorar todas las opciones) o si es necesario comparar varias elecciones. Si toma una decisión y el subproblema restante tiene la misma estructura, el enfoque voraz funciona. Si debe comparar varias elecciones, utilice programación dinámica.
# 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')Indicio de subproblemas solapados: programación dinámica
Si el mismo subproblema se resuelve varias veces en una descomposición recursiva, necesita programación dinámica con memoización. Dibuje el árbol de recursión y busque nodos repetidos. Para Fibonacci, fib(3) se calcula dos veces en el árbol de fib(5). Para el cambio de monedas con monedas [1,3,4] y objetivo 6, los subproblemas para los objetivos 3, 2 y 1 aparecen varias veces. Subproblemas solapados más subestructura óptima = programación dinámica.
# 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 voraces clásicos
Problemas en los que el enfoque voraz es demostrablemente correcto: (1) Programación de actividades/intervalos: estrategia voraz basada en el tiempo de finalización más temprano. (2) Árbol de expansión mínima: algoritmos de Prim y Kruskal. (3) Codificación de Huffman: combinar siempre los dos nodos con menor frecuencia. (4) Mochila fraccionaria: elegir los elementos con la mayor proporción valor/peso. (5) Juego de saltos: realizar un seguimiento del índice máximo alcanzable. Todos estos problemas cuentan con una justificación basada en un argumento de intercambio.
# 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)Cuándo falla el enfoque voraz: contraejemplos
Encontrar un contraejemplo es la forma más rápida de refutar una hipótesis voraz. Para el cambio de monedas con monedas [1, 3, 4] y objetivo 6, el enfoque voraz (empezando por la moneda más grande) elige 4 y después 1+1: 3 monedas. La programación dinámica encuentra 3+3: 2 monedas. Para la mochila 0/1, el enfoque voraz por proporción elige el elemento con la mejor proporción, pero puede pasar por alto combinaciones que aprovechen mejor la capacidad. Si puede construir un contraejemplo en menos de un minuto, cambie a programación dinámica.
# 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)Tabla comparativa: enfoque voraz frente a programación dinámica
Diferencias clave, una junto a otra: complejidad temporal: el enfoque voraz suele ser O(n log n) (dominado por la ordenación); la programación dinámica es O(n × estados). Complejidad espacial: el enfoque voraz usa O(1) espacio auxiliar; la programación dinámica usa O(estados). Corrección: el enfoque voraz necesita una demostración; la programación dinámica siempre es correcta si los estados y la recurrencia son correctos. Aplicabilidad: enfoque voraz para programación de actividades, árboles de expansión y Huffman; programación dinámica para mochila, alineación de secuencias y caminos mínimos con 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')Marco de decisión
Flujo de decisión para entrevistas: (1) ¿Puede demostrar la propiedad de elección voraz mediante un argumento de intercambio? Si la respuesta es sí → enfoque voraz. (2) ¿Se solapan los subproblemas (se alcanza el mismo estado de varias formas)? Si la respuesta es sí → programación dinámica. (3) ¿El problema pide contar o enumerar todas las soluciones? → programación dinámica o backtracking. (4) ¿El problema pide un único valor óptimo con un orden natural? Sospeche que puede resolverse con un enfoque voraz. (5) En caso de duda, implemente la programación dinámica: siempre es correcta si la recurrencia es correcta, aunque sea más 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: enfoque voraz frente a programación dinámica
Los problemas de intervalos se dividen entre los enfoques voraz y de programación dinámica. Intervalos no solapados (eliminar el menor número): ordénelos por tiempo de finalización y elija intervalos de forma voraz; el enfoque voraz es demostrablemente óptimo. Programación de intervalos ponderados (maximizar el peso total): se necesita programación dinámica porque los intervalos de mayor peso pueden solaparse con muchos intervalos ligeros, lo que requiere comparar todos los subconjuntos válidos. El factor diferenciador es si todos los intervalos tienen el mismo peso (enfoque voraz) o un peso variable (programación dinámica).
# 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]])) # 2Reconocimiento de indicios del problema
Indicios habituales en los enunciados: «número mínimo de operaciones», «beneficio máximo», «selección óptima» → podría tratarse de un enfoque voraz o de programación dinámica; compruebe si hay solapamiento. «cuente el número de formas» → siempre programación dinámica. «encuentre cualquier planificación válida» → podría resolverse con un enfoque voraz. «todas las posibilidades» → backtracking. «no se pueden tomar elementos adyacentes» → programación dinámica (robador de casas). «reuniones, intervalos, tareas» → probablemente un enfoque voraz. Relacionar los indicios con las familias de algoritmos acelera el diagnóstico de problemas de 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}')Demostración de la corrección del enfoque voraz
Para demostrar que un algoritmo voraz es correcto, utilice el argumento de intercambio: (1) suponga que existe una solución óptima OPT que difiere de la solución voraz G en la primera elección; (2) demuestre que puede intercambiar la elección voraz por la elección de OPT sin aumentar el valor de la función objetivo; (3) por inducción, la solución voraz es tan buena como cualquier solución óptima. En las entrevistas no necesita una demostración completa, pero explicar la intuición del argumento de intercambio demuestra un conocimiento 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')Comprobación rápida
Compruebe su comprensión de los conceptos de Data Structures & Algorithms — Coding Interview Prep de esta lección.
Resumen de la lección
En esta lección ha aprendido que: el enfoque voraz es correcto cuando se cumple la propiedad de elección voraz, que puede demostrarse mediante un argumento de intercambio; se necesita programación dinámica cuando los subproblemas se solapan (se alcanza el mismo subproblema de varias formas) y no pueden resolverse mediante una única regla voraz; y la forma más rápida de refutar una hipótesis voraz es construir un contraejemplo con entradas no convencionales. A continuación, resolveremos problemas de programación y combinación de intervalos mediante el enfoque voraz de ordenar por tiempo de finalización.
Preguntas frecuentes
¿La lección «Voraces frente a PD: cuándo usar cada enfoque» es gratis?
Sí — el texto completo de «Voraces frente a PD: cuándo usar cada enfoque» es gratis para leer aquí en la web. Para practicarla de forma interactiva (editor de código integrado y tutor de IA 24/7) y desbloquear el resto del curso de Coding Interview Prep, actualiza a CoddyKit PRO. El curso de Coding Interview Prep incluye 4 lecciones en total.
¿Qué aprenderé en «Voraces frente a PD: cuándo usar cada enfoque»?
Identifique las características de los problemas resolubles con un algoritmo voraz frente a los que requieren programación dinámica, usando la propiedad de elección voraz y el argumento de intercambi… Practicas Coding Interview Prep con código real que ejecutas directamente en el navegador, y un tutor de IA 24/7 responde tus preguntas mientras trabajas en la lección.
¿Necesito experiencia previa para empezar Coding Interview Prep?
No se requiere experiencia previa. Coding Interview Prep en CoddyKit está estructurado para principiantes hasta estudiantes avanzados, así que puedes empezar aquí o desde el inicio y avanzar a tu ritmo. Esta es la lección 1 de 4.
¿Cuánto tiempo toma la lección «Voraces frente a PD: cuándo usar cada enfoque»?
La mayoría de las lecciones de CoddyKit toman alrededor de 5–10 minutos. Cada una es compacta e interactiva, así que avanzas constantemente y retomas exactamente por donde dejaste en la web y la app.
¿Puedo escribir y ejecutar código en esta lección de Coding Interview Prep?
Sí. Cada lección de Coding Interview Prep incluye un editor de código integrado, así que escribes y ejecutas código real directamente en tu navegador y obtienes retroalimentación instantánea de IA — sin configuración local necesaria.
Todas las lecciones de este curso
- Voraces frente a PD: cuándo usar cada enfoque
- Planificación y fusión de intervalos
- Jump Game I y II
- Task Scheduler y Gas Station