Sie sind auf Seite 1von 9

Resolución de los ejercicios de la clase 8

Divisibilidad y congruencia

Taller de Álgebra I

Segundo cuatrimestre de 2016

Introducción
A continuación les presentamos algunas soluciones para los ejercicios de la clase 8 del Taller, que buscan
aprovechar lo que venimos aprendiendo de Haskell para ejercitar los conceptos de divisibilidad de números
enteros. Muchos de estos ejercicios se caracterizan por el hecho de que gran parte de su resolución consiste
en obtener y demostrar algunos resultados matemáticos. Esto resulta sumamente interesante, porque nos
muestra cómo en muchas ocasiones necesitamos del auxilio de la matemática para comprobar que cierto
programa que elaboramos efectivamente produce el resultado que esperamos.

Divisibilidad
Algoritmo de la división
Enunciado

Implementar la función division :: Integer -> Integer -> (Integer, Integer)


division a d debe funcionar para a ≥ 0, d > 0, y no se pueden usar div, mod ni (/).

Resolución

El teorema del algoritmo de la división afirma la existencia de cociente y resto enteros para la
división entre dos números enteros. Más formalmente, dados a, d ∈ Z, con d 6= 0, asegura que existen
q, r ∈ Z, tales que a = q · d + r, y 0 ≤ |r| < d. La demostración del teorema es constructiva y presenta un
algoritmo, que podemos transformar sencillamente en un programa en Haskell para encontrar estos números.
A continuación, exponemos la solución para el caso particular en que a ≥ 0 y d > 0. Queda como ejercicio
extender la misma al caso general en que a es un entero cualquiera y d 6= 0.
La idea, como siempre que hacemos recursión, es resolver nuestro problema a partir de la solución de un
caso que sea, en algún sentido, “más sencillo”. De esta forma, buscamos reducir todas las instancias posibles
a un subconjunto de casos que podemos resolver de forma ad hoc, sin hacer una llamada recursiva: nuestros
casos base.
Para este problema, los casos base serán todos aquellos en que 0 ≤ a < d. Si esto sucede, podemos tomar
q = 0 y r = a, y como a = 0 · d + a, tenemos resuelta la división.
En caso contrario, debe ser que a ≥ d, y por lo tanto, a − d ≥ 0. Supongamos que sabemos resolver la
división entre (a − d) y d; es decir, tenemos q 0 y r0 tales que (a − d) = q 0 · d + r0 , y 0 ≤ r0 < d. Entonces,
a = q 0 · d + r0 + d = (q 0 + 1) · d + r0 , por lo que q = q 0 + 1, r = r0 funcionan como cociente y resto de la
división.
La resolución que acabamos de esbozar puede llevarse a un programa en Haskell de la siguiente manera:
division :: Integer -> Integer -> ( Integer , Integer )
division a d
| a < d = (0 , a )
| a >= d = ( fst ( division ( a - d ) d ) + 1 , snd ( division ( a - d ) d ) )

1
Podemos ver que division (a - d) d aparece dos veces en la misma línea de nuestra función. Esto no
solo hace que nuestro código sea más difícil de leer, y más propenso a contener errores, sino que provoca que
la computadora compute innecesariamente este resultado dos veces. Una solución a estos problemas es usar
la palabra clave where. Empleando, además, pattern matching, podemos obtener este código:
division :: Integer -> Integer -> ( Integer , Integer )
division a d
| a < d = (0 , a )
| a >= d = ( q + 1 , r )
where (q , r ) = division ( a - d ) d

Divisores y números primos


Enunciado

(a) Implementar la función divParcial :: Integer -> Integer -> [Integer]


divParcial n m debe funcionar bien siempre que 0 < m ≤ n.
(b) Utilizando divParcial, programar divisores :: Integer -> [Integer]
(c) Utilizando divisores, programar esPrimo :: Integer -> Bool

Resolución

Dados dos números enteros k, n, decimos que k divide a n (k | n), o que k es un divisor de n, si existe
q ∈ Z tal que n = q · k.
Queremos escribir un programa en Haskell que, dado n > 0, encuentre todos sus divisores positivos; en
otras palabras, el conjunto

divisores(n) = {k ∈ Z | 1 ≤ k ≤ n y k | n}

