8.3-8.6-8.15

Anuncio
Ejercicio 8.3. Recorridos en un tablero N ×N . Se dispone de un tablero
N ×N cuyos casilleros están pintados de color blanco o negro (sin ningún criterio
definido). Se debe dar todas las maneras (si hay) de llegar del casillero (1, 1)
al casillero (N, N ) pisando sólo casilleros blancos. Desde un casillero (i, j) es
posible moverse sólo en las direcciones N, S, E, O. No se permiten movimientos
en diagonal. Utilice el método de backtracking para resolver el problema.
Dado que no se dispone de niguna información a cerca de como estan distribuidos los cuadros negros en el tablero, la única manera de listar todas las
soluciones es mediante la técnica de backtracking. Vamos a describir de manera precisa cual es el grafo cuyo recorrido nos proporciona las soluciones del
problema. Por supuesto, este grafo no tiene existencia real en el programa que
computa los caminos.
Representamos al tablero como una matriz T de tamaño N ×N con entradas
en el conjunto {B, N }, representando estos últimos a los casilleros blancos y
negros. Desde el casillero p = (i, j) sólo es permitido ”avanzar” al casillero
de arriba: (i − 1, j) si i > 1 ,
de la derecha: (i, j + 1) si j < N,
de abajo: (i + 1, j) si i < N y
de la izquierda: (i, j − 1) si j > 1.
Utilizamos la notación p → q para indicar que q es uno de los movimientos
permitidos desde p. El grafo en cuestión tiene como nodos a los casilleros blancos
del tablero, y tenemos una arista de p hasta q si y sólo si p → q. Una solución
será un camino p1 , ..., pk en este grafo tal que p1 = (1, 1), pk = (N, N ).
Ejercicio: Dibuje el grafo correspondiente

B N B B
 B B B N

 N B B B

 N N N B
B B N B
al tablero

B
B 

B 

