Skip to content

fix: compute LCM with integer arithmetic to avoid float precision loss and overflow#810

Open
gaoflow wants to merge 1 commit into
TheAlgorithms:masterfrom
gaoflow:fix/lcm-integer-arithmetic
Open

fix: compute LCM with integer arithmetic to avoid float precision loss and overflow#810
gaoflow wants to merge 1 commit into
TheAlgorithms:masterfrom
gaoflow:fix/lcm-integer-arithmetic

Conversation

@gaoflow

@gaoflow gaoflow commented Jul 24, 2026

Copy link
Copy Markdown

Summary

math/lcm.Lcm routes its entire computation through float64, which produces
silently wrong results for inputs that are not particularly large. The most
basic LCM identity is violated:

// math/lcm/lcm.go (current)
func Lcm(a, b int64) int64 {
	return int64(math.Abs(float64(a*b)) / float64(gcd.Iterative(a, b)))
}

lcm(x, 1) == x is violated (no external oracle needed)

Lcm(9007199254740993, 1) returns 9007199254740992 — a value smaller than
its first input
. 9007199254740993 is 2^53 + 1, which is not exactly
representable in float64, so the int64 -> float64 -> int64 round trip drops
the low bit. lcm(x, 1) must equal x; this needs no reference implementation
to see it is wrong.

lcm of two positives returns 0

Lcm(4294967296, 8589934592) (i.e. 2^32 and 2^33) returns 0. The LCM of
two positive integers cannot be 0. Here a*b is an int64 multiply evaluated
before the float64(...) cast, so it wraps mod 2^64 when |a*b| >= 2^63;
the true LCM (2^33) fits comfortably in int64, but the intermediate product
overflows first.

Further cases (verified against an exact math/big.Int oracle)

An independent oracle using math/big.Int (lcm = (|a|/gcd)*|b|, divide first,
exact bignum — no third-party LCM/GCD library) confirms:

call current result correct (big.Int)
Lcm(4294967296, 8589934592) 0 8589934592
Lcm(3037000500, 3037000500) 3037000499 3037000500 (lcm(x,x)==x)
Lcm(9007199254740993, 1) 9007199254740992 9007199254740993 (lcm(x,1)==x)
Lcm(1000000007, 1000000009) 1000000016000000000 1000000016000000063 (off by 63)

Controls are unaffected: Lcm(12,18)=36, Lcm(28,35)=140,
Lcm(4503599627370497,2)=9007199254740994, Lcm(104729,104743)=10969629647.
The existing tests ((1,5),(2,5),(10,5),(5,5)) only cover a*b < 2^53 with no
overflow, so they pass today (latent gap).

Root cause

Two independent failure modes, both from the float64 detour:

  1. Intermediate int64 overflow: a*b wraps mod 2^64 before the
    float64 cast, so the value passed to math.Abs is already wrong even when
    the true LCM fits in int64.
  2. float64 precision loss: float64 has 53 bits of significand, so it
    cannot represent every int64; once |a*b| (or |a| itself) exceeds 2^53
    the round trip drops low bits.

The GCD helper (math/gcd.Iterative) is correct and is left unchanged; the
defect is purely the float64 routing of the LCM formula.

Fix

Replace the float64 formula with integer arithmetic that divides |a| by gcd
before multiplying by |b|, so the running intermediate never exceeds the
true LCM and no float conversion occurs:

func Lcm(a, b int64) int64 {
	g := gcd.Iterative(a, b)
	if g == 0 {
		return 0
	}
	return (absInt64(a) / absInt64(g)) * absInt64(b)
}

Dividing first is safe because g divides a exactly, so |a|/g is an integer
no larger than |a|, and the product equals the true LCM. A g == 0 guard
handles gcd.Iterative(0, 0) (LCM(0, x) is conventionally 0). Minimal,
stdlib-only, style-matching the existing code.

Tests

math/lcm/lcm_test.go is extended (table-driven, repo style) with regression
cases, while preserving the existing four:

  • Lcm(4294967296, 8589934592) == 8589934592 (Mode A overflow)
  • Lcm(3037000500, 3037000500) == 3037000500 (Mode A lcm(x,x)==x)
  • Lcm(9007199254740993, 1) == 9007199254740993 (Mode B 2^53+1)
  • Lcm(1000000007, 1000000009) == 1000000016000000063 (Mode B precision)

All pre-fix these returned the wrong values shown above; post-fix they pass.

Verification

  • go test ./math/lcm/ ./math/gcd/ — PASS (existing + new tests)
  • go test ./math/... — PASS; go build ./... — clean
  • go vet ./math/lcm/ ./math/gcd/ — clean
  • gofmt -l math/lcm/lcm.go math/lcm/lcm_test.go — clean
  • Existing tests preserved and still passing.

The change is confined to math/lcm/; math/gcd is untouched.

…s and overflow

Lcm previously computed |a*b|/gcd via float64, which fails in two ways:
- int64 multiply overflow: a*b wraps mod 2^64 before the float64 cast, so
  e.g. Lcm(4294967296, 8589934592) returned 0 (impossible for positive inputs)
  and Lcm(3037000500, 3037000500) returned 3037000499, violating lcm(x,x)==x.
- float64 precision loss: float64 cannot represent every int64 past 2^53, so
  e.g. Lcm(9007199254740993, 1) returned 9007199254740992, violating the
  identity lcm(x,1)==x, and Lcm(1000000007, 1000000009) was off by 63.

Replace the float64 formula with integer arithmetic that divides |a| by gcd
before multiplying by |b|, so the running intermediate never exceeds the true
LCM and no float conversion occurs. Add regression tests covering both modes
while preserving the existing test cases.
@codacy-production

Copy link
Copy Markdown

Not up to standards ⛔

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant