Skip to content

Commit 90b32ed

Browse files
authored
Add simultaneous congruence solving using the Chinese remainder theorem (#17)
1 parent 1e3b32f commit 90b32ed

File tree

2 files changed

+77
-0
lines changed

2 files changed

+77
-0
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
describe("Solve simultaneous congruences", function()
2+
local ssc = require("math.solve_simultaneous_congruences")
3+
4+
it("should handle cases with solutions", function()
5+
assert.equal(0, ssc({ { 0, 3 } }))
6+
assert.equal(65, ssc({ { 1, 8 }, { 2, 9 } }))
7+
assert.equal(1, ssc({ { 1, 54 }, { 1, 73 }, { 1, 997 }, { 1, 102353 } }))
8+
assert.equal(39, ssc({ { 0, 3 }, { 3, 4 }, { 4, 5 } }))
9+
assert.equal(23, ssc({ { 2, 3 }, { 3, 5 }, { 2, 7 } }))
10+
assert.equal(34, ssc({ { 1, 3 }, { 4, 5 }, { 6, 7 } }))
11+
assert.equal(388, ssc({ { 3, 7 }, { 3, 5 }, { 4, 12 } }))
12+
assert.equal(87, ssc({ { 2, 5 }, { 3, 7 }, { 10, 11 } }))
13+
assert.equal(29, ssc({ { 2, 3 }, { 1, 4 }, { 7, 11 } }))
14+
assert.equal(125, ssc({ { 6, 7 }, { 8, 9 }, { 4, 11 }, { 8, 13 } }))
15+
assert.equal(89469, ssc({ { 6, 11 }, { 13, 16 }, { 9, 21 }, { 19, 25 } }))
16+
end)
17+
18+
it("should handle cases without solution", function()
19+
assert.equal(nil, ssc({ { 5, 17 }, { 4, 17 } }))
20+
assert.equal(nil, ssc({ { 3, 4 }, { 0, 6 } }))
21+
assert.equal(nil, ssc({ { 3, 13 }, { 7, 1 } }))
22+
end)
23+
24+
it("should throw error when a modulus is zero", function()
25+
assert.has_error(function()
26+
ssc({ { 0, 0 } })
27+
end)
28+
end)
29+
30+
it("should throw error when a modulus is negative", function()
31+
assert.has_error(function()
32+
ssc({ { 0, -1 } })
33+
end)
34+
end)
35+
end)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
local modular_inverse = require("math.modular_inverse")
2+
3+
local function all_moduli_prod(congruences)
4+
local res = 1
5+
for _, congruence in ipairs(congruences) do
6+
res = res * congruence[2]
7+
end
8+
return res
9+
end
10+
11+
-- Solves system of simultaneous congruences, i.e. the system of the form:
12+
-- ```
13+
-- x = a_0 mod m_0
14+
-- x = a_1 mod m_1
15+
-- ...
16+
-- ```
17+
-- where `a_0`, `a_1`, ... and `m_0`, `m_1`, ... are given.
18+
--
19+
-- The system is represented by a list of pairs.
20+
-- The pair `{a, m}` represents a congruence `x = a mod m`.
21+
-- For the input `{{a_0, m_0}, {a_1, m_1}, ...}`,
22+
-- it finds a number `x`, such that
23+
-- `x = a_i mod m_i` and `0 < x < m_0 * m_1 * ...`.
24+
-- The implementation is based on the Chinese remainder theorem.
25+
return function(congruences)
26+
local all_prod = all_moduli_prod(congruences)
27+
28+
local res = 0
29+
for _, congruence in ipairs(congruences) do
30+
local residue = congruence[1]
31+
local modulus = congruence[2]
32+
local cur_prod = math.floor(all_prod / modulus)
33+
local cur_inv = modular_inverse(cur_prod, modulus)
34+
if cur_inv == nil then
35+
-- moduli of the congruences are not co-prime
36+
return nil
37+
end
38+
res = (res + residue * cur_inv * cur_prod) % all_prod
39+
end
40+
41+
return res
42+
end

0 commit comments

Comments
 (0)