Anda di halaman 1dari 8

Terminación en Funcional

Algoritmos y Estructuras de Datos I


Segundo Cuatrimestre 2004

Hasta ahora hemos visto cómo especificar algoritmos (i.e., describir su comportamiento) y cómo
escribir programas en lenguaje funcional. Dada cierta especificación, deberı́amos ser capaces de
escribir una función (recursiva) que la satisfaga. Ahora bien, ¿cómo podemos estar seguros de que la
función que escribimos realmente satisface la especificación? Si miramos la Regla I (interpretación
semántica) de especificaciones, veremos que esta pregunta se puede dividir en dos partes:

1. ¿Cómo podemos estar seguros de que la función nos da un resultado en un número finito de
pasos?
2. ¿Cómo podemos estar seguros de que el resultado de la función hace verdadera a la post-
condición?
La primera pregunta se refiere a la terminación de una función, mientras que la segunda habla
sobre su correctitud. Terminación es lo mı́nimo que debe cumplir una función para poder ser
considerada un algoritmo; mientras que correctitud es lo que necesitamos de un algoritmo para
poder decir que satisface una especificación. En este curso no haremos demostraciones formales de
correctitud de algoritmos en funcional1 , pero sı́ veremos cómo probar formalmente su terminación.

1. Evaluaciones que no terminan


Antes de buscar una forma de demostrar formalmente que un programa en funcional termina,
conviene preguntarnos si puede pasar que no termine en primer lugar. Consideremos la siguiente
definición de función:
func1 :: Integer → Integer → Integer
func1 0 acum = acum
func1 i acum = func1 (i−1) (2∗acum)

Esta definición es sólo una numeración de reglas, las definiciones de por sı́ no terminan ni dejan
de terminar. Lo que podemos preguntarnos es qué sucede si intentamos evaluar una expresión
usando la definición anterior. Es decir, sı́ tiene sentido preguntarnos si termina, por ejemplo, la
evaluación de la expresión func1 6 5. En este caso efectivamente termina y su valor es 320.
También es interesante, en este caso, considerar una expresión cuya evaluación no termina.
Consideremos, entonces la expresión func1 (-6) 5. Como primer paso de la evaluación de esta
expresión se obtiene (aplicando la segunda ecuación) func1 (-7) 10, pero al evaluar recursiva-
mente esta expresión obtenemos en un paso func1 (-8) 20 y ası́ indifinidamente.
Consideremos ahora esta otra función:
func2 :: [a] → (a,a)
func2 [x1,x2] = (x1,x2)
func2 (x:xs) = (x, snd (func2 xs))
1 Se puede probar formalmente la correctitud de un algoritmo en lenguaje funcional usando una técnica que se

conoce como inducción estructural, similar a la inducción en los naturales. Este tema se presenta en Algoritmos y
Estructuras de Datos II.

1
Intuitivamente, lo que func2 hace es devolver el primer y el último elemento de una lista. Por
ejemplo, la evaluación de func2 "hola, fonola" termina y su valor es (’h’, ’a’). Veamos,
ahora, lo que sucede con func2 "h"; como no hay ninguna regla que nos diga qué debe hacer
func2 cuando su entrada es una lista de longitud 1, esta expresión produce un error. Ahora bien,
¿podemos decir entonces que la evaluación de esta expresión termina? Para poder afirmar que
la evaluación terminó, deberı́amos tener un valor (distinto de ⊥) asociado a la expresión. Sin
embargo, no hay ningún valor definido de tipo (Char, Char) asociado a func2 "h", con lo cual,
diremos que esta expresión no termina2 .

2. Funciones parciales
Consideremos la función func1; ya hemos visto (al menos intuitivamente) que para algunos
valores de la entrada su evaluación termina, mientras que para otros valores no. Las funciones
como esta, que sólo asignan un valor (definido) a un subconjunto propio de su dominio, se llaman
funciones parciales, mientras que a las funciones definidas sobre todo su dominio (con la sóla
excepción de ⊥) se las llama totales. Las funciones parciales aparecen con mucha frecuencia en
Computación. Ahora bien, si alguien desea usar una función parcial, en general necesita conocer
cuáles son los elementos del dominio para los cuáles la función está definida. Veamos la siguiente
especificación:

∆ : Z = term1(i, acum : Z)
P ≡ {i ≥ 0}
Q ≡ {True}

