LOS 10 MEJORES ALGORITMOS NUMÉRICOS

 

 

1.- Exponenciación modular

2.- Multiplicación modular inversa

3.- Prueba de Primalidad (Método Fermat)

4.- La función de Totient de Euler

5.- La criba de Eratóstenes

6.- Convexidad

7.- Algoritmos euclidianos básicos y extendidos

8.- Criba segmentada

9.- Teorema del resto chino

10.- Teorema de Lucas

 

&&&&&&&&&&

&&&&&&&

&&&

&

 

 

 

1.- Exponenciación modular

 

Dados tres números x, y, p, calcula (xy) % p.

 

Ejemplos :

Entrada: x = 2, y = 3, p = 5          Salida: 3

Explicación: 2^3 % 5 = 8 % 5 = 3.

 

Entrada: x = 2, y = 5, p = 13      Salida: 6

Explicación: 2^5 % 13 = 32 % 13 = 6.

 

Veamos las soluciones iterativa y recursiva

 

Iterativa:

 

int power(int x, unsigned int y)

{

    int res = 1;     // Inicializa

   

    while (y > 0)

    {

        // Si y es impar, multiplica x con el resultado

        if (y & 1)  res = res*x;

   

        y = y>>1; // y = y/2

        x = x*x;  // x^2

    }

    return res;

El problema con las soluciones anteriores es que el desbordamiento puede ocurrir para un gran valor de n o x. Por lo tanto, la potencia se evalúa generalmente bajo el módulo de un gran número.

 

A continuación se muestra la propiedad modular fundamental que se utiliza para calcular eficientemente la potencia de cálculo en la aritmética modular.

 

(ab) mod p = ( (a mod p) (b mod p) ) mod p

 

Por ejemplo a = 50, b = 100, p = 13

50 mod 13 = 11

100 mod 13 = 9

 

(50 * 100) mod 13 = ( (50 mod 13) * (100 mod 13) ) mod 13

o (5000) mod 13 = ( 11 * 9 ) mod 13

o 8 = 8

A continuación se muestra la aplicación basada en la propiedad anterior.

 

Recursiva:

 

#include <iostream>

using namespace std;

  

int power(int x, unsigned int y, int p) 

    int res = 1;     // Inicializa 

  

    x = x % p; // Actualizar x si es mayor o igual a p   

    if (x == 0) return 0;  

    while (y > 0) 

    { 

        // Si y es impar, multiplica x con el resultado 

        if (y & 1)  res = (res*x) % p; 

         y = y>>1; // y = y/2 

        x = (x*x) % p; 

    } 

    return res; 

  

// Programa principal 

int main() 

    int x = 2; 

    int y = 5; 

    int p = 13; 

    cout << "El resultado es " << power(x, y, p); 

    return 0; 

Salida :  6

La complejidad de la solución es O(Log y).

 

 

 

2.- Multiplicación modular inversa

 

Dados dos números enteros "a" y "m", encuentre el inverso multiplicativo modular de "a" bajo el módulo "m".

 

El inverso multiplicativo modular es un número entero "x" tal que.

 a x ≡ 1 (modulo m)

El valor de x debe estar en {0, 1, 2, ... m-1}, es decir, en el rango del módulo entero m.

 

El inverso multiplicativo de "un módulo m" existe si y sólo si a y m son relativamente primos (es decir, si gcd(a, m) = 1).

 Ejemplo:

Entrada: a = 3, m = 11

Salida: 4

Ya que (4*3) mod 11 = 1, 4 es el módulo inverso de 3(bajo 11).

Uno podría pensar, 15 también como una salida válida como "(15*3) mod 11"

es también 1, pero 15 no está en el anillo {0, 1, 2, ... 10}, así que no

válido.

 

Un método sencillo es probar todos los números de 1 a m. Por cada número x, compruebe si (a*x)%m es 1. A continuación se muestra la aplicación de este método.

 

#include<iostream>

using namespace std;

  

int modInverse(int a, int m)

{

    a = a%m;

    for (int x=1; x<m; x++)

       if ((a*x) % m == 1)

          return x;

}

  

// Principal

int main()

{

    int a = 3, m = 11;

    cout << modInverse(a, m);

    return 0;

}

Salida: 4

Complejidad temporal: O(m)

 

 

3.- Prueba de Primalidad (Método Fermat)

 

Si se le da un número n, compruebe si es primo o no. Hemos introducido y discutido el método de la Escuela para la prueba de primacía en el Conjunto 1.

Prueba de Primalidad | Conjunto 1 (Introducción y Método Escolar)

En este post, se discute el método de Fermat. Este método es un método probabilístico y se basa en el pequeño teorema de Fermat.

 

Teorema de Fermat:

Si n es un número primo, entonces por cada a, 1 < a < n-1,

an-1 ≡ 1 (mod n)

 O

an-1 % n = 1 

 

Ejemplo: Dado que 5 es primo, 24 ≡ 1 (mod 5) [o 24%5 = 1],

         34 ≡ 1 (mod 5) y 44 ≡ 1 (mod 5)

 

         Como el 7 es primo, 26 ≡ 1 (mod 7),

         36 ≡ 1 (mod 7), 46 ≡ 1 (mod 7)

         56 ≡ 1 (mod 7) y 66 ≡ 1 (mod 7)

 

Si un número dado es primo, entonces este método siempre vuelve a ser verdadero. Si un número dado es compuesto (o no primo), entonces puede devolver verdadero o falso, pero la probabilidad de producir un resultado incorrecto para el compuesto es baja y puede reducirse haciendo más iteraciones.

 

// El valor más alto de k indica la probabilidad de que sea correcto

// los resultados de las entradas compuestas se hacen más altos. Para el primer

// entradas, el resultado es siempre correcto

1) Repita las siguientes k veces:

      a) Elija una al azar en el rango [2, n - 2]

      b) Si gcd(a, n) ≠ 1, entonces devuelve falso

      c) Si an-1 &nequiv; 1 (mod n), entonces devuelve falso

