Problem 1 - Multiples of 3 and 5 Link to heading
The problem statement asks to find the sum of all multiples of 3 and 5 not greater than 1000.
Bruteforcing solution Link to heading
To bruteforce the solution it is enough to generate a list of multiples, prevent double counting and then perform the sum. In order to test Object Oriente Design I’ll be overengineering the solution.
First, I’d like to implement a class to deliver multiples of a number. Since i’d like to iterate with them, I’ll implement the iterator protocol
class Multiples():
def __init__(self, n, m=None):
"""
Returns up to the m multiples of number n
n - base number
m - max number of multiples
"""
if n <= 1 or (m is not None and m <= 1):
raise ValueError("Invalid initialization")
self.n = n
self.counter = 0
self.max = m if m else None
def __iter__(self):
return self
def __next__(self):
self.counter+=1
if self.max and self.counter > self.max:
raise StopIteration
return self.n*self.counter
so to compute the first 10 multiples
multiples_3 = Multiples(3,10)
for m in multiples_3:
print(m)
Outputs
3
6
9
12
15
18
21
24
27
30
Since I think that every code I write is an opportunity to sharpen my skills, I decide to write some simple test cases for this class
import unittest
import typing
class TestMultiples(unittest.TestCase):
def test_raises_invalid_m(self):
with self.assertRaises(ValueError):
Multiples(-1, 10)
def test_raises_invalid_n(self):
with self.assertRaises(ValueError):
Multiples(3, -1)
def test_is_iterable(self):
self.assertIsInstance(Multiples(2,2), typing.Iterator)
def test_delivers_count_multiples(self):
count=0
expected=10
m = Multiples(2,expected)
for i in m:
count+=1
self.assertEqual(count, expected)
Now, we can compute the multiples with a simple loop. Now, taking care of not repeating shared multiples, instead of using a list, we can use a set that allows unique values. In this manner, we can get the list of non-repeated multiples, which is part of the solution we need to get the sum,
combined_multiples = set()
multiples_3 = Multiples(3,10)
multiples_5 = Multiples(5,10)
for i in range(10):
from_3 = next(multiples_3)
from_5 = next(multiples_5)
combined_multiples.add(from_3)
combined_multiples.add(from_5)
list(combined_multiples)
Displays
[3, 35, 5, 6, 40, 9, 10, 12, 45, 15, 18, 50, 20, 21, 24, 25, 27, 30]
So, we can do
for i in range(10):
from_3 = next(multiples_3)
from_5 = next(multiples_5)
if from_3 < upper_bound:
combined_multiples.add(from_3)
if from_5 < upper_bound
combined_multiples.add(from_5)
But we don’t want to repeat code like a parrot, so we create the closure,
upper_bound = 1000
combined_multiples = set()
def addToSet(number):
if number < upper_bound:
combined_multiples.add(number)
And proceed to complete the loop as,
while True:
from_3 = next(multiples_3)
from_5 = next(multiples_5)
if from_3 > upper_bound and from_5 > upper_bound:
break
addToSet(from_3)
addToSet(from_5)
and finally, use the old-reliable reducer to compute the sum,
import functools
functools.reduce(lambda x,y: x+y, list(combined_multiples), 0)
We can write the full code, and include some time computations,
import functools
import time
upper_bound = 1000
combined_multiples = set()
multiples_3 = Multiples(3)
multiples_5 = Multiples(5)
def addToSet(number):
if number < upper_bound:
combined_multiples.add(number)
start = time.time()
while True:
from_3 = next(multiples_3)
from_5 = next(multiples_5)
if from_3 > upper_bound and from_5 > upper_bound:
break
addToSet(from_3)
addToSet(from_5)
result = functools.reduce(lambda x,y: x+y, list(combined_multiples), 0)
end = time.time()
bruteforce_time = end-start
print(f"Multiples sum {result}")
print(f"Took {bruteforce_time} seconds to bruteforce")
The output of this typically is,
Multiples sum 233168
Took 0.000244140625 seconds to bruteforce
Smartforcing the solution Link to heading
As a scientist and math admirer, I consider other approaches to this problem. So, what about using math to solve it? We know that multiples of $N$ are of the form
$$ p(i) = N,i \qquad \forall i > 0 $$
So the multiples of 3 are of the form
$$ m_{3}(i) = 3i $$
If we compute that is the $i$ needed to reach 1000
$$ 3i = 1000 \rightarrow i = \frac{1000}{3} \approx 333.\bar{3} $$
The same reasoning with 5
$$ 5i = 1000 \rightarrow i = \frac{1000}{5} = 200 $$
Hence we only need to compute 333 multiples of 3 and 199 of 5, to get the multiples under 1000. A new issue arises, and is the double counting. The least common multiplier is
$$ 3 i = 5 j \implies i=5 ;\quad j=3 $$
So 15 is the LCM between 3 and 5. We need to remove its multiples from the list of generated multiples to consider in the sum.
$$ 15i = 1000 \rightarrow i = \frac{1000}{15} = 66.\bar{6} $$
The sum of the first n multiples of number m (for each k, a multiple is $m k$)
$$ s_{n}(m) = \sum_{k=0}^{n}m k = m \sum_{k=0}^{n} k = m \left(\frac{n(n+1)}{2}\right) $$
Thanks to the known result of Gauss summation
$$ \sum_{k=0}^{n} k = \frac{n(n+1)}{2} $$
Hence we have the three sums
$$ \begin{cases} s_{333}(3) = 3 \left(\frac{333(333+1)}{2}\right) = \alpha \\ s_{199}(5) = 5 \left(\frac{199(199+1)}{2}\right) = \beta \\ s_{66}(15) = 15 \left(\frac{66(66+1)}{2}\right) = \gamma \end{cases} $$
So the total count of multiples is,
$$ t = \alpha + \beta - \gamma = 3 \left(\frac{333(333+1)}{2}\right) + 5 \left(\frac{199(199+1)}{2}\right) - 15 \left(\frac{66(66+1)}{2}\right) = 233168 $$
took about 20-30 mins to compute by brain 🧠, but compute time and costs is 0. The fun was huge though. Now, let’s code and see that the Smartforcing takes less than 0.9% of the time needed in compute (yes, in percentage).
Code the smartway Link to heading
Now that we have how to compute the sum of multiples by means of the Gauss summation,
class GaussSumMultiples():
def __init__(self, n, m):
self.n = n
self.m = m
def __call__(self):
return self.n * ((self.m*(self.m + 1))/2)
import unittest
class TestGaussSumMultiples(unittest.TestCase):
def test_sum_10(self):
s = GaussSumMultiples(1, 10)
self.assertEqual(s(), 5*11.)
unittest.main(argv=[''], verbosity=2, exit=False)
import time
start = time.time()
alpha = GaussSumMultiples(3,333)()
beta = GaussSumMultiples(5, 199)()
gamma= GaussSumMultiples(15, 66)()
result = alpha + beta - gamma
end = time.time()
print(f"Multiples sum {result}")
smartforce_time = end-start
print(f"Took {smartforce_time} seconds to smart compute")
Multiples sum 233168.0
Took 6.29425048828125e-05 seconds to smart compute
Comparing experiments Link to heading
import functools
import time
import pandas as pd
import matplotlib.pyplot as plt
experiments = 100000
def smart():
start = time.time()
alpha = GaussSumMultiples(3,333)()
beta = GaussSumMultiples(5, 199)()
gamma= GaussSumMultiples(15, 66)()
result = alpha + beta - gamma
end = time.time()
return end - start
def bruteforce():
upper_bound = 1000
combined_multiples = set()
multiples_3 = Multiples(3)
multiples_5 = Multiples(5)
def addToSet(number):
if number < upper_bound:
combined_multiples.add(number)
start = time.time()
while True:
from_3 = next(multiples_3)
from_5 = next(multiples_5)
if from_3 > upper_bound and from_5 > upper_bound:
break
addToSet(from_3)
addToSet(from_5)
result = functools.reduce(lambda x,y: x+y, list(combined_multiples), 0)
end = time.time()
return end-start
smart_times = list()
bruteforce_times = list()
data = [
{"Smart": smart(), "Brute Force": bruteforce()}
for _ in range(experiments)
]
df_transposed = pd.DataFrame(data)
time_pct = 100*(df_transposed.mean()['Smart']/df_transposed.mean()['Brute Force'])
print(f"Computation time needed {time_pct}% of the Brute forcing!!")
The output says a lot about this
Computation time needed 0.9025702306961663% of the Brute forcing!!
Big take Link to heading
Getting an appropriate algorithm can reduce the required computation time down to 1% of what it would cost by means of bruteforce
Why does this matter? Link to heading
Considering the resulting execution statistics
print("--- Execution Time Statistics ---")
print(df_transposed.describe())
--- Execution Time Statistics ---
Smart Brute Force
count 1.000000e+05 100000.000000
mean 8.511162e-07 0.000094
std 2.322563e-06 0.000070
min 0.000000e+00 0.000081
25% 7.152557e-07 0.000088
50% 9.536743e-07 0.000089
75% 9.536743e-07 0.000093
max 5.559921e-04 0.018821
As we are dealing with really small numbers, we can switch to logarithmic scale and obtain cleaner view of the results. Also, we need to remove the 0.000000e+00 from the results to reduce numerical artifacts.
df_filtered = df_transposed.replace(0, np.nan)
# Create 50 log-spaced bins
bins = np.logspace(
np.log10(df_filtered.min().min()),
np.log10(df_filtered.max().max()),
50,
)
df_filtered.plot(kind='hist', bins=bins, alpha=0.6, figsize=(10, 10))
plt.title('Performance Comparison: Smart vs. Brute Force')
plt.xlabel('Execution Time (seconds)')
plt.ylabel('Frequency')
plt.grid(True, linestyle='--', alpha=0.5)
plt.xscale("log")
plt.show()
So, we get, on average, 2 orders of magnitude improvement in execution time. Algorithms matter, and that’s why in software, having a toolkit of known algorithms and problem solving skills.
AI Slopping the code Link to heading
Naive prompt Link to heading
Using Gemini Pro 3.1 I tried asking a simple prompt for a simple problem
Consider The [problem statement](https://projecteuler.net/problem=1) asks to find the sum of all multiples of 3 and
5 not greater than 1000. Deliver a python script that outputs
Multiples sum <the actual sum>
Took 0.1234 seconds to compute.
Write the file ai_slopped_problem1.py
Output code and further analysis Link to heading
import time
def main():
# Start the high-resolution timer
start_time = time.perf_counter()
# Calculate the sum of all multiples of 3 or 5 up to and including 1000.
# If you need strictly "below 1000" (as in the original Project Euler),
# change 1001 to 1000.
total = sum(x for x in range(1001) if x % 3 == 0 or x % 5 == 0)
# Stop the timer
end_time = time.perf_counter()
elapsed_time = end_time - start_time
# Print the exact requested output format
print(f"Multiples sum {total}")
print(f"Took {elapsed_time:.4f} seconds to compute.")
if __name__ == "__main__":
main()
Critique Link to heading
Running the code produced gives the output,
Multiples sum 234168 Took 0.0003 seconds to compute.
Which is not even correct. The prompt was simple, but the problem statement was simple too. I had a conversation with a fellow developer that said that with proper guardrails, we developers should not check AI generated code. I find that this little slipped detail about the stopping condition for the range(1001) shows how dangerous unattended AI development can be. My harness can have guardrails about adding proper testing, and other stuff, but what about such logical mistakes like, using the wrong algorithm, or delivering potentially the incorrect result? asking the AI to write unit tests, we surely will get passing tests. So I tried it, and got the results
import time
import unittest
# ---------------------------------------------------------
# 1. The Core Logic (Extracted for testability)
# ---------------------------------------------------------
def sum_multiples(limit):
"""Calculates the sum of all multiples of 3 or 5 up to and including the limit."""
return sum(x for x in range(limit + 1) if x % 3 == 0 or x % 5 == 0)
# ---------------------------------------------------------
# 2. The Original Main Execution
# ---------------------------------------------------------
def run_main():
start_time = time.perf_counter()
total = sum_multiples(1000)
end_time = time.perf_counter()
print(f"Multiples sum {total}")
print(f"Took {end_time - start_time:.4f} seconds to compute.")
# ---------------------------------------------------------
# 3. The Unit Tests
# ---------------------------------------------------------
class TestSumMultiples(unittest.TestCase):
def test_small_limit(self):
# Multiples of 3 and 5 up to and including 10 are: 0, 3, 5, 6, 9, 10
# Sum = 33
self.assertEqual(sum_multiples(10), 33)
def test_limit_nine(self):
# Multiples of 3 and 5 up to and including 9 are: 0, 3, 5, 6, 9
# Sum = 23
self.assertEqual(sum_multiples(9), 23)
def test_target_problem(self):
# The main problem limit (not greater than 1000)
self.assertEqual(sum_multiples(1000), 234168)
def test_zero_limit(self):
# Edge case: limit is 0
self.assertEqual(sum_multiples(0), 0)
if __name__ == "__main__":
# If you want to run the tests, uncomment the line below:
# unittest.main()
# By default, we run the tests in this script so you can see them pass.
# We use argv=['first-arg-is-ignored'] to prevent unittest from reading command line args.
unittest.main(argv=['first-arg-is-ignored'], exit=False)
print("\n--- Running Main Script ---\n")
run_main()
And the tests pass indeed
![[Screenshot 2026-08-05 at 20.05.13.png]]
Which is not surprise, still with the incorrect logic inside. As I discussed on a (previous blog)[2026/08/reflections-on-blogging-in-ai-era-2026/]