Esta especificación describe a todos los algoritmos que toman dos variables enteras, devuelven un
entero, y terminan siempre que la primera de sus variables sea mayor o igual que cero3 . En partic-
ular, func1 satisface esta especificación, por cuanto termina toda vez que su primer parámetro (i)
es mayor o igual que cero. Lo que este ejemplo ilustra es que podemos identificar la precondición
de una función con un conjunto de valores de la entrada para los cuáles la función termina.
Como en general escribimos funciones para intentar satisfacer cierta especificación, de aquı́ en
más nos preocuparemos sólo por el problema de la terminación de una función f cada vez que se
cumpla cierta precondición Pf . Para formalizar un poco esta noción, recurrireremos a una notación
especial que nos permita hacer referencia a todas las variables de entrada de una función: x. Por
ejemplo, en func1 x, con x hacemos referencia a dos variables de tipo entero, mientras que en
func2 y, con y hacemos referencia a sólo una lista.
Ahora sı́, dadas una función f y una precondición Pf , diremos que f termina si para cua-
lesquiera variables de entrada x que estén definidas y hagan verdadera a Pf , la evaluación de f x
termina.

3. Demostración de terminación
Al mirar una función como func1, puede parecer evidente que ésta termina si tomamos como
precondición Pfunc1 ≡ {i ≥ 0}. Uno podrı́a estar tentado de creer que dada cualquier función,
siempre es evidente si termina o no cuando se cumple su precondición. Consideremos, entonces, la
siguiente función:
2 No serı́a correcto pensar que como al evaluar func2 "h" en Hugs, éste nos devuelva el control, la evaluación de

esta expresión termina. Si nos guiáramos por esto, deberı́amos concluir que, como Hugs puede abortar la evaluación
de func1 (-6) 5 con un mensaje de error (e.g. si se queda sin memoria) entonces la evaluación de esta expresión
termina.
3 Es interesante notar que esta especificación no dice nada sobre cómo debe ser ∆; sólo pide que se llegue a algún

valor en un número finito de pasos cuando i ≥ 0.

2
func3 :: Int → Bool
func3 n
| n == 1 = True
| n ‘mod‘ 2 == 0 = func3 (n ‘div‘ 2)
| otherwise = func3 (3∗n + 1)
¿es evidente si func3 termina cuando consideramos Pfunc3 ≡ {x > 0}? (quien piense que es evidente
que no termina, deberı́a encontrar rápidamente un ejemplo donde efectivamente no termine, quien
piense que es evidente que termina, deberı́a encontrar alguna justificación. . . )
Este ejemplo muestra claramente que no podemos basarnos sólo en la intuición para determinar
si una función recursiva cualquiera termina. Lo siguiente que uno puede preguntarse es si tiene
alguna forma mecánica de determinar en una cantidad finita de pasos (i.e., un algoritmo!) si
dada una función cualquiera termina. La respuesta es que no existe ninguna forma mecánica de
determinar si cualquier función dada termina 4 .
¿Esto significa que no tenemos que preocuparnos por si las funciones que escribimos terminan ya
que de todas formas esto nunca se puede saber? No; como veremos a continuación, existen formas
de demostrar formalmente que ciertas funciones sı́ terminan. En general, cuando escribimos una
función recursiva, tenemos en mente que debe terminar (dentro de su precondición); el esquema
de demostración que veremos no hace más que formalizar esta intuición.

3.1. Contextos de evaluación


Consideremos las siguientes funciones:
func4 :: Int → Int → Bool
func4 x y = (y > 0) && (x ‘div‘ y /= 0)

func5 :: Int → Int → Int


func5 x y
| y == 0 = x
| y>0 = x ‘div‘ y
| x == 0 = y
| otherwise = y ‘div‘ x

func6 :: Int → Int → Int


func6 x y = if (y == 0) then x else (x ‘div‘ y)

func7 :: [Int] → Bool


