- Santiago Andrés Leal Omaña
- Santiago Guerrero Vargas
El problema para resolver es encontrar la subsecuencia contigua en una secuencia de números reales que produce el mayor producto acumulado en comparación al resto de la secuencia. Es decir, se debe buscar una subsecuencia de la secuencia original, tal que, al multiplicar todos sus elementos, se obtiene el producto máximo en comparación con todas las demás subsecuencias.
Este problema se puede abordar mediante el uso de un algoritmo eficiente que recorra la secuencia y calcule el producto acumulado de cada subsecuencia, para luego compararlas y encontrar la subsecuencia con el mayor producto. Sin embargo, debido a que hay un número infinito de subsecuencias posibles en una secuencia de longitud n, se necesitará un enfoque más eficiente para resolver este problema.
Entrada: Una secuencia de números reales de longitud n.
Salida: Una subsecuencia con el producto mayor.
Escribir tres algoritmos (evidente recursivo, memorizado y bottom-up) asociados a un diseño de un problema que se puede resolver, usando la estrategia de programación dinámica.
El desarrollo del taller consistirá en analizar tres algoritmos asociados al problema de encontrar para una secuencia de elementos X = ⟨x1, . . . , xn⟩, xi ∈ R la subsecuencia de elementos contiguos cuyo producto es máximo.
Por ejemplo, para la subsecuencia ejemplo X = ⟨−7, 12, −7, 0, 14, −7, 5⟩, la subsecuencia contigua de elementos contiguos cuyo producto es máximo es X[1 : 3]
A continuacion se muestran los 3 algortimos realizados para la solución de nuestro problema.
Este algortimo evalua un intervalo incial, que va desde el incio de la secuencia incial, y en cada llamado iterativo va recortando el intervalo a usar, posteriormente se compara cada llamado recursivo en busqueda por el que posea el mayor valor (producto) y este es el que se retorna.
FUNCTION rec(seq)
SET val,x,y to Rec_2(seq,0,length of seq - 1)
RETURN subsequence from seq[x] to seq[y+1]
FUNCTION Rec_2(sec,i,j)
IF i is equal to j
RETURN sec[i],i,j
ENDIF
SET max_product to 1
FOR k FROM i to j+1
max_product = max_product * sec[k]
ENDFOR
SET t1,t2,t3 to Rec_2(sec,i,j-1)
SET t4,t5,t6 to Rec_2(sec,i+1,j)
IF max_product is greater than t1 and max_product is greater than t4
RETURN max_product,i,j
ELSEIF t1 is greater than max_product and t1 is greater than t4
RETURN t1,t2,t3
ELSE
RETURN t4,t5,t6
ENDIFPara poder definir la complejidad, se usa el teorema maestro para calcular el Big_O del algoritmo.
Esta ecuacion puede ser evaluada de igual manera que el algoritmo recursivo de fibonnaci, entonces, podemos definir la complejidad en
Se busca encontrar el intervalo interno con el producto maximo, dentro de la secuencia dada.
- Inicio: Se sabe que el producto maximo existe dentro de la secuencia, por lo que se inicia buscando dentro de toda la secuencia desde la posicion 0 y la ultima posición.
- Avance: Como puede que la secuencia completa no sea el producto maximo de la misma, se vuelve a llamar la recursion corriendo ambos indices, el inicio se mueve hacia la derecha y el final hacia la izquierda.
- Final: Cuando la secuencia en la que se esta buscando corresponde a un solo elemento se detiene la recursion y se retorna el valor que existe en esa secuencia
En este caso el taller fue realizado en el lenguaje python para poder obtener una solucion mas simple y con una sintaxis mas corta.
En este algoritmo se usa matriz para poder memorizar el producto posible de cada intervalo que existe, por lo que en cada llamado recursivo verificamos si el producto que deseamos obtener ya fue calculado, y de lo contrario se calcula el producto dentro de ese intervalo.
FUNCTION MemorizadoRecursivo2(sec,i,j,M)
IF i is equal to j
M[i][j] = sec[i]
RETURN sec[i],i,j
ENDIF
IF M[i][j] is greater than negative infinity
RETURN M[i][j],i,j
ENDIF
SET max_product to 1
FOR k FROM i to j+1
max_product = max_product * sec[k]
ENDFOR
M[i][j] = max_product
CALL MemorizadoRecursivo2(sec,i,j-1,M)
CALL MemorizadoRecursivo2(sec,i+1,j,M)
RETURN M
FUNCTION subSequence(X, memo)
SET max_val to maximum value in memo
SET i to 0 and j to length of X - 1
FOR x FROM 0 to length of X
FOR y FROM x to length of X
IF memo[x][y] is equal to max_val
SET i to x and j to y
BREAK
ENDIF
ENDFOR
ENDFOR
RETURN subsequence from X[i] to X[j+1]
FUNCTION rec(seq)
SET M as a matrix with dimensions (length of seq) x (length of seq) with all values set to negative infinity.
CALL MemorizadoRecursivo2(seq,0,length of M - 1,M)
RETURN subSequence(seq, M)La complejidad del algoritmo se puede analizar utilizando el Teorema Maestro, que se aplica a las funciones recursivas que se dividen en subproblemas de tamaño
En este caso, el algoritmo recursivo divide el problema original en dos subproblemas de tamaño
La complejidad del algoritmo está determinada por el costo de resolver cada subproblema, que en este caso es proporcional al tamaño del subproblema. En cada llamada recursiva, el tamaño del subproblema disminuye en 1, lo que significa que la complejidad del algoritmo está dada por:
Donde
Según el Teorema Maestro, la complejidad del algoritmo se puede expresar como:
En este caso,
Sin embargo, como se realiza una multiplicación en cada iteración del bucle "for", la complejidad real del algoritmo es cercana a
La invariante del código es que en cada llamada recursiva, la función MemorizadoRecursivo2() calcula el producto máximo de la subsecuencia de la secuencia sec desde el índice i hasta el índice j y almacena el resultado en la matriz M. Luego, la función llama a sí misma recursivamente para calcular los productos máximos de las subsecuencias más cortas y actualiza la matriz M con los resultados. Finalmente, la función devuelve la matriz M actualizada.
- Inicio: La matriz M está inicializada con todos sus valores establecidos en float('-inf'), lo que indica que no se han calculado los productos máximos para ninguna subsecuencia de la secuencia dada.
- Avance: Ocurre en cada llamada recursiva de la función MemorizadoRecursivo2(). La función calcula el producto máximo de la subsecuencia de la secuencia sec desde el índice i hasta el índice j, almacena el resultado en la matriz M y llama a sí misma recursivamente para calcular los productos máximos de las subsecuencias más cortas.
- Final: Cuando la función llega a la condición base de la recursión, es decir, cuando el índice de inicio i es igual al índice de finalización j, la función asigna el valor de la secuencia a la matriz M. Luego, la función comprueba si el valor de la matriz M es mayor que el valor mínimo posible. Si el valor de la matriz M ya se ha calculado y almacenado, devuelve el valor de la matriz M. De lo contrario, devuelve la matriz M actualizada. En cualquier caso, la matriz M contiene los productos máximos de todas las subsecuencias de la secuencia dada, y se devuelve como resultado de la función.
En este caso el taller fue realizado en el lenguaje python para poder obtener una solucion mas simple y con una sintaxis mas corta.
La función bottomUp() utiliza la técnica de programación dinámica de abajo hacia arriba (bottom-up) para resolver el problema de encontrar la subsecuencia continua de una secuencia dada con el producto máximo. Toma la secuencia sec y una matriz M como entrada y devuelve la matriz M actualizada con los productos máximos de todas las subsecuencias de la secuencia dada.
La función utiliza dos bucles for anidados para iterar sobre todos los pares posibles de índices de inicio y finalización de la subsecuencia. Luego, calcula el producto máximo de la subsecuencia de la secuencia sec desde el índice de inicio hasta el índice de finalización y almacena el resultado en la matriz M. La función comprueba si el valor de sec[k] es cero durante el cálculo del producto máximo y detiene el ciclo si encuentra un valor cero.
Al final de la función, la matriz M contiene los productos máximos de todas las subsecuencias de la secuencia dada, y se devuelve como resultado de la función. La técnica bottom-up se utiliza en lugar de la técnica top-down para evitar el costo de la recursión y mejorar el rendimiento del algoritmo.
FUNCTION bottomUp(sec,M)
FOR i FROM 0 to length of sec
FOR j FROM i to length of sec
SET max_product to 1
FOR k FROM i to j+1
IF sec[k] is not equal to 0
max_product = max_product * sec[k]
ELSE
BREAK
ENDIF
ENDFOR
M[i][j] = max_product
ENDFOR
ENDFOR
RETURN M
FUNCTION subSequence(X, memo)
SET max_val to maximum value in memo
SET i to 0 and j to length of X - 1
FOR x FROM 0 to length of X
FOR y FROM x to length of X
IF memo[x][y] is equal to max_val
SET i to x and j to y
BREAK
ENDIF
ENDFOR
ENDFOR
RETURN subsequence from X[i] to X[j+1]
FUNCTION rec(seq)
SET M as a matrix with dimensions (length of seq) x (length of seq) with all values set to negative infinity.
CALL bottomUp(seq,M)
RETURN subSequence(seq, M)La complejidad de la función bottomUp() se puede analizar en términos del número de operaciones que realiza. El bucle exterior for itera n veces, y el bucle interior for itera en el peor de los casos n veces, lo que da un total de
Sin embargo, el algoritmo contiene una optimización que evita realizar cálculos innecesarios para valores de sec[k] iguales a cero. Esto reduce el número de iteraciones del bucle interno en una cantidad desconocida, pero en general no cambia la complejidad asintótica del algoritmo.
Por lo tanto, la complejidad temporal de la función bottomUp() se puede expresar en términos del teorema maestro como
La invariante del algoritmo es que, en cada iteración del bucle interior, la matriz M contiene los productos máximos de todas las subsecuencias que comienzan en el índice de inicio i y terminan en cada índice desde i hasta j inclusive.
-
Inicio: Antes de que se ejecute el bucle exterior, la matriz M está inicializada con ceros. Durante la primera iteración del bucle exterior (cuando i=0), la matriz M contiene los productos máximos de todas las subsecuencias que comienzan en el índice 0 y terminan en cada índice desde 0 hasta j inclusive. Esto es cierto porque aún no se han iterado por el bucle exterior y, por lo tanto, todas las subsecuencias posibles comienzan en el índice 0.
-
Avance: En cada iteración del bucle exterior, el índice de inicio i se incrementa en 1, y se actualiza la matriz M con los productos máximos de todas las subsecuencias que comienzan en el índice i y terminan en cada índice desde i hasta j inclusive. El bucle interior itera a través de todas las subsecuencias posibles con un índice de inicio k entre i y j inclusive, y calcula el producto máximo de la subsecuencia. Este producto máximo se almacena en la matriz M en la posición correspondiente.
-
Final: Después de que el bucle exterior haya terminado de iterar sobre todos los índices de inicio posibles (i=0 a i=n-1), la matriz M contiene los productos máximos de todas las subsecuencias de la secuencia dada. Por lo tanto, la invariante se mantiene en todo momento durante la ejecución del algoritmo.
En este caso el taller fue realizado en el lenguaje python para poder obtener una solucion mas simple y con una sintaxis mas corta.
Tomando en cuenta la siguiente tabla
| Número de elementos | Recursivo | Memorizado | Tabulado |
|---|---|---|---|
| 1 | 0.000000000000000 | 0.001009225845337 | 0.000000000000000 |
| 2 | 0.000000000000000 | 0.000000000000000 | 0.001460313796997 |
| 3 | 0.000000000000000 | 0.000000000000000 | 0.000000000000000 |
| 4 | 0.001002311706543 | 0.000000000000000 | 0.000000000000000 |
| 5 | 0.000000000000000 | 0.001001358032227 | 0.000000000000000 |
| 6 | 0.000000000000000 | 0.001001834869385 | 0.000000000000000 |
| 7 | 0.000000000000000 | 0.001198768615723 | 0.000000000000000 |
| 8 | 0.000000000000000 | 0.001259803771973 | 0.000000000000000 |
| 9 | 0.001420736312866 | 0.000000000000000 | 0.000000000000000 |
| 10 | 0.001997709274292 | 0.000000000000000 | 0.001000404357910 |
| 11 | 0.002469778060913 | 0.000997543334961 | 0.000000000000000 |
| 12 | 0.005327701568604 | 0.000997543334961 | 0.001005887985229 |
| 13 | 0.010728359222412 | 0.000000000000000 | 0.001020431518555 |
| 14 | 0.018995046615601 | 0.001003980636597 | 0.000000000000000 |
| 15 | 0.039257287979126 | 0.000998735427856 | 0.000000000000000 |
| 16 | 0.079669475555420 | 0.001003980636597 | 0.001000404357910 |
| 17 | 0.166922807693481 | 0.000000000000000 | 0.000999212265015 |
| 18 | 0.308875560760498 | 0.002153158187866 | 0.001008272171021 |
| 19 | 0.636044025421143 | 0.001071453094482 | 0.000000000000000 |
| 20 | 1.228482246398926 | 0.001003265380859 | 0.000998973846436 |
| 21 | 2.368465423583984 | 0.001030445098877 | 0.000987768173218 |
| 22 | 4.770162343978882 | 0.001506090164185 | 0.000515222549438 |
| 23 | 9.434110164642334 | 0.001267671585083 | 0.001489400863647 |
| 24 | 18.756065368652344 | 0.001566410064697 | 0.001013755798340 |
| 25 | 37.797737598419189 | 0.001954793930054 | 0.001538753509521 |
| 26 | 76.355279445648193 | 0.002066373825073 | 0.000000000000000 |
| 27 | 153.477586984634399 | 0.002011299133301 | 0.000999212265015 |
| 28 | 317.570328235626221 | 0.002567529678345 | 0.001086711883545 |
Observando esta tabla, se puede ver que para la mayoría de los casos, el algoritmo tabulado es el más rápido de los tres. Sin embargo, hay algunos casos donde el algoritmo memorizado es más rápido. En general, el algoritmo recursivo es el más lento y menos eficiente, especialmente cuando el número de elementos se vuelve grande.
Después de realizar el análisis y la obtención de datos a través de los algoritmos implementados, se procede a tabular dicha información. Con el fin de poder graficar estos resultados de manera clara y concisa se empleará la librería "pandas" de Python.
La generación de gráficos es una técnica fundamental para visualizar la información obtenida de un estudio. Asimismo, esto permite interpretar y analizar de manera fácil los datos obtenidos. A través de esta herramienta, podemos apreciar de manera más sencilla cuáles son los valores más representativos, las tendencias y patrones dentro del conjunto de datos procesados.
Por lo tanto, se muestra a continuación la tabla con los resultados de los 28 datos procesados por los algoritmos implementados para su posterior visualización de manera gráfica.
Después de realizar el análisis y la comparación entre los algoritmos mediante la tabla y las gráficas obtenidas gracias a la librería "pandas" de Python, se puede apreciar claramente que los algoritmos basados en memoria son significativamente mejores que el algoritmo recursivo. Es importante destacar que, el algoritmo recursivo alcanza valores exponencialmente superiores a partir de la iteración número 20.
En última instancia, es imprescindible realizar una comparación exhaustiva entre la complejidad práctica calculada y la complejidad teórica expresada como f(n). Para llevar a cabo este análisis, se empleará la librería de Python denominada "statsmodels", gracias a la cual podremos contrastar los resultados tabulados con la función de complejidad mediante la aplicación de una regresión OLS (regresión por mínimos cuadrados ordinarios). Esta herramienta matemática cuenta con dos indicadores clave que nos permitirán evaluar la calidad del ajuste obtenido. El primero de ellos es el coeficiente de determinación, también conocido como R-squared, el cual adopta valores entre 0 y 1. Cuanto más cercano sea este valor a 1, mayor será el grado de ajuste proporcionado por la regresión. Por otro lado, el segundo indicador que se utilizará es el coeficiente de correlación, cuyo cálculo se basa en la relación existente entre dos variables. De esta forma, su análisis permitirá obtener un valor preciso del tiempo requerido para un valor dado de n.
La tabla presentada muestra los resultados de la regresión OLS aplicada a los datos tabulados del algoritmo recursivo. El valor R-squared, que representa el coeficiente de determinación, tiene un valor de 1.000, lo que indica que la dispersión observada en los datos se ajusta perfectamente a la complejidad identificada. Además, el coeficiente denominado como 'coef' tiene un valor de 1.174 × 10−6, lo que permite establecer una relación exacta entre ambos factores.
Por lo tanto, se puede afirmar que la ecuación del tiempo dado n elementos está correctamente representada por la regresión OLS obtenida a partir de los datos tabulados del algoritmo recursivo. En consecuencia, esta ecuación puede ser utilizada para predecir el tiempo requerido para ejecutar el algoritmo en función del número de elementos que se estén procesando.
En la tabla presentada se muestran los resultados de la regresión OLS aplicada a los datos tabulados del algoritmo implementado. El valor R-squared, que representa el coeficiente de determinación, es de 0.560, lo que indica que la dispersión observada en los datos tiene una relación positiva con la complejidad identificada. Además, el coeficiente denominado como 'coef' tiene un valor de 8.229 × 10−8, lo que permite establecer una relación entre el tiempo requerido para ejecutar el algoritmo y el número de elementos que se están procesando.
Así pues, se puede afirmar que la ecuación del tiempo dado n elementos está correctamente representada por la regresión OLS obtenida a partir de los datos tabulados del algoritmo implementado. En consecuencia, esta ecuación puede ser utilizada para predecir el tiempo requerido para ejecutar el algoritmo en función del número de elementos que se estén procesando.
El valor de R-squared obtenido es de 0.207, lo que indica que el modelo de regresión utilizado explica únicamente un 20.7% de la variabilidad de la variable dependiente en función de la variable independiente. El valor del coeficiente de regresión denominado 'coef' es 3.918e-08, lo que sugiere una relación muy débil entre las variables. Se puede observar que tanto el intercepto como la variable independiente tienen valores significativos, lo que sugiere que ambos términos son importantes para explicar la variabilidad de la variable dependiente. En general, se podría afirmar que la relación entre las variables no es muy fuerte y que el modelo utilizado no explica bien la variabilidad en la variable dependiente, sin embargo la complejidad teorica si es la presentada.
Las pruebas unitarias son una técnica de programación que consiste en realizar pruebas exhaustivas a un conjunto de funciones o métodos, de manera individual y controlada, con el objetivo de verificar el correcto funcionamiento del código, antes de ser implementado.
En el caso de los algoritmos dinámicos, estas pruebas son particularmente importantes, ya que estos algoritmos utilizan técnicas complejas y recursivas, donde es fácil caer en la generación de errores sutiles que pueden llevar a resultados inconsistentes. A continuación se presenta una breve explicación de cada prueba unitaria:
-
test_case_0: se comprueba que el algoritmo implementado en las funciones
Recursive,MemoryyBottomUpretorna correctamente los valores para un caso base. En este caso, se utiliza una lista con un solo elemento. -
test_case_1: se comprueba que el algoritmo implementado en las funciones
Recursive,MemoryyBottomUpretorna correctamente el valor máximo de una lista con dos elementos, uno de ellos negativo. -
test_case_2: se comprueba que el algoritmo implementado en las funciones
Recursive,MemoryyBottomUpretorna correctamente una lista con tres elementos (dos de ellos negativos). -
test_case_3: se comprueba que el algoritmo implementado en las funciones
Recursive,MemoryyBottomUpretorna correctamente una lista con cuatro elementos (uno de ellos negativo). -
test_case_4: se comprueba que el algoritmo implementado en las funciones
Recursive,MemoryyBottomUpretorna correctamente una lista con cinco elementos (dos de ellos negativos). -
test_case_5: se comprueba que el algoritmo implementado en las funciones
Recursive,MemoryyBottomUpretorna correctamente una lista con seis elementos (tres de ellos negativos).
Todas las pruebas fueron exitosas, lo que demuestra que las funciones Recursive, Memory y BottomUp están implementadas correctamente y devuelven los resultados esperados.
Todos los codigos usados se pueden encontrar dentro de la carpeta Codigos presentada en este documento