Cuando queremos resolver este problema de forma recursiva, nos encontramos con un inconveniente:
en general, conocer los divisores de (n − 1) (o de un número cualquiera menor que n) no nos dice mucho
acerca de los divisores de n. Debemos, entonces, buscar una forma de simplificar el problema que no consista
en modificar el valor de n. Una técnica que suele funcionar en esta situación es agregar a la función otro
parámetro sobre el que sí resulte útil hacer recursión. Esto quiere decir que vamos a resolver un caso más
general del problema, para luego quedarnos con el caso particular que nos resulta útil.
Una manera de hacer esto es construir la lista de divisores de forma parcial; es decir, dado un valor
0 < m ≤ n, obtener el conjunto

divParcial(n, m) = {k ∈ Z | 1 ≤ k ≤ m y k | n}

Podemos observar que, si conocemos todos los divisores de n hasta m − 1, es fácil obtener todos los
divisores hasta m: basta con verificar si m divide o no a n, y en función de esto, agregarlo o no a la lista.
Como definimos divParcial para 0 < m, esto no funciona si m = 1; pero este es un caso que podemos resolver
sencillamente, dado que el único valor que podría pertenecer a la lista es 1, que es siempre un divisor de n
para todo n > 0.
Así, considerando este último caso como caso base, podemos escribir

{1}
 si m = 1
divParcial(n, m) = {m} ∪ divParcial(n, m) si m > 1 y m | n

divParcial(n, m) si m > 1 y m - n

lo cual puede traducirse en el siguiente programa en Haskell:


divParcial :: Integer -> Integer -> [ Integer ]
divParcial n 1 = [1]
divParcial n m | mod n m == 0 = m : divParcial n ( m - 1)
| otherwise = divParcial n ( m - 1)

2
Ahora, podemos utilizar esta función para hallar todos los divisores positivos de un número n, ya que
todos ellos son menores o iguales que n; basta con tomar m = n. Así, obtenemos:
divisores :: Integer -> [ Integer ]
divisores n = divParcial n n

Por último, decimos que un entero p > 1 es primo si ningún natural k con 1 < k < p divide a p. Como
1 y p siempre son divisores de p, esto equivale a decir que p tiene exactamente dos divisores positivos. Por
lo tanto, podemos determinar fácilmente si un número p > 1 es primo a partir de la lista de sus divisores:
esPrimo :: Integer -> Bool
esPrimo p = length ( divisores p ) == 2

Algoritmo de Euclides
Enunciado

(a) Programar la función mcd :: Integer -> Integer -> Integer


que utilice el algoritmo de Euclides calcule el máximo común divisor entre dos números.
mcd a b debe funcionar siempre que a > 0, b ≥ 0.
(b) Programar la función euclides :: Integer -> Integer -> (Integer, Integer)
que utilice el algoritmo de Euclides extendido para obtener dos valores (s, t) tales que (a : b) = sa + tb.

Resolución

El algoritmo de Euclides es un procedimiento antiquísimo (data de alrededor del 300 a.C.), extrema-
damente simple y eficiente, para encontrar el máximo común divisor entre dos números a y b, que notaremos
(a : b).
Se basa en que dados a, b ∈ Z, si tomamos k ∈ Z un entero cualquiera, se cumple

(a : b) = (a + k · b : b)

Si q y r son el cociente y el resto de la división de a por b, tenemos que a = q · b + r, de donde r = a − q · b,


y entonces
(a : b) = (a − q · b : b) = (r : b) = (b : r)

La idea es aplicar este procedimiento repetidas veces, y es posible ver que siempre se llegará a un caso en
el que el valor de r será 0. Así, podemos resolver nuestro problema con una formulación recursiva, tomando
como casos base aquellos en que b = 0. A estos casos los podemos resolver directamente, ya que se cumple
que (a : 0) = a.
Es interesante notar que, si inicialmente b > a, el primer paso del algoritmo invierte estos valores, dejando
b > a, propiedad que se mantiene durante el resto del procedimiento.
Por ejemplo, los pasos para calcular (30 : 48) son:

a b Procedimiento
30 48 Dividimos 30 por 48, q = 0, r = 30
48 30 Dividimos 48 por 30, q = 1, r = 18
30 18 q = 1, r = 12
18 12 q = 1, r = 6
12 6 q = 2, r = 0
6 0 Obtenemos 6 como resultado

La escritura de este algoritmo en Haskell es bastante directa. Podemos aprovechar la función division
que programamos anteriormente, con lo cual el resultado es:
mcd :: Integer -> Integer -> Integer
mcd a 0 = a
mcd a b = mcd b r
where (q , r ) = division a b

3
Una variante interesante de este algoritmo es el llamado algoritmo de Euclides extendido, que nos
permite, mediante una pequeña adaptación, obtener dos números enteros s y t tales que s · a + t · b = (a : b).
El caso base es similar al algoritmo original: si b = 0, tenemos que (a : b) = (a : 0) = a = 1 · a + 0 · b. Es
decir, basta con tomar s = 1 y t = 0.
Para el caso recursivo, sean q y r el cociente y el resto de la división entera entre a y b, y asumamos que
tenemos dos números s0 y t0 tales que (b : r) = s0 · b + t0 · r. Esto implica que
(a : b) = (b : r) = s0 · b + t0 · r

y, como a = q · b + r, es decir, r = q · b − a, podemos deducir que

(a : b) = s0 · b + t0 · (a − q · b) = t0 · a + (s0 − q) · b
lo cual implica que el problema queda resuelto si tomamos s = t0 y t = (s0 − q).
Una posible implementación de esto en Haskell, usando pattern matching y la palabra clave where para
hacer el código más legible, es la siguiente:
euclides :: Integer -> Integer -> ( Integer , Integer )
euclides a 0 = (1 , 0)
euclides a b = (t , s - t * q )
where (s , t ) = euclides b r
(q , r ) = division a b

Clases de congruencia
Los ejercicios que siguen giran en torno al concepto de clases de congruencia. Una clase de congruencia
será para nosotros un subconjunto de los números enteros, que puede ser:

1. El conjunto vacío (∅).


2. Un conjunto de la forma P = {p ∈ Z | p ≡ a (mód b)}, donde a y b son números enteros.

En el caso de las clases de congruencia no vacías, podemos intepretarlas como los conjuntos formados
por todos los enteros que pueden obtenerse a partir de a dando una cantidad entera de “saltos” de tamaño
b, ya sea hacia adelante o hacia atrás. Por este motivo, diremos que a es la base y b es el salto de la clase
de congruencia P . Estos dos valores son los que utilizaremos en Haskell para representar a una clase de
congruencia no vacía. Notar que, si bien se trata de conjuntos que tienen infinitos elementos, podemos
representarlos a todos ellos mediante un par de valores bien elegidos.
En Haskell, representaremos las clases de congruencia mediante el siguiente tipo de datos.
data ClaseCongr = Vacio | CongruentesA Integer Integer deriving Show

Como siempre, una valor del tipo de datos solo almacena el constructor con el que se lo creó y los
parámetros que se le pasaron al mismo. Es nuestra responsabilidad como programadores, a la hora de
realizar funciones que utilicen el tipo, interpretar y darle un significado a estos valores.
Para resolver los ejercicios utilizaremos estas dos funciones, que son sumamente sencillas y cuya imple-
mentación queda como ejercicio:

multiplo :: Integer -> Integer -> Bool, que determina si el primero de sus argumentos es múl-
tiplo del segundo (debe funcionar correctamente cuando su segundo argumento es 0).
congruentes :: Integer -> Integer -> Integer -> Bool, que verifica si sus dos primeros argu-
mentos son congruentes módulo el tercero, asumiendo que el tercer argumento es distinto de cero.

Ejercicio 1
Enunciado

Programar la función pertenece :: Integer -> ClaseCongr -> Bool, que determine si un entero forma
parte de una clase de congruencia.

4
Por ejemplo:
pertenece 13 Vacio False
pertenece 13 (CongruentesA 5 4) True

Resolución

Evaluar la pertenencia de un número k ∈ Z a una clase de congruencia es sencillo, y podemos hacerlo


considerando estos dos casos:

1. Si la clase es el conjunto vacío, es claro que k no pertenece a ella.

2. Si la clase es del tipo {p ∈ Z | p ≡ a (mód b)}, debemos verificar si k cumple esta condición, para lo
cual utilizaremos la función congruentes.

Traduciendo esto a un programa en Haskell:


pertenece :: Integer -> ClaseCongr -> Bool
pertenece k Vacio = False
pertenece k ( CongruentesA a b ) = congruentes k a b

Ejercicio 2
Enunciado

Programar la función incluido :: ClaseCongr -> ClaseCongr -> Bool, que dadas dos clases de con-
gruencia P1 y P2 , determine si P1 ⊆ P2 .
Por ejemplo: incluido (CongruentesA 4 6) (CongruentesA 10 3) True

Resolución

Hay dos casos de este problema que son sumamente sencillos de resolver:

La clase de congruencia vacía está incluida trivialmente en cualquier clase de congruencia.

Ninguna clase de congruencia no vacía está incluida en la clase de congruencia vacía.

Consideremos, entonces, dos clases de congruencia no vacías:

P1 = {p ∈ Z | p ≡ a1 (mód b1 )}
P2 = {p ∈ Z | p ≡ a2 (mód b2 )}

Dado un elemento de P1 , es condición necesaria para que P1 ⊆ P2 que dicho elemento pertenezca a P2 .
Como a1 ∈ P1 , entonces, deberá pasar que a1 ∈ P2 , es decir, a1 ≡ a2 (mód b2 ). Si esto no sucede, es claro
que P1 6⊆ P2 .
Supongamos que a1 ∈ P2 ; lo que quisiéramos ver es en qué casos sucede que para todo elemento p ∈ P1 se
cumple p ∈ P2 . Veamos que esto pasa si y solo si b1 (el “salto” existente entre elementos de P1 ) es múltiplo
de b2 (el “salto” entre elementos de P2 ):

(⇒) Como cualquier elemento de P1 es un elemento de P2 , en particular a1 y a1 + b1 pertenecen a P2 . Esto


quiere decir que a1 ≡ a2 (mód b2 ) y a1 + b1 ≡ a2 (mód b2 ), y por lo tanto, a1 ≡ a1 + b1 (mód b2 ).
Dicho de otro modo, b2 | (a1 + b1 ) − a1 = b1 , o sea, b1 es múltiplo de b2 .

(⇐) Consideremos un elemento p ∈ P1 ; entonces, p ≡ a1 (mód b1 ). Como b2 | b1 , esto implica que p ≡


a1 (mód b2 ), y como a1 ≡ a2 (mód b2 ), tenemos que p ≡ a2 (mód b2 ), y por lo tanto, p ∈ P2 .

Resumiendo lo anterior, resulta que P1 ⊆ P2 si y solo si a1 ≡ a2 (mód b2 ), y además b1 es múltiplo de


b2 . A partir de esto, podemos programar la función incluido en Haskell de esta forma:

5
incluido :: ClaseCongr -> ClaseCongr -> Bool
incluido Vacio _ = True
incluido _ Vacio = False
incluido ( CongruentesA a1 b1 ) ( CongruentesA a2 b2 ) =
congruentes a1 a2 b2 && multiplo b1 b2

Ejercicio 3
Enunciado

Implementar la función show de la clase ClaseCongr, para que las clases de congruencia se muestren de esta
forma:
Prelude > CongruentesA 3 8
{ a en Z | a = 3 ( mod 8) }

Prelude > Vacio


{}

Resolución

Para mostrar nuestras clases de congruencia por pantalla, la función show que tenemos que definir deberá
tomar un elemento de tipo ClaseCongr y devolver un String. Dado que el tipo ClaseCongr tiene dos
constructores, tendremos que hacer pattern matching y definir cómo se comporta la función show para cada
uno de ellos. Esto quiere decir que el “esqueleto” del código que vamos a usar para hacer que ClaseCongr
sea instancia de Show será algo de esta pinta:
instance Show ClaseCongr where
show Vacio = ...
show ( CongruentesA a b ) = ...

eliminando, además, la declaración deriving Show de la definición del tipo ClaseCongr.