func7 [] = True
func7 xs = (head xs) == (last xs)
Ninguna de estas funciones es recursiva. Luego, para asegurarnos de que terminan sólo debe-
mos comprobar que las funciones a partir de las cuáles están definidas (i.e., div :: Int -> Int
-> Int, (==) :: Int -> Int -> Bool, head :: [a] -> a, last :: [a] -> a, etc.) están bien
usadas; es decir, que siempre que se las utiliza se cumple su precondición.
Una inspección rápida sobre estas funciones podrı́a convencernos de que siempre terminan.
Ahora bien, observemos la aparición de la expresión x ‘div‘ y en func4. La función div en este
caso se indefinirı́a si el valor de y fuera 0; sin embargo, podemos asegurar que esto no sucede ya
que para evaluar esta expresión, previamente fue necesario evaluar (y > 0) y determinar que su
valor era True. Es decir, el contexto en el que aparece una expresión es importante para poder
inferir si se la está usando en forma correcta.
Los restantes ejemplos muestran otras formas de obtener información de contexto. Ası́, en
func5, que la guarda y >0 sea verdadera nos da suficiente información para inferir que se puede
evaluar la expresión x ‘div‘ y; mientras que el hecho de que todas las guardas anteriores no se
hayan cumplido, alcanza para concluir que no es cierto que x == 0, y por lo tanto, es seguro evaluar
y ‘div‘ x. En func6, que la guarda del if sea falsa, asegura que x ‘div‘ y puede evaluarse.
4 Una demostración sumamente elegante de este hecho se ve en Lógica y Computabilidad.

3
Por último, en func7, cuando xs no hace pattern matching con [] podemos inferir que |xs| > 0 y,
por lo tanto, que se pueden evaluar head xs y last xs. Expresaremos los contextos de evaluación
usando términos de tipo bool del lenguaje de especificación.

3.2. Hipótesis de la demostración


El esquema para demostrar terminación tiene algunas hipótesis fuertes que debe cumplir una
función para que pueda aplicársele. Además, dada la variedad de formas en que se puede definir
una función en Haskell, para presentar el esquema de demostración utilizaremos una serie de
definiciones que nos permitan tratar a todas las declaraciones de funciones en forma homogénea
(sin importar si se usaron guardas, pattern matching, etc.).
Empecemos con algunas definiciones. De aquı́ en más, pensaremos las declaraciones de funciones
en términos de ecuaciones. Podemos identificar las ecuaciones por la presencia de un =. Por ejemplo,
func4 y func6 constan de sólo una ecuación, mientras que en func5 aparecen cuatro y en func7
aparecen dos. Además, consideraremos que asociada a la parte izquierda de cada ecuación hay una
guarda implı́cita, que representará lo que debe ser verdadero para que se evalue la parte derecha
de la ecuación. Una guarda implı́cita, en este sentido, es un término de tipo bool del lenguaje de
especificación. Por ejemplo, la guarda implı́cita de las ecuaciones de func4 y func6 es {True},
porque no hay ninguna condición sobre x e y para evaluar el lado derecho de la ecuación. La
guarda implı́cita de la primera ecuación de func5 es, obviamente, {y = 0}, mientras que la de
la primera ecuación de func7 es {|xs| = 0}. Si bien están relacionada, no se deben confundir las
guardas implı́citas con las guardas (a secas) como las que se usan en func5. A estas últimas, para
evitar confusiones, las llamaremos también guardas explı́citas.
De esta forma, podemos generalizar la definición de cualquier función y considerarla una se-
cuencia (finita) de ecuaciones de la forma:

f x/(B1 , G1 ) =R1
f x/(B2 , G2 ) =R2
..
.
f x/(Bn , Gn )=Rn
Donde f es el nombre de la función, x son sus variables de entrada y Bi , Gi y Ri son respectiva-
mente la guarda implı́cita, la guarda explı́cita y el lado derecho asociados a la i-ésima ecuación.
Vale recordar que Bi es un término booleano del lenguaje de especificación, Gi es una expresión
de tipo Bool de Haskell y los Ri son expresiones en Haskell, todos del mismo tipo. En el caso
en que una ecuación no tenga guarda explı́cita (como sucede con todas las ecuaciones de func4,
func6 y func7), podemos asumir que Gi es la expresión de Haskell True.
Las hipótesis del esquema de demostración aplicado a la función f son:

1. Está demostrado que cada función g que aparece en alguna Gi termina si se utiliza cierta
precondición (conocida) Pg .
2. Está demostrado que cada función g que aparece en alguna Ri (con g 6= f ) termina si se
utiliza cierta precondición (conocida) Pg