N 
B
El algoritmo caminos(T, c, p) imprime todas las soluciones cuyo tramo inicial
es c, que es un camino cualquiera desde (1, 1) hasta p. Obtenemos todas las
soluciones llamando a caminos(T, [(1, 1)], (1, 1)). Note que la precondición exige
que T [1, 1] = B. Para evitar tener que buscar la existencia de un casillero q en
la secuencia c, vamos a utilizar un marca ∗ en el tablero que nos indica que ese
casillero ya fue pisado.
proc caminos(in/out T : array[1..N, 1..N ] of {B, N, ∗},
in c : [par], in p : par)
{c es un camino en el grafo asociado a T desde (1, 1) hasta p y
para todo q, T [q] = ∗ si y sólo si q está en c}
if p = (N, N ) then mostrar(c)
1
else
for each q such that p → q do
if T [q] = B then T [q] := ∗; caminos(T, c / q, q)
else skip
Utilizando la definición de → dada arriba podemos eliminar el for del programa. Utilizamos p.1 y p.2 para denotar la primera y segunda componente de
p, resp.
proc caminos(in/out T : array[1..N, 1..N ] of {B, N, ∗},
in c : [par], in p : par)
{c es un camino en el grafo asociado a T desde (1, 1) hasta p y
para todo q, T [q] = ∗ si y sólo si q está en c}
if p = (N.N ) then mostrar(c)
else
if p.1 > 1 ∧ T [p.1 − 1, p.2] = B
then T [p.1 − 1, p.2] := ∗; caminos(T, c / (p.1 − 1, p.2), (p.1 − 1, p.2))
if p.2 < N ∧ T [p.1, p.2 + 1] = B
then T [p.1, p.2 + 1] := ∗; caminos(T, c / (p.1, p.2 + 1), (p.1, p.2 + 1))
if p.1 < N ∧ T [p.1 + 1, p.2] = B
then T [p.1 + 1, p.2] := ∗; caminos(T, c / (p.1 + 1, p.2), (p.1 + 1, p.2))
if p.2 > 1 ∧ T [p.1, p.2 − 1] = B
then T [p.1, p.2 − 1] := ∗; caminos(T, c / (p.1, p.2 − 1), (p.1, p.2 − 1))
Ejercicio 8.6. Listado de las hojas de un árbol binario.
i. Desarrolle una algoritmo recursivo que devuelva la lista de hojas de un
árbol binario T . Elimine luego la recursión de este algoritmo.
ii. Lo mismo que en i pero ahora se deben listar las hojas con su profundidad.
Por ejemplo para el árbol
h hhh i , a, h ii , b, h ii , c , h h i , d, h ii i
se debe devolver [(a, 2), (d, 1)].
i. La resolución de este problema es una simple modificación del listado de
los nodos en preorden, cuyo versión iterativa es:
Preorden de un árbol binario.
|[U : T ree; ST : Stack[T ree];
empty stack(ST );
push(T, ST );
preorden := [];
2
while not is empty stack(ST ) do
U := top(ST );
pop(ST );
if is null(U ) then skip;
else
preorden := preorden / root(U );
push(right(U ), ST );
push(lef t(U ), ST );
]|
(1)
El algoritmo utiliza la pila ST de árboles para almacenar los subárboles
del árbol original T que aún no se han listado. La secuencia preorden es una
variable de acumulación que al finalizar el algoritmo alojará al preorden de T .
Mientras el ciclo se ejecuta se mantiene la siguiente condición invariante:
”al concatenar preorden con los preordenes de los árboles alojados en ST ,
desde el tope hacia abajo, obtenemos el preorden de T ”
Dicho de manera un poco más formal, si ST = [U1 , ..., Uk ] entonces
P reorden(T ) = preorden + +P reorden(U1 ) + +...P reorden(Uk ).
Es un buen ejercicio ejecutar el algoritmo para un input chico y corroborar que
tal condición se mantiene al finalizar cada iteración.
El algoritmo de recolección de hojas utiliza la pila de la misma manera
que el preoden. La diferencia se establece en la linea (1), puesto que debemos
discriminar para almacenar solamente las hojas.
|[U : T ree; ST : Stack[T ree];
empty stack(ST );
push(T, ST );
hojas := [];
while not is empty stack(ST ) do
U := top(ST );
pop(ST );
if is null(U ) then skip;
else
if is null(lef t(U )) ∧ is null(right(U ))
then hojas := hojas / root(U );
else
push(right(U ), ST );
push(lef t(U ), ST );
]|
3
(2)
ii.Vamos a resolver ahora el problema de listar las hojas con su profundidad.
Vamos a utilizar una pila para mantener las tareas pendientes, de manera similar a lo efectuado en i, sólo que ahora la pila nos debe brindar más información,
puesto que en el momento de recolectar una hoja (linea (2) del programa anterior) debemos saber su profundidad. Luego recurrimos a una pila que contenga
pares (n, U ) donde n es un natural que indica la profundidad del subárbol U en
T . Cuando U tenga como raiz a una hoja de T , entonces n será la profundidad
de tal hoja. La variable P alojaráa una par de este tipo y utilizamos P.1 y P.2
para denotar la primera y segunda componente de p, resp.
ST : Stack[N at × T ree]
|[U : T ree; P : N at × T ree; ST : Stack[N at × T ree];
empty stack(ST );
push( (0, T ) , ST );
hojas := [];
while not is empty stack(ST ) do
P := top(ST );
pop(ST );
if is null(P.2) then skip;
else
if is null(lef t(P.2)) ∧ is null(right(P.2))
then hojas := hojas / P ;
else
push( (P.1 + 1, right(U )) , ST );
push( (P.1 + 1, lef t(U )) , ST );
]|
Ejercicio 8.15. Orden topológico.
a. Pruebe que un grafo dirigido acı́clico tiene siempre un vértice con valencia
entrante 0.
b. Sea G = (V, A) un grafo dirigido acı́clico, con |V | = n. Un orden
topológico para G es un orden para sus vértices tal que si existe un camino
desde v hasta w, entonces v aparece antes que w en ese orden. Pruebe por
inducción constructiva que bajo las hipótesis dadas siempre existe un orden
topológico. (El argumento inductivo debe arrojar el orden.) Ayuda: si v tiene
valencia entrante 0, entonces claramente puede ocupar el primer lugar en el
orden.
c. Implemente de manera eficiente el algoritmo dado en b. Para esto deberá
en cada paso disponerse de un vertice con valencia entrante 0. Para esto se
llevará:
i. Un array que dé para cada vértice su valencia entrante (Problema
61.a). Cuando se selecciona un vértice de valencia entrante 0, se le ”quita” del
grafo y habrá que actualizar el arreglo de incidencias.
ii. Una pila o cola que mantenga los vértices con valencia entrante 0
del grafo actual.
4
a. Supongamos que todo vértice tiene valencia ”in” mayor que 0, y sea
v0 un vértice de G = (N, A). Entonces existe v1 ∈ N tal que (v1 , v0 ) ∈ A.
Esta construcción se puede efectuar indefinidamente. En efecto, supongamos
seleccionados v0 , ..., vk−1 ∈ A tal que (vi , vi−1 ) ∈ A para i = 1, ..., k − 1. Como
la valencia ”in” de vk−1 es distinta de 0, entonces existe vk tale que (vk , vk−1 ) ∈
A. Como existe una cantidad finita de vértices, a partir de un determinado
momento seleccionaremos vértices que ya han sido visitados. Sea v n el primer
vértice que se repite, digamos vn = vm , con m < n. Entonces vn , vn−1 , ..., vm es
un ciclo. Luego vale el contrarecı́proco: si G es aı́clico entonces existe un vértice
con valencia ”in” igual a 0.
b. Efectuaremos un argumento inductivo sobre el número de vértices n del
grafo G = (N, A). El caso base n = 1 tiene respuesta trivial: el listado del único
vértice es un orden topológico.
Supongamos n > 1. Por el inciso (a) existe un vértice v0 con valencia ”in”
igual a 0. Sea G0 = (N − {v0 }, A0 ), donde A0 es el subconjunto de A formado
por las aristas que no tienen a v0 como origen ni como destino. Claramente G0
es un grafo dirigido acı́clico con n − 1 vértices. Luego, por hipótesis inductiva,
existe un orden topológico v1 , ..., vn−1 para los vértices de G0 .
Veamos que v0 , v1 , ..., vn−1 es un orden topológico para G. En efecto, sea
v, w vértices distintos tales que se puede llegar a w desde v por un camino c.
Veamos que v antecede a w en la secuencia v0 , v1 , ..., vn−1 .
Claramente v0 6= w, ya que v0 tiene valencia entrante 0. Si v0 = v entonces
la afirmación es trivial. Supongamos entonces que v 6= v0 6= w. Ya que v0 posee
valencia entrante 0, tenemos que ningún camino de v a w pasa por v0 . Luego
c es necesarimente un camino en G0 . Como v1 , ..., vn−1 es un orden topológico
para los vértices de G0 , tenemos que v antecede a w en la secuencia v1 , ..., vn−1 .
c. Vamos ahora a ocuparnos de algunos aspectos importantes de la implementación, principalmente los referidos a la obtención de un vértice de valencia
0. No nos referiremos a los detalles relativos a la implementación del grafo G
(input del algoritmo) ni de la lista s en la que se muestra al resultato. De esta
manera el programa podrá ser adaptado para distintas implementaciones.
NOTA: En el algoritmo utilizaremos la notación
(1)
for each w such that (v, w) ∈ A do ...
Usualmente el grafo viene dado en una lista o arreglo en la que se dipone para
cada vértice la lista de sus adyacentes. Por ejemplo G podrı́a ser un arreglo
que en el lugar G[v] aloja la lista de todos los w tales que (v, w) ∈ A. En
este caso, (1) deberá ser reemplazado por un recorrido por la lista G[v]. Otra
posibilidad es que G esté dado mediante una matriz de booleanos, de manera
que G[v, w] = true si y sólo si (v, w) ∈ N . En tal caso para implementar (1)
recorreremos la fila v de la matriz.
5
Consideraciones similares valen para la variable s en la que se muestra el
orden topológico. Utilizaremos por ejemplo la instrucción
insert v at the end of s
La variable s podrı́a ser por ejemplo un cola, o un arreglo con un puntero al
final. FIN NOTA.
El algoritmo desarrollado en b consiste básicamente en extraer en cada paso
un vértice v de valencia ”in” 0 del grafo. El vértice v se agrega a la lista como
último elemento, y el grafo se ”achica” quitando el vértice v y todas sus aristas. Denotaremos mediante G − {v} a este grafo. Dado que en cada paso sólo
necesitamos conocer un vértice de valencia 0 del grafo ”actual” G, no es necesario mantener a G como variable de estado. Basta con mantener un cola que
contenga a los vértices con esta propiedad. Por supuesto, se debe resolver el
siguiente problema: si C es la cola que contiene a los vértices de G con valencia
”in” 0, y a G se le extrae el vértice v, ¿como debe actualizarse C de manera de
contener a los vértices con valencia ”in” 0 de G−{v}? Es claro que si w (distinto
de v) tiene valencia 0 en G, entonces tendrá también valencia 0 en G−{v}. Pero
puede ocurrir que un vértice w con valencia 1 de G pertenezca a una arista de la
forma (w, v). Luego w tendrá valencia ”in” 0 en G − {v}. Luego se infiere que es
necesario contar con la información de las valéncias ”in” de todos los vértices,
no sólo los de valencia 0. La forma indicada de resolver el problema es llevar un
arreglo de valencias ”in” V al, tal que V al[v] aloje la valencia ”in” del nodo v.
En el problema anterior se ha programado la inicialización de V al. El problema
de la actualización de V al y C en cada paso se resuelve de la siguiente manera.
Cuando se extrae el vértice v se debe restar 1 a V al[w], para todo vértice w
tal que (v, w) ∈ A. Además se debe luego recorrer V al para verificar si hay un
nuevo vértice con valencia ”in” 0, el cual se debe incorporar a C.
La función tendrá la especificación:
func topological sorting(G0 : graf o) dev s : list of items
{pre G0 acı́clico ∧ N = {1, ..., n}}
{pos s es un orden topológico para G0 }
Como mencionamos antes no es necesario llevar una variable de estado G
que represente al grafo actual. Levaremos las variables locales:
V al : array [1, n] of nat;
C : queue of items;
Mediante V al[v] = −1 indicamos que v ya no pertenece al grafo actual G.
De esta manera G queda definido a través de los vértices w tales que V al[w] ≥
0. Podemos citar entonces las condiciones que se mantendrán invariantes al
finalizar cada paso, y que constituirán el invarinte del loop principal:
6
1.
s con
2.
3.
Un sorting topologico para el grafo original G0 se obtiene concatenando
un sorting topológico para el grafo actual G.
Si w es un vértice de G entonces V al[w] es su valencia ”in” en G.
C contiene al conjunto de ls vértices w de G tales que V al[w] = 0.
Inicialización del ciclo: La inicialización de V al está dada por el algoritmo
array de valencias in, desarrollado en el problema anterior. La inicialización
de C consiste en recorrer V al recolectando los vértices w tales que V al[w] = 0.
Guarda y final del ciclo: Según la condición invariante 1, obtendremos
un orden topológico para G0 cuando G sea el grafo sin nodos, que es equivalente
a decir que C es vacı́a (por qué?). Por lo tanto la guarda es ¬is empty(C).
El programa cmpleto es:
func topological sorting(G0 : graf o) dev s : list of items
{G0 acı́clico ∧ G = G0 ∧ N = {1, ..., n}}
|[V al : array [1, n] of nat;
C : queue of items;
v : nat;
array de valencias in(G, V al);
C := empty;
v := 0;
while v 6= n do if V al[v] = 0 then insert(v, C); v := v + 1;
s := [];
while ¬is empty(C) do
v := f irst(C);
dequeue(C);
V al[v] := −1;
insert v at the end of s;
for each w such that (v, w) ∈ A do
V al[w] := V al[w] − 1;
if V al[w] = 0 then insert(w, C);
end
end
]|
{pos s es un orden topológico para G0 }
7
Descargar