El caso de Vacio es sencillo, porque basta con devolver el String "{}" . Si, en cambio, la clase de
congruencia es del tipo CongruentesA a b, tenemos que construir el String que vamos a devolver a partir
de ciertas partes fijas ("{a en Z | a = ", " (mod " y ")}"), entre las cuales hay que intercalar los números
a y b. Como no podemos concatenar un String y un Integer, primero hace falta convertir estos números
a String. Afortunadamente, el tipo Integer es instancia de la clase Show, por lo que contamos con una
función show :: Integer -> String que hace este trabajo por nosotros (es la función que utiliza Haskell
cada vez que quiere mostrar un número entero por pantalla). Teniendo en cuenta todo lo anterior, podemos
completar la función show de ClaseCongr como sigue:
instance Show ClaseCongr where
show Vacio = " {} "
show ( CongruentesA a b ) =
" { a en Z | a = " ++ show a ++ " ( mod " ++ show b ++ " ) } "

Ejercicio 4
Enunciado

Programar iguales :: ClaseCongr -> ClaseCongr -> Bool, que determina si dos clases de congruen-
cia tienen los mismos elementos.
Por ejemplo, iguales (CongruentesA 22 5) (CongruentesA 2 5) True.

Hacer que ClaseCongr sea instancia de Eq, utilizando la igualdad programada anteriormente.

6
Resolución

Para programar iguales, podemos aprovechar la función incluido, que ya programamos anteriormente.
Teniendo en cuenta que dos conjuntos P1 y P2 son iguales si y solo si P1 ⊆ P2 y P2 ⊆ P1 , podemos formular
el siguiente programa:
iguales :: ClaseCongr -> ClaseCongr -> Bool
iguales p1 p2 = incluido p1 p2 && incluido p2 p1
Utilizar esta función para que ClaseCongr sea instancia de Eq es una cuestión meramente sintáctica.
instance Eq ClaseCongr where
p1 == p2 = iguales p1 p2

Ejercicio 5
Enunciado

Si P1 y P2 son clases de congruencia, definimos su suma como


P1 + P2 = {(a + b) | a ∈ P1 , b ∈ P2 }

Verificar que la suma de dos clases de congruencia es también una clase de congruencia, y programar una
función suma :: ClaseCongr -> ClaseCongr -> ClaseCongr que calcule esta suma.
Por ejemplo, suma (CongruentesA 3 6) (CongruentesA 2 4) CongruentesA 5 2.

Idea matemática

Consideremos dos clases de congruencia P1 y P2 . Si alguna de ellas es vacía, el resultado de la suma tal y
como está definida en el enunciado es el conjunto vacío. Analicemos, entonces, los casos en que ambas clases
de congruencia son no vacías; es decir:
P1 = {p ∈ Z | p ≡ a1 (mód b1 )}
P2 = {p ∈ Z | p ≡ a2 (mód b2 )}
e intentemos caracterizar la suma de ambas, es decir, el conjunto
P+ = {(p1 + p2 ) | p1 ∈ P1 y p2 ∈ P2 }

Para poder resolver el problema, queremos poder expresar este último conjunto, también, como una clase
de congruencia. En otras palabras, queremos determinar si P+ es el conjunto vacío, y en caso de que no lo
sea, determinar una base y un salto apropiados.
Dado que a1 ∈ P1 y a2 ∈ P2 , es fácil ver que (a1 + a2 ) ∈ P+ . Esto quiere decir que P+ no es vacía, y que
a = a1 + a2 nos sirve como base. En cuanto al salto, vamos a demostrar que es (b1 : b2 ), es decir, el m.c.d.
entre b1 y b2 ; o lo que es lo mismo, que si definimos la clase de congruencia
P = {z ∈ Z | z ≡ a1 + a2 (mód (b1 : b2 ))}
entonces P+ = P . Vamos a probarlo viendo que se cumple la doble inclusión entre los conjuntos.

P+ ⊆ P : Sea p un elemento cualquiera de P+ . Luego, p = p1 + p2 con p1 ∈ P1 y p2 ∈ P2 . Como p1 ≡