De estas hipótesis se desprende que no se puede utilizar este esquema de demostración sobre
cualquier función. Quedan afuera, por ejemplo, las funciones mutuamente recursivas. Como ejem-
plo de función mutuamente recursiva podemos considerar:
func8a, func8b :: Int → Int → Int
func8a 0 y = y
func8a x y = func8b x (y−1)

func8b x 0 = x
func8b x y = func8a (x−1) y
donde func8a está definida usando func8b y viceversa.

4
4. Demostrar que la función no da errores
Como ya vimos, para demostrar que una función termina se debe comprobar que no puede
dar un error. Esta es la parte más sencilla de la demostración; es sólo una formalización de los
chequeos que uno ya tiene incorporados.
Dada una función definida mediante las ecuaciones

f x/(B1 , G1 ) =R1
f x/(B2 , G2 ) =R2
..
.
f x/(Bn , Gn )=Rn
y dada Pf una precondición de f (i.e. un predicado del lenguaje de especificación donde sólo las
variables x aparecen libres), si se cumplen simultáneamente:

R1 Si x están definidas y hacen verdadero Pf , entonces existe un i entre 1 y n tal que x hacen
verdadero a Bi
R2 Si g y (g puede ser f) es una expresión que aparece en Gi o en Ri , entonces si x hace verdadero
Pf ∧ Cgy , entonces y hace verdadero Pg (donde Cgy representa el contexto de evaluación de
la expresión g y en consideración)

entonces si x están bien definidas y hacen verdadera a Pf , entonces la evaluación de f no da un


error.
La razón de ser de estas reglas es sencilla. R1 dice que nunca puede suceder que no se encuentre
una ecuación apropiada para la entrada (como sucedı́a con func2 []). R2, por otro lado, dice
que ninguna función se evalua fuera de su precondición. Por lo tanto si, por hipótesis, todas las
funciones que se utilizan terminan si se las utiliza con su precondición, no puede ser que f no
termine usando Pf por motivo de un error.

5. Demostrar que la función no se evalua infinitamente


Lo que necesitamos para terminar nuestro esquema de demostración es dar una forma de
garantizar que la función termina su evaluación en un número finito de pasos. Esta es la parte
más difı́cil de la demostración. Para que una función recursiva termine, cada paso recursivo tiene
que acercarnos de alguna manera a alguno de los casos base. La idea, entonces, es dar una forma
numérica de medir cómo es que cada paso recursivo se acerca a la solución del problema.
La formalización de esta idea la hacemos usando funciones variantes. Una función variante para
f x será una función Fv (x) con valor entero, que puede darse de manera explı́cita mediante un
término de tipo Z del lenguaje de especificación, o implı́cita mediante un predicado F V (x, y : Z)
(donde las variables x son de los tipos que corresponda) tal que, para cada asignación de valores
a las variables de entrada x, existe un único entero y que hace verdadero a F V (x, y). En este caso
decimos que el valor de Fv (x) es y. Para garantizar que una f termina, será necesario proponer
alguna función variante, y demostrar que ésta cumple los requisitos que veremos a continuación.
Para simplificar el esquema de demostración, conviene suponer que en cada ecuación de la
forma
f x/(Bi , Gi ) = Ri
Ri es un caso base o un caso recursivo, pero no ambas cosas. Vale notar que en situaciones como
en
func9 :: [Int] → Bool
func9 xs = (not (null xs)) && (head xs == 0 || (func9 (tail xs)))

func10 :: Int → Int → Int


func10 x y = if ( x == 0 ) then y

5
else if ( y == 0 ) then x
else (func10 (x−1) (y−1))
esto no se cumple. Como es trivial convertir una función de estas a otra equivalente que sı́ cumpla
con la propiedad expresada más arriba, podemos asumir esta forma sin perder generalidad. Por
ejemplo, en el caso anterior tendrı́amos que las funciones equivalentes son:
func9’ :: [Int] → Bool
func9’ xs
| (null xs) = False
| (head xs == 0) = True
| otherwise = func9’ (tail xs)

func10’ :: Int → Int → Int


func10’ x y
| ( x == 0 ) =y
| ( y == 0 ) =x
| otherwise = func10’ (x−1) (y−1)
Lo que esto significa es que podemos demostrar la terminación de func9 y func10, pero para ello
las consideramos como si fueran de la forma func9’ y func10’. Ahora sı́, podemos ver el esquema
de demostración formal.
Dada una función definida mediante las ecuaciones

