@@ -40,13 +40,43 @@ pub fn ln_factorial(x: u64) -> f64 {
4040///
4141/// # Remarks
4242///
43- /// Returns `0.0` if `k > n`
43+ /// Returns `0.0` if `k > n`.
44+ ///
45+ /// The result is exact whenever `C(n, k)` fits in a `u128`, and correctly
46+ /// rounded to the nearest `f64` beyond `2^53`. Larger coefficients fall back
47+ /// to `exp(ln_factorial(n) - ln_factorial(k) - ln_factorial(n - k))`, which
48+ /// is accurate to about `1e-12` relative.
4449pub fn binomial ( n : u64 , k : u64 ) -> f64 {
4550 if k > n {
46- 0.0
47- } else {
48- ( 0.5 + ( ln_factorial ( n) - ln_factorial ( k) - ln_factorial ( n - k) ) . exp ( ) ) . floor ( )
51+ return 0.0 ;
52+ }
53+ match binomial_u128 ( n, k) {
54+ Some ( exact) => exact as f64 ,
55+ // Overflow: the result exceeds ~3.4e38 / n, far past 2^53, so
56+ // exactness is unattainable anyway; fall back to logs.
57+ None => ( ln_factorial ( n) - ln_factorial ( k) - ln_factorial ( n - k) )
58+ . exp ( )
59+ . round ( ) ,
60+ }
61+ }
62+
63+ /// Computes `C(n, k)` exactly in integer arithmetic, or `None` if an
64+ /// intermediate value overflows a `u128`. Requires `k <= n`.
65+ ///
66+ /// `C(n, i + 1) = C(n, i) * (n - i) / (i + 1)`, and the division is exact at
67+ /// every step, so the whole computation stays in integer arithmetic. The
68+ /// previous `f64` implementation rounded `exp(ln n! - ln k! - ln (n-k)!)` to
69+ /// the nearest integer, which was off by up to ~1e5 (57 ulp) for coefficients
70+ /// near 2^63 that f64 could still represent to full precision.
71+ fn binomial_u128 ( n : u64 , k : u64 ) -> Option < u128 > {
72+ // C(n, k) == C(n, n - k); walking up the smaller side minimises both the
73+ // iteration count and the chance of overflowing the integer path.
74+ let k = k. min ( n - k) ;
75+ let mut acc: u128 = 1 ;
76+ for i in 0 ..k {
77+ acc = acc. checked_mul ( ( n - i) as u128 ) ? / ( i as u128 + 1 ) ;
4978 }
79+ Some ( acc)
5080}
5181
5282/// Computes the natural logarithm of the binomial coefficient
@@ -75,16 +105,32 @@ pub fn multinomial(n: u64, ni: &[u64]) -> f64 {
75105/// Computes the multinomial coefficient: `n choose n1, n2, n3, ...`
76106///
77107/// Returns `None` if the elements in `ni` do not sum to `n`.
108+ ///
109+ /// # Remarks
110+ ///
111+ /// The result is exact whenever the coefficient (and every intermediate
112+ /// prefix product) fits in a `u128`, and correctly rounded to the nearest
113+ /// `f64` beyond `2^53`. Larger coefficients fall back to
114+ /// `exp(ln n! - sum ln ni!)`, accurate to about `1e-12` relative.
78115pub fn checked_multinomial ( n : u64 , ni : & [ u64 ] ) -> Option < f64 > {
79- let ( sum , ret ) = ni. iter ( ) . fold ( ( 0 , ln_factorial ( n ) ) , |acc , & x| {
80- ( acc . 0 + x , acc . 1 - ln_factorial ( x ) )
81- } ) ;
116+ if ni. iter ( ) . sum :: < u64 > ( ) != n {
117+ return None ;
118+ }
82119
83- if sum == n {
84- Some ( ( 0.5 + ret. exp ( ) ) . floor ( ) )
85- } else {
86- None
120+ // n! / (n1! n2! ... nk!) == prod_i C(s_i, n_i) with s_i = n_1 + ... + n_i:
121+ // a product of binomial coefficients, each computed exactly.
122+ let mut acc: u128 = 1 ;
123+ let mut prefix: u64 = 0 ;
124+ for & k in ni {
125+ prefix += k;
126+ let Some ( product) = binomial_u128 ( prefix, k) . and_then ( |c| acc. checked_mul ( c) ) else {
127+ // Overflow: fall back to logs (the old implementation's only path).
128+ let ret = ni. iter ( ) . fold ( ln_factorial ( n) , |a, & x| a - ln_factorial ( x) ) ;
129+ return Some ( ret. exp ( ) . round ( ) ) ;
130+ } ;
131+ acc = product;
87132 }
133+ Some ( acc as f64 )
88134}
89135
90136// Initialization for pre-computed cache of 171 factorial
@@ -158,6 +204,49 @@ mod tests {
158204 assert_eq ! ( binomial( 5 , 7 ) , 0.0 ) ;
159205 }
160206
207+ /// Every `C(n, k)` that fits in a `u128` must round-trip exactly (the
208+ /// conversion `u128 -> f64` is correctly rounded, so `expected as f64` is
209+ /// the best possible double). The old `exp(ln ...)`-based implementation
210+ /// failed this for e.g. `C(50, 25)` (off by 2) and `C(67, 33)` (off by
211+ /// 116736).
212+ #[ test]
213+ fn test_binomial_is_exact_where_representable ( ) {
214+ for n in 0 ..=170u64 {
215+ let mut expected: u128 = 1 ;
216+ for k in 0 ..=n / 2 {
217+ assert_eq ! (
218+ binomial( n, k) ,
219+ expected as f64 ,
220+ "C({n}, {k}) should be {expected}"
221+ ) ;
222+ assert_eq ! ( binomial( n, n - k) , expected as f64 , "C({n}, {}) symmetry" , n - k) ;
223+ let Some ( product) = expected. checked_mul ( ( n - k) as u128 ) else {
224+ break ;
225+ } ;
226+ expected = product / ( k as u128 + 1 ) ;
227+ }
228+ }
229+ }
230+
231+ /// Coefficients too large for the integer path fall back to logs; check the
232+ /// fallback is close and consistent with `ln_binomial`.
233+ #[ test]
234+ fn test_binomial_log_fallback ( ) {
235+ // C(200, 100) = 9.0548514656103281165404177077e58 (overflows u128)
236+ prec:: assert_relative_eq!(
237+ binomial( 200 , 100 ) ,
238+ 9.0548514656103281165e58 ,
239+ epsilon = 0.0 ,
240+ max_relative = 1e-11
241+ ) ;
242+ prec:: assert_relative_eq!(
243+ binomial( 1000 , 500 ) ,
244+ ln_binomial( 1000 , 500 ) . exp( ) ,
245+ epsilon = 0.0 ,
246+ max_relative = 1e-11
247+ ) ;
248+ }
249+
161250 #[ test]
162251 fn test_ln_binomial ( ) {
163252 assert_eq ! ( ln_binomial( 1 , 1 ) , 1f64 . ln( ) ) ;
@@ -176,6 +265,36 @@ mod tests {
176265 assert_eq ! ( 35.0 , multinomial( 7 , & [ 3 , 4 ] ) ) ;
177266 }
178267
268+ /// A two-part multinomial is a binomial coefficient; a three-part one has
269+ /// the closed form `C(n, a) * C(n - a, b)`. Both must be exact where
270+ /// representable (mirrors `test_binomial_is_exact_where_representable`).
271+ #[ test]
272+ fn test_multinomial_is_exact_where_representable ( ) {
273+ for n in 0 ..=170u64 {
274+ for k in 0 ..=n / 2 {
275+ assert_eq ! ( multinomial( n, & [ k, n - k] ) , binomial( n, k) , "n={n} k={k}" ) ;
276+ }
277+ }
278+ // 60! / (20!)^3 = 577831214478475823831865900 (fits u128); the
279+ // conversion `u128 -> f64` is correctly rounded:
280+ assert_eq ! (
281+ multinomial( 60 , & [ 20 , 20 , 20 ] ) ,
282+ 577831214478475823831865900_u128 as f64
283+ ) ;
284+ }
285+
286+ #[ test]
287+ fn test_multinomial_log_fallback ( ) {
288+ // 300! / (100!)^3 overflows u128; check against ln-space value
289+ let ln_ref = ln_factorial ( 300 ) - 3.0 * ln_factorial ( 100 ) ;
290+ prec:: assert_relative_eq!(
291+ multinomial( 300 , & [ 100 , 100 , 100 ] ) ,
292+ ln_ref. exp( ) ,
293+ epsilon = 0.0 ,
294+ max_relative = 1e-11
295+ ) ;
296+ }
297+
179298 #[ test]
180299 #[ should_panic]
181300 fn test_multinomial_bad_ni ( ) {
0 commit comments