De A* e IDA* a algoritmos genéticos, annealing y Q-learning — cómo elegir y entender el algoritmo correcto para tu problema.
Los algoritmos de este módulo comparten la misma idea base: explorar el espacio de estados buscando una solución barata, pero varían en:
A*, IDA*, UCS, B&B: exploran sistemáticamente el espacio.
Garantías fuertes (completud, optimalidad) pero costo exponencial.
SA, GA, Monte Carlo, RL: exploran con azar.
Escalan mejor, pero ofrecen convergencia en probabilidad, no certeza absoluta.
Gradient Descent/Ascent y variantes.
Excelente para modelos diferenciables (deep learning, control).
| Algoritmo | Función de prioridad | Completitud | Optimalidad |
|---|---|---|---|
| BFS (unit costs) | Profundidad g(s) | ✔ Sí | ✔ Sí (costos iguales) |
| Greedy Best-First | h(s) | ✖ No siempre | ✖ No |
| A* | f(s)=g(s)+h(s) | ✔ Sí | ✔ Sí (h admisible) |
def astar(start, is_goal, neighbors, cost, h):
open = PriorityQueue()
open.push((h(start), 0, start))
g = {start: 0.0}
parent = {start: None}
while open:
f, gs, s = open.pop_min()
if is_goal(s):
return reconstruct(parent, s), g[s]
if gs > g[s]:
continue
for t in neighbors(s):
c = gs + cost(s, t)
if t not in g or c < g[t]:
g[t] = c
parent[t] = s
open.push((c + h(t), c, t))
return None, inf
// i-th bit set?
bool has(int mask, int i) { return mask & (1 << i); }
// add i
int add(int mask, int i) { return mask | (1 << i); }
// remove i
int rem(int mask, int i) { return mask & ~(1 << i); }
Ejemplo clásico: Subset Sum.
| Enfoque | Tiempo | Memoria |
|---|---|---|
| Fuerza bruta | O(2ⁿ) | O(1) |
| MitM balanceado | O(2ⁿ⁄² · n) | O(2ⁿ⁄²) |
Arreglo de ejemplo: [3, 9, 10, 15, 20, 7], objetivo T = 25.
En mínimos locales, el gradiente es 0. En funciones convexas suaves, GD converge al mínimo global con una tasa O(1/k) (o lineal si es fuertemente convexa).
Función de juguete: f(x, y) = x² + 0.5 y². Empezamos lejos del mínimo y seguimos −∇f.
def grad_descent(grad, x0, step=1e-2, iters=1000, tol=1e-6):
x = x0
for k in range(iters):
g = grad(x)
if (g * g).sum()**0.5 < tol:
break
x = x - step * g
return x
Hill climbing es la versión codiciosa: siempre tomamos el mejor vecino que mejore la energía (o costo).
Función rugosa f(x) = x²/8 + 0.5 sin(3x) + 0.2 cos(5x). Observa cómo hill climbing puede quedarse atrapado.
La temperatura T controla cuánto exploramos. Al inicio T alta ⇒ más azar; al final T baja ⇒ comportamiento tipo hill climbing.
Usamos la misma función 1D rugosa. Cada punto es un individuo.
def ga_binary(fitness, d, pop=100, gens=200, pc=0.9, pm=None, elite=2):
import random
pm = (1.0/d) if pm is None else pm
P = [[random.randint(0,1) for _ in range(d)] for _ in range(pop)]
def tournament(P, k=3):
cand = random.sample(P, k)
return max(cand, key=fitness)
def crossover(p, q):
if random.random() > pc: return p[:], q[:]
i = random.randint(1, d-1)
return p[:i] + q[i:], q[:i] + p[i:]
def mutate(x):
for i in range(d):
if random.random() < pm:
x[i] ^= 1
for _ in range(gens):
P.sort(key=fitness, reverse=True)
nextP = P[:elite]
while len(nextP) < pop:
p, q = tournament(P), tournament(P)
c1, c2 = crossover(p, q)
mutate(c1); mutate(c2)
nextP += [c1, c2]
P = nextP[:pop]
best = max(P, key=fitness)
return best, fitness(best)
Piensa en un videojuego tipo CartPole o Pong. El agente no tiene una lista de "si pasa esto, haz esto". En vez de eso:
| Componente RL | ¿Qué significa? | Ejemplo intuitivo |
|---|---|---|
| Estado s | Lo que el agente observa del mundo en este instante. | En CartPole: posición del carrito, ángulo y velocidad de la barra. |
| Acción a | Decisión que el agente puede tomar. | Empujar el carrito a la izquierda o derecha. |
| Recompensa r | Número que dice qué tan buena fue esa acción. | +1 por cada timestep que la barra siga arriba; 0 si se cae. |
| Política π(a|s) | Regla que dice qué acción tomar en cada estado. | "Si la barra se inclina a la derecha, empujo a la derecha" (pero aprendida, no programada a mano). |
No necesariamente usa Q-learning; puede usar Policy Gradients, PPO, etc. Q-learning es sólo una forma concreta de hacer RL.
Q-learning intenta aprender una función Q(s,a) que diga: "Si estoy en el estado s y tomo la acción a, ¿cuál será mi recompensa total esperada (hacia el futuro)?".
Supón un pasillo lineal con 3 estados:
Acciones posibles: Izq o Der. Solo en G obtienes recompensa +1.
| Estado | Q(·, Izq) | Q(·, Der) | Mejor acción (política π) |
|---|---|---|---|
| S1 | 0.05 | 0.40 | Der (ir hacia S2) |
| S2 | 0.10 | 0.80 | Der (ir a G) |
| G | 0.00 | 0.00 | No importa (episodio termina) |
Así se interpreta una Q-table: en cada fila (estado), elegimos la acción con Q más alta como la acción "natural" de la política.
Ambiente mini 4×4:
| Familia | Ejemplos | Garantía | Escalabilidad |
|---|---|---|---|
| Determinista óptimo | A*, IDA*, UCS | Óptimo global | Exponencial |
| Acotado / podas | Branch-and-Bound, MitM | Óptimo si bounds sólidos | Mejor, depende de bounds |
| Local / heurístico | Hill Climb, GD | Mínimos locales | Muy escalable |
| Estocástico | SA, GA, RL | Óptimo en probabilidad | Poly–Exp |
1. ¿Qué hace a A* óptimo sobre grafos con costos positivos?
2. ¿Qué mejora principal aporta Meet-in-the-Middle para problemas tipo subset sum?
3. En Simulated Annealing, un movimiento que empeora la energía (ΔE > 0) se acepta con probabilidad:
4. ¿Qué componente NO es típico de un algoritmo genético?
5. En Q-learning tabular, ¿qué se almacena típicamente en la tabla Q?
6. ¿Cuándo es más razonable preferir GA / SA sobre A*?