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