Tema 1: LA EFICIENCIA DE LOS ALGORITMOS 2. Supongamos diseñado un algoritmo de coste exponencial Θ(K ), usando la definición de Θ, el número de pasos requerido por el mismo lo podemos escribir como n T(n)=aK , donde a es una constante real positiva. En un cierto tiempo t y sobre un m determinado sistema se resuelve un problema de tamaño m; esto es TS(m)=aK =t. Si tenemos un sistema c veces más rápido y el mismo algoritmo, el tiempo de cálculo lo n podemos expresar como TS’(n)=(a/c)K . La pregunta es, en el mismo tiempo t, ¿qué tamaño del problema m’ se puede resolver?: n ALGORITMOS Y ESTRUCTURAS DE DATOS 2 Escuela Universitaria de Informática / Facultad de Informática Departamento de Sistemas Informáticos y Computación Universidad Politécnica de València m m’ m m’ m m’ aK =(a/c)K ⇒ K =(1/c)K ⇒ cK =K ⇒ m m m’=logk(cK )=logkc+logk K =logkc+m Para conocer la mejora obtenida en el caso de un algoritmo de coste polinómico hay que proceder de manera similar. 4. Estudio del algoritmo de búsqueda secuencial: función Búsqueda (var v:TipoVector; x:TipoBase) : entero; var i: entero; encontrado:lógico; i:=1; encontrado:=falso; mientras (i<=n) ∧ not encontrado hacer Si v[i]=x entonces encontrado:=cierto Sino i:=i+1; fmientras; si encontrado devuelve i sino devuelve 0; ffunción; Curso 2000-01 SELECCIÓN DE PROBLEMAS RESUELTOS • • • Problemas resueltos por: A. Casanova F. Marqués N. Prieto • ¿Cuál es el tamaño del problema?: El número n de elementos del vector. ¿Existen instancias significativas?. Sí. Para un número de elementos determinado, el coste del algoritmo va a depender de si el elemento buscado x está o no en el vector y en el caso de que esté, de la posición del vector en la que se encuentre. Nótese que las instrucciones de inicio, la guarda del bucle, el cuerpo del bucle y las instrucciones de después del bucle tienen un coste constante (independiente de n), y el número de veces que se realiza el bucle depende de si x está o no en el vector y en el caso en que esté, depende de la posición. ¿Cuál es la complejidad del algoritmo en el caso peor?. El caso peor corresponde a la situación de búsqueda sin éxito (x no está en v). Si tomamos como instrucción crítica de la función la evaluación de la guarda del bucle y contamos cuantas veces se repite, tenemos que: Tp(n)=n+1. ¿Cuál es la complejidad del algoritmo en el caso mejor?. El caso mejor se da cuando x se encuentra en la primera posición del vector. Para esta instancia la guarda del bucle se evalúa sólo dos veces, en la segunda encontrado está a cierto y la búsqueda ha concluido: Tm(n)=2. Estúdiese el coste medio del algoritmo suponiendo que la búsqueda siempre tiene éxito y considerando que la probabilidad de que el elemento buscado se encuentra en cualquiera de las posiciones del vector es la misma. Podemos identificar una instancia mediante la posición del vector i en la que se encuentra x, la descripción de todas las instancias posibles la haremos variando la i entre 1 y n. Si consideramos como instrucción crítica la evaluación de la guarda del bucle, el coste 2 de cada una de las instancias será i+1. Por lo tanto y considerando que todas las instancias son equiprobables, la probabilidad de cada una de ellas será (1/n). Así: n n i=1 i=1 Tµ (n) = ∑ (1 / n)(i + 1) = (1 / n)∑ i + 1 = (1 / n) (n + 3) = • n +3 2 12. Estudio del algoritmo de búsqueda binaria: Estúdiese el coste medio del algoritmo suponiendo que es equiprobable que el elemento buscado esté o no en el vector y en el caso de encontrarse, todas las posiciones son equiprobables. El coste de la búsqueda sin éxito es n+1 y la probabilidad de esta instancia es 1/2. La búsqueda sin éxito tiene una probabilidad de 1/2 también y por lo tanto el término aportado por cada instancia al coste medio es (1/2n)(i+1). Así: 1 2 función BuscaBinaria (var v:TipoVector;x:TipoBase):entero; var izq,der,medio: entero; encontrado:lógico; izq:=1; der:=n; encontrado:=falso; mientras (izq<=der) ∧ no encontrado hacer medio:=(izq+der) div 2; si v[medio]=x entonces encontrado:=cierto sino si v[medio]>x entonces der:=medio-1 sino izq:=medio+1 fmientras; si encontrado devuelve medio sino devuelve 0; ffunción; 1 1 n 1 1 n 3n + 5 1 i + 1 = (n + 1) + (i + 1) = (n + 1) + (n + 3) = 2 n 2 2 n i=1 2 2n 2 4 i=1 n Tµ (n) = (n + 1) + ∑ 5. n 2 número de cifras que tiene un número m es log10m+1, y por lo tanto es equivalente decir que el coste del algoritmo es lineal con el número de cifras de m que con el logaritmo del valor de m. ∑ función Suma (var v:TipoVector): natural; var i,j,x:natural; x:=0; para i:=1 hasta n hacer para j:=1 hasta v[i] hacer x:=x+1; fpara fpara devuelve x ffunción; Si consideramos como instrucción crítica x:=x+1, el número de veces que se repite esta instrucción y por lo tanto la complejidad temporal de esta función, es: n T(n) = ∑ v[i]= S • • El tamaño del problema es el número de elementos del vector (n). Existen instancias significativas que están definidas en función de si x está o no en v, y si está, de la posición en el vector en la que se encuentre. El caso peor corresponde al de la búsqueda sin éxito. Si tomamos como instrucción crítica la evaluación de la guarda del bucle, tenemos que Tp(n)= log2n +1∈ Θ(log n). El caso mejor se da cuando el elemento que se busca (x) se encuentra en la posición central del vector, en este caso el número de veces que se evalúa la guarda es 2 y Tm(n)= 2∈ Θ(1). Por lo tanto podemos decir que este algoritmo tiene un coste O(log n) y Ω(1). i=1 y coincide con la suma de los elementos del vector; es decir es 1 si la suma de los elementos del vector es 1 ó es 0 si todos los elementos son 0. Evidentemente esto no es correcto ya que el coste de esta función ha de ser como mínimo n que es el número de veces que se repite la estructura de repetición más externa. ¿Dónde está el error en el razonamiento anterior? En la elección de la instrucción crítica; deberíamos haber tomado la comparación implícita del bucle más interno (j<=v[i]). En este caso tenemos que el coste del algoritmo viene dado por: n T(n) = ∑ 1 + v[i]= n + S i=1 11. El procedimiento cifras obtiene, a partir de un cierto valor entero m, una secuencia de los dígitos que lo componen (sobre el vector v) y el número de cifras que tiene el número (sobre la variable núm_cifras). Si tomamos como tamaño del problema el valor de m y considerando como instrucción crítica la evaluación de la guarda del bucle (q>0), tenemos que T(m)=log10m+1 ∈ Θ(logm). Si tomamos como tamaño del problema el número de cifras de m, llamémosle n, y considerando como instrucción crítica la evaluación de la guarda del bucle (q>0), tenemos que T(n)=n+1∈Θ(n). No hay contradicción entre los resultados ya que el 3 14. Para encontrar el k-ésimo elemento más pequeño de un vector podemos aplicar el método de seleccionar el mínimo k veces de la misma forma que se hace en el algoritmo de ordenación por Selección Directa. Para que el problema esté correctamente planteado es necesario que sobre el dominio de los elementos del vector esté definida una relación de orden. Tipo TipoVector=vector[1..n] de TipoBase; función k_Min (ent v:TipoVector) devuelve TipoBase; var i,j,pos_min:entero; min:TipoBase; para i:=1 hasta k hacer min:=v[i]; pos_min:=i; para j:=i+1 hasta n hacer si v[j]<min entonces min:=v[j];pos_min:=j; fsi fpara v[pos_min]:=v[i]; v[i]:=min; fpara devuelve v[k]; ffunción; 4 • • • Estudio de la complejidad temporal del algoritmo: Tamaño del problema: número de elementos del vector v y k. No hay instancias significativas. Si tomamos como instrucción crítica la evaluación de la guarda del si_entonces, el número de veces que se repite y por lo tanto el coste del algoritmo es: k T(n, k) = ∑ n k ∑ 1 = ∑ n − i = nk − i=1 j=i+1 i=1 (1 + k)k ∈ Θ(nk) 2 17. Veamos las dos soluciones que se proponen para realizar la mezcla natural o fusión de dos vectores ordenados de forma creciente. En los dos casos utilizaremos tres índices i,j,k para recorrer respectivamente el primer y segundo vector de entrada y el de salida. (a) Tipo t_v1=vector[1..n] de enteros; t_v2=vector[1..m] de enteros; t_v3=vector[1..m+n] de enteros; Procedimiento mezcla(u:t_v1; v:t_v2; sal w:t_v3); Var i,j,k : natural; P={n≥1 ∧ m≥1 ∧ ∀i:1≤i<n:v[i]≤v[i+1] ∧ ∀i:1≤i<m:u[i]≤u[i+1]} i:=1;j:=1;k:=1; mientras i≤n y j≤m hacer Si (u[i]≤v[j]) entonces w[k]:=u[i]; i:=i+1; Sino w[k]:=v[i]; j:=j+1; fsi k:=k+1; fmientras; si i≤n entonces para l:=i hasta n hacer w[k]:=v[l]; k:=k+1; fpara; sino para l:=j hasta m hacer w[k]:=u[l]; k:=k+1; fpara fprocedimiento; Complejidad Asintótica: • Talla del problema: número de elementos de los vectores de entrada n,m. • No hay instancias significativas • Si tomamos como instrucción crítica el incremento de la variable local k, tenemos que el número de veces que ésta se repite y por lo tanto el coste del algoritmo es u[m+1]:= +∞; para k:=1 hasta n+m hacer Si (u[i]≤v[j]) entonces w[k]:=v[i]; i:=i+1; Sino w[k]:=u[i]; j:=j+1; fpara fprocedimiento; Complejidad Asintótica: • Talla del problema: número de elementos de los vectores de entrada n, m. • No hay instancias significativas • Si tomamos como instrucción crítica el incremento de la variable local k, tenemos que el número de veces que ésta se repite y por lo tanto el coste del algoritmo es, de nuevo, T(n,m)∈ Θ(n+m) 20. La estrategia a seguir es similar a la búsqueda binaria. Suponemos que el tipo base del vector es entero y que está ordenado de forma estrictamente creciente (no hay repetidos). Utilizaremos dos índices para el recorrido del vector, izq y der, éstos definen el intervalo de búsqueda en [izq..der]; calculamos la posición central en este intervalo sobre la variable local m, y comparamos v[m] con m (el elemento con su índice) si v[m] ya es mayor que m para comprobar si la propiedad se cumple (v[i]=i), hay que buscar en la mitad inferior y si v[m] es menor que m hay que buscar en la mitad superior. El algoritmo quedaría: { ∀i: 1≤i<n: v[i]<v[i+1] } función Buscar(var v:TipoVector) devuelve entero; var izq,der,m:entero; izq:=1; der:=n; m:=(izq+der) div 2; {Inicio} mientras (v[m]≠m ∧ izq<der) hacer {1≤izq≤m<der≤n} opción v[m]<m: izq:=m+1; {1≤izq≤n} v[m]>m: der:=m; {1<der≤n} fopción ; {1≤izq≤m≤der≤n} m:=(izq+der) div 2 ; {1≤m≤n} fmientras {(v[m]=m) ∨ (izq=der=m)} si izq>der devuelve 0 sino devuelve m; ffunción; T(n,m)∈Θ(n+m) (b) Para esta segunda versión modificaremos los tipos de los vectores de entrada para extender el rango una posición: tipo t_v1=vector[1..n+1] de enteros; t_v2=vector[1..m+1] de enteros; Procedimiento mezcla_con_centinelas (u:t_v1;v:t_v2; sal w:t_v3); Var i,j,k : natural; {n≥1 ∧ m≥1 ∧ ∀i:1≤i<n:v[i]≤v[i+1] ∧ ∀i:1≤i<m:u[i]≤u[i+1] } i:=1; j:=1; v[n+1]:= +∞; 5 6 Tema 2: DISEÑO Y ANÁLISIS DE ALGORITMOS RECURSIVOS. 2. El número de llamadas que hace la función se puede expresar mediante las siguientes ecuaciones de recurrencia: llamadas(n)=1+llamadas(n-1)+llamadas(n-2) llamadas(1)=1 llamadas(0)=1 n≥2 3. Perfil de la función: {x>0 ∨ y>0, x≥0 ∧ y≥0} función num_caminos(x,y:entero): entero; Dado que sólo hay dos tipos de movimientos permitidos en la retícula, ↑ y →, se intentará reducir el problema a los puntos desde los cuales se puede alcanzar el punto (x,y) con estas acciones elementales: Esta recurrencia no se puede resolver por sustitución. Existen métodos conocidos para resolver este tipo de recurrencias1, que no se han considerado en esta asignatura. De cualquier manera, aquí se tratará de obtener una aproximación. Considérese la traza de llamadas que se efectuaría para n=4 y que se despliega a continuación: (x-1,y) (x,y) (x,y-1) fib(4) fib(3) fib(2) fib(2) fib(1) fib(1) fib(0) fib(1) fib(0) Si se hiciera una traza análoga para n=5, se podría comprobar que, aproximadamente, el número de llamadas se duplica. A continuación, se va a demostrar por inducción que ∀n:n≥2:2n/2≤llamadas(n)≤2n • • Caso base: n entre 2 y 3. Si n=2 se comprueba que 2≤llamadas(2)=3≤22=4. Si n=3 se comprueba que 23/2≤llamadas(3)=5≤23=8. Caso general: n>3. Como se ha hecho notar, en este caso se tiene que • Caso base: Para los puntos que están sobre los ejes, x=0, y=0, sólo hay una manera de llegar desde el origen, moviéndose a lo largo del eje correspondiente. • Caso general: Para el resto de puntos, x>0, y>0, suponiendo que se hubiese calculado de cuántas formas diferentes se puede llegar a (x,y-1), x>0, y-1≥0, y de cuántas a (x-1,y), x-1≥0, y>0, según el gráfico anterior, bastaría con sumar ambas cantidades. Función limitadora: t=x+y>0, que decrece en uno en cada llamada. Esta función limitadora tiene una interpretación gráfica, puesto que toma el mismo valor para todos los puntos que se encuentran en la misma diagonal: llamadas(n)=1+llamadas(n-1)+llamadas(n-2) siendo n-1≥2, n-2≥2. Por hipótesis de inducción, se puede suponer que 2(n-1)/2≤llamadas(n-1)≤2n-1 y que 2(n-2)/2≤llamadas(n-2)≤2n-2 Por lo tanto: 2(n-1)/2+2(n-2)/2≤llamadas(n-1)+llamadas(n-2)+1=llamadas(n) luego 2n/2≤llamadas(n), dado que 2(n-1)/2+2(n-2)/2<2(n-2)/2+2(n-2)/2=2(n-1)/2+1=2n/2 La otra desigualdad se obtendría de una forma análoga. Lo que se acaba de demostrar, permite establecer una cota inferior y superior al coste de fib(n), dado que cada llamada, resueltos los subproblemas generados, se resuelve con un paso de programa. Se puede concluir que coste(n)∈Ω(2n/2), coste(n)∈O(2n). Se desea llamar la atención sobre el hecho de que ambas cotas no son del mismo orden, como se comprueba a continuación: lím n→∞ 2n = lím 2n − n / 2 = lím 2n / 2 = ∞ n→∞ n→∞ 2n / 2 t=1 t=2 t=3 t=4... Así, el problema para un punto (excepto para puntos sobre los ejes), se reduce a llegar previamente a dos puntos sobre la diagonal anterior, y en dicha medida, más cercanos al origen. Transcripción algorítmica. {x>0 ∨ y>0, x≥0 ∧ y≥0} función num_caminos(x,y:entero): entero; opción x=0 ∨ y=0:devuelve 1; x>0 ∧ y>0:devuelve num_caminos(x-1,y)+num_caminos(x,y-1) fopción ffunción; Para estudiar el comportamiento temporal del algoritmo, se puede expresar el coste en función de m, el número de diagonal en el que se encuentra el punto, del siguiente modo: coste(m)=2coste(m-1)+k 1 Véase por ejemplo, el método de la ecuación característica, presentado en Fundamentos de Algoritmia. G. Brassard, T. Bratley. Prentice Hall, 1997, capítulo 4. 7 La dificultad reside en que esta ecuación no es cierta para todos los puntos de una m dada, porque los extremos (puntos sobre los ejes) se resuelven con un paso de programa. 8 1 Véase, por ejemplo, el esquema de llamadas que se generan para el punto (2,2): (2,2) • (1,2) (0,2) Caso general: t>1. Se puede dar el caso de que el punto esté en un extremo de la diagonal, es decir, sobre uno de los dos ejes. En ese caso: (2,1) (1,1) (0,1) (1,1) (1,0) (0,1) (2,0) Una cota inferior se puede establecer truncando todos los caminos en el mismo nivel de la recursión, como, para el ejemplo anterior, se muestra en la siguiente figura: el punto está sobre el eje de abcisas). Si el punto no está en ninguno de los ejes (x>0, y>0), entonces Dado que (x-1,y) y (x,y-1) están sobre la diagonal t-1, por hipótesis de inducción se puede suponer que x − 1 + y (x + y − 1)! = x − 1 (x − 1)! y! x + y − 1 (x + y − 1)! = num_caminos(x,y-1)= x x!(y − 1)! num_caminos(x-1,y)= (2,2) (0,2) (2,1) (1,1) (1,1) coste(m)=2coste(m-1)+k coste(m)=k' m>mínimo(x,y) m=mínimo(x,y) Si el punto es central en su diagonal, es decir x=y=m/2, entonces: coste(m)=2coste(m-1)+k=22coste(m-2)+k(2+1)= =23coste(m-3)+k(22+21+1)=.....= =2icoste(m-i)+k(2i-1+....+4+2+1)=.... =2m/2coste(m/2)+k(2m/2-1+...+2+1) cuya solución es Θ(2m/2). 4. Cualquier camino que llegue desde el origen hasta (x,y) estará formado por una secuencia de x+y movimientos, x hacia la derecha e y hacia arriba. Los diferentes caminos se deben al orden en que se van efectuando estos movimientos, es decir, cada camino se caracteriza por seleccionar cuáles de los x+y movimientos, desde el primero que se realiza hasta el último, es uno de los x movimientos hacia la derecha. El número de estos caminos se puede calcular entonces como las combinaciones2 de x+y x + y (x + y)! = . x! y! x Este cálculo debe coincidir con el que obtiene la función recursiva del problema anterior. Se puede demostrar por inducción sobre t=x+y, el número de diagonal en el que se encuentra el punto (x,y). • Caso base: t=1. En esta diagonal se tiene: 1 x=0, y=1: num_caminos(0,1)=1, que coincide con , ó 0 2 m m! Es oportuno recordar que = n!(m − n)! n num_caminos(x,y)= (2,0) Esta cota se puede calcular con la siguiente relación de recurrencia: elementos tomados de x en x, Cxx + y = y por lo tanto x + y (x + y − 1)! (x + y − 1)! (x + y)(x + y − 1)! + = = x! y! (x − 1)! y! x!(y − 1)! y 6. Perfil y especificación: función igualSum(v:vector[1..n] de entero;x:entero;i:entero): booleano; {x≥0,1≤i≤n, ∀k:1≤k≤n:v[k]≥0} n {igualSum(v,x,i) ↔ ∑ v[k] = x } k =i • • Caso base. El vector tiene sólo un elemento. i=n: el problema se resuelve comprobando si v[i]=x. Caso general. El vector v[i..n] tiene más de un elemento. i<n: n ∑ v[k] = x k =i sii n ∑ v[k] = x–v[i]. k =i+1 es decir igualSum(v,x,i) sii igualSum(v,x-v[i],i+1). Cabe notar que si x-v[i]<0, entonces ya se puede detectar que el resto de elementos, v[i+1..n] no pueden sumar esta cantidad, dado que todos los elementos son ≥0. Función limitadora: t=n-i+1, que decrece en uno en cada llamada recursiva. Transcripción algorítmica. función igualSum(v:vector[1..n] de entero;x:entero;i:entero): booleano; opción i=n: devuelve v[i]=x; i<n: si x<v[i] devuelve F sino devuelve igualSum(v,x-v[i],i+1) fsi fopción ffunción; La llamada inicial será igualSum(v,x,1). 9 + y cuando el x + y cuando x x num_caminos(x,y)=num_caminos(x-1,y)+num_caminos(x,y-1). m>1 m=1 (1,2) x + y x =1, (valor de 0 x + y (valor de también con x + y num_caminos(x,y)=1, que coincide con punto está sobre el eje de ordenadas) y (1,0) Una forma sencilla de dar una cota superior al coste es considerar la siguiente recurrencia: coste(m)=2coste(m-1)+k coste(m)=k' que es Θ(2m). x=1, y=0: num_caminos(1,0)=1, que coincide con . 1 10 12. Perfil y especificación: 7. Perfil y especificación: función capicúa(c:cadena; ini,fin:índice):booleano; función matriz_simétrica(M:matriz;n:índice): booleano; Pre: {1≤n} Post: {matriz_simétrica(M,n) ↔ ↔ M[i,j]=M[j,i] para cada par de índices i,j,1≤j≤n,1≤i≤n ↔ ∀banda:1≤banda≤n:(∀i:1≤i≤n:M[banda,i]=M[i,banda]) Pre: {ini≤fin} Post: {capicua(c,ini,fin) ↔ ∀i:ini≤i≤fin:c.palabra[i]=c.palabra[fin-i+ini]} Esta definición se ilustra en la figura siguiente, que corresponde a una cadena capicúa: ini • Caso base: n=1. La matriz tiene un solo elemento, M[1,1], que es trivialmente igual a sí mismo; luego la matriz es simétrica. • Caso general: n>1. Si la banda más externa, la formada por la fila n y la columna n tiene sus elementos dispuestos simétricamente: ∀i:1≤i≤n:M[n,i]=M[i,n] (*) entonces la matriz será simétrica sii lo es la submatriz formada por las n-1 primeras filas y las n-1 primeras columnas. columna n n-1 7 1 fila n 7 1 ... 3 En el caso en que se detecte que la n-ésima banda no es simétrica, la matriz no puede serlo. Función limitadora: t=n, dimensión de la matriz considerada. En el caso general, la matriz tiene dimensión 2 ó mayor, por lo que la llamada recursiva que eventualmente se genera, es para una matriz de dimensión no menor que 1, como exige la precondición. Transcripción algorítmica: función matriz_simétrica(M:matriz;n:índice): booleano; opción n=1: devuelve V; n>1: si banda_simétrica(M,n) entonces devuelve matriz_simétrica(M,n) sino devuelve F fsi fopción ffunción; en donde: • banda_simétrica es una función booleana que comprueba la condición (*), y cuyo código, expresado iterativamente sería: columna n 7 1 ... fila n 7 1 ... 3 i i b_sim:=V; i:=1; mientras i<n ∧ b_sim hacer si M[n,i]≠M[i,n] entonces b_sim:=F sino i:=i+1 fsi fmientras; devuelve b_sim; • La primera llamada sería matriz_simétrica(M,Dim), siendo Dim la dimensión de la matriz M. 11 ... a fin r a r a ... Tal como se observa, si c.palabra[ini]=c.palabra[fin], la cadena será capicúa sii lo es la subcadena encerrada entre estos dos caracteres. Esta observación conduce a: • Caso base: si c tiene como mucho dos caracteres, ini=fin, o ini+1=fin, entonces, basta con comparar c.palabra[ini], con c.palabra[fin] (que son el mismo carácter si ini=fin). • Caso general: si c tiene tres o más caracteres, ini+1<fin, entonces se comparan los caracteres extremos, y si coinciden, el problema se reduce a comprobar si la subcadena c.palabra[ini+1..fin-1] es capicúa. Hay que tener en cuenta que si c.palabra[ini] y c.palabra[fin] difieren, la cadena no es capicúa. Función limitadora: t=n≥1, siendo n=fin-ini+1 el número de caracteres de la cadena. En el caso general, n≥3, si se genera una llamada recursiva, es para una cadena con dos caracteres menos, pero que tendrá como poco un carácter: el subproblema generado es también el de comprobar si una cadena de longitud mayor o igual que uno, más corta que la de partida, es capicúa. De alcanzarse el caso base (la cadena es capicúa), se tendrían dos elementos si la cadena inicialmente fuera de longitud par; si fuera de longitud impar, se reduciría a un caso base de un elemento. Transcripción algorítmica. {ini≤fin} función capicúa(c:cadena; ini,fin:índice):booleano; opción ini+1≥fin: devuelve c.palabra[ini]=c.palabra[fin]; ini+1<fin: {ini+1≤fin-1} si c.palabra[ini]=c.palabra[fin] entonces devuelve capicúa(c,ini+1,fin-1) sino devuelve F fsi fopción ffunción; La llamada inicial será capicúa(c,1,c.longitud). 14. La talla del problema es n, y no presenta diferentes instancias. En el caso base, el tiempo de ejecución es constante. En el caso general, la operación concatena tiene un coste constante. Ecuaciones de recurrencia: coste(n)=coste(n/2)+k2 coste(n)=k1 n>0 n=0 en donde, por simplificar la notación, n/2 denota la división entera por 2. Esta recurrencia es Θ(log2n). 12 Es interesante considerar qué sucedería con respecto al coste si en la llamada recursiva se permutaran los parámetros de la siguiente forma: concatena(añade(vacía,car(nmod2)),bin(c)), entonces esta operación, produciendo la misma cadena resultante, tendría sin embargo un coste proporcional a kn/2. Renombrando k/2 como k2: • • Cas general: amb i<n, Cas base: amb i=n, avaluació(a,x,i)=ai+x(avaluació(a,x,i+1)) avaluació(a,x,i)=ai Funció limitadora: t=(n-i+1), estrictament decreixent i acotada. Transcripció algorítmica: coste(n)=coste(n/2)+k2n n>0 coste(n)=k1 n=0 Cuya resolución por sustitución es como sigue3: coste(n)=coste(n/2)+k2n =coste(n/22)+k2n(1+1/2) =coste(n/23)+k2n(1+1/2+1/22) =coste(n/24)+k2n(1+1/2+1/22+1/23)= ...= =coste(n/2i)+k2n(1+1/2+1/22+...+1/2i-1)= ...= =coste(0)+k2n(1+1/2+1/22+...+1/2log2(n)+1-1) dado que se llega al caso base después de log2n+1 reducciones de la talla del problema. Esta recurrencia es Θ(n), ya que la serie (1+ 1/2 + 1/4 +...+ 1/2m +...) está acotada por una constante. { } funció avaluació(a:vector[0..n] de reals; x:real; i:enter): real; opció i<n: torna (a[i]+ x*avaluació(a,x,i+1)); i=n: torna (a[n]) fopció ffunció; {avaluació(a,x,i)=ai +x(ai+1+x(ai+2 +x(...(an-2+x(an-1+xan))...)))} Crida inicial: result:= avaluació(a,X,0), éssent result una variable real. 15. En este algoritmo, hay que tener en cuenta que al ejecutar en la primera llamada inserción_directa(a,1,n), el vector queda ordenado. Ello quiere decir que, en el resto de llamadas, la ordenación por inserción se hará sobre subvectores ordenados, que es el mejor caso para esta operación. En resumen: • Coste de la primera llamada: kn2, • Coste de las siguientes llamadas: coste(n-1)=coste(n-2)+k2(n-1) n-1>0 coste(0)=k1, que resuelta la recurrencia lleva a un término k3n2. En resumen, el algoritmo es Θ(n2). Si se extrae inserción_directa del algoritmo y se ejecuta antes de recorrer, el resultado alcanzado es el mismo. Pero ahora el coste de recorrer es: coste(n)=coste(n-1)+k2 coste(0)=k1, n>0 que es Θ(n). El coste total será debido al coste de inserción_directa(a,1,n) más el de recorrer(a,1,n), que será Θ(n2) en el caso peor, y Θ(n) en el mejor, debido a las diferentes instancias que muestra la ordenación por inserción. 20. Perfil i especificació: { } funció avaluació(a:vector[0..n] de reals; x:real; i:enter): real; {avaluació(a,x,i) = ai +x(ai+1+x(ai+2 +x(...(an-2+x(an-1+xan))...))) = ai+xai+1+x2ai+2+...+xn-i-1an-1+xn-ian } on ai≡a[i]. Llavors la definició recursiva per al cas general es pot resumir de la forma següent: podem avaluar tot el polinomi, multiplicant per x l'avaluació de lo de dintre del parèntesi, sumant-li després el terme independent parcial: 3 i Se debe tener en cuenta que, al igual que sucede con la división real, (n div 2 )div2=ndiv2 13 i+1 , i≥1 14 Tema 3: TIPOS ABSTRACTOS DE DATOS. TIPOS LINEALES. 1. Las operaciones del TAD permiten desplazar el punto de interés a lo largo de las posiciones de la lista, pero no hay ninguna operación que diga qué orden ocupa la posición de interés en la secuencia. Por ello, se recorrerá secuencialmente la lista, utilizando una variable entera contador que cuente los elementos que se visitan en el recorrido: principio(l); {la lista l puede estar vacía, y su punto de interés estar a la derecha del todo} contador:=0; mientras ¬esfin(l) hacer contador:=contador+1 fmientras 3. a) El procedimiento que se pide tendrá la siguiente especificación: {l=L1L2…Ln ∧ e=E ∧ 1SRVQ ∧ ∀k:1≤k<n:Lk≤Lk+1} inserta_ord(l,e) {l=L’1L’2…L’i-1EL’i+1…L’n+1 ∧ L1…Li-1=L’1…L’i-1 ∧ Li…Ln=L’i+1…L’n+1 ∧ pos=i+1 ∧ ∀k:1≤k<n+1:L’k≤L’k+1} b) Se recorrerá la lista de izquierda a derecha mediante un bucle en el que se irá avanzando el punto de interés, buscando el primer elemento que sea mayor o igual que e. Según esta estrategia, a lo largo del bucle se tendrá: l=L1L2..Lpos-1Lpos…Ln, 1≤pos≤n+1, en donde los elementos anteriores al punto de interés (realzados en negrita), son los elementos que se ha comprobado que son menores que e. Se usará una variable booleana menores que indique que todos los elementos revisados hasta el momento son menores que e. El cuerpo de la operación quedaría como sigue: principio(l); menores:=V; mientras ¬esfin(n) ∧ menores hacer si eUHFXSHUDO HQWRQFHV menores:=F sino siguiente(l) fmientras; inserta(l,e); Nótese que tanto si el bucle acaba llevando el punto de interés a la derecha del todo, o sobre un elemento de la lista mayor o igual que e, se debe insertar el elemento en dicha posición. c) Sobre la representación del tipo, la operación seguiría la misma estrategia: l.pos:=l.prim; menores:=V; mientras l.pos≠l.ultimo ∧ menores hacer si eOSRVAVLJAGDWR HQWRQFHV menores:=F sino l.pos:=l.pos^.sig fmientras; ... 15 Situado el punto de interés en el lugar que corresponde, la operación se completaría con el código que implementa la inserción de un elemento en referencia a dicho punto de interés. (La resolución del ejercicio 10 discute una variante de dicha inserción). 4. Perfil de la operación: procedimiento invertirq(ent/sal q: cola); Especificación: {q=Q1Q2…Qn, n` invertirq(q) {n=0→ q=λ , n>0 → q= Q’1Q’2…Q’n ∧ Q’1Q’2…Q’n = QnQn-1…Q1 } Identificación de caso base y caso general. • Caso base: q=λ. La cola coincide con su invertida. • Caso general: q≠λ. Es decir, se tiene una cola q=Q1Q2…Qn, con uno o más elementos, n>0. Para reducir el problema a uno análogo, pero más pequeño o cercano al caso base, se desencola el primer elemento, reduciendo el número de elementos de la cola. Supóngase que se invierte la secuencia de elementos Q2…Qn que quedan en la cola y se llega a tener que q=Q’1…Q’n-1=Qn…Q2. Entonces bastará con encolar Q1, para conseguir la cola invertida. Nótese que la función limitadora t=n, n≥0, se reduce de uno en uno en cada llamada recursiva, hasta llegar a hacerse cero en el caso base. Ello, además de asegurar que efectivamente el tamaño del problema se va reduciendo recursivamente, indica que el número de llamadas que se harán al procedimiento para invertir una cola de n elementos es n, lo que implica un coste lineal, dado que se puede suponer que las operaciones del tipo cola tienen un coste Θ(1) (véase las implementaciones eficientes propuestas en clase de teoría). Transcripción algorítmica: procedimiento invertirq(ent/sal q:cola); var e:elemento; si ¬ vaciaq?(q) entonces e:=recupera(q); desencolar(q); invertirq(q); encolar(q,e) {sino la cola ya está invertida} fsi fprocedimiento; En función del número de elementos de la cola, el coste del algoritmo se puede expresar como coste(n)= k + coste(n-1) n>0 coste(n)= k’ n=0 que corresponde a un coste Θ(n), como por otra parte, ya se había hecho notar al discutir la función limitadora. El ejercicio 13 del boletín pide que se calcule también este coste en el caso de que la cola estuviese implementada con vectores “no circulares”. Demuéstrese que el coste es entonces Θ(n2). 6. a) { q=Q1Q2...Qm,Qm+1...Qn ∧ 0≤m≤n ∧ pos=m ∧ e=E ∧ p=P } Procediment encolar_PR(e/s q:cola; e:elemento; p:prioridad); { ( p=normal → q=Q1Q2...Qm,Qm+1...QnE ∧ 0≤m≤n ∧ pos=m ) ∨ ( p=urgente → q=Q1Q2...QmE,Qm+1...Qn ∧ 0≤m≤n ∧ pos=m+1 ) } 16 b) És suficient amb modificar la declaració d'una cola del mode següent: cola= record primero: enlace; pos: enlace; ultimo: enlace end; c) Procedure encolar_PR(var q:cola; e:elemento; p:prioridad); var pr: enlace; Begin New(pr); pr^.dato:=e; if q.primero=nil {cua buida} then begin pr^.siguiente:=nil; q.primero:=pr; q.ultimo:=pr; if p=urgente then q.pos:=pr: end else {cua no buida} if p=normal then begin pr^.siguiente:=nil; q.ultimo^.seg:=pr; q.ultimo:=pr end else begin {p=urgente} if q.pos = nil {el primer urgent } then begin pr^.siguiente:=q.primero; q.primero:=pr end else begin pr^.siguiente:=q.pos^.siguiente; q.pos^.siguiente:=pr; end; if q.pos=q.ultimo then {l'últim urgent} q.ultimo:=pr; q.pos:=pr; end end; on l’actualització dels punters apareix amb línies discontínues. 10. Para que sea directamente accesible el elemento anterior desde uno dado, se cambia la definición del tipo nodo a: nodo= tupla dato: elemento; pred,sig: ptr ftupla El campo pred permite el acceso al que se refiere el párrafo anterior, de forma que una lista l=L1L2..Ln, n≥0, vendrá representada por la estructura de la siguiente figura. Nótese que el nodo ficticio no tiene ningún predecesor, por ello el campo correspondiente está a nil. pri pos ult l n i l L1 pr Ln ... pri pos ult l aux E Lpos …. y pri pos ... Ln ult l aux ... E elements amb prioritat normal 17 n i l Implementación de la operación de inserción. El uso del nodo ficticio al principio de la lista, hace que únicamente la operación creal afecte al campo pri de la tupla que implementa a la lista. Habrá que distinguir dos casos: • El punto de interés está sobre uno de los elementos de la lista. La inserción sólo afectará a l.pos. • El punto de interés está a la derecha del todo, es decir l.pos=l.ult. Nótese que en el caso de la lista vacía, ambos punteros hacen referencia al nodo ficticio. La inserción afectará a ambos punteros. Suponiendo que se ha creado un nuevo nodo para albergar el elemento a través de un puntero auxiliar, estos dos casos serán respectivamente: Els subcasos que apareixen quan s’encua un element de prioritat urgent en una cua no buida es deuen a que alguna de les parts, la d’elements urgents o la de normals, pot estar buida. Per exemple, siga el cas en el que q.pos=nil (la cua sols té elements de prioritat normal) i la prioritat d’e és urgent: primero pos ultimo q nil Lpos ... ... Ln E n i l donde la actualización de los punteros se muestra con líneas discontinuas. 18 n i l En el siguiente procedimiento, se resalta en negrita las diferencias que hay entre el código de esta operación, y el de la operación de inserción del TAD Lista con punto de interés visto en clase. procedimiento insertar(ent/sal l:lista; e:elemento); var aux:ptr; {inserción del nuevo nodo en la lista} NEW(aux); aux^.dato:=e; aux^.sig:=l.pos^.sig; aux^.pred:=l.pos; l.pos^.sig:=aux; {ajuste del punto de interés según los casos} si l.pos=l.ult entonces l.pos:=aux; l.ultimo:=aux sino aux^.sig^.pred:=aux; l.pos:=aux fsi fprocedimiento; Transcripción algorítmica: procedimiento cambiar_signo(p:pila;sal p’: pila); var e:elemento; si ¬ vaciap?(p) entonces e:=-1*tope(p); desapilar(p); cambiar_signo(p,p’); apilar(p’,e) sino creap(p’) fsi fprocedimiento; 15. Especificación de la operación: función sombrero?(p,p’: pila):booleano; 12. a) El tipo Lista con punto de interés, dado que las operaciones se refererirán a aquella línea sobre la que se haya situado el punto de interés. Así se podría declarar tipo texto=lista en donde el tipo lista se habría declarado como una lista con punto de interés cuyos elementos fuesen de un tipo línea, convenientemente definido. b) Por ejemplo, considérese la operación de intercambiar una línea dada con la que le precede en el texto. Se debería implementar de acuerdo a la siguiente especificación: {f=L1L2…Li-1Li…Ln ∧ pos=i ∧ 2SRVQ` procedimiento intercambiar_adelante(ent/sal f: texto) {f=L’1L’2…L’i-1L’i…L’n = L1…LiLi-1…Ln ∧ pos=i } Con la representación vista en clase, en la cual f.pos apuntaría al nodo que alberga la línea Li-1, el cuerpo del procedimiento quedaría: línea_aux:=f.pos^.dato; f.pos^.dato:=f.pos^.sig^.dato; f.pos^.sig^.dato:=línea_aux; 14. Especificación de la operación: procedimiento cambiar_signo(p:pila;sal p’:pila); {p= P1P2…Pn, n p’= P’1P’2…P’m, m` b:=sombrero?(p,p’) {b= (P’i..P’m=P1..Pn, para algún i, 1LPQ` Identificación de caso base y caso general. • Caso base: p=λ. La pila p es sombrero de cualquier otra pila, vacía o no. • Caso general: p≠λ. Es decir, p=P1P2…Pn, con uno o más elementos, n>0. Si p’ está vacía, p no puede ser sombrero de p’. En caso contrario, se pueden comparar los topes de las pilas: Si los topes de ambas coinciden, entonces el problema se puede reducir desapilando en ambas pilas: p =P1P2...Pn-1Pn, p’=P’1P’2…P’m-1Pn, entonces p será sombrero de p’ sii P1…Pn-1=P’i…P’m-1, para algún i. Si los topes difieren, no es necesario hacer más comprobaciones: p no puede ser sombrero de p’. La reducción recursiva del problema a un problema menor se pone de manifiesto tomando como función limitadora t=n, n≥0, la cual decrece en uno cada vez que se genera una nueva llamada. Transcripción algorítmica: {p= P1P2…Pn, n` cambiar_signo(p,p’) {p’=P’1...P’n=-P1…-Pn} Identificación de caso base y caso general. • Caso base: p=λ. La pila p’debe ser la pila vacía. • Caso general: p≠λ. Es decir, p=P1P2…Pn, con uno o más elementos, n>0. Si se reducen los elementos de la pila a P1P2…Pn-1, y se obtiene en p’ su copia con los elementos cambiados de signo -P1-P2…-Pn-1, bastará apilar -Pn en p’ para resolver el problema. Para este diseño, cuya transcripción algorítmica se da a continuación, se puede aplicar una discusión sobre la función limitadora y el coste análoga a la que, para la operación de invertir una cola, se hace en la resolución del problema 4. 19 procedimiento sombrero?(ent/sal p,p’:pila); opción ¬ vaciap?(p):opción ¬vaciap?(p’):si tope(p)=tope(q) entonces desapilar(p); desapilar(p’); devuelve sombrero(p,p’); sino devuelve F fsi; vacia?(p’): devuelve F fopción; vaciap?(p): devuelve V fopción fprocedimiento; En función del número de elementos n, de la pila p, el coste del algoritmo será: 20 En el caso peor, cuando p es sombrero de p’: coste(n)= k + coste(n-1) n>0 coste(n)=k’ n=0 que pertenece a Θ(n) En el caso mejor, cuando el tope de p no aparece como tope de p’, el coste es Θ(1). 21