Cómo implementar programación dinámica en Swift

En nuestra exploración de algoritmos, hemos aplicado muchas técnicas para producir resultados. Algunos conceptos han utilizado patrones específicos de iOS, mientras que otros se han generalizado. Aunque no se ha mencionado explícitamente, algunas de nuestras soluciones han utilizado un estilo de programación particular llamado programación dinámica. Si bien es sencillo en teoría, su aplicación a veces puede ser matizada. Cuando se aplica correctamente, la programación dinámica puede tener un poderoso efecto sobre cómo escribir código. En este ensayo, presentaremos el concepto y la implementación de la programación dinámica.

Guardar para más adelante

Si ha comprado algo a través de Amazon.com, estará familiarizado con el término del sitio: "Guardar para más tarde". Como la frase lo indica, los compradores tienen la opción de agregar artículos a su carrito o guardarlos en una "Lista de deseos" para verlos más tarde. Al escribir algoritmos, a menudo enfrentamos una elección similar de completar acciones (realizar cálculos) a medida que los datos se interpretan o almacenan los resultados para su uso posterior. Los ejemplos incluyen la recuperación de datos JSON de un servicio RESTful o el uso de Core Data Framework:

En iOS, los patrones de diseño pueden ayudarnos a calcular el tiempo y coordinar cómo se procesan los datos. Las técnicas específicas incluyen operaciones de subprocesos múltiples (por ejemplo, Grand Central Dispatch), notificaciones y delegación. La programación dinámica (DP), por otro lado, no es necesariamente una sola técnica de codificación, sino más bien cómo pensar en las acciones (por ejemplo, subproblemas) que ocurren cuando una función opera. La solución DP resultante podría diferir según el problema. En su forma más simple, la programación dinámica se basa en el almacenamiento y la reutilización de datos para aumentar la eficiencia del algoritmo. El proceso de reutilización de datos también se llama memorización y puede tomar muchas formas. Como veremos, este estilo de programación ofrece numerosos beneficios.

Fibonacci revisitado

En el ensayo sobre Recursión, comparamos la construcción de la secuencia clásica de valores de matriz utilizando técnicas iterativas y recursivas. Como se discutió, estos algoritmos fueron diseñados para producir una secuencia de matriz, no para calcular un resultado particular. Teniendo esto en cuenta, podemos crear una nueva versión de Fibonacci para devolver un único valor Int:

func fibRecursive (n: Int) -> Int {
    si n == 0 {
        volver 0
    }
    
    si n <= 2 {
        volver 1
    }
    
    return fibRecursive (n: n-1) + fibRecursive (n: n-2)
}

A primera vista, parece que esta función aparentemente pequeña también sería eficiente. Sin embargo, luego de un análisis adicional, vemos que se deben hacer numerosas llamadas recursivas para que calcule cualquier resultado. Como se muestra a continuación, dado que fibRecursive no puede almacenar valores calculados previamente, sus llamadas recursivas aumentan exponencialmente:

Fibonacci Memoized

Probemos una técnica diferente. Diseñado como una función Swift anidada, fibMemoized captura el valor de retorno de la matriz de su subfunción fibSequence para calcular un valor final:

extensión Int {
    
    // versión memorizada
    función mutante fibMemoized () -> Int {
        
        // construye la secuencia de la matriz
        func fibSequence (_ secuencia: Array  = [0, 1]) -> Array  {
            
            var final = Array  ()
            
            // copia mutada
            salida var = secuencia
            
            let i: Int = output.count
            
            // establece la condición base - tiempo lineal O (n)
            si yo == yo mismo {
                salida de retorno
            }
            
            dejar resultados: Int = salida [i - 1] + salida [i - 2]
            output.append (resultados)
            
            // establecer iteración
            final = fibSequence (salida)
            regreso final
            
        }
        
        
        // calcular producto final - tiempo constante O (1)
        dejar resultados = fibSequence ()
        responda: int = resultados [results.endIndex - 1] + resultados [results.endIndex - 2]
        respuesta de regreso
        
    }
}

Aunque fibSquence incluye una secuencia recursiva, su caso base está determinado por el número de posiciones de matriz solicitadas (n). En términos de rendimiento, decimos que fibSequence se ejecuta en tiempo lineal u O (n). Esta mejora del rendimiento se logra al memorizar la secuencia de matriz necesaria para calcular el producto final. Como resultado, cada permutación de secuencia se calcula una vez. El beneficio de esta técnica se ve al comparar los dos algoritmos, que se muestran a continuación:

Caminos más cortos

La memorización de códigos también puede mejorar la eficiencia de un programa hasta el punto de hacer que las preguntas aparentemente difíciles o casi irresolubles respondan. Un ejemplo de esto se puede ver con el algoritmo de Dijkstra y los caminos más cortos. Para revisar, creamos una estructura de datos única llamada Path con el objetivo de almacenar metadatos transversales específicos:

// la clase de ruta mantiene objetos que comprenden la "frontera"
ruta de clase {
    
    var total: Int
    destino var: Vértice
    var anterior: ¿Ruta?
   // inicialización de objeto
    en eso(){
        destino = Vértice ()
        total = 0
    }
}

Lo que hace que Path sea útil es su capacidad de almacenar datos en nodos visitados previamente. Similar a nuestro algoritmo revisado de Fibonacci, Path almacena los pesos de borde acumulativos de todos los vértices atravesados ​​(total), así como un historial completo de cada vértice visitado. Utilizado de manera efectiva, esto le permite al programador responder preguntas tales como la complejidad de navegar a un destino particular Vertex, si el recorrido fue exitoso (para encontrar el destino), así como la lista de nodos visitados en todo momento. Dependiendo del tamaño y la complejidad del gráfico, no tener esta información disponible podría significar que el algoritmo demore tanto (re) calcular los datos que se vuelve demasiado lento para ser efectivo, o no poder resolver preguntas vitales debido a datos insuficientes.

¿Te gustó este ensayo? Lea y descubra mi otro contenido en Medium u obtenga el libro completo en formato EPUB, PDF o Kindle.