f x/(B1 , G1 ) =R1
f x/(B2 , G2 ) =R2
..
.
f x/(Bn , Gn )=Rn
y dadas Pf una precondición de f que verifica R1 y R2, y una función variante Fv (x), si se verifica
simultáneamente:

R3 Si x están bien definidas y hacen verdadero a Pf , entonces:


1. si la función variante está dada en forma explı́cita, entonces Fv (x) no es ⊥
2. si la función variante está dada en forma implı́cita, entonces es verdadero el predicado
(∃y : Z)(F V (x, y) ∧ (∀z : Z)(F V (x, z) → z = y))
R4 Para todo i y j, entre 1 y n, si Ri es un caso base, y Rj es un caso recursivo, entonces i < j
R5 Sea m > 0 la cantidad de casos base de la función. Existe un entero k tal que se cumple
((Pf ∧ ¬B1 (x) ∧ · · · ∧ ¬Bm (x)) → Fv (x) > k)
R6 Para todo i entre 1 y n, si Ri es un caso recursivo, entonces en cada aparición de f y en Ri se
cumple ((Pf ∧ ¬B1 ∧ . . . ∧ ¬Bi−1 ∧ Bi (x)) → Fv (x) > Fv (y))

entonces f x termina.
Analicemos lo que nos dicen estas reglas. R3 pide que tenga sentido evaluar Fv (x) cuando
vale la precondición. R4 es una condición fuerte que garantiza que todo caso base sea alcanzable;
condición que no es cumplida por la siguiente función:
func11 :: Int → Int
func11 x = x ∗ (func11 (x−1))
func11 0 = 1
R5 pide que exista una cota para el valor que puede tomar Fv (x) sin llegar al caso base. Es decir,
que si en algún momento Fv (x) corresponde a un valor por debajo de la cota, necesariamente se
tiene que estar en un caso base. Por último, R6 pide que cada vez que se entra en un paso recursivo,
el valor de la función variante aplicado a los argumentos del paso recursivo sea estrictamente menor
que el valor de los argumentos en dicha evaluación.

6
Suponiendo que R1 y R2 se cumplen (es decir, que la evaluación de f x no produce un error),
¿por qué R3–R6 garantizan que ésta no se lleva a cabo infinitamente? Supongamos que queremos
evaluar f x; como suponemos que x satisface Pf , por R3, Fv (x) es un entero distinto de ⊥. Ahora
bien, si Fv (x) es menor que la cota k de R5, entonces por dicha regla y la regla R1, se tiene que
cumplir la guarda de alguno de los casos base de f y, por lo tanto, f x se evalua en cero pasos
recursivos. Si, por el contrario, Fv (x) es un entero mayor que k entonces, por R6 cada evaluación
recursiva que se realice deberá llegar a un caso base en, a lo sumo, Fv (x) − k pasos. Luego, como
cada evaluación recursiva termina, la evaluación de f x necesariamente termina.

6. Ejemplos
En lo que sigue vamos a aplicar nuestra maquinaria a algunos casos concretos. Supongamos
que nos hemos propuesto demostrar la terminación de una implementación de la función sp, que
busca el menor entero primo no menor que un entero n recibido como parámetro, que debe ser
mayor a 1. Formalmente, sp se comporta del siguiente modo:

∆ : Z = sp(n : Z)
Psp ≡ {n > 1}
Qsp ≡ {esSigPrimo(∆, n)}

donde
Pp−1
esPrimo(p : Z) ≡ {(p > 1 ∧ i=2 β(p mod i = 0) = 0)}

esSigPrimo(p, n : Z) ≡ {(p ≥ n ∧ esPrimo(p) ∧ ¬(∃q : Z)(n ≤ q < p ∧ esPrimo(q)))}.

Tomemos una implementación en Haskell y probemos que termina en una cantidad finita de pasos.
sp :: Int → Int
sp n | primo n = n
| otherwise = sp (n+1)

Para ello, empecemos por traducirla al lenguaje ecuacional definido en la Sección 3.2. Es necesario
escribir cada guarda según el esquma sp x/(Bi , Gi ) = Ri , donde Bi sea una expresión de tipo B
del lenguaje de especificación que sea verdadera exactamente cuando la expresión Haskell de tipo
Bool Gi evalúe a True:

