Skip to content

fix(maths): add the leftover prime factor in sum_of_divisors - #15666

Open
bougieL wants to merge 1 commit into
TheAlgorithms:masterfrom
bougieL:fix/sum-of-divisors
Open

bougieL wants to merge 1 commit into
TheAlgorithms:masterfrom
bougieL:fix/sum-of-divisors

Conversation

@bougieL

@bougieL bougieL commented Oct 29, 2026

Copy link
Copy Markdown

Describe your change

  • Add an algorithm?
  • Fix a bug or typo in an existing algorithm?
  • Add or change doctests? -- Note: changed together with the code because the doctests are the regression proof for this bug; all the new cases fail on the current master.
  • Documentation change?

Checklist

  • I have read CONTRIBUTING.md.
  • This pull request is all my own work -- I have not plagiarized.
  • I know that pull requests will not be merged if they fail the automated tests.
  • This PR only changes one algorithm file. To ease review, please open separate PRs for separate algorithms.
  • All new Python files are placed inside an existing directory.
  • All filenames are in all lowercase characters with no spaces or dashes.
  • All functions and variable names follow Python naming conventions.
  • All function parameters and return values are annotated with Python type hints.
  • All functions have doctests that pass the automated testing.
  • All new algorithms include at least one URL that points to Wikipedia or another similar explanation.
  • If this pull request resolves one or more open issues, then the description above includes the issue number(s) with a closing keyword: "Fixes #ISSUE-NUMBER".

Description

maths/basic_maths.py::sum_of_divisors returned the wrong value for most inputs.

Two defects in the same function:

  1. The leftover prime factor was dropped. The function trial-divides by 2, then by odd i up to sqrt(n), multiplying in the geometric-series term for each prime power it finds. Whatever prime factor remains above that bound at the end was never multiplied in. Its sibling number_of_divisors in the same file does handle this (if n > 1: div *= 2), so sum_of_divisors was missing the analogous step. Consequence: sum_of_divisors(3) returned 1 instead of 4, and 94 of the first 119 integers were wrong.

    >>> sum_of_divisors(3)    # master -> 1, correct -> 4
    >>> sum_of_divisors(7)    # master -> 1, correct -> 8
    >>> sum_of_divisors(12)   # master -> 7, correct -> 28
    
  2. The arithmetic was done in floats. int(n / 2), int(n / i), the / in the geometric series, and the final int(s) truncated away the low bits, so large inputs were wrong by orders of magnitude:

    >>> sum_of_divisors(2**62)     # master -> 9223372036854775808
    9223372036854775807            # correct
    >>> sum_of_divisors(3**40)     # master -> 8380746196804800
    18236498188585393201           # correct
    

Changes

  • Add the missing if n > 1: s *= n + 1 step, mirroring number_of_divisors in the same file.
  • Switch to exact integer arithmetic: n //= 2, n //= i, // in the geometric series, math.isqrt(n) for the trial-division bound (replacing int(math.sqrt(n))), and drop the now-redundant int(s).
  • Add doctests covering a prime, a composite with a large prime factor, and large powers of two. The existing sum_of_divisors(100) == 217 doctest is unchanged and still passes.

How I tested

$ uvx ruff check maths/basic_maths.py && uvx ruff format --check maths/basic_maths.py
All checks passed!
1 file already formatted

$ uvx ty check maths/basic_maths.py
All checks passed!

$ .venv/bin/python -m pytest maths/basic_maths.py --doctest-modules -q
4 passed in 0.02s

$ .venv/bin/python -m pytest maths tests -q --doctest-modules
917 passed in 1.94s

Regression proof — before the change, against an independent factorization of
σ(n) = ∏ (p^(e+1) − 1)/(p − 1):

n=3:  old=1     expected=4   FAILS
n=12: old=7     expected=28  FAILS
n=7:  old=1     expected=8   FAILS

After the change, sum_of_divisors(n) == sigma_exact(n) for every n in
1..50000 (0 mismatches). All the new doctest cases fail on current master:

$ .venv/bin/python -c "exec(compile(open('maths/basic_maths.py.old').read(),'old','exec')); ..."
n=3: old=1 expected=4 FAILS on old code
n=12: old=7 expected=28 FAILS on old code
n=7: old=1 expected=8 FAILS on old code
sum_of_divisors(2**62) -> 9223372036854775808, expected 9223372036854775807 FAILS on old code

Submitted with gh CLI by @bougieL

sum_of_divisors only multiplied in the prime factors it found while
trial-dividing, so any prime factor above the trial-division bound was
dropped: 94 of the first 119 inputs returned the wrong answer
(sum_of_divisors(3) -> 1 instead of 4, sum_of_divisors(12) -> 7 instead
of 28). number_of_divisors in the same file already handles this case
with `if n > 1: div *= 2`.

Also compute with // instead of /: the function accumulated a float and
truncated with int(s), so sum_of_divisors(2**62) returned
9223372036854775808 instead of 9223372036854775807 and large inputs were
off by orders of magnitude (3**40 was wrong by ~10^4). math.isqrt
replaces int(math.sqrt(n)) for the same reason.

Verified against an independent factorization of sigma(n) for
n = 1..50000: 0 mismatches.
@algorithms-keeper algorithms-keeper Bot added awaiting reviews This PR is ready to be reviewed enhancement This PR modified some existing files labels Oct 29, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting reviews This PR is ready to be reviewed enhancement This PR modified some existing files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant