Esquemas Algorıtmicos 20011130 1. (2,5p) Una empresa de

Transcripción

Esquemas Algorıtmicos 20011130 1. (2,5p) Una empresa de
Esquemas Algorı́tmicos
20011130
1. (2,5p) Una empresa de alquiler de vehı́culos debe renovar su flota de
forma que ningún automóvil en activo tenga más de N = 10 meses y
para planificar las compras dispone de la previsión del precio de los
automóviles para los próximos M = 100 meses (los precios pueden
subir o bajar cada mes):
w1 , w2 , ..., wM
El objetivo es gastar el menor dinero posible en adquisiciones, decidiendo en qué meses renovar los vehı́culos. Escribe un algoritmo recursivo y justifica que el coste temporal es exponencial en función de N .
Transfórmalo mediante programación dinámica recursiva y justifica el
coste en función de N y M .
Ayuda: Una función f (m, n) que puede ser adecuada representa el
coste mı́nimo durante los m primeros meses, si al final tenemos un
automóvil de antigüedad máxima n meses.
2. (2,5p) Transforma el algoritmo de la pregunta anterior en un algoritmo
de programación dinámica iterativa. Explica cómo mejora la eficiencia
temporal. ¿Es posible reducir el uso de memoria?
3. (3p) Supongamos que abordamos el problema anterior mediante ramificación y poda y que representamos la solución como un vector de M
enteros x1 , ..., xM , donde xi = 1 indica que hay que comprar y xi = 0
que no se compra. Dado un estado (x1 , ..., xm , ∗, ∗, ...∗), ¿qué funciones
de cota emplearı́as?
4. (2p) Discute las ventajas e inconvenientes de usar para el recorrido de
un árbol en ramificación y poda:
(a) un recorrido en profundidad (algoritmo de retroceso);
(b) un recorrido guiado por la función de cota optimista g.
Esquemas Algorı́tmicos
20011130
1. Supondremos que inicialmente la empresa no tiene vehı́culos y que
el primer mes es necesario comprar. A partir de ese momento, la
decisión de renovar o no un vehı́culo debe tener en cuenta dos efectos
contrapuestos:
(a) por un lado conviene alargar al máximo la antigüedad del vehı́culo;
(b) por otro, conviene aprovechar los momentos de precios bajos.
Supongamos que f (m, n) es la inversión mı́nima para mantener la
flota durante m meses, siempre y cuando la última renovación se haya
producido en los n meses anteriores. Esto significa que m es un entero
entre 1 y M y n un valor entre 0 y N . En ese caso, es trivial que
f (1, n) = w1
dado que el primer mes es preciso adquirir los vehı́culos.
Por otra parte, en el mes m > 1 puede decidirse renovar (xm = 1) o no
(xm = 0) la flota. Como no sabemos a priori cuál de las dos opciones
es la óptima, debemos comparar ambas y tomar la que produce una
inversión mı́nima. Si no se renueva, es obvio que la inversión es la
misma que para los m − 1 meses anteriores, aunque entonces, la antigüedad máxima es n−1. Si se toma la decisión contraria, entonces la
inversión aumenta en wm respecto a la necesaria para los m − 1 meses
anteriores, pero la antigüedad máxima será N − 1. En resumen:
f (m, n) = min{f (m − 1, n − 1), wm + f (m − 1, N − 1)}
Sin embargo, la fórmula anterior no es correcta debido a dos causas:
(a) la recursión no se detiene debido a que no se han incluido los
casos triviales;
(b) si n = 0 es preciso renovar (no tiene sentido n − 1).
Por ello, la fórmula correcta es:


si m = 1
w1
f (m, n) = wm + f (m − 1, N − 1)
si m > 1 ∧ n = 0


