Ir al contenido

DFS, branch and bound e IDA*

Se expande el nodo inicial y, en cada paso, se expande uno de los nodos generados más recientemente (una pila). Si una rama no tiene éxito se hace backtrack: se vuelve al padre y se prueba otro hijo. Conviene ordenar los hijos por la probabilidad de llegar a la solución (búsqueda dirigida).

DFS(v):
Procesar(v)
for each hijo u de v:
DFS(u)
  • Para un árbol de n nodos con costo constante por nodo: Ts(n) = Θ(n).
  • Ventaja: la memoria es lineal en la profundidad d del árbol (solo la pila del camino actual), no en el número de nodos.

Se cambia el for por un pardo: cada hijo es una tarea independiente, y el padre espera a todas con SYNC (en OpenMP, taskwait).

secuencial: un nodo a la vezparalelo: una tarea por hijo1253467backtrackel número es el orden de visita · memoria O(d)tasktasktaskwait (SYNC)T∞ = O(h): la rama más larga · W = O(n)

Cómo leerlo: a la izquierda, el número es el orden en que un solo hilo visita los nodos; la flecha punteada es la vuelta atrás. A la derecha, cada subárbol coloreado es una tarea que puede correr en otro hilo; la raíz hace taskwait y no termina hasta que acaban las dos. Así, cuando DFS(root) retorna, se recorrió todo el árbol.

Trabajo y profundidad para un árbol binario balanceado

Sección titulada «Trabajo y profundidad para un árbol binario balanceado»

Las dos recurrencias de la lámina 14, resueltas paso a paso:

  • Trabajo (todas las operaciones): W(n) = 2W(n/2) + Θ(1). Por el teorema maestro (a = 2, b = 2, f = Θ(1) = O(n1−ε)) es W(n) = Θ(n): el paralelo no hace más trabajo que el secuencial.
  • Profundidad (camino crítico, los dos hijos corren a la vez): T(n) = T(n/2) + Θ(1) ⇒ T(n) = Θ(1) + Θ(1) + … (log n veces) = Θ(log n).
  • Con p procesadores (Brent): Tp = O(W/p + T∞) = O(n/p + log n).

Para un árbol general de altura h (puede no estar balanceado), el camino crítico es h: Tp = O(n/p + h). Para un grafo con n vértices y m aristas: O((n + m)/p + h).

Programa propio que recorre el árbol de la participación 9 con tareas y corte en profundidad 2 (compilado con g++ -fopenmp y ejecutado con 4 hilos):

void dfs_tareas(int v, int prof, int corte) {
for (int u : arbol[v].hijos) {
visitados++; // std::atomic<int>
if (arbol[u].solucion) { /* reportar */ }
if (prof < corte) {
#pragma omp task firstprivate(u, prof, corte)
dfs_tareas(u, prof + 1, corte);
} else {
dfs_tareas(u, prof + 1, corte); // debajo del corte: secuencial
}
}
#pragma omp taskwait
}
// main: #pragma omp parallel → #pragma omp single → dfs_tareas(raiz, 0, 2);
salida
nodos = 36, aristas = 35
DFS secuencial:
s1 en el paso 7
s2 en el paso 20
s3 en el paso 35
DFS con tareas (corte en profundidad 2):
hilo 2 encontro s3
hilo 0 encontro s1
hilo 0 encontro s2
aristas recorridas = 35 (sin trabajo redundante)

El orden en que aparecen las soluciones cambia entre ejecuciones (depende de qué hilo toma qué tarea), pero el total de aristas siempre es 35.

Es un DFS orientado a encontrar la mejor solución:

  1. Cada vez que encuentra una solución mejor, actualiza la mejor solución y su costo (la cota).
  2. Antes de bajar por una rama, estima el mejor costo posible en ella. Si ni en el mejor caso puede mejorar la cota, poda la rama.
  3. Al terminar queda la solución óptima.

Con factor de ramificación b y profundidad d, el árbol completo tiene

