Reconocimiento de DP: subproblemas superpuestos
Identifique cuándo la recursión por fuerza bruta vuelve a resolver el mismo subproblema, dibuje el árbol de recursión de Fibonacci y observe el crecimiento exponencial.
Reconocimiento de DP: subproblemas superpuestos 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.
¿Qué es la programación dinámica?
La programación dinámica (DP) resuelve problemas complejos dividiéndolos en subproblemas más sencillos y superpuestos, resolviendo cada subproblema una sola vez y almacenando el resultado para evitar cálculos redundantes. La DP se aplica cuando un problema tiene dos componentes: subproblemas superpuestos (el mismo subproblema se resuelve varias veces en una recursión ingenua) y subestructura óptima (la solución óptima puede construirse a partir de soluciones óptimas para los subproblemas). Sin ambos componentes, la DP no resulta útil.
# Two ingredients of DP:
# 1. Overlapping sub-problems:
# fib(5) -> fib(4) + fib(3)
# fib(4) -> fib(3) + fib(2) <- fib(3) computed twice!
# Without caching: O(2^n) calls for Fibonacci
# 2. Optimal substructure:
# Shortest path from A to C through B:
# shortest(A,C) = shortest(A,B) + shortest(B,C)
# The sub-path A->B must itself be the shortest
# Contrast with greedy: greedy makes one locally optimal
# choice; DP tries all choices and picks the best.
print('DP = overlapping sub-problems + optimal substructure')Fibonacci: el punto de partida clásico de la DP
La sucesión de Fibonacci (fib(n) = fib(n-1) + fib(n-2)) es el ejemplo canónico de subproblemas superpuestos. La recursión ingenua tiene un tiempo exponencial O(2^n) porque vuelve a calcular repetidamente los mismos valores. El árbol de recursión de fib(6) muestra que fib(3) se calcula 3 veces, fib(2) 5 veces, y así sucesivamente. Este crecimiento exponencial es precisamente lo que elimina la DP al almacenar los resultados calculados.
import time
def fib_naive(n):
if n <= 1:
return n
return fib_naive(n-1) + fib_naive(n-2)
# Count the calls:
call_count = [0]
def fib_count(n):
call_count[0] += 1
if n <= 1: return n
return fib_count(n-1) + fib_count(n-2)
fib_count(10)
print(f'Calls for fib(10): {call_count[0]}') # 177 calls for n=10!
call_count[0] = 0
fib_count(20)
print(f'Calls for fib(20): {call_count[0]}') # 21891 calls
# n=30 -> ~2.7 million calls: exponential growthVisualización del árbol de recursión
Dibujar el árbol de recursión de fib(5) revela el desperdicio: cada nodo genera dos hijos y los subárboles idénticos aparecen repetidamente. El número total de nodos del árbol es O(2^n). Cuando vea este patrón —llamadas de función idénticas con los mismos argumentos repetidas en el árbol—, es una señal de que la DP puede ser útil almacenando los resultados en caché. Esta habilidad de visualización es crucial: si puede identificar los subárboles repetidos, sabrá que la DP es aplicable.
# fib(5) recursion tree (simplified):
# fib(5)
# / \
# fib(4) fib(3)
# / \ / \
# fib(3) fib(2) fib(2) fib(1)
# / \ \
# fib(2) fib(1) fib(1)
# / \
# fib(1) fib(0)
# fib(3) appears TWICE
# fib(2) appears THREE TIMES
# Each redundant call wastes exponential time
# Key insight: fib(n) only has O(n) DISTINCT sub-problems
# (fib(0), fib(1), ..., fib(n))
# DP computes each ONCE -> O(n) total
print('Distinct sub-problems: O(n) but naive calls: O(2^n)')Identificación de subproblemas superpuestos
Para reconocer subproblemas superpuestos, escriba la recursión de fuerza bruta y después pregúntese: «¿hay varias llamadas recursivas con los MISMOS argumentos?». Si la respuesta es afirmativa, la DP puede ser útil. Algunas señales habituales en las descripciones de problemas son: «número mínimo/máximo de X», «cuántas formas hay de hacer Y» y «¿podemos lograr Z?». Estos patrones de redacción casi siempre indican un problema con subestructura óptima, en el que la respuesta en la posición i depende de las respuestas en posiciones anteriores.
# DP signal phrases in problem statements:
# 'minimum number of coins to make amount X'
# 'maximum profit from stock trades'
# 'number of ways to climb n stairs'
# 'can you reach the last index?'
# 'longest common subsequence'
# 'edit distance between two strings'
# All have this shape:
# solve(input) = f(solve(smaller_input_1), solve(smaller_input_2), ...)
# And multiple branches end up calling solve with the same argument.
# If the recursion tree has repeated nodes: DP
# If subproblems are all independent: divide-and-conquer (no DP needed)
print('Repeated arguments in recursion tree -> DP')Explicación de la subestructura óptima
La subestructura óptima significa que la solución óptima del problema puede construirse a partir de soluciones óptimas para sus subproblemas. Por ejemplo, el camino más corto de A a C pasando por B es óptimo si y solo si los subcaminos A→B y B→C son óptimos individualmente. Si se cumple esta propiedad, puede construir la solución óptima global de abajo arriba a partir de óptimos locales. Los problemas que carecen de subestructura óptima (por ejemplo, el camino más largo en un grafo general con ciclos) no pueden resolverse mediante DP.
# Optimal substructure examples:
# SHORTEST PATH: shortest(A,C) = min over all B: shortest(A,B) + w(B,C)
# -> Sub-paths must be optimal: YES, has optimal substructure
# LONGEST PATH (no cycles, DAG): can also use DP
# -> Longer path through node B means sub-path A->B must be longest
# LONGEST PATH (with cycles): NO optimal substructure
# -> Best path from A to C might reuse nodes: sub-problems not independent
# COIN CHANGE: min coins for amount n = 1 + min(min coins for n-coin_i)
# -> YES: optimal for n-coin_i is needed for optimal n
print('Optimal substructure: build global optimum from local optima')Subir escaleras: su primera DP
Subir escaleras (LeetCode #70): ¿cuántas formas distintas hay de subir n escalones dando 1 o 2 pasos cada vez? Sea dp[i] = número de formas de llegar al escalón i. Puede llegar al escalón i desde el escalón i-1 (un paso) o desde el escalón i-2 (dos pasos), por lo que dp[i] = dp[i-1] + dp[i-2]. ¡Esto es Fibonacci! Casos base: dp[1] = 1, dp[2] = 2. Reconocer que «subir escaleras» se reduce a Fibonacci es una idea clásica en las entrevistas técnicas.
def climb_stairs(n):
if n <= 2:
return n
dp = [0] * (n + 1)
dp[1] = 1 # 1 way to reach step 1
dp[2] = 2 # 2 ways to reach step 2: (1+1) or (2)
for i in range(3, n + 1):
dp[i] = dp[i-1] + dp[i-2] # come from i-1 or i-2
return dp[n]
for n in range(1, 8):
print(f'climb_stairs({n}) = {climb_stairs(n)}')
# 1, 2, 3, 5, 8, 13, 21 -- Fibonacci sequence!El marco de DP: defina, formule la recurrencia y determine el orden
Un marco fiable de DP en 3 pasos: 1. Defina el estado — ¿qué representa dp[i] (o dp[i][j])? Escríbalo en inglés. 2. Escriba la recurrencia — exprese dp[i] en función de subproblemas más pequeños. Incluya todos los casos. 3. Determine el orden de llenado — asegúrese de que dp[i-1] (y las demás dependencias) se calculen antes que dp[i]. Los casos base inicializan el borde. Este marco convierte la intuición difusa sobre DP en un plan concreto de implementación.
# Framework applied to climbing stairs:
# Step 1 - Define state:
# dp[i] = number of distinct ways to reach step i
# Step 2 - Recurrence:
# dp[i] = dp[i-1] + dp[i-2] (come from step i-1 or i-2)
# Step 3 - Fill order:
# Compute dp[1], dp[2], dp[3], ..., dp[n] in order
# Because dp[i] depends on dp[i-1] and dp[i-2] (smaller)
# Base cases: dp[1]=1, dp[2]=2
# Framework applied to coin change:
# Step 1: dp[amount] = minimum coins to make that amount
# Step 2: dp[i] = 1 + min(dp[i-coin] for coin in coins if i >= coin)
# Step 3: Fill i from 1 to amount
# Base: dp[0] = 0 (zero coins for zero amount)
print('DP framework: define state -> recurrence -> fill order')Cuándo NO usar DP
DP no siempre es la respuesta. Use greedy cuando una única elección óptima a nivel local conduzca siempre a la solución óptima global (activity selection, jump game I). Use divide y vencerás cuando los subproblemas no se solapen (merge sort, binary search). Use BFS cuando el problema consista en encontrar el camino más corto en un grafo no ponderado. DP es correcto, pero a menudo resulta excesivo cuando existe un enfoque greedy o más sencillo. En las entrevistas, explique por qué eligió DP en lugar de las alternativas.
# DP vs alternatives:
# Problem: can you jump to the end of the array?
# Greedy: track max reachable index -> O(n) O(1) BETTER than DP
# Problem: shortest path unweighted graph?
# BFS: O(V+E) BETTER than DP on general graph
# Problem: sort an array?
# Comparison sort: O(n log n), no DP needed
# DP IS the right choice when:
# - Greedy fails (choices interact)
# - Need to count/enumerate all possibilities
# - Problem has 'how many ways' or 'minimum/maximum' flavor
# - Recursion tree clearly shows overlapping sub-problems
print('Ask: does greedy fail? If yes, consider DP.')Conteo de subproblemas distintos
El número de subproblemas distintos determina la complejidad temporal y espacial de DP. En una DP 1D sobre una entrada de tamaño n, hay O(n) subproblemas. En una DP 2D sobre dos entradas de tamaños m y n, hay O(mn) subproblemas. Cada subproblema se resuelve en O(k) tiempo (para k opciones en cada paso), lo que da un tiempo total de O(n*k) o O(mn*k). Cuente siempre primero los subproblemas distintos: así obtendrá la complejidad temporal de DP antes incluso de escribir el código.
# Sub-problem count examples:
# Problem | Sub-problems | Each costs | Total
# Fibonacci | O(n) | O(1) | O(n)
# Coin change | O(amount) | O(coins) | O(amount * coins)
# LCS (m,n chars) | O(m*n) | O(1) | O(m*n)
# Edit distance | O(m*n) | O(1) | O(m*n)
# 0/1 Knapsack | O(n*W) | O(1) | O(n*W)
# Matrix chain | O(n^2) | O(n) | O(n^3)
# Rule: DP time = (# distinct sub-problems) * (time per sub-problem)
print('Time = subproblems * work-per-subproblem')Robar casas: decisiones superpuestas
Robar casas (LeetCode #198) plantea encontrar la cantidad máxima que puede robar de una fila de casas sin robar casas adyacentes. En cada casa, debe elegir entre robarla (sumar su valor y saltarse la anterior) o no robarla (tomar el mejor resultado anterior). dp[i] = max(dp[i-1], dp[i-2] + nums[i]). Este patrón de elección en cada paso es la recurrencia de DP 1D más sencilla y aparece en docenas de problemas de entrevistas.
def rob(nums):
if not nums: return 0
if len(nums) == 1: return nums[0]
dp = [0] * len(nums)
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
for i in range(2, len(nums)):
dp[i] = max(dp[i-1], # skip house i
dp[i-2] + nums[i]) # rob house i
return dp[-1]
print(rob([1, 2, 3, 1])) # 4: rob house 0 and 2 (1+3)
print(rob([2, 7, 9, 3, 1]))# 12: rob house 0, 2, 4 (2+9+1)
print(rob([2, 1, 1, 2])) # 4: rob house 0 and 3Comprobación de coherencia: fuerza bruta frente a DP
Verifique siempre su solución de DP comparándola con una solución de fuerza bruta sobre entradas pequeñas. La fuerza bruta es su referencia fiable. Cuando DP coincida con la fuerza bruta en todos los casos de prueba, sabrá que la recurrencia es correcta. Solo entonces optimice el espacio. Este enfoque basado en pruebas — fuerza bruta → DP de arriba abajo → DP de abajo arriba → DP optimizada en espacio — es la forma profesional de desarrollar y verificar soluciones de DP durante una entrevista.
# Brute-force for house robber (exponential)
def rob_brute(nums, i=0):
if i >= len(nums):
return 0
# Option 1: rob house i
rob_it = nums[i] + rob_brute(nums, i + 2)
# Option 2: skip house i
skip_it = rob_brute(nums, i + 1)
return max(rob_it, skip_it)
# Verify on small inputs:
test_cases = [[1,2,3,1], [2,7,9,3,1], [2,1,1,2]]
for tc in test_cases:
bf = rob_brute(tc)
dp = rob(tc)
print(f'{tc}: brute={bf}, dp={dp}, match={bf==dp}')Comprobación rápida
Ponga a prueba 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 aprendió: dos ingredientes de DP (subproblemas superpuestos y subestructura óptima), cómo visualizar el árbol de recursión para identificar llamadas repetidas, el marco de DP en tres pasos (definir el estado, la recurrencia y el orden de llenado), y los primeros ejemplos, incluidos Fibonacci, subir escaleras y robar casas. A continuación, implementará DP de arriba abajo con memoización.
Preguntas frecuentes
¿La lección «Reconocimiento de DP: subproblemas superpuestos» es gratis?
Sí — el texto completo de «Reconocimiento de DP: subproblemas superpuestos» 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 «Reconocimiento de DP: subproblemas superpuestos»?
Identifique cuándo la recursión por fuerza bruta vuelve a resolver el mismo subproblema, dibuje el árbol de recursión de Fibonacci y observe el crecimiento exponencial. 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 «Reconocimiento de DP: subproblemas superpuestos»?
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
- Reconocimiento de DP: subproblemas superpuestos
- DP top-down con memoización
- DP bottom-up con tabulación
- Cambio de monedas y escalera de coste mínimo