Skip to content
Open
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
41 changes: 41 additions & 0 deletions maths/chinese_remainder_theorem.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,31 @@ def extended_euclid(a: int, b: int) -> tuple[int, int]:
>>> extended_euclid(7, 5)
(-2, 3)

The recursion goes (10, 6) -> (6, 4) -> (4, 2) -> (2, 0) and then unwinds, so the
intermediate results of extended_euclid(10, 6) are:

>>> extended_euclid(2, 0) # base case
(1, 0)
>>> extended_euclid(4, 2)
(0, 1)
>>> extended_euclid(6, 4)
(1, -1)

The result (x, y) satisfies a * x + b * y == gcd(a, b). Here gcd(10, 6) is 2, not 1,
because 10 and 6 are not coprime:

>>> x, y = extended_euclid(10, 6)
>>> 10 * x + 6 * y
2

When a < b the first step only swaps the arguments (a // b is 0):

>>> extended_euclid(5, 7)
(3, -2)
>>> x, y = extended_euclid(5, 7)
>>> 5 * x + 7 * y
1

"""
if b == 0:
return (1, 0)
Expand All @@ -45,6 +70,22 @@ def chinese_remainder_theorem(n1: int, r1: int, n2: int, r2: int) -> int:
>>> chinese_remainder_theorem(6,1,4,3)
14

The result satisfies both congruences when the moduli are coprime:

>>> n = chinese_remainder_theorem(5, 1, 7, 3)
>>> n % 5, n % 7
(1, 3)

Non-coprime moduli are NOT supported. 6 and 4 share the factor 2, so
extended_euclid(6, 4) gives a * x + b * y == 2, not 1, and the formula returns
14, which satisfies neither congruence (a valid answer, 7, exists modulo 12):

>>> n = chinese_remainder_theorem(6, 1, 4, 3)
>>> n % 6, n % 4
(2, 2)
>>> 7 % 6, 7 % 4
(1, 3)

"""
(x, y) = extended_euclid(n1, n2)
m = n1 * n2
Expand Down
78 changes: 78 additions & 0 deletions work/R23-T055/TRACE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# R23-T055: tracing `maths/chinese_remainder_theorem.py`

How to reproduce (from the repo root):

python -m pdb work/R23-T055/trace_driver.py
(Pdb) b /full/path/to/maths/chinese_remainder_theorem.py:32 # the `return (y, x - k * y)` line
(Pdb) c
(Pdb) p (a, b, x, y, k)
(Pdb) c # repeat

The values below were read at that breakpoint with pdb and also checked by hand.
`extended_euclid(a, b)` returns `(x, y)` with `a*x + b*y = gcd(a, b)`.

## 1. `extended_euclid(10, 6)`

Going down (each call recurses on `(b, a % b)`):

| call | a | b | a % b | next call |
|---|---|---|---|---|
| 1 | 10 | 6 | 4 | (6, 4) |
| 2 | 6 | 4 | 2 | (4, 2) |
| 3 | 4 | 2 | 0 | (2, 0) |
| 4 | 2 | 0 | - | base case, returns `(1, 0)` |

Coming back up (`k = a // b`, returns `(y, x - k*y)`, where `(x, y)` is what the deeper call returned):

| call | a | b | x (from below) | y (from below) | k | returns (y, x - k*y) | check a*x' + b*y' |
|---|---|---|---|---|---|---|---|
| 4 | 2 | 0 | | | | (1, 0) | 2*1 + 0*0 = 2 |
| 3 | 4 | 2 | 1 | 0 | 2 | (0, 1) | 4*0 + 2*1 = 2 |
| 2 | 6 | 4 | 0 | 1 | 1 | (1, -1) | 6*1 + 4*(-1) = 2 |
| 1 | 10 | 6 | 1 | -1 | 1 | (-1, 2) | 10*(-1) + 6*2 = 2 |

Result `(-1, 2)`. Every row ends at 2, because gcd(10, 6) = 2.

## 2. `chinese_remainder_theorem(5, 1, 7, 3)`

First `extended_euclid(5, 7)`:

| call | a | b | x (from below) | y (from below) | k = a // b | returns (y, x - k*y) | check |
|---|---|---|---|---|---|---|---|
| 5 | 1 | 0 | | | | (1, 0) | 1*1 = 1 |
| 4 | 2 | 1 | 1 | 0 | 2 | (0, 1) | 2*0 + 1*1 = 1 |
| 3 | 5 | 2 | 0 | 1 | 2 | (1, -2) | 5*1 + 2*(-2) = 1 |
| 2 | 7 | 5 | 1 | -2 | 1 | (-2, 3) | 7*(-2) + 5*3 = 1 |
| 1 | 5 | 7 | -2 | 3 | 0 | (3, -2) | 5*3 + 7*(-2) = 1 |

(Call order going down: (5,7) -> (7,5) -> (5,2) -> (2,1) -> (1,0).)
So `x, y = 3, -2`.

Then the CRT formula, with `n1=5, r1=1, n2=7, r2=3`:

| step | value |
|---|---|
| m = n1 * n2 | 35 |
| n = r2*x*n1 + r1*y*n2 | 3*3*5 + 1*(-2)*7 = 45 - 14 = 31 |
| (n % m + m) % m | (31 + 35) % 35 = 31 |

Check: 31 % 5 = 1 and 31 % 7 = 3, both correct.

## Observations turned into doctests
- Intermediate results of the (10, 6) trace: `extended_euclid(2, 0) == (1, 0)`, `extended_euclid(4, 2) == (0, 1)`, `extended_euclid(6, 4) == (1, -1)`.
- `10*x + 6*y == 2` (the gcd, not 1) for `extended_euclid(10, 6)`.
- `extended_euclid(5, 7) == (3, -2)` and the first step has `k = 0` (it just swaps the arguments).
- `chinese_remainder_theorem(5,1,7,3) % 5, % 7 == (1, 3)`.
- Non-coprime input: see below.

## What surprised me
The existing doctest `chinese_remainder_theorem(6, 1, 4, 3) -> 14` looks like a normal
passing example but **14 is wrong**: 14 % 6 = 2 (not 1) and 14 % 4 = 2 (not 3).
6 and 4 are not coprime (gcd 2), so `extended_euclid(6, 4)` gives `(1, -1)` with
`6*1 + 4*(-1) = 2` instead of 1, and the formula `n = r2*x*n1 + r1*y*n2` only works when
that sum is 1: here n = 3*1*6 + 1*(-1)*4 = 14. A real answer exists (7 satisfies
7 % 6 = 1 and 7 % 4 = 3, valid modulo lcm = 12), but this implementation does not find it.
The algorithm's own docstring says "If GCD(a,b) = 1", so non-coprime input is outside the
contract, and the function silently returns a wrong number instead of raising.
I did not change the code (this task is doctests only); the new doctests document the
behaviour, and a future change could raise `ValueError` when `gcd(n1, n2) != 1`.
14 changes: 14 additions & 0 deletions work/R23-T055/trace_driver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Small driver to step through with pdb (run from the repo root).

python -m pdb work/R23-T055/trace_driver.py

Useful pdb commands: b maths/chinese_remainder_theorem.py:30 c p a, b, x, y, k w n s
"""

import sys

sys.path.insert(0, ".")
from maths.chinese_remainder_theorem import chinese_remainder_theorem, extended_euclid

print("extended_euclid(10, 6) =", extended_euclid(10, 6))
print("chinese_remainder_theorem(5, 1, 7, 3) =", chinese_remainder_theorem(5, 1, 7, 3))