Encontrando o menor múltiplo comum em Java

Encontrando o menor múltiplo comum em Java

1. Visão geral

*O https://en.wikipedia.org/wiki/Least_common_multiple[Least Common Multiple] (LCM) de dois números inteiros diferentes de zero _ (a, b) _ é o menor número inteiro positivo que é perfeitamente divisível por _a_ e _b_ .*

Neste tutorial, aprenderemos sobre diferentes abordagens para encontrar o LCM de dois ou mais números. Devemos observar que números inteiros negativos e zero não são candidatos ao LCM .

2. Cálculo de LCM de dois números usando um algoritmo simples

Podemos encontrar o LCM de dois números usando o simples fato de que https://en.wikipedia.org/wiki/Multiplication [multiplicação] é adição repetida .

* 2.1 Algoritmo *

O algoritmo simples para encontrar o LCM é uma abordagem iterativa que utiliza algumas propriedades fundamentais do LCM de dois números.

Em primeiro lugar, sabemos que o* LCM de qualquer número com zero é o próprio zero *. Portanto, podemos fazer uma saída antecipada do procedimento sempre que um dos inteiros especificados for 0.

Em segundo lugar, também podemos fazer uso do fato de que o limite inferior do LCM de dois números inteiros diferentes de zero é o maior dos valores absolutos dos dois números .

Além disso, como explicado anteriormente, o LCM nunca pode ser um número inteiro negativo. Portanto, usaremos apenas valores absolutos dos números inteiros para encontrar os múltiplos possíveis até encontrarmos um múltiplo comum.

Vamos ver o procedimento exato que precisamos seguir para determinar o lcm (a, b):

  1. Se a = 0 ou b = 0, retorne com lcm (a, b) = 0, caso contrário, vá para a etapa 2.

  2. Calcular valores absolutos dos dois números.

  3. Inicialize lcm como o mais alto dos dois valores calculados na etapa 2.

  4. Se lcm for divisível pelo valor absoluto mais baixo, retorne.

  5. Aumente lcm pelo valor absoluto mais alto entre os dois e vá para a etapa 4.

Antes de começarmos com a implementação dessa abordagem simples, vamos fazer um teste a seco para encontrar lcm (12, 18).

Como 12 e 18 são positivos, vamos pular para a etapa 3, inicializando lcm = max (12, 18) = 18 e prosseguir.

Em nossa primeira iteração, lcm = 18, que não é perfeitamente divisível por 12. Então, aumentamos em 18 e continuamos.

Na segunda iteração, podemos ver que lcm = 36 e agora é perfeitamente divisível por 12. Portanto, podemos retornar do algoritmo e concluir que lcm (12, 18) é 36.

2.2 Implementação

Vamos implementar o algoritmo em Java. Nosso método _lcm () _ precisa aceitar dois argumentos inteiros e fornecer seu LCM como um valor de retorno.

Podemos notar que o algoritmo acima envolve executar algumas operações matemáticas nos números, como encontrar valores absolutos, mínimos e máximos. Para esse fim, podemos usar os métodos estáticos correspondentes da classe Math, como _abs () _, _min (), _ e _max () _, respectivamente.

Vamos implementar nosso método _lcm () _:

public static int lcm(int number1, int number2) {
    if (number1 == 0 || number2 == 0) {
        return 0;
    }
    int absNumber1 = Math.abs(number1);
    int absNumber2 = Math.abs(number2);
    int absHigherNumber = Math.max(absNumber1, absNumber2);
    int absLowerNumber = Math.min(absNumber1, absNumber2);
    int lcm = absHigherNumber;
    while (lcm % absLowerNumber != 0) {
        lcm += absHigherNumber;
    }
    return lcm;
}

Em seguida, vamos também validar este método:

@Test
public void testLCM() {
    Assert.assertEquals(36, lcm(12, 18));
}

O caso de teste acima verifica a exatidão do método _lcm () _ afirmando que lcm (12, 18) é 36.

3. Usando a abordagem de fatoração primária

*O https://en.wikipedia.org/wiki/Fundamental_theorem_of_arithmetic[ teorema fundamental da aritmética] afirma que é possível expressar exclusivamente cada número inteiro maior que um como produto de potências de números primos.*

Portanto, para qualquer número inteiro N> 1, temos N = (2 ^ k1 ^) (3 ^ k2 ^) (5 ^ k3 ^) *…

Usando o resultado desse teorema, entenderemos agora a abordagem de fatoração primária para encontrar o LCM de dois números.

* 3.1 Algoritmo *

A abordagem de fatoração primária calcula o LCM a partir da decomposição primária dos dois números. Podemos usar os fatores primos e expoentes da fatoração primária para calcular o LCM dos dois números:

Quando | a | = (2 ^ p1 ^)* (3 ^ p2 ^) (5 ^ p3 ^) … + e | b | = (2 ^ q1 ^) (3 ^ q2 ^) (5 ^ q3 ^) … + então, *cm (a, b) = (2 ^ max (p ~ 1 ~, q ~ 1 ~) ^ ) (3 ^ max (p ~ 2 ~, q ~ 2 ~) ^) (5 ^ max (p ~ 3 ~, q ~ 3 ~) ^)…

