Skip to content

Commit 3f1b0db

Browse files
authored
Merge pull request #127 from BrianLusina/feat/math-perfect-squares
feat(math, perfect squares): least number pefect squares that sum to n
2 parents ce7438e + cf998dc commit 3f1b0db

File tree

4 files changed

+162
-3
lines changed

4 files changed

+162
-3
lines changed

algorithms/search/binary_search/divide_chocolate/test_divide_chocolate.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import unittest
22
from typing import List
33
from parameterized import parameterized
4-
from algorithms.search.binary_search.divide_chocolate import maximize_sweetness, maximize_sweetness_2
4+
from algorithms.search.binary_search.divide_chocolate import (
5+
maximize_sweetness,
6+
maximize_sweetness_2,
7+
)
58

69
TEST_CASES = [
710
([1, 2, 3, 4, 5, 6, 7, 8, 9], 5, 6),

pymath/perfect_square/README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Perfect Squares
2+
3+
Given an integer, n, return the least number of perfect square numbers that sum to n.
4+
5+
> 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
6+
> multiplying a whole integer by itself. For example, 1, 4, 9 and 16 are perfect squares, but 3, 5, and 11 are not.
7+
8+
## Constraints
9+
10+
- 1 <= `n` < 10^3
11+
12+
## Solution
13+
14+
One of the first solutions that comes to mind is to keep subtracting perfect squares (like 9, 16, 25, …) until we reach
15+
zero, counting how many times it takes. But that greedy idea doesn’t always find the smallest number of squares. For
16+
example, trying the biggest square each time may miss a better combination. Instead, this problem has a clever
17+
mathematical shortcut that utilizes some deep results from number theory, specifically the Four-Square theorem and the
18+
Three-Square theorem.
19+
20+
The Four-Square theorem says every number can be written as the sum of at most four perfect squares. So, the answer will
21+
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
22+
three squares, and these are exactly the numbers that look like 4^a(8b+7) That means, if a number (after dividing out
23+
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
24+
algorithm instead of trying all combinations. First, we remove all factors of 4 from the number, because multiplying or
25+
dividing by 4 doesn’t change how many squares are needed; it just scales them. Then, we check the remainder when divided
26+
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
27+
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
28+
conditions are true, we know from the theorems that it must be 3. So, rather than testing every combination, this
29+
approach uses mathematical reasoning to narrow the answer step by step, making it very fast and elegant.
30+
31+
Let’s look at the algorithm steps:
32+
33+
- Keep dividing n by 4 while it is divisible by 4. This simplifies the number without changing the answer. If a number
34+
is built from perfect squares, then four times that number is built from the same squares, just doubled. So, dividing
35+
by 4 doesn’t affect how many squares we need; it only makes the number smaller to work with.
36+
- If the reduced number has a remainder of 7 when divided by 8 (n % 8 == 7), return 4 immediately, because it must need
37+
four squares.
38+
- Check if n is a perfect square itself. If yes, return 1.
39+
- 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
40+
a perfect square. If such a pair exists, return 2.
41+
- If none of the above conditions are true, return 3. By elimination, the number can be expressed as the sum of three
42+
squares.
43+
44+
### Time Complexity
45+
46+
We check if the number can be decomposed into the sum of two squares, which takes O(sqrt(n)) iterations. In the remaining
47+
cases, we perform the check in constant time.
48+
49+
### Space Complexity
50+
51+
The algorithm consumes a constant space, regardless of the size of the input number, so O(1).

pymath/perfect_square/__init__.py

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,87 @@
11
from math import sqrt
22

33

4-
# function to check if a number is a perfect square
5-
def is_square(n):
4+
def is_square(n: int) -> bool:
5+
"""
6+
Checks if a number is a perfect square.
7+
Args:
8+
n (int): The number to check.
9+
Returns:
10+
bool: True if n is a perfect square, False otherwise.
11+
"""
612
if n < 0:
713
return False
814
else:
915
return sqrt(n).is_integer()
16+
17+
18+
def is_perfect_square(n: int) -> bool:
19+
"""
20+
Checks if a number is a perfect square.
21+
Args:
22+
n (int): The number to check.
23+
Returns:
24+
bool: True if n is a perfect square, False otherwise.
25+
"""
26+
if n < 0:
27+
return False
28+
sqrt_num = int(sqrt(n))
29+
return sqrt_num * sqrt_num == n
30+
31+
32+
def num_squares(n: int) -> int:
33+
"""
34+
Finds the least number of perfect square numbers that sum to n.
35+
Args:
36+
n (int): The target number to find the least number of perfect square numbers that sum to it.
37+
Returns:
38+
int: The least number of perfect square numbers that sum to n.
39+
"""
40+
if n < 0:
41+
raise ValueError("n must be non-negative")
42+
if n == 0:
43+
return 0
44+
45+
dp = [float("inf")] * (n + 1)
46+
dp[0] = 0
47+
48+
for i in range(1, n + 1):
49+
j = 1
50+
while j * j <= i:
51+
dp[i] = min(dp[i], dp[i - j * j] + 1)
52+
j += 1
53+
54+
return dp[n]
55+
56+
57+
def num_squares_2(n: int) -> int:
58+
"""
59+
Finds the least number of perfect square numbers that sum to n.
60+
Args:
61+
n (int): The target number to find the least number of perfect square numbers that sum to it.
62+
Returns:
63+
int: The least number of perfect square numbers that sum to n.
64+
"""
65+
if n < 0:
66+
raise ValueError("n must be non-negative")
67+
if n == 0:
68+
return 0
69+
70+
# Apply reduction by removing factors of 4
71+
while n % 4 == 0:
72+
n = n // 4
73+
74+
# Check if n is of form (8k + 7)
75+
if n % 8 == 7:
76+
return 4
77+
78+
# Check if n itself is a perfect square
79+
if is_perfect_square(n):
80+
return 1
81+
82+
# Check if n is the sum of two perfect squares
83+
for value in range(1, int(sqrt(n)) + 1):
84+
if is_perfect_square(n - value * value):
85+
return 2
86+
87+
return 3
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import unittest
2+
from parameterized import parameterized
3+
from pymath.perfect_square import num_squares, num_squares_2
4+
5+
6+
TEST_CASES = [
7+
(1, 1),
8+
(12, 3),
9+
(13, 2),
10+
(23, 4),
11+
(997, 2),
12+
]
13+
14+
15+
class NumOfPerfectSquaresTestCases(unittest.TestCase):
16+
@parameterized.expand(TEST_CASES)
17+
def test_num_of_perfect_squares(self, n: int, expected: int):
18+
actual = num_squares(n)
19+
(self.assertEqual(expected, actual) @ parameterized.expand(TEST_CASES))
20+
21+
def test_num_of_perfect_squares_2(self, n: int, expected: int):
22+
actual = num_squares_2(n)
23+
self.assertEqual(expected, actual)
24+
25+
26+
if __name__ == "__main__":
27+
unittest.main()

0 commit comments

Comments
 (0)