Enunciado del Problema
El problema original a resolver se encuentra en Hackerrank. Se muestra a continuación una versión traducida del mismo.
Los números de Fibbonacci tienen la siguiente forma:
Dado un array que contiene elementos, calcular .
El formato de entrada es:
- La primera línea contiene el tamaño del array, .
- En las siguientes líneas hay un número, la i-ésima línea contiene .
El formato de salida consiste en imprimir un sólo número entero. Este es el resto de la división entera del máximo común divisor pedido entre .
Las restricciones son las siguientes:
Solución
La primera observación a realizar es que, dado el tamaño que pueden tomar los subíndices , no se pueden pretender calcular directamente los números de Fibonacci. Debemos obtener propiedades sobre el máximo común divisor de estos que nos permitan calcularlo de forma indirecta. La siguiente serie de proposiciones persigue este objetivo.
- Proposición 1
- Sean . Se tiene que .
Demostración La prueba se realiza por inducción sobre para un arbitrario en cada paso. Para es trivial denotando . Supongamos el resultado cierto para .
donde se ha utilizado en primer lugar la hipótesis de inducción para y posteriormente la definición de la sucesión dos veces.
- Proposición 2
- Sean . Se tiene que .
Demostración En primer lugar:
donde se ha utilizado que para cualquier .
Por inducción se llega a que
Luego los términos consecutivos de la sucesión de Fibonacci son primos relativos entre sí.
Ahora, para usamos la proposición anterior:
Como y son primos relativos:
- Proposición 3
- Sean . Se tiene que .
Demostración Si es trivial. Supongamos que sin pérdida de generalidad. Tenemos que . Podemos repetir el proceso hasta que aparezca un 0 en los índices. Estamos haciendo en definitiva el Algoritmo de Euclides sobre los índices y por ser el mismo proceso tenemos garantizado que el índice final no nulo es el máximo común divisor. Esto es:
- Proposición 4
- Sean y . Se tiene que .
Demostración Usaremos que :
Repetimos el proceso veces y, usando la definición del máximo común divisor de elementos sobre los , obtenemos el resultado.
De la proposición anterior se tiene automáticamente el Algoritmo 1, que resuelve el problema.
- Algoritmo 1
- Dados con . Realizamos el siguiente algoritmo:
-
- Calculamos .
-
- Calculamos
La única cuestión restante consiste en la implementación del algoritmo anterior. Para calcular el máximo común divisor de la lista utilizamos el Algoritmo de Euclides para los dos primeros elementos, que quedan sustituidos por el resultado obtenido. Repetimos el proceso hasta obtener un único número final que es el máximo común divisor buscado. Este algoritmo tiene eficiencia veces la del propio Algoritmo de Euclides.
Para calcular tenemos que tener en cuenta que puede ser hasta . Es fácil realizar un algoritmo lineal para este propósito calculando todos los términos progresivamente y guardando solo los dos últimos en cada iteración. Sin embargo, esto no alcanza los objetivos en el peor caso. Se propone la siguiente idea para obtener una solución logarítmica sobre :
Consecuentemente, por inducción:
En nuestro caso:
Por consiguiente:
El problema se ha reducido a la exponenciación de la matriz . La exponenciación de matrices se puede conseguir logarítmica de forma análoga a la exponenciación (con exponente natural) de un número. El Algoritmo 2 muestra un pseudo-código recursivo para este propósito.
- Algoritmo 2
- Función que devuelve el resultado de elevar la matriz a .
-
def Potencia(A,n): if n == 1: return A elif n % 2 == 0: B = Potencia(A,n/2) return B*B else: B = Potencia(A,n/2) return A*B*B
Puesto que puede ser , el valor obtenido por el Algoritmo 2 en este caso no cabría en memoria. De todas formas, debemos imprimir el resultado módulo . Puesto que el módulo de una suma o producto es el módulo de la suma o productos de los módulos, podemos aplicar el módulo a cada operación realizada manteniendo el funcionamiento. De esta forma los números con los que trabajará el algoritmo serán menores que , por lo que puede ejecutarse sin ningún problema.
Implementación en Haskell
Para la implementación en Haskell de la solución, usamos la función gcd
de la
biblioteca estándar y la convertimos en una función sobre listas usando foldr
.
El cálculo de Fibonacci lo hacemos usando la exponenciación de matrices para calcular el par en función del par . En concreto, si llamamos :
El código queda como sigue:
import Control.Monad
-- GCD sobre listas de enteros
gcdList :: [Int] -> Int
gcdList = foldr gcd 0
-- Entrada y salida según el formato
-- Acaba imprimiendo: fib ((gcdList list) - 1)
main :: IO ()
main = do n <- readLn :: IO Int
list <- replicateM n (readLn :: IO Int)
putStrLn $ show $ fib ((gcdList list)-1)
-- Calcula fibonacci mod 10**9+7
fib :: (Integral a) => a -> a
fib = fst . fib2
-- Calcula el par (fib n, fib (n + 1)) mod 10**9+7
-- El caso de inducción usa la exponenciación de matrices implícitamente.
-- Reescribimos la suma y el producto para usarlos a módulo 10**9+7.
fib2 :: (Integral a) => a -> (a,a)
fib2 0 = (1, 1)
fib2 1 = (1, 2)
fib2 n
| even n = ((a.*a) .+ (b.*b), (c.*c) .- (a.*a))
| otherwise = ((c.*c) .- (a.*a), (b.*b) .+ (c.*c))
where (a,b) = fib2 (n `div` 2 - 1)
hkr = 1000000000+7
(.+) = \x y -> mod ((+) x y) hkr
(.-) = \x y -> mod ((-) x y) hkr
(.*) = \x y -> mod ((*) x y) hkr
c = a + b
Más material sobre el problema puede encontrarse en el repositorio correspondiente del doble grado.