Nexp≤∑i=0dbi=bd+1−1b−1=Θ(bd)N_\text{exp} \le \sum_{i=0}^{d} b^i = \frac{b^{d+1}-1}{b-1} = \Theta(b^d)

Si la poda es efectiva, solo se explora una fracción α (0 < α ≤ 1): Nexp = α·bd y Ts = Θ(α·bd).

Tp≈αbdp+Θ(d),S=TsTp=O ⁣(αbdαbd/p+d),E=O ⁣(11+p dαbd)T_p \approx \frac{\alpha b^d}{p} + \Theta(d), \qquad S = \frac{T_s}{T_p} = O\!\left(\frac{\alpha b^d}{\alpha b^d/p + d}\right), \qquad E = O\!\left(\frac{1}{1 + \dfrac{p\,d}{\alpha b^d}}\right)

Cómo leer E: el término p·d / (α·bd) es “overhead / trabajo útil”. Mientras el árbol explorado (α·bd) sea mucho más grande que p·d, la eficiencia es cercana a 1. Si la poda es muy buena (α pequeño), queda poco trabajo y la eficiencia baja: paradójicamente, un buen B&B secuencial es más difícil de paralelizar bien.

Ejemplo: programación lineal entera (láminas 19–20)

Sección titulada «Ejemplo: programación lineal entera (láminas 19–20)»

Minimizar f(x) = cᵀx = 2x₁ + x₂ − x₃ − 2x₄ con xi ∈ {0, 1} y Ax ≥ b:

5x1+2x2+x3+2x4≥8x1−x2−x3+2x4≥23x1+x2+x3+3x4≥5\begin{aligned} 5x_1 + 2x_2 + x_3 + 2x_4 &\ge 8\\ x_1 - x_2 - x_3 + 2x_4 &\ge 2\\ 3x_1 + x_2 + x_3 + 3x_4 &\ge 5 \end{aligned}

El árbol fija una variable por nivel (x₁, luego x₂…). En cada nodo se pregunta: ¿todavía se puede cumplir cada restricción? Para la fila i, lo mejor que se puede lograr es sumar lo ya fijado más los coeficientes positivos de las variables libres:

∑xj fijasA[i,j] xj+∑xj libresmax⁡(A[i,j],0) ≥ bi\sum_{x_j \text{ fijas}} A[i,j]\,x_j + \sum_{x_j \text{ libres}} \max(A[i,j], 0) \ \ge\ b_i

Si alguna fila falla, la rama se poda (nodo gris en la lámina).

min f(x) = 2x₁ + x₂ − x₃ − 2x₄, x ∈ {0,1}⁴, Ax ≥ b↑ podado por costo (B&B)x₁=0x₁=1x₂=0x₂=1x₃=0x₃=1x₄=0x₄=1x₃=0x₃=1x₄=0x₄=1f = −1 (óptimo)f = 1Poda por factibilidad (círculos rojos)Σ fijas A·x + Σ libres máx(A, 0) < b_ini poniendo a 1 lo que ayuda se llega a b_iPoda por costo (recuadro)cota = Σ fijas c·x + Σ libres mín(c, 0)x = (1,1,·,·): cota = 2 + 1 − 1 − 2 = 0 ≥ −1 → no mejorasolo factibilidad: 13 nodos visitados · con cota de costo: 9 (de 31 del árbol completo)

Cómo leerlo: cada nivel fija una variable. Rojo claro = rama podada porque ya no puede cumplir alguna fila (por ejemplo x₁ = 0: en la fila 1 lo máximo es 2 + 1 + 2 = 5 < 8). Verde = solución factible. El recuadro es lo que además poda branch and bound: tras encontrar f = −1, la rama x₁ = x₂ = 1 tiene cota 2 + 1 − 1 − 2 = 0, que no mejora −1.

Recorrido verificado con un programa propio:

salida de bnb_ple.cpp
Solo poda por factibilidad (backtracking):
solucion x = (1,0,1,1) f = -1
solucion x = (1,1,0,1) f = 1
nodos visitados = 13 de 31
Branch and bound (factibilidad + cota de costo):
solucion x = (1,0,1,1) f = -1
nodos visitados = 9 de 31
optimo: x = (1,0,1,1), f = -1
Práctica: comprueba a mano por qué se poda (1, 0, 0, ·)