min{f (m − 1, n − 1), wm + f (m − 1, N − 1)} en los demás casos
Expresado como pseudocódigo:
2
int w[M+1];
// datos
int fr (int m, int n) {
if( m == 1 ) return w[1];
else if( n == 0 ) return w[m]+fr(m-1, N-1);
else return min( fr(m-1, n-1), w[m]+fr(m-1, N-1) );
}
Es evidente que en el peor caso, el coste temporal del algoritmo crece
exponencialmente con M : si N es grande, siempre se realizan dos
llamadas recursivas que generan un árbol binario de profundidad M ,
esto es, con un número de nodos del orden de 2M .
Transformado mediante programación dinámica queda:
int w[M+1];
int A[M+1][N+1];
// almacén
int pdr (int m, int n) {
if( A[m][n] < 0 ) {
// si no está calculado
if ( m == 1 )
A[m][n] = w[1];
else if ( n == 0 )
A[m][n] = w[m]+pdr(m-1, N-1);
else
A[m][n] = min( pdr(m-1, n-1), w[m]+pdr(m-1, N-1) );
}
return A[m][n];
}
int f (int M, int N) {
for (int m=0; m<=M; m++)
for (int n=0; n<=N; n++)
A[m][n]=-1;
// inicialización
return pdr(M, N);
}
El coste temporal es ahora distinto. La función principal tiene un
bucle doble con coste M N . La función recursiva no puede ser llamada
más de (M + 1)(N + 1) + 1 veces: una desde la función principal y dos
por cada valor del almacén que puede ser cero en la lı́nea recursiva.
Por tanto, el coste temporal es O(M N ).
2. Una versión iterativa del algoritmo anterior requiere que A[m−1][n−1]
y A[m − 1][N ] hayan sido calculados previamente. Esto es sencillo de
3
conseguir, ya que basta con que los elementos se calculen por orden
creciente de m:
int w[M+1];
int A[M+1][N+1];
// almacén
int pdi (int M, int N) {
for (int m=0; m<=M; m++) {
for (int n=0; n<=N; n++) {
if ( m == 1 )
A[m][n] = w[1];
else if ( n == 0 )
A[m][n] = w[m]+A[m-1][N-1];
else
A[m][n] = min( A[m-1][m-1], w[m]+A[m-1][N-1] );
}
}
return A[M][N];
}
Es evidente que el coste es ahora proporcional al número de iteraciones
del algoritmo, esto es, a M N . Por otro lado, es posible utilizar menos
memoria si se guardan sólo los resultados de la fila m − 1 de la matriz.
Esto se puede conseguir sustituyendo A[m] por A[m%2] y A[m − 1]
por A[1 − m%2] en el algoritmo anterior.
3. Es evidente que
g(x1 , ..., xm , ∗, ..., ∗) =
m
X
wk xk
k=1
es una cota optimista del gasto que hay que realizar. Sin embargo,
dado que es preciso realizar algunas renovaciones en el tiempo restante
se puede proceder de la siguiente manera:
P
(a) Tómese g = m
k=1 wk xk .
(b) Sea i el último ı́ndice tal que xi = 1 (última renovación).
(c) Cálculese el número mı́nimo de renovaciones necesarias en el tiempo restante: µ = (M − i)/N .
(d) Cálculese los µ valores menores entre wm+1 , ..., wM (el coste de
esta operación es lineal).
(e) Súmense dichos valores a g.
4
Una función de cota pesimista se puede obtener tomando cualquier
decisión de las posibles. Una que no conducirá a resultados muy malos
es prolongar al máximo la vida de los vehı́culos, esto es,
P
(a) Hágase u = m
k=1 wk xk .
(b) Tómese i, el último ı́ndice tal que xi = 1 (última renovación).
(c) Mientras i + N < M hágase i = i + N y súmese wi a u.
En lo anterior hemos supuesto que x1 , ..., xm es una solución para los
primeros m meses.
4. El recorrido en profundidad tiene la ventaja esencial de que la lista
de estados vivos crece moderadamente (proporcionalmente sólo a la
profundidad del árbol). El inconveniente mayor es que si empieza por
una rama con soluciones malas, no pasará a otra hasta que explore la
rama completamente, con lo que es probable que nunca encuentre una
solución aproximada.
El recorrido inteligente aumenta la probabilidad de encontrar buenas
soluciones aproximadas. Sin embargo, suele conducir rápidamente al
agotamiento de la memoria disponible.
5

Documentos relacionados