Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import unittest
from typing import List
from parameterized import parameterized
from algorithms.search.binary_search.divide_chocolate import maximize_sweetness, maximize_sweetness_2
from algorithms.search.binary_search.divide_chocolate import (
maximize_sweetness,
maximize_sweetness_2,
)

TEST_CASES = [
([1, 2, 3, 4, 5, 6, 7, 8, 9], 5, 6),
Expand Down
51 changes: 51 additions & 0 deletions pymath/perfect_square/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Perfect Squares

Given an integer, n, return the least number of perfect square numbers that sum to n.

> A perfect square is an integer that is the square of an integer. In other words, it is an integer that is the result of
> multiplying a whole integer by itself. For example, 1, 4, 9 and 16 are perfect squares, but 3, 5, and 11 are not.

## Constraints

- 1 <= `n` < 10^3

## Solution

One of the first solutions that comes to mind is to keep subtracting perfect squares (like 9, 16, 25, …) until we reach
zero, counting how many times it takes. But that greedy idea doesn’t always find the smallest number of squares. For
example, trying the biggest square each time may miss a better combination. Instead, this problem has a clever
mathematical shortcut that utilizes some deep results from number theory, specifically the Four-Square theorem and the
Three-Square theorem.

The Four-Square theorem says every number can be written as the sum of at most four perfect squares. So, the answer will
always be one of 1, 2, 3, or 4. The Three-Square theorem tells us that some numbers can’t be expressed as the sum of
three squares, and these are exactly the numbers that look like 4^a(8b+7) That means, if a number (after dividing out
all factors of 4) is of the form 8b+7, then it needs four squares. Using these ideas, we can build a simple check-and-decide
algorithm instead of trying all combinations. First, we remove all factors of 4 from the number, because multiplying or
dividing by 4 doesn’t change how many squares are needed; it just scales them. Then, we check the remainder when divided
by 8. If it’s 7, the number must have four squares. Otherwise, we check if it’s already a perfect square (then the answer
is 1). If not, we test if it can be written as the sum of two perfect squares (then the answer is 2). If none of those
conditions are true, we know from the theorems that it must be 3. So, rather than testing every combination, this
approach uses mathematical reasoning to narrow the answer step by step, making it very fast and elegant.

Let’s look at the algorithm steps:

- Keep dividing n by 4 while it is divisible by 4. This simplifies the number without changing the answer. If a number
is built from perfect squares, then four times that number is built from the same squares, just doubled. So, dividing
by 4 doesn’t affect how many squares we need; it only makes the number smaller to work with.
- If the reduced number has a remainder of 7 when divided by 8 (n % 8 == 7), return 4 immediately, because it must need
four squares.
- Check if n is a perfect square itself. If yes, return 1.
- Try to write n as a sum of two perfect squares. Iterate over all possible i from 1 to √n, and check if n - i² is also
a perfect square. If such a pair exists, return 2.
- If none of the above conditions are true, return 3. By elimination, the number can be expressed as the sum of three
squares.

### Time Complexity

We check if the number can be decomposed into the sum of two squares, which takes O(sqrt(n)) iterations. In the remaining
cases, we perform the check in constant time.

### Space Complexity

The algorithm consumes a constant space, regardless of the size of the input number, so O(1).
82 changes: 80 additions & 2 deletions pymath/perfect_square/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,87 @@
from math import sqrt


# function to check if a number is a perfect square
def is_square(n):
def is_square(n: int) -> bool:
"""
Checks if a number is a perfect square.
Args:
n (int): The number to check.
Returns:
bool: True if n is a perfect square, False otherwise.
"""
if n < 0:
return False
else:
return sqrt(n).is_integer()


def is_perfect_square(n: int) -> bool:
"""
Checks if a number is a perfect square.
Args:
n (int): The number to check.
Returns:
bool: True if n is a perfect square, False otherwise.
"""
if n < 0:
return False
sqrt_num = int(sqrt(n))
return sqrt_num * sqrt_num == n


def num_squares(n: int) -> int:
"""
Finds the least number of perfect square numbers that sum to n.
Args:
n (int): The target number to find the least number of perfect square numbers that sum to it.
Returns:
int: The least number of perfect square numbers that sum to n.
"""
if n < 0:
raise ValueError("n must be non-negative")
if n == 0:
return 0

dp = [float("inf")] * (n + 1)
dp[0] = 0

for i in range(1, n + 1):
j = 1
while j * j <= i:
dp[i] = min(dp[i], dp[i - j * j] + 1)
j += 1

return dp[n]


def num_squares_2(n: int) -> int:
"""
Finds the least number of perfect square numbers that sum to n.
Args:
n (int): The target number to find the least number of perfect square numbers that sum to it.
Returns:
int: The least number of perfect square numbers that sum to n.
"""
if n < 0:
raise ValueError("n must be non-negative")
if n == 0:
return 0

# Apply reduction by removing factors of 4
while n % 4 == 0:
n = n // 4

# Check if n is of form (8k + 7)
if n % 8 == 7:
return 4

# Check if n itself is a perfect square
if is_perfect_square(n):
return 1

# Check if n is the sum of two perfect squares
for value in range(1, int(sqrt(n)) + 1):
if is_perfect_square(n - value * value):
return 2

return 3
27 changes: 27 additions & 0 deletions pymath/perfect_square/test_perfect_squares.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import unittest
from parameterized import parameterized
from pymath.perfect_square import num_squares, num_squares_2


TEST_CASES = [
(1, 1),
(12, 3),
(13, 2),
(23, 4),
(997, 2),
]


class NumOfPerfectSquaresTestCases(unittest.TestCase):
@parameterized.expand(TEST_CASES)
def test_num_of_perfect_squares(self, n: int, expected: int):
actual = num_squares(n)
(self.assertEqual(expected, actual) @ parameterized.expand(TEST_CASES))

def test_num_of_perfect_squares_2(self, n: int, expected: int):
actual = num_squares_2(n)
self.assertEqual(expected, actual)


if __name__ == "__main__":
unittest.main()
Loading