2) Retorno verdadero [probablemente primo].

 

#include <bits/stdc++.h>

using namespace std;

  

int power(int a, unsigned int n, int p)

{

    int res = 1;     

    a = a % p; 

  

    while (n > 0)

    {

        if (n & 1)   res = (res*a) % p;

  

        n = n>>1; // n = n/2

        a = (a*a) % p;

    }

    return res;

}

  

int gcd(int a, int b)

{

    if(a < b)

        return gcd(b, a);

    else if(a%b == 0)

        return b;

    else return gcd(b, a%b);  

}

  

// Si n es primo, entonces siempre regresa verdadero, Si n es

// compuesto que devuelve falso con alta probabilidad

// Un valor más alto de k aumenta la probabilidad de que sea correcto resultado

bool isPrime(unsigned int n, int k)

{

   if (n <= 1 || n == 4)  return false;

   if (n <= 3) return true;

  

   while (k>0)

   {

       // Escoge un número al azar en [2..n-2]        

       // En los casos de las esquinas, asegúrese de que n > 4

       int a = 2 + rand()%(n-4);  

  

       if (gcd(n, a) != 1)

          return false;

   

       if (power(a, n-1, n) != 1)

          return false;

  

       k--;

    }

  

    return true;

}

  

// Principal

int main()

{

    int k = 3;

    isPrime(11, k)?  cout << " Verdadero \n": cout << " Falso \n";

    isPrime(15, k)?  cout << " Verdadero \n": cout << " Falso \n";

    return 0;

}

Salida:   Verdadero      Falso

 

La complejidad temporal de esta solución es O(k Log n). Tenga en cuenta que la función de energía toma O(Log n) tiempo.

 

Observe que el método anterior puede fallar incluso si aumentamos el número de iteraciones (más alto k). Existen algunos números compuestos con la propiedad de que para cada a < n, gcd(a, n) = 1 y an-1 ≡ 1 (mod n). Tales números se llaman números de Carmichael. La prueba de primacía de Fermat se utiliza a menudo si se necesita un método rápido para el filtrado, por ejemplo en la fase de generación de claves del algoritmo criptográfico de clave pública RSA.

 

 

 

4.- La función de Totient de Euler

 