a1 (mód b1 ) y (b1 : b2 ) | b1 , entonces p1 ≡ a1 (mód (b1 : b2 )). Análogamente, p2 ≡ a2 (mód (b1 : b2 )).
Por lo tanto, p = p1 + p2 ≡ a1 + a2 (mód (b1 : b2 )), es decir, p ∈ P .
P ⊆ P+ : Sea p un elemento cualquiera de P . Como p ≡ a1 + a2 (mód (b1 : b2 )), entonces p =
a1 + a2 + k · (b1 : b2 ), para algún k ∈ Z.
Sabemos que (b1 : b2 ) puede escribirse como combinación lineal entera de b1 y b2 . Es decir, existen
s, t ∈ Z tales que (b1 : b2 ) = s · b1 + t · b2 . Reemplazando en lo anterior, tenemos que p = a1 + a2 + k ·
(s · b1 + t · b2 ) = (a1 + k · s · b1 ) + (a2 + k · t · b2 ).
Si tomamos p1 = a1 + k · s · b1 y p2 = a2 + k · s · b2 , vemos que p1 ∈ P1 , p2 ∈ P2 y p = p1 + p2 . Por lo
tanto, p ∈ P+ .

Esto demuestra que la suma entre ambas clases de congruencia, es decir, el conjunto definido como P+ ,
es efectivamente la clase de congruencia con base (a1 + a2 ) y salto (b1 : b2 ).

7
Resolución en Haskell

Teniendo en cuenta los razonamientos anteriores, la resolución del ejercicio en Haskell es directa, asu-
miendo que ya tenemos programada la función mcd.
suma :: ClaseCongr -> ClaseCongr -> ClaseCongr
suma Vacio _ = Vacio
suma _ Vacio = Vacio
suma ( CongruentesA a1 b1 ) ( CongruentesA a2 b2 ) =
CongruentesA ( a1 + a2 ) ( mcd b1 b2 )

Ejercicio 6
Enunciado

Verificar que la intersección entre dos clases de congruencia (P1 ∩ P2 ) también es una clase de congruencia, e
implementar interseccion :: ClaseCongr -> ClaseCongr -> ClaseCongr, que calcule esta intersección.

Idea matemática

Al igual que en el caso anterior, si consideramos dos clases de congruencia P1 y P2 tales que alguna de
ellas es vacía, la intersección resulta trivialmente vacía. Por lo tanto, consideremos dos clases de congruencia
no vacías:
P1 = {p ∈ Z | p ≡ a1 (mód b1 )}
P2 = {p ∈ Z | p ≡ a2 (mód b2 )}
y la intersección de ambas, es decir, el conjunto

P ∩ = P1 ∩ P2

Tratemos de identificar primero los casos en que esta interesección es vacía. Para que un elemento p esté
simultáneamente en P1 y en P2 , deben existir k1 , k2 ∈ Z tales que p = a1 + k1 · b1 = a2 + k2 · b2 . Es decir:

P1 ∩ P2 6= ∅ ⇔ (∃ k1 , k2 ∈ Z) a1 + k1 · b1 = a2 + k2 · b2
⇔ (∃ k̃1 , k2 ∈ Z) a1 = a2 + k̃1 · b1 + k2 · b2
⇔ (∃ k ∈ Z) a1 = a2 + k · (b1 : b2 )
⇔ a1 ≡ a2 (mód (b1 : b2 )) ,
donde la anteúltima equivalencia se desprende del resultado del Ejercicio 1, teniendo en cuenta que el conjunto
{a2 + k̃1 · b1 + k2 · b2 | k̃1 , k2 ∈ Z} se puede pensar como la clase de congruencia P+ que proviene de la suma
de las clases de congruencia P1 = {z ∈ Z | z ≡ 0 (mód b1 )} y P2 = {z ∈ Z | z ≡ a2 (mód b2 )}. De esta
forma, sabemos que los casos en que P∩ = ∅ serán exactamente aquellos en que P1 y P2 sean tales que
a1 6≡ a2 (mód (b1 : b2 )).
Veamos ahora qué pasa cuando la intersección no es vacía. Nos gustaría ver que en estos casos efectiva-
mente tenemos una clase de congruencia, y encontrar una base y un salto apropiados.
Supongamos que P∩ no es vacía, y tomemos un elemento p0 ∈ P∩ . ¿Qué pasa si ahora miramos cualquier
elemento p ∈ P∩ , y calculamos su diferencia con p0 ?1 Tengamos en cuenta que tanto p0 como p pertenecen
a P1 . Luego, p0 ≡ a1 ≡ p (mód b1 ) ⇒ p0 − p ≡ 0 (mód b1 ); es decir, la diferencia entre p0 y p tiene que ser
un múltiplo de b1 . Aplicando el mismo razonamiento, podemos deducir que esta diferencia debe ser también
un múltiplo de b2 . Por lo tanto, p0 − p será múltiplo de m = (bb11 ·b
:b2 ) , el m.c.m. entre b1 y b2 . O sea, dado un
2