Vamos ver como calcular o LCM de 12 e 18 usando esta abordagem:

Primeiramente, precisamos representar os valores absolutos dos dois números como produtos de fatores primos: + 12 = 2 2 3 = 2² 3¹ + 18 = 2 3 3 = 2¹

Podemos notar aqui que os principais fatores nas representações acima são 2 e 3.

Em seguida, vamos determinar o expoente de cada fator primo para o LCM. Fazemos isso retirando seu poder superior das duas representações.

Usando essa estratégia, a potência de 2 no LCM será máxima (2, 1) = 2, e a potência de 3 no LCM será máxima (1, 2) = 2.

Finalmente, podemos calcular o LCM multiplicando os fatores primos pela potência correspondente obtida na etapa anterior. Conseqüentemente, temos lcm (12, 18) = 2² *3² = 36.

====* 3.2 Implementação *

Nossa implementação Java usa a representação de fatoração primária dos dois números para encontrar o LCM.

Para esse propósito, nosso método getPrimeFactors () _ precisa aceitar um argumento inteiro e fornecer sua representação de fatoração primária. Em Java,* podemos representar a fatoração primária de um número usando um _HashMap *em que cada chave indica o fator primário e o valor associado à chave significa o expoente do fator correspondente.

Vamos ver uma implementação iterativa do método _getPrimeFactors () _:

public static Map<Integer, Integer> getPrimeFactors(int number) {
    int absNumber = Math.abs(number);

    Map<Integer, Integer> primeFactorsMap = new HashMap<Integer, Integer>();

    for (int factor = 2; factor <= absNumber; factor++) {
        while (absNumber % factor == 0) {
            Integer power = primeFactorsMap.get(factor);
            if (power == null) {
                power = 0;
            }
            primeFactorsMap.put(factor, power + 1);
            absNumber/= factor;
        }
    }

    return primeFactorsMap;
}

Sabemos que os mapas de fatoração primos de 12 e 18 são \ {2 → 2, 3 → 1} e \ {2 → 1, 3 → 2}, respectivamente. Vamos usar isso para testar o método acima:

@Test
public void testGetPrimeFactors() {
    Map<Integer, Integer> expectedPrimeFactorsMapForTwelve = new HashMap<>();
    expectedPrimeFactorsMapForTwelve.put(2, 2);
    expectedPrimeFactorsMapForTwelve.put(3, 1);

    Assert.assertEquals(expectedPrimeFactorsMapForTwelve,
      PrimeFactorizationAlgorithm.getPrimeFactors(12));

    Map<Integer, Integer> expectedPrimeFactorsMapForEighteen = new HashMap<>();
    expectedPrimeFactorsMapForEighteen.put(2, 1);
    expectedPrimeFactorsMapForEighteen.put(3, 2);

    Assert.assertEquals(expectedPrimeFactorsMapForEighteen,
      PrimeFactorizationAlgorithm.getPrimeFactors(18));
}

Nosso método _lcm () _ primeiro usa o método _getPrimeFactors () _ para encontrar o mapa de fatoração primário para cada número. Em seguida, ele usa o mapa de fatoração principal de ambos os números para encontrar seu LCM. Vamos ver uma implementação iterativa deste método:

public static int lcm(int number1, int number2) {
    if(number1 == 0 || number2 == 0) {
        return 0;
    }

    Map<Integer, Integer> primeFactorsForNum1 = getPrimeFactors(number1);
    Map<Integer, Integer> primeFactorsForNum2 = getPrimeFactors(number2);

    Set<Integer> primeFactorsUnionSet = new HashSet<>(primeFactorsForNum1.keySet());
    primeFactorsUnionSet.addAll(primeFactorsForNum2.keySet());

    int lcm = 1;

    for (Integer primeFactor : primeFactorsUnionSet) {
        lcm* = Math.pow(primeFactor,
          Math.max(primeFactorsForNum1.getOrDefault(primeFactor, 0),
            primeFactorsForNum2.getOrDefault(primeFactor, 0)));
    }

    return lcm;
}

Como boa prática, verificaremos agora a correção lógica do método _lcm () _:

@Test
public void testLCM() {
    Assert.assertEquals(36, PrimeFactorizationAlgorithm.lcm(12, 18));
}

4. Usando o algoritmo euclidiano

Existe uma relação interessante entre o LCM e GCD (Maior Divisor Comum) de dois números que indicam que o valor *absolute do produto de dois números é igual ao produto de seu GCD e LCM *.

Como afirmado, mcd (a, b) lcm (a, b) = | a b |.

Conseqüentemente, lcm (a, b) = | a b |/gcd (a, b) *.

Usando esta fórmula, nosso problema original de encontrar lcm (a, b) agora foi reduzido para apenas encontrar mcd (a, b).

