Repository: scalekit (difficulty: hard)

Issue:
divisor_harmonic_total takes forever on large N

divisor_harmonic_total(10) == 27 and divisor_harmonic_total(20000) ==
201177 are instant and match a hand check. But the documented bound is
N up to 2 * 10**13, and there the linear loop is unusable:
divisor_harmonic_total(10**12) was still running after fifteen minutes
on my machine. Expected: 27785452449086, in well under a second, since
N // i only takes O(sqrt(N)) distinct values so the whole sum needs
only O(sqrt(N)) work.

For reference, the quantity equals the number of ordered pairs (a, b)
of positive integers with a * b <= N, and equivalently the sum of the
divisor counts d(x) for x in [1, N]; the tests cross-check both. The
suite wraps each large call in a watchdog worker process with a 10
second per-call budget, so the O(N) version now fails deterministically
instead of hanging. Bounds: 1 <= N <= 2 * 10**13. The contract
(return sum of N // i for i in [1, N]) must not change.

Source files:
core.py: """Divisor summation helpers for scalekit.

This module exposes divisor_harmonic_total(N), which returns
sum(N // i for i in range(1, N + 1)). The quantity equals the number
of ordered pairs (a, b) of positive integers with a * b <= N, and also
the sum of the divisor counts d(x) over all x in [1, N].

Contract and bounds:
- N is an integer with 1 <= N <= 2 * 10**13
- any single call in the stated range must complete within a few
  seconds on commodity hardware
"""


def divisor_harmonic_total(N):
    """Return the total by accumulating N // i for every i up to N."""
    total = 0
    for i in range(1, N + 1):
        total += N // i
    return total


Test files:
test_core.py: """Tests for core.divisor_harmonic_total.

Large calls run in a watchdog worker process with a hard per-call time
budget: within the documented bounds (N up to 2 * 10**13) only an
O(sqrt(N)) algorithm can answer, so a correct O(N) loop fails these
tests by running out of budget.
"""

import math
import multiprocessing as mp
import random

from core import divisor_harmonic_total

LARGE_BUDGET_SECONDS = 10.0


def _worker(args, q):
    q.put(divisor_harmonic_total(*args))


def _call_with_timeout(args, timeout=LARGE_BUDGET_SECONDS):
    ctx = mp.get_context("spawn")
    q = ctx.Queue()
    p = ctx.Process(target=_worker, args=(args, q))
    p.start()
    p.join(timeout)
    if p.is_alive():
        p.terminate()
        p.join()
        raise AssertionError(
            "divisor_harmonic_total{} did not finish within {}s; the stated "
            "bounds need an O(sqrt(N)) algorithm".format(tuple(args), timeout)
        )
    if p.exitcode != 0:
        raise AssertionError(
            "divisor_harmonic_total{} worker exited with code {}".format(
                tuple(args), p.exitcode
            )
        )
    return q.get(timeout=5.0)


def _brute_total(N):
    return sum(N // i for i in range(1, N + 1))


def _brute_pair_count(N):
    # counts ordered pairs (a, b) with a * b <= N as the sum of divisor
    # counts d(x) for x in [1, N], each d(x) found by trial division
    total = 0
    for x in range(1, N + 1):
        r = math.isqrt(x)
        d = 0
        for a in range(1, r + 1):
            if x % a == 0:
                d += 2
        if r * r == x:
            d -= 1
        total += d
    return total


def test_small_matches_brute():
    for N in range(1, 401):
        assert divisor_harmonic_total(N) == _brute_total(N)
    rng = random.Random(1011)
    for _ in range(120):
        N = rng.randrange(401, 20001)
        assert divisor_harmonic_total(N) == _brute_total(N)


def test_matches_pair_counting_brute():
    for N in (1, 2, 10, 100, 999, 3000):
        assert divisor_harmonic_total(N) == _brute_pair_count(N)


def test_large_within_budget():
    assert _call_with_timeout((10**12,)) == 27785452449086


def test_larger_within_budget():
    assert _call_with_timeout((2 * 10**13,)) == 615623694389393


Patch:
--- a/core.py
+++ b/core.py
@@ -11,10 +11,18 @@
   seconds on commodity hardware
 """
 
+import math
+
 
 def divisor_harmonic_total(N):
-    """Return the total by accumulating N // i for every i up to N."""
-    total = 0
-    for i in range(1, N + 1):
-        total += N // i
-    return total
+    """Return the total with the hyperbola symmetry trick.
+
+    The total counts lattice points (a, b) with a * b <= N. Let
+    K = isqrt(N); every such point has a <= K or b <= K, and the points
+    with a <= K plus the points with b <= K double count exactly the
+    K * K points where both coordinates are at most K (all of which
+    satisfy a * b <= K * K <= N). Hence the answer is
+    2 * sum(N // i for i in [1, K]) - K * K, in O(sqrt(N)) iterations.
+    """
+    K = math.isqrt(N)
+    return 2 * sum(N // i for i in range(1, K + 1)) - K * K