From 22002c9939fdeff1d7ef659a688ad7f396dd564a Mon Sep 17 00:00:00 2001 From: Piotr Idzik <65706193+vil02@users.noreply.github.com> Date: Fri, 2 Jun 2023 13:17:26 +0200 Subject: [PATCH] Generalize NthUglyNumber (#4209) --- .../thealgorithms/maths/NthUglyNumber.java | 113 +++++++++++------- .../maths/NthUglyNumberTest.java | 87 ++++++++++++++ 2 files changed, 156 insertions(+), 44 deletions(-) create mode 100644 src/test/java/com/thealgorithms/maths/NthUglyNumberTest.java diff --git a/src/main/java/com/thealgorithms/maths/NthUglyNumber.java b/src/main/java/com/thealgorithms/maths/NthUglyNumber.java index 4c040f570..6daeb2673 100644 --- a/src/main/java/com/thealgorithms/maths/NthUglyNumber.java +++ b/src/main/java/com/thealgorithms/maths/NthUglyNumber.java @@ -1,57 +1,82 @@ -// Ugly numbers are numbers whose only prime factors are 2, 3 or 5. The sequence 1, 2, 3, 4, 5, 6, 8, 9, 10, 12, 15, … shows the first 11 ugly numbers. -// By convention, 1 is included. -// A program to find the nth Ugly number -// Algorithm : -// Initialize three-pointers two, three, and five pointing to zero. -// Take 3 variables nm2, nm3, and nm5 to keep track of next multiple of 2,3 and 5. -// Make an array of size n to store the ugly numbers with 1 at 0th index. -// Initialize a variable next which stores the value of the last element in the array. -// Run a loop n-1 times and perform steps 6,7 and 8. -// Update the values of nm2, nm3, nm5 as ugly[two]*2, ugly[three]*3, ugly[5]*5 respectively. -// Select the minimum value from nm2, nm3, and nm5 and increment the pointer related to it. -// Store the minimum value in variable next and array. -// Return next. package com.thealgorithms.maths; -import java.util.*; +import java.util.HashMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.lang.IllegalArgumentException; -class NthUglyNumber { - /* Function to get the nth ugly number*/ - public long getNthUglyNo(int n) { - long[] ugly = new long[n]; - int two = 0, three = 0, five = 0; - long nm2 = 2, nm3 = 3, nm5 = 5; - long next = 1; +/** + * @brief class computing the n-th ugly number (when they are sorted) + * @details the ugly numbers with base [2, 3, 5] are all numbers of the form 2^a*3^b^5^c, + * where the exponents a, b, c are non-negative integers. + * Some properties of ugly numbers: + * - base [2, 3, 5] ugly numbers are the 5-smooth numbers, cf. https://oeis.org/A051037 + * - base [2, 3, 5, 7] ugly numbers are 7-smooth numbers, cf. https://oeis.org/A002473 + * - base [2] ugly numbers are the non-negative powers of 2, + * - the base [2, 3, 5] ugly numbers are the same as base [5, 6, 2, 3, 5] ugly numbers + */ +public class NthUglyNumber { + ArrayList uglyNumbers = new ArrayList<>(Arrays.asList(1L)); + final int[] baseNumbers; + HashMap positions = new HashMap<>(); - ugly[0] = 1; + /** + * @brief initialized the object allowing to compute ugly numbers with given base + * @param baseNumbers the given base of ugly numbers + * @exception IllegalArgumentException baseNumber is empty + */ + NthUglyNumber(int[] baseNumbers) { + if (baseNumbers.length == 0) { + throw new IllegalArgumentException("baseNumbers must be non-empty."); + } - for (int i = 1; i < n; i++) { - next = Math.min(nm2, Math.min(nm3, nm5)); + this.baseNumbers = baseNumbers; + for (final var baseNumber : baseNumbers) { + this.positions.put(baseNumber, 0); + } + } - ugly[i] = next; - if (next == nm2) { - two = two + 1; - nm2 = ugly[two] * 2; - } - if (next == nm3) { - three = three + 1; - nm3 = ugly[three] * 3; - } - if (next == nm5) { - five = five + 1; - nm5 = ugly[five] * 5; + /** + * @param n the zero-based-index of the queried ugly number + * @exception IllegalArgumentException n is negative + * @return the n-th ugly number (starting from index 0) + */ + public Long get(int n) { + if (n < 0) { + throw new IllegalArgumentException("n must be non-negative."); + } + + while (uglyNumbers.size() <= n) { + addUglyNumber(); + } + + return uglyNumbers.get(n); + } + + private void addUglyNumber() { + uglyNumbers.add(computeMinimalCandidate()); + updatePositions(); + } + + private void updatePositions() { + final var lastUglyNumber = uglyNumbers.get(uglyNumbers.size() - 1); + for (final var baseNumber : baseNumbers) { + if (computeCandidate(baseNumber) == lastUglyNumber) { + positions.put(baseNumber, positions.get(baseNumber) + 1); } } - return next; } - public static void main(String[] args) { - Scanner sc = new Scanner(System.in); - System.out.println("Enter the value of n : "); - int n = sc.nextInt(); - NthUglyNumber ob = new NthUglyNumber(); - long ugly = ob.getNthUglyNo(n); - System.out.println("nth Ugly number is : " + ugly); + private long computeCandidate(int candidateBase) { + return candidateBase * uglyNumbers.get(positions.get(candidateBase)); + } + + private long computeMinimalCandidate() { + long res = Long.MAX_VALUE; + for (final var baseNumber : baseNumbers) { + res = Math.min(res, computeCandidate(baseNumber)); + } + return res; } } diff --git a/src/test/java/com/thealgorithms/maths/NthUglyNumberTest.java b/src/test/java/com/thealgorithms/maths/NthUglyNumberTest.java new file mode 100644 index 000000000..597d08922 --- /dev/null +++ b/src/test/java/com/thealgorithms/maths/NthUglyNumberTest.java @@ -0,0 +1,87 @@ +package com.thealgorithms.maths; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.HashMap; + +import org.junit.jupiter.api.Test; + +public class NthUglyNumberTest { + @Test + public void testGetWithNewObject() { + HashMap testCases = new HashMap<>(); + testCases.put(0, 1L); + testCases.put(1, 2L); + testCases.put(2, 3L); + testCases.put(3, 4L); + testCases.put(4, 5L); + testCases.put(5, 6L); + testCases.put(9, 12L); + testCases.put(19, 36L); + testCases.put(52, 270L); + testCases.put(1078, 84934656L); + testCases.put(1963, 6973568802L); + + for (final var tc : testCases.entrySet()) { + var uglyNumbers = new NthUglyNumber(new int[] {2, 3, 5}); + assertEquals(uglyNumbers.get(tc.getKey()), tc.getValue()); + + var otherUglyNumbers = new NthUglyNumber(new int[] {5, 25, 6, 2, 3, 5}); + assertEquals(otherUglyNumbers.get(tc.getKey()), tc.getValue()); + } + } + + @Test + public void testGetWithSameObject() { + HashMap testCases = new HashMap<>(); + testCases.put(0, 1L); + testCases.put(1, 2L); + testCases.put(2, 3L); + testCases.put(3, 4L); + testCases.put(4, 5L); + testCases.put(5, 6L); + testCases.put(6, 7L); + testCases.put(1499, 1984500L); + testCases.put(1572, 2449440L); + testCases.put(1658, 3072000L); + testCases.put(6625, 4300800000L); + + var uglyNumbers = new NthUglyNumber(new int[] {7, 2, 5, 3}); + for (final var tc : testCases.entrySet()) { + assertEquals(uglyNumbers.get(tc.getKey()), tc.getValue()); + } + + assertEquals(uglyNumbers.get(999), 385875); + } + + @Test + public void testGetWithBase1() { + var uglyNumbers = new NthUglyNumber(new int[] {1}); + assertEquals(uglyNumbers.get(10), 1); + } + + @Test + public void testGetWithBase2() { + var uglyNumbers = new NthUglyNumber(new int[] {2}); + assertEquals(uglyNumbers.get(5), 32); + } + + + @Test + public void testGetThrowsAnErrorForNegativeInput() { + var uglyNumbers = new NthUglyNumber(new int[] {1, 2}); + assertThrows( + IllegalArgumentException.class, + () -> uglyNumbers.get(-1) + ); + } + + @Test + public void testConstructorThrowsAnErrorForEmptyInput() { + assertThrows( + IllegalArgumentException.class, + () -> new NthUglyNumber(new int[] {}) + ); + } +}