Concedido, existem várias estratégias para encontrar GCD de dois números. No entanto, o https://en.wikipedia.org/wiki/Euclidean_algorithm [algoritmo euclidiano] é conhecido por ser um dos mais eficientes de todos.

Por esse motivo, vamos entender brevemente o cerne desse algoritmo, que pode ser resumido em duas relações:

  • mcd (a, b) = mcd (| a% b |, | a |); onde | a | > = | b |

  • mcd (p, 0) = mcd (0, p) = | p |

Vamos ver como podemos encontrar lcm (12, 18) usando as relações acima:

Temos gcd (12, 18) = gcd (18% 12, 12) = gcd (6,12) = gcd (12% 6, 6) = gcd (0, 6) = 6

Portanto, lcm (12, 18) = | 12 x 18 |/gcd (12, 18) = (12 x 18)/6 = 36

Agora veremos uma implementação recursiva do algoritmo euclidiano :

public static int gcd(int number1, int number2) {
    if (number1 == 0 || number2 == 0) {
        return number1 + number2;
    } else {
        int absNumber1 = Math.abs(number1);
        int absNumber2 = Math.abs(number2);
        int biggerValue = Math.max(absNumber1, absNumber2);
        int smallerValue = Math.min(absNumber1, absNumber2);
        return gcd(biggerValue % smallerValue, smallerValue);
    }
}

A implementação acima usa os valores absolutos dos números - como o GCD é o maior número inteiro positivo que divide perfeitamente os dois números, não estamos interessados ​​em divisores negativos.

Agora estamos prontos para verificar se a implementação acima funciona conforme o esperado:

@Test
public void testGCD() {
    Assert.assertEquals(6, EuclideanAlgorithm.gcd(12, 18));
}

4.1 LCM de dois números

Usando o método anterior para encontrar o GCD, agora podemos calcular facilmente o LCM. Novamente, nosso método _lcm () _ precisa aceitar dois números inteiros como entrada para retornar seu LCM. Vamos ver como podemos implementar esse método em Java:

public static int lcm(int number1, int number2) {
    if (number1 == 0 || number2 == 0)
        return 0;
    else {
        int gcd = gcd(number1, number2);
        return Math.abs(number1 *number2)/gcd;
    }
}

Agora podemos verificar a funcionalidade do método acima:

@Test
public void testLCM() {
    Assert.assertEquals(36, EuclideanAlgorithm.lcm(12, 18));
}

====* 4.2 LCM de números grandes usando a classe BigInteger *

Para calcular o LCM de grandes números, podemos aproveitar a classe _https://docs.oracle.com/javase/8/docs/api/java/math/BigInteger.html [BigInteger] _.

Internamente, o método* gcd () _ da classe _BigInteger usa um algoritmo híbrido para otimizar o desempenho da computação. Além disso, como os objetos BigInteger são imutáveis ​​, a implementação aproveita as instâncias mutáveis ​​dos *_https://github.com/openjdk/jdk/blob/6bab0f539fba8fb441697846347597b4a0ade428/src/java.base/share/BassesInteger .java [MutableBigInteger] _ class para evitar realocações freqüentes de memória .

*Para começar, ele usa o algoritmo euclidiano convencional* para substituir repetidamente o número inteiro mais alto por seu módulo pelo número inteiro mais baixo.

Como resultado, o par não apenas se torna cada vez menor, mas também mais próximo após divisões sucessivas **. * *Eventualmente, a diferença no número de ints necessária para manter a magnitude dos dois objetos MutableBigInteger em seus respectivos _int [ ] _ matrizes de valor atingem 1 ou 0.

Nesse estágio, a estratégia é alterada para o* https://en.wikipedia.org/wiki/Binary_GCD_algorithm [algoritmo BCD GCD] para obter resultados de computação ainda mais rápidos *.

Nesse caso, também, calcularemos o LCM dividindo o valor absoluto do produto dos números pelo respectivo GCD. Semelhante aos exemplos anteriores, nosso método lcm () _ usa dois valores _BigInteger como entrada e retorna o LCM para os dois números como BigInteger. Vamos vê-lo em ação:

public static BigInteger lcm(BigInteger number1, BigInteger number2) {
    BigInteger gcd = number1.gcd(number2);
    BigInteger absProduct = number1.multiply(number2).abs();
    return absProduct.divide(gcd);
}

Por fim, podemos verificar isso com um caso de teste:

@Test
public void testLCM() {
    BigInteger number1 = new BigInteger("12");
    BigInteger number2 = new BigInteger("18");
    BigInteger expectedLCM = new BigInteger("36");
    Assert.assertEquals(expectedLCM, BigIntegerLCM.lcm(number1, number2));
}

5. Conclusão

Neste tutorial, discutimos vários métodos para encontrar o múltiplo menos comum de dois números em Java.

Além disso, também aprendemos sobre a relação entre o produto de números com seu LCM e GCD. Dados os algoritmos que podem calcular o GCD de dois números com eficiência, também reduzimos o problema do cálculo de LCM para um dos cálculos do GCD.

Como sempre, o código fonte completo para a implementação Java usada neste artigo está disponível em GitHub.