¿La función de Euler Totient? (n) para una entrada n es la cuenta de los números en {1, 2, 3, ..., n} que son relativamente primos a n, es decir, los números cuyo GCD (Greatest Common Divisor) con n es 1.

 

Ejemplos :

 

?(1) = 1 

gcd(1, 1) es 1

 

?(2) = 1

gcd(1, 2) es 1, pero gcd(2, 2) es 2.

 

?(3) = 2

gcd(1, 3) es 1 y gcd(2, 3) es 1

 

?(4) = 2

gcd(1, 4) es 1 y gcd(3, 4) es 1

 

?(5) = 4

gcd(1, 5) es 1, gcd(2, 5) es 1,

gcd(3, 5) es 1 y gcd(4, 5) es 1

 

?(6) = 2

gcd(1, 6) es 1 y gcd(5, 6) es 1,

 

¿Cómo se computa ?(n) para una entrada n?

Una solución simple es iterar a través de todos los números del 1 al n-1 y contar los números con gcd con n como 1. A continuación se muestra la implementación del método simple para calcular la función Totient de Euler para un entero de entrada n.

 

#include <iostream>

using namespace std;

 

int gcd(int a, int b)

{

    if (a == 0) return b;

    return gcd(b % a, a);

}

 

int phi(unsigned int n)

{

    unsigned int result = 1;

    for (int i = 2; i < n; i++)

        if (gcd(i, n) == 1)

            result++;

    return result;

}

 

// Principal

int main()

{

    int n;

    for (n = 1; n <= 10; n++)

        cout << "phi("<<n<<") = " << phi(n) << endl;

    return 0;

}

Salida:

phi(1) = 1

phi(2) = 1

phi(3) = 2

phi(4) = 2

phi(5) = 4

phi(6) = 2

phi(7) = 6

phi(8) = 4

phi(9) = 6

phi(10) = 4

 

El código anterior llama a la función gcd O(n) veces. La complejidad temporal de la función gcd es O(h) donde "h" es el número de dígitos en un número menor de dos números dados. Por lo tanto, un límite superior en la complejidad temporal de la solución anterior es O(nLogn)

 

 

 

5.- La criba de Eratóstenes

 

Dado un número n, imprime todos los primos menores o iguales que n. También se da que n es un número pequeño.

 

Ejemplo:

Entrada : n =10

Salida : 2 3 5 7

 

Entrada : n = 20

Salida: 2 3 5 7 11 13 17 19

La criba de Eratóstenes es una de las formas más eficientes de encontrar todos los primos más pequeños que n cuando n es menor de 10 millones o similar

Crear una lista de números enteros consecutivos del 2 al n: (2, 3, 4, …, n).

Inicialmente, dejemos que p sea igual a 2, el primer número primo.

Empezando por p2, cuente en incrementos de p y marque cada uno de estos números mayores o iguales a p2 en la lista. Estos números serán p(p+1), p(p+2), p(p+3), etc..

Encuentra el primer número mayor que p en la lista que no está marcado. Si no existe tal número, detente. De lo contrario, dejemos que p sea ahora igual a este número (que es el siguiente primo), y repita desde el paso 3.

A continuación se muestra la implementación del algoritmo anterior. En la siguiente implementación, se utiliza una matriz booleana arr[] de tamaño n para marcar múltiplos de números primos

 

#include <bits/stdc++.h>

using namespace std;

  

void SieveOfEratosthenes(int n)

{

    // Crear una matriz booleana "prime[0..n]" e inicializar

    // todas las entradas son verdaderas. Un valor en primo[i] será

    // finalmente ser falso si no soy un primo, sino verdadero.    bool prime[n+1];

    memset(prime, true, sizeof(prime));

  

    for (int p=2; p*p<=n; p++)

    {

        if (prime[p] == true)

        {

            // Actualizar todos los múltiplos de p mayor que o 

            // igual al cuadrado de ella

            // números que son múltiplos de p y son

            // menos de p^2 ya han sido marcados. 

            for (int i=p*p; i<=n; i += p)

                prime[i] = false;

        }

    }

  

    for (int p=2; p<=n; p++)

       if (prime[p])

          cout << p << " ";

}

  

// Principal

int main()