sp n / (esPrimo(n), primo n) = n
sp n / (True, True) = sp (n+1)
Vamos a suponer demostrado que la función primo termina siempre que se satisfaga su precondición
Pprimo ≡ {x > 0}. Ahora, dado que sp no es recursiva sobre otra funcion que no sea sı́ misma
(asumiendo que para evaluar la función Haskell primo, usada en la primera guarda de sp, nunca
haga falta evaluar nuevamente a sp), que cada guarda es o bien un caso base (la primera) o un
caso recursivo (la segunda), pero no ambos, y que se tienen precondiciones adecuadas para las
funciones Haskell usadas (primo), la definición ecuacional dada arriba se adecua a las hipótesis
enunciadas en la Sección 3.2. Por lo tanto, nos alcanza con verificar que las reglas R1−R6 se
satisfacen para garantizar la finalización de sp en todos los casos pertinentes:

R1 En este caso, ver que cualquier entrada válida hace verdadera a alguna de las guardas
implı́citas es trivial, ya que la segunda no es otra cosa que True. Luego, no hay riesgo de
que dada una entrada aceptada en la precondición, no haya ninguna ecuación aplicable a la
misma.
R2 Se debe verificar que en ningún momento se utilice una función violando su precondición.

7
• En la primera ecuación, o sea el caso base, alcanza con observar que la precondicin
de sp evaluada en n es más fuerte que la de primo evaluada en n; es decir que n > 1
implica n > 0.
• ¿Qué sucede en el caso recursivo? Sabemos que la guarda implı́cita del caso base es
falsa. En particular, esto implica que n 6= 2, ya que 2 es primo. Por otro lado, la
precondición Psp garantiza n > 1. Luego, por contexto de ejecución y precondición de
la función podemos asegurar que, al ejecutar el paso recursivo sp (n+1), n > 2, con lo
cual n + 1 > 3 > 1 y se satisface la precondición Psp evaluada en n + 1.

El cumplimiento de las dos reglas anteriores garantiza que la evaluación de sp no provocará er-
rores para ningún valor dentro de su dominio. Para asegurar que la evaluación puede llevarse a
cabo en una cantidad finita de pasos o aplicaciones de las reglas ecuacionales, vamos a definir una
función variante del siguiente modo:

n!+1
!
X
Fv (n) = k · β(esPrimo(k) ∧ (∀i : Z)(n ≤ i < k → ¬esPrimo(i))) −n
k=n

Para entender esta función variante, conviene tener en cuenta que para n > 1, siempre debe existir
un primo p tal que n ≤ p ≤ n! + 15 . La cota que usaremos en la demostración es −1.
Ahora vamos a corroborar que las demás reglas R3−R6 se satisfacen para Fv .

R3 La sumatoria de Fv está acotada y los predicados que intervienen nunca se indefinen (si
las variables no se indefinen). Por lo tanto, Fv (n) no es ⊥.
R4 Claramente, el caso base está antes que el caso recursivo.
Pn!+1
R5 Como k=n k · β(esPrimo(k) ∧ (∀i : Z)(n ≤ i < k → ¬esPrimo(i))) ≥ n siempre que
n > 1 (lo cual garantiza la precondición), entonces ((Psp ∧¬esPrimo(n)) → Fv (n) ≥ 0 > −1)
es siempre verdadero.
R6 Como sólo tenemos un caso recursivo, lo que tenemos que comprobar es que

((Psp ∧ ¬esPrimo(n) ∧ True) → Fv (n) > Fv (n + 1))

Ahora bien, como n es positivo y no es primo, entonces


Pn!+1
k · β(esPrimo(k) ∧ (∀i : Z)(n ≤ i < k → ¬esPrimo(i)) =
Pk=n
(n+1)!+1
k=n+1 k · β(esPrimo(k) ∧ (∀i : Z)(n + 1 ≤ i < k → ¬esPrimo(i))

con lo cual,

(n+1)!+1
X
Fv (n + 1) = k · β(esPrimo(k) ∧ (∀i : Z)(n + 1 ≤ i < k → ¬esPrimo(i)) − (n + 1)
k=n+1
n!+1
X
= k · β(esPrimo(k) ∧ (∀i : Z)(n ≤ i < k → ¬esPrimo(i)) − n − 1
k=n
= Fv (n) − 1

5 Se puede ver usando un razonamiento similar al que se usa en general para probar que existen infinitos primos.

Anda mungkin juga menyukai