Segundo Ingenierı́a Técnica en Informática de Gestión y de
Sistemas. Febrero 2004 1) Dadas tres matrices A, B y C de dimensiones n × n, cuyos valores son dı́gitos entre 0 y 9, se quiere modificar la matriz C de manera que contenga en cada fila i la ”menor”de las filas i de las matrices A y B, en caso de que no coincidan, y cuando estas filas coinciden dejar en la fila de C el valor original. Se resuelve el problema con el siguiente esquema: para i = 1, 2, . . . , n menor(A[i],B[i],C[i]) finpara donde: menor(a,b,var c:array [1,...,n] de 0,...,9): i=1 mientras i ≤ n Y a[i] = b[i] i=i+1 finmientras si i ≤ n si a[i] < b[i] copiar(a[i],c[i]) en otro caso copiar(b[i],c[i]) finsi finsi donde la función copiar copia cada uno de los elementos del primer vector en las posiciones del segundo vector. a) (1 puntos) Obtener Ω, O y θ del tiempo de ejecución. b) (0.5 puntos) Obtener θ del tiempo promedio. c) (0.75 puntos) Obtener o (o-pequeña) del número promedio de instrucciones que se ejecutan, suponiendo que en cada posición es equiprobable que aparezca uno de los dı́gitos del 0 al 9. d) (0.75 puntos) ¿Qué se puede decir sobre los tiempos de ejecución si los elementos de las matrices son cadenas de caracteres de longitud m en vez de ser dı́gitos, las comparaciones se hacen carácter a carácter y las copias consisten en copiar referencias al principio de la cadena? Solución: a) La función copiar tiene un orden lineal en n, ya que se trata de copiar cada uno de los elementos del vector origen. P El tiempo de ejecución tendrá la forma ni=1 tmenor (i), con tmenor el tiempo de la función menor. En cada llamada a la función menor se puede llegar hasta el final del bucle mientras, y no se copia nada de las matrices A y B en la C, con lo que el coste es lineal; o se sale del bucle sin llegar al final, con lo que se realiza una copia de n elementos. De este modo sabemos que en la función menor, tanto el caso¡más ¢ P favorable como el más desfavorable tiene tiempo lineal, y por tanto el tiempo es t(n) = ni=1 an ∈ θ n2 . b) Dado que hemos obtenido el valor de θ independientemente de la forma de la entrada, este valor es válido también para el tiempo promedio. c) Para calcular el o del número de instrucciones consideramos que el interior del bucle para se ejecuta n veces, y por cada pasada hay que contar el número promedio de instrucciones en la función P menor: tp (n) = n + 1 + ni=1 tpmenor (i). Como sabemos que el orden es n2 sólo nos interesa este término en el sumatorio. En menor se ejecuta la inicialización de la variable, el paso por el bucle, y la comparación, tras la cual posiblemente se copia un vector: tpmenor = 1 + tmientras (n) + tsi . Para calcular tmientras consideramos que se hace una comparación más que veces se entra, con lo que la última comparación para no entrar al interior del bucle la consideramos aparte, y se entra una primera 1 vez si los dos primeros dı́gitos son iguales, lo que ocurre con probabilidad 10 , se entra una segunda vez ³ ´2 1 si los dos primeros son iguales y los dos segundos también (probabilidad 10 ). En general tenemos ³ ´2 ³ ´n 1 − 1 1 1 1 11 tmientras = 1 + 2 10 +2 10 + ... + 2 10 = 1 + 2 10 1−101n+1 < 9 . Por tanto, el paso por el bucle no 10 afecta al término de mayor orden del tiempo de ejecución, ni a o. La función copiar se ejecuta si no coinciden las filas en las matrices A y B, y esto ocurre con probabilidad 1 − 101n . Si en cada copia se ejecutan 2n instrucciones (paso por un ¡bucle¢ y actualización de un elemento), el término de mayor orden en la función menor es 2n, y tp (n) ∈ o 2n2 . d) La única diferencia con el caso anterior es el coste de las comparaciones, pues las asignaciones siguen haciéndose en una única instrucción. En el caso en que las cadenas sean distintas en el primer carácter tenemos una única operación por comparación, y los costes coincidirı́an con los calculados. En el caso más desfavorable de que las cadenas sean iguales hasta el último carácter los costes cambian multiplicándose por m, salvo la o del promedio, pues se obtenı́a de la parte de copia de la función menor, y el coste de la copia no ha cambiado. Tiene más sentido considerar valores aleatorios en las cadenas, y por lo tanto que el tiempo de ejecución de la comparación es el promedio. Para calcular el coste de las comparaciones en este caso se sigue el mismo razonamiento que para la comparación de dos filas. En este caso el coste promedio de las ³ ´2 ³ ´m 1 1 1 comparaciones es 1 + 2 10 + 2 10 + . . . + 2 10 , que también está acotado por una constante, con lo que los costes siguen siendo los mismos que cuando tenemos dı́gitos. 2) (4 puntos) En una celebración a la que asisten n invitados que se sentarán en m mesas, con n múltiplo de m, se pretende hacer una asignación óptima de los invitados a las mesas. Para esto se dispone de una tabla de ”amistad” a:array[1,...,n][1,...,n] de naturales, donde a[i, j] indica la amistad que siente la persona i hacia la j. La tabla no es simétrica (la amistad no es recı́proca). El grado de amistad en una mesa es la suma de los grados de amistad entre todos los sentados a ella. Se tratará de maximizar la suma de todos los grados de amistad de las mesas. a) (1 punto) Dar un esquema de un método de avance rápido para este problema. b) (2 puntos) Se puede resolver por backtracking con un esquema no recursivo de optimización. Indicar cómo serı́a el árbol de soluciones y cómo se representarı́an estas, qué variables se usan en la solución por backtracking, y programar las funciones generar, retroceder, solucion, criterio (no es necesario que esté optimizado) y mashermanos. c) (1 punto) Para resolver el problema por Branch and Bound indicar alguna forma de calcular las cotas inferior y superior y la estimación del beneficio en cada nodo, y cómo el método de avance rápido se puede utilizar en combinación con el Branch and Bound. Solución: a) El problema consiste en asignar los n invitados a las m mesas, por lo que la solución se almacena en un vector s:array[1,...,n] de 0,...,m, que se inicializa a cero para indicar que no se ha asignado mesa a ningún invitado. Podemos empezar completando mesa a mesa, asignando un invitado aleatoriamente a la primera mesa, después sentando a esa mesa al invitado con el que se maximiza la amistad con el ya asignado, a continuación eligiendo al que maximiza la amistad con los dos asignados, etc. Cuando se acaba de asignar invitados a una mesa se pasa a completar del mismo modo otra, etc. Además de s utilizamos una tabla de asignaciones t=array[1,...,m][1,...,n/m] de 0,...,n, que indicará en t[i, j] cuál es el j-ésimo invitado en la mesa i. Un esquema será: //Para cada mesa para mesa = 1, . . . , m invitado = 1 mientras s[invitado] 6= 0 invitado = invitado + 1 finmientras //Elige el primero que encuentra sin asignar s[invitado] = mesa t[mesa, 1] = invitado pos = 2 maximo = −1 para i = 1, . . . , n //Para cada no asignado si s[i] = 0 suma = 0 para j = 1, . . . , pos − 1 suma = suma + a[t[mesa][j]][i] + a[i][t[mesa][j]] finpara //Calcula su amistad con todos los ya asignados a la mesa si suma > maximo maximo = suma invitado = i finsi finsi finpara //Y asigna a la mesa al que maximiza la amistad s[invitado] = mesa t[mesa, pos] = invitado pos = pos + 1 finpara b) El árbol puede tener n niveles más el raı́z. En el nivel i se decidirá la mesa a la que se asigna el invitado i. Por tanto, cada nodo no terminal tendrá m hijos, que corresponden a las m mesas donde se puede asignar el invitado. La solución se almacena en un array s, como el del apartado a). Al ser un problema de optimización llevaremos una solución óptima actual (SOA), que es un array de las mismas dimensiones que s, que estará inicializado a 0, y que al final contendrá la solución óptima. En VOA guardamos la suma de los grados de amistad de las distintas mesas correspondiente a la solución almacenada en SOA. Está inicializado a 0. La variable nivel indica el nivel del árbol por el que vamos. Cuando vuelva a valer 0 es porque se ha recorrido todo el árbol y se acaba la ejecución. También usamos una tabla t como la del apartado a), pues es necesario guardar la asignación que se está haciendo, para comparar su valor con el de la solución óptima que se lleve. n Además, utilizamos un array pm:array[1,...,m] de 0,..., m , para indicar las posiciones ocupadas en cada mesa. Estará inicializado a 0, y es necesario porque no se asignan todos los invitados a una mesa antes de empezar con la siguiente, al contrario de lo que se hacı́a en el apartado a). El esquema del algoritmo puede ser: inicializar variables nivel = 1 generar(s,t,pm,nivel) repetir si solucion(s,pm,nivel) ga=gradoamistad(t) si ga >VOA VOA=ga SOA← s finsi finsi si criterio(s,pm,nivel) generar(s,t,pm,nivel) en otro caso mientras nivel 6= 0 Y NO mashermanos(s,nivel) retroceder(s,pm,nivel) finmientras si nivel 6= 0 generar(s,t,pm,nivel) finsi finsi hasta nivel = 0 Un nodo es solución si es terminal y el último invitado que se ha asignado completa la última mesa que quedaba: solucion(s,pm,nivel): n devolver nivel = n Y pm[s[nivel]] = m Se puede seguir por un nodo si no es terminal y si no hemos asignado invitados de más a una mesa: criterio(s,pm,nivel): n devolver nivel 6= n Y pm[s[nivel]] ≤ m Se podrı́a intentar incluir algún criterio para desechar nodos desde los que no se puede mejorar el valor VOA. Esto se puede hacer calculando los grados de amistad que se lleva en cada mesa (como se hace en el apartado a)), y completándolos con los valores máximos de amistad que se pueden poner, y si el valor que da es menor que VOA no seguir. Dado que no nos piden una función criterio optimizada no la programaremos. Un nodo tiene más hermanos si no corresponde a intentar asignar a la última mesa: mashermanos(s,nivel): devolver s[nivel] < m Al generar un nodo se asigna a un invitado una mesa, se apunta un ocupante más en la mesa, y en t se pone la posición que ocupa el invitado en la mesa. Si no es el primer hijo de un nodo ya se habı́a intentado asignar al invitado una mesa, por lo que hay que deshacer esa asignación restando uno al número de ocupantes de la mesa antes de volver a asignarle la siguiente. generar(s,t,pm,nivel): si s[nivel] 6= 0 pm[s[nivel]] − − finsi s[nivel] + + pm[s[nivel]]++ t[s[nivel]][pm[s[nivel]]] = nivel Al retroceder se desasigna el invitado a la mesa en la que estaba antes de deshacer la asignación y retroceder en el árbol. retroceder(s,pm,nivel): pm[s[nivel]] − − s[nivel]] − − nivel − − No es necesario deshacer las asignaciones en t pues quedan invalidadas al contabilizar en pm un invitado menos en la mesa. c) El árbol coincidirá con el del backtracking del apartado b), aunque se recorre en otro orden. Se puede utilizar la misma tabla t del apartado anterior, y a partir de ella calcular la cota inferior en un nodo de nivel nivel sumando los grados de amistad de todas las mesas, con los invitados ya asignados a la mesa: P Ppm[i] Pj CI(nodo)= m i=1 j=1 k=1 (a[t[i][j]][t[i][k]] + a[t[i][k]][t[i][j]]) También se puede usar un avance rápido para calcular una solución a partir de un nodo. De esta forma la solución óptima a partir del nodo tiene valor mayor o igual a la obtenida con avance rápido, por lo que este sirve como cota inferior. Habrı́a que plantear un problema de avance rápido pero con los huecos que quedan en las mesas, las asignaciones ya realizadas, y sumando el valor calculado al que ya tenemos en CI(nodo). Para la cota superior podemos considerar que todas las asignaciones que quedan por hacer corres- ponden al mayor grado de amistad de la tabla. Para esto se calcula el máximo de la tabla al principio (coste n2 ), y será: P Pn P j CS(nodo)=CI(nodo)+ m i=1 m j=pm[i]+1 k=1 2 ∗ maximo Se puede mejorar (reducir la cota superior) calculando el máximo de cada fila y columna (lo que también se hace con coste n2 ), con lo que tendrı́amos la suma de tres términos: los grados de amistad entre ya asignados (que es el primer valor de la CI que hemos dado); otra parte de ya asignados con otros invitados que no sabemos los que son, por lo que se toma el máximo de la fila y columna para el invitado ya asignado; y por último las amistades entre invitados todavı́a no asignados, para los que se utiliza el máximo de toda la tabla: ¡n ¢ Pm ¡n ¢ P Ppm[i] CI(nodo)+ m i=1 j=1 (maxf il[j] + maxcol[j]) m − pm[i] + i=1 2 ∗ maximo m − pm[i] − 1 La estimación del beneficio puede ser cualquiera de las dos cotas o la media, pero, ya que tenemos un método de avance rápido, puede usarse este para una estimación más próxima al óptimo. 3) (2 puntos) Se dispone de un dispositivo hardware con el que se puede evaluar eficientemente el contenido de una matriz cuadrada de datos de tamaño menor o igual que b × b que representan una imagen. En la evaluación se pueden utilizar varias funciones; una de ellas es maxcadena, con la que se obtiene la zona contigua en vertical u horizontal de datos iguales (un vector fila o columna). La cabecera de maxcadena es: maxcadena(a:array[1,...,n][1,...,n] de datos;var x,y,z,t:enteros) donde a es la matriz de datos, n representa la dimensión de la matriz, y la zona más grande de datos contiguos tiene extremos en las posiciones (x,y) y (z,t) del array. Programar por divide y vencerás la solución del mismo problema para matrices cuadradas de datos con dimensiones mayores que b × b, basándonos en la función que se puede ejecutar eficientemente para problemas de tamaño menor que b × b. Solución: Dividiremos cada problema en cuatro subproblemas de dimensiones n2 × n2 . Puede que durante la ejecución el tamaño con que se trabaja no sea par, y que las submatrices no sean cuadradas. Por no com- plicar la solución consideraremos que las submatrices siguen siendo cuadradas. Esto se puede conseguir, por ejemplo, completando la matriz de tamaño n hasta otra de tamaño 2k b, con k el menor valor tal que n ≤ 2k b. Los valores que se ponen en la zona ampliada deben ser distintos entre sı́ y también de los que tienen adyacentes en la matriz a original. Los problemas se dividirán recursivamente hasta llegar a problemas de dimensión menor o igual que b × b. En la combinación compararemos las cuatro subsoluciones óptimas quedándonos con la mejor, pero, dado que una cadena máxima puede quedar cortada en horizontal o vertical, habrá que evaluar también las fronteras. El esquema será: DV(a:array[1,...,n][1,...,n] de datos;n:entero;var x,y,z,t:entero) si n ≤ base maxcadena(a,x,y,z,t) en otro caso DV(a, n2 ,x11,y11,z11,t11) DV(&a[1][n/2 + 1], n2 ,x12,y12,z12,t12) DV(&a[n/2 + 1][1], n2 ,x21,y21,z21,t21) DV(&a[n/2 + 1][n/2 + 1], n2 ,x22,y22,z22,t22) combinar(a,x11,y11,z11,t11,x12,y12,z12,t12,x21,y21,z21,t21,x22,y22,z22,t22,x,y,z,t) finsi Donde combinar tiene la forma: combinar(a,x11,y11,z11,t11,x12,y12,z12,t12,x21,y21,z21,t21,x22,y22,z22,t22;var x,y,z,t) x = x11 y = y11 z = z11 t = t11 si z − x + t − y < z12 − x12 + t12 − y12 x = x12 y = y12 z = z12 t = t12 finsi si z − x + t − y < z21 − x21 + t21 − y21 x = x21 y = y21 z = z21 t = t21 finsi si z − x + t − y < z22 − x22 + t22 − y22 x = x22 y = y22 z = z22 t = t22 finsi //Se comprueba la frontera en cada fila para i = 1, 2, . . . , n si a[i][n/2] = a[i][n/2 + 1] j = n/2 − 1 mientras j > 0 Y a[i][j] = a[i][n/2] j =j−1 finmientras k = n/2 + 2 mientras k < n + 1 Y a[i][k] = a[i][n/2] k =k+1 finmientras si z − x + t − y < k − j − 1 x=i y =j+1 z=i t=k−1 finsi finsi finpara //Y en cada columna para j = 1, 2, . . . , n si a[n/2][j] = a[n/2 + 1][j] i = n/2 − 1 mientras i > 0 Y a[i][j] = a[n/2][j] i=i−1 finmientras k = n/2 + 2 mientras k < n + 1 Y a[k][j] = a[n/2][j] k =k+1 finmientras si z − x + t − y < k − i − 1 x=i+1 y=j z =k−1 t=j finsi finsi finpara 4) (1 punto) Dada una cadena, c:array[1,...,n] de caracteres, se quiere contar el número de subcadenas contiguas ascendentes, c[i] < c[i+1] < . . . < c[j]. En la cadena bacbd tenemos las subcadenas ascendentes: b, a, ac, c, b, bd, d. Aunque este problema se puede resolver fácilmente en tiempo lineal, queremos resolverlo con un esquema de programación dinámica, para lo que hay que obtener la fórmula de recursión, los valores de los casos base, e indicar las tablas que se usan y cómo se obtiene la solución, que es el número de subcadenas ascendentes, sin necesidad de obtener las cadenas. Solución: Podemos ir obteniendo el número de subcadenas ascendentes desde la primera posición de la cadena hasta llegar a la última, con lo que ponemos el problema hasta la casilla i en función de los problemas hasta casillas anteriores a la i: N (i) = 1 + N (i − 1) ∗ (c[i − 1] < c[i]) con lo que contamos siempre la subcadena de un único carácter y contamos tantas subcadenas como hay hasta la posición anterior si el nuevo carácter es posterior, en orden alfabético, al anterior. Como caso base tendremos N (0) = 0, para poder calcular N (1) con la misma fórmula. También se puede tomar N (1) = 1 como caso base y formar la tabla a partir de la posición 2. Sólo es necesario un array para guardar los números de subcadenas, pues no nos piden que digamos cuáles son. Y el array es unidimensional. Al acabar de completar el array se obtiene la solución sumando los valores que hay en él. Con el ejemplo el array quedará:
b a c b d 1 1 2 1 2 y el número de subcadenas es 7.