elemento p0 ∈ P∩ , cualquier otro elemento p ∈ P∩ tendrá que cumplir p ≡ p0 (mód m).


Lo anterior, claro, no quiere decir que cualquier número p que cumpla p ≡ p0 (mód m) es necesariamente
parte de P∩ . Sin embargo, en este caso, eso resulta ser cierto. ¿Cómo lo probamos? Si p satisface lo anterior,
entonces lo podemos escribir como p = p0 + k · m. Por un lado, p0 ≡ a1 (mód b1 ), y como b1 | m,
k · m ≡ 0 (mód b1 ), con lo cual p ≡ a1 (mód b1 ), y luego p ∈ P1 . Análogamente, se puede ver que p ∈ P2 .
1 Todavía no sabemos si P∩ tiene más de un elemento, así que no pedimos que p0 y p sean distintos.

8
Ahora sí sabemos que, dado un elemento p0 ∈ P∩ , podemos afirmar que P∩ = {p ∈ Z | p ≡ p0 (mód m)}.
En otras palabras, la intersección entre P1 y P2 es la clase de los enteros congruentes a p módulo m. Aún resta
determinar un valor para p, que puede ser cualquier elemento válido de P∩ . Ahora bien, si P∩ es una clase
de congruencia de salto m, seguro existirá un entero c, 0 ≤ c < m, tal que c ∈ P∩ . Como podemos verificar
de forma sencilla la pertenencia de un elemento a P∩ (viendo si pertenece a P1 y P2 simultáneamente),
una solución posible es escribir un programa que revise estos m valores posibles para c, dando así con uno
adecuado para tomar como base de la clase de congruencia.

Resolución en Haskell

Asumimos, al igual que en el ejercicio anterior, que ya contamos con la implementación de la función
mcd.
interseccion :: ClaseCongr -> ClaseCongr -> ClaseCongr
interseccion Vacio _ = Vacio
interseccion _ Vacio = Vacio
interseccion ( CongruentesA a1 b1 ) ( CongruentesA a2 b2 )
| mod a1 d /= mod a2 d = Vacio
| otherwise = CongruentesA base m
where d = mcd b1 b2
m = div ( b1 * b2 ) d
base = e nc o n tr a r Co n g ru e n te a1 b1 a2 b2 0

e n co n t ra r C on g r ue n t e ::
Integer -> Integer -> Integer -> Integer -> Integer -> Integer
e n co n t ra r C on g r ue n t e a1 b1 a2 b2 n
| mod a1 b1 == mod n b1 && mod a2 b2 == mod n b2 = n
| otherwise = e n c on t r ar C o ng r u en t e a1 b1 a2 b2 ( n +1)

Ejercicio 7
Enunciado

Implementar tieneSolucion :: Integer -> ClaseCongr -> Bool que diga si una ecuación de congruen-
cia tiene solución. Explícitamente, tieneSolucion t (CongruentesA a m) True si y solo si la ecuación
t · x ≡ a (mód m) tiene solución.

Resolución

Vamos a asumir que la clase de congruencia que recibimos por parámetro es no vacía, dado que, de lo
contrario, el ejercicio no tiene sentido. Es fundamental tener en cuenta que la ecuación de congruencia

t · x ≡ a (mód m)

es equivalente a la ecuación diofántica


t·x−m·y =a
que, como habrán demostrado en las clases teóricas de la materia, tiene solución si y solo si (t : m) | a. Esto
quiere decir que podemos resolver el ejercicio con el siguiente programa:
tieneSolucion :: Integer -> ClaseCongr -> Bool
tieneSolucion t ( CongruentesA a m ) = multiplo a ( mcd t m )