{

    int n = 30;

    cout << " Los siguientes son los números primos menores "

         << " o iguales a " << n << endl;

    SieveOfEratosthenes(n);

    return 0;

}

Salida:

Los siguientes son los números primos menores o iguales a 30

2 3 5 7 11 13 17 19 23 29

Complejidad temporal: O(n*log(log(n))

 

 

 

6.- Convexidad

 

Dado un conjunto de puntos en el plano, el casco convexo del conjunto es el polígono convexo más pequeño que contiene todos los puntos del mismo.

 

¿Cómo comprobar si dos segmentos de línea dados se cruzan?

 

La idea del Algoritmo de Jarvis es simple, empezamos desde el punto más a la izquierda (o punto con un valor mínimo de coordenadas x) y seguimos envolviendo puntos en sentido contrario a las agujas del reloj. La gran pregunta es, dado un punto p como punto actual, ¿cómo encontrar el siguiente punto en la salida? La idea es usar la orientación() aquí. El siguiente punto se selecciona como el punto que supera a todos los demás puntos en orientación antihoraria, es decir, el siguiente punto es q si para cualquier otro punto r, tenemos "orientación(p, q, r) = antihorario". A continuación se presenta el algoritmo detallado.

 

1) Inicializar p como el punto más a la izquierda.

2) Hacer el seguimiento mientras no volvemos al primer (o más a la izquierda) punto.

.....a) El siguiente punto q es el punto tal que el trillizo (p, q, r) es en sentido contrario a las agujas del reloj para cualquier otro punto r.

.....b) next[p] = q (Guarda q como el siguiente de p en el casco convexo de salida).

.....c) p = q (Poner p como q para la siguiente iteración).

 

#include <bits/stdc++.h>

using namespace std;

  

struct Point

{

    int x, y;

};

  

// Para encontrar la orientación del trillizo ordenado (p, q, r).

// La función devuelve los siguientes valores

// 0 --> p, q y r son colineales

// 1 --> En el sentido de las agujas del reloj

// 2 --> En sentido contrario a las agujas del reloj

int orientation(Point p, Point q, Point r)

{

    int val = (q.y - p.y) * (r.x - q.x) -

              (q.x - p.x) * (r.y - q.y);

  

    if (val == 0) return 0;  // colineal

    return (val > 0)? 1: 2;

}

  

void convexHull(Point points[], int n)

{

    if (n < 3) return;

    vector<Point> hull;

    int l = 0;

    for (int i = 1; i < n; i++)

        if (points[i].x < points[l].x)

            l = i;

  

    // Empieza desde el punto más a la izquierda, sigue moviéndote en sentido contrario a las agujas del reloj

    // hasta llegar al punto de partida de nuevo.  Este bucle corre O(h)

    // veces donde h es el número de puntos en el resultado o la salida.

    int p = l, q;

    do

    {

        hull.push_back(points[p]);

  

        // Buscar un punto 'q' tal que la orientación(p, x,

        // q) es en sentido contrario a las agujas del reloj para todos los puntos "x". La idea

        // ...es llevar un registro de las últimas visitas a la mayoría de los relojes...

        // punto sabio en la q. Si algún punto 'i' es más contrareloj-

        // sabio que q, luego actualizar q.

        q = (p+1)%n;

        for (int i = 0; i < n; i++)

        {

           if (orientation(points[p], points[i], points[q]) == 2)

               q = i;

        }

  

        // Ahora q es el más en sentido contrario a las agujas del reloj con respecto a p

        // Ponga p como q para la siguiente iteración, para que q se añada a

        // resultado 'hull'

        p = q;

  

    } while (p != l);   

    for (int i = 0; i < hull.size(); i++)

        cout << "(" << hull[i].x << ", "

              << hull[i].y << ")\n";

}

  

// Principal

int main()

{

    Point points[] = {{0, 3}, {2, 2}, {1, 1}, {2, 1},

                      {3, 0}, {0, 0}, {3, 3}};

    int n = sizeof(points)/sizeof(points[0]);

    convexHull(points, n);

    return 0;

}

Salida:

La salida son puntos del casco convexo.

(0, 3)

(0, 0)

(3, 0)

(3, 3)