Con x₁ = 1, x₂ = 0, x₃ = 0 y x₄ libre, fila 1: 5·1 + 0 + 0 + máx(2, 0) = 7 < 8. No se puede cumplir aunque x₄ = 1, así que se poda sin bajar más.

Profundización iterativa e IDA* (láminas 21–22)

Sección titulada «Profundización iterativa e IDA* (láminas 21–22)»

Problema de DFS: si la solución está poco profunda pero en una rama de la derecha, DFS puede perderse mucho tiempo en una rama profunda de la izquierda (o en una infinita).

Profundización iterativa: se hace DFS con un límite de profundidad; si no hay solución, se sube el límite y se repite.

ronda 1: límite = 13 nodos visitadosronda 2: límite = 27 nodos visitadosronda 3: límite = 315 nodos visitados

Cómo leerlo: tres rondas sobre el mismo árbol binario. Cada ronda repite lo de la anterior y baja un nivel más (línea roja = límite). La meta (verde) recién aparece en la ronda 3. Se visitan 3 + 7 + 15 = 25 nodos contra 15 de un solo recorrido: la repetición cuesta poco porque el último nivel tiene casi la mitad de los nodos (con b mayor, aún menos).

IDA* usa un límite de costo en vez de profundidad. Para cada nodo x:

l(x)=g(x)+h(x)l(x) = g(x) + h(x)
  • g(x): costo de llegar desde la raíz hasta x (conocido).
  • h(x): estimación heurística del costo de x a la solución.
  • Tras una ronda fallida, el nuevo límite es el menor l(x) que superó el límite anterior (así no se salta ninguna solución).
  • Si h es admisible (nunca sobreestima el costo real), IDA* encuentra la solución óptima.

Ejemplo clásico (8-puzzle, lámina 12): g = movimientos hechos, h = número de fichas fuera de lugar (admisible: cada ficha mal puesta necesita al menos 1 movimiento).

La tabla del PDF tiene las celdas desalineadas. Reconstruida:

AlgoritmoTsT∞TpEscalabilidad
LinealO(n)O(log n)O(n/p + log n)buena
BinariaO(log n)—O(logp n)moderada
DFSO(n)O(d)O(n/p + d)buena
B&BO(bd)O(d)O(bd/p + d)alta / variable
IDA*O(K·bd)O(K·d)O(K·bd/p + K·d)alta / variable

n: elementos · b: factor de ramificación (b > 1) · d: profundidad máxima · p: procesadores · K: número de iteraciones de IDA*.

Patrón común: siempre es trabajo / p + camino crítico. Lo que cambia es cuánto trabajo hay (n, bd, K·bd) y qué tan largo es el camino que no se puede repartir (log n, d, K·d).

  • Pensar que B&B siempre explora bd nodos: ese es el peor caso; la poda reduce a α·bd.
  • Olvidar el taskwait en el DFS con tareas: la función retorna antes de que terminen los hijos y el resultado queda incompleto.
  • Crear una tarea por nodo hasta las hojas: el overhead de tareas supera el trabajo. Usar un corte.
  • Confundir g(x) (costo ya pagado) con h(x) (estimación del que falta).

Autoevaluación

0/6 correctas · 0 respondidas
1. Ventaja principal de DFS frente a BFS según la lámina 10:
2. DFS paralelo en un árbol binario balanceado: profundidad T∞ =
3. En el ejemplo de PLE, ¿por qué se poda x₁ = 0?
4. En IDA*, l(x) = g(x) + h(x). ¿Qué es h(x)?
5. Si la poda de B&B es muy efectiva (α pequeño), la eficiencia paralela…
6. El speedup superlineal en B&B paralelo puede ocurrir porque…

Fuente ·U5.3 Algoritmos-busqueda.pdf (láminas 10–23) · programas propios: dfs_tareas.cpp, bnb_ple.cpp