Complejidad temporal: Para cada punto del casco examinamos todos los demás puntos para determinar el siguiente punto. La complejidad temporal es ?(m * n) donde n es el número de puntos de entrada y m es el número de puntos de salida o del casco (m <= n). En el peor de los casos, la complejidad temporal es O(n 2). El peor caso ocurre cuando todos los puntos están en el casco (m = n)

 

 

 

7.- Algoritmos euclidianos básicos y extendidos

 

El GCD de dos números es el mayor número que divide a ambos. Una forma sencilla de encontrar el GCD es factorizar ambos números y multiplicar los factores comunes.

 

 

Algoritmo euclidiano básico para el GCD

El algoritmo se basa en los siguientes hechos.

 

Si restamos un número menor de un número mayor (reducimos un número mayor), el GCD no cambia. Así que si seguimos restando repetidamente el mayor de dos, terminamos con GCD.

Ahora, en lugar de la resta, si dividimos un número más pequeño, el algoritmo se detiene cuando encontramos el resto 0.

A continuación se muestra una función recursiva para evaluar el GCD usando el algoritmo de Euclides.

 

#include <bits/stdc++.h>

using namespace std;

  

int gcd(int a, int b)

{

    if (a == 0)

        return b;

    return gcd(b % a, a);

}

  

// Principal

int main()

{

    int a = 10, b = 15;

    cout << "GCD(" << a << ", " 

         << b << ") = " << gcd(a, b) 

                        << endl;

    a = 35, b = 10;

    cout << "GCD(" << a << ", " 

         << b << ") = " << gcd(a, b) 

                        << endl;

    a = 31, b = 2;

    cout << "GCD(" << a << ", " 

         << b << ") = " << gcd(a, b) 

                        << endl;

    return 0;

}

Salida:

GCD(10, 15) = 5

GCD(35, 10) = 5

GCD(31, 2) = 1

Complejidad temporal: O(Log min(a, b))

 

 

 

8.- Criba segmentada

 

Dado un número n, imprime todos los primos menores que n. Por ejemplo, si el número dado es 10, salida 2, 3, 5, 7.

Un enfoque sencillo es ejecutar un bucle de 0 a n-1 y comprobar que cada número tiene primacía. Un mejor enfoque es usar la Criba de Eratóstenes.

 

void simpleSieve(int limit)

{

    // Crear una matriz booleana "mark[0..limit-1]" y

    // Inicializar todas las entradas como verdaderas. Un valor

    // en mark[p] será finalmente falso si 'p' no es

    // un primo, si no es cierto.

    bool mark[limit];

    memset(mark, true, sizeof(mark));

  

    // Uno por uno atraviesa todos los números para que su

    // los múltiplos pueden ser marcados como compuestos.

    for (int p=2; p*p<limit; p++)

    {

        if (mark[p] == true)

        {

            for (int i=p*p; i<limit; i+=p)

                mark[i] = false;

        }

    }

  

    for (int p=2; p<limit; p++)

        if (mark[p] == true)

            cout << p << "  ";

}

 

 

9.- Teorema del resto chino

 

Se nos dan dos matrices num[0..k-1] y rem[0..k-1]. En num[0..k-1], cada par es coprime (gcd para cada par es 1). Necesitamos encontrar el mínimo número positivo x de tal manera que:

 

     x % num[0] = rem[0],

     x % num[1] = rem[1],

     .......................

     x % num[k-1] = rem[k-1]

 

Básicamente, se nos dan números k que son coprimos por pares, y se nos dan restos de estos números cuando un número desconocido x es dividido por ellos. Tenemos que encontrar el mínimo valor posible de x que produce residuos dados.

 

Ejemplo:

Entrada: num[] = {5, 7}, rem[] = {1, 3}

Salida: 31

Explicación:

31 es el número más pequeño tal que:

  (1) Cuando lo dividimos por 5, obtenemos el resto 1.

  (2) Cuando lo dividimos por 7, obtenemos el resto 3.

 

Que num[0], num[1], ...num[k-1] sean números enteros positivos que se copriman por pares. Entonces, para cualquier secuencia dada de números enteros rem[0], rem[1], ... rem[k-1], existe un número entero x que resuelve el siguiente sistema de congruencias simultáneas.

La primera parte es clara en cuanto a la existencia de una x. La segunda parte establece básicamente que todas las soluciones (incluyendo la mínima) producen el mismo resto cuando se dividen por el subproducto de n[0], num[1], .. num[k-1]. En el ejemplo anterior, el producto es 3*4*5 = 60. Y 11 es una solución, otras soluciones son 71, 131, .. etc. Todas estas soluciones producen el mismo resto cuando se dividen por 60, es decir, son de la forma 11 + m*60 donde m >= 0.

 

Una aproximación sencilla para encontrar x es comenzar con 1 y aumentarlo de a uno y comprobar si dividiéndolo con los elementos dados en num[] produce los correspondientes restos en rem[]. Una vez que encontramos tal x, la devolvemos.

 

#include<bits/stdc++.h>

using namespace std;

  

// k es el tamaño de num[] y rem[].  Devuelve el más pequeño

// número x tal que:

// x % num[0] = rem[0], 

// x % num[1] = rem[1], 

// ..................

// x % num[k-2] = rem[k-1]

// Asunción: Los números en num[] son coprime a pares 

// (gcd por cada par es 1)

int findMinX(int num[], int rem[], int k)

{

    int x = 1; // Initialize result

  

    while (true)

    {

        int j;

        for (j=0; j<k; j++ )

            if (x%num[j] != rem[j])

               break;

  

        if (j == k)     return x;

  

        x++;

    }

  

    return x;

}

  

// Principal

int main(void)

{

    int num[] = {3, 4, 5};

    int rem[] = {2, 3, 1};

    int k = sizeof(num)/sizeof(num[0]);

    cout << "x is " << findMinX(num, rem, k);

    return 0;

}

Salida:    x   es   11

 

 

 

10.- Teorema de Lucas

 

Dados tres números n, r y p, calcular el valor de Cr,n mod p.

 

Ejemplo:

Entrada: n = 10, r = 2, p = 13

Salida: 6

Explicación: C2,10 es 45 y 45 % 13 es 6.

 

Hemos introducido el problema de desbordamiento y discutido la solución basada en la Programación Dinámica en el set 1 anterior. La complejidad temporal de la solución basada en la programación dinámica es O(n*r) y requiere O(n) espacio. El tiempo y el espacio extra se vuelven muy altos para los grandes valores de n, especialmente los valores cercanos a 109.

 

#include<bits/stdc++.h>

using namespace std;

  

int nCrModpDP(int n, int r, int p)

{

    int C[r+1];

    memset(C, 0, sizeof(C));

   C[0] = 1;

   for (int i = 1; i <= n; i++)

    {

        for (int j = min(i, r); j > 0; j--)

        C[j] = (C[j] + C[j-1])%p;

    }

    return C[r];

}

  

// Función basada en el Teorema de Lucas que devuelve nCr % p

// Esta función funciona como la conversión de decimal a binario

// función recursiva.  Primero calculamos los últimos dígitos de

// n y r en la base p, luego se repiten para los dígitos restantes

int nCrModpLucas(int n, int r, int p)

{

   if (r==0)   return 1;

  

   int ni = n%p, ri = r%p;

  

   // Calcular el resultado de los últimos dígitos calculados arriba, y

   // para los dígitos restantes.  Multiplique los dos resultados y

   // calcular el resultado de la multiplicación en el módulo p.

   return (nCrModpLucas(n/p, r/p, p) * // Last digits of n and r

           nCrModpDP(ni, ri, p)) % p;  // Remaining digits

}

  

// Principal

int main()

{

    int n = 1000, r = 900, p = 13;

    cout << "Value of nCr % p is " << nCrModpLucas(n, r, p);

    return 0;

}

Salida:

El valor de Cr,n % p es 8

Complejidad temporal: La complejidad temporal de esta solución es O(p2 * Logp n). Hay dígitos O(Logp n) en la representación de la base p de n. Cada uno de estos dígitos es más pequeño que p, por lo tanto, los cálculos para los dígitos individuales toman O(p2). Nótese que estos cálculos se hacen usando el método DP que toma O(n*r) tiempo.

 

 

&&&&&&&&&&

&&&&&&&

&&&

&