Skip to main content

sim_lib_discrete_comb/
count.rs

1//! Exact combinatorial counting functions over `num_bigint::BigUint`.
2
3use num_bigint::BigUint;
4
5use crate::error::CombError;
6
7/// Largest `n` accepted by [`factorial_checked`] before [`CombError::LimitExceeded`].
8///
9/// `factorial` folds an unbounded range of growing big-integer products; an
10/// uncapped value parsed from untrusted text would hang or exhaust memory.
11pub const MAX_FACTORIAL_INPUT: u64 = 50_000;
12
13/// Largest `n` accepted by [`integer_partition_count_checked`] before
14/// [`CombError::LimitExceeded`].
15///
16/// `integer_partition_count` allocates `vec![..; n + 1]` and runs an `O(n^2)`
17/// big-integer recurrence, so an uncapped value is an out-of-memory / hang risk.
18pub const MAX_PARTITION_INPUT: u64 = 10_000;
19
20fn big(n: u64) -> BigUint {
21    BigUint::from(n)
22}
23
24/// `n!` (with `factorial(0) == 1`).
25pub fn factorial(n: u64) -> BigUint {
26    (1..=n).fold(BigUint::from(1u32), |acc, i| acc * big(i))
27}
28
29/// `n!` with an explicit input ceiling ([`MAX_FACTORIAL_INPUT`]).
30///
31/// Returns [`CombError::LimitExceeded`] for `n` beyond the cap instead of
32/// folding an unbounded range from untrusted input.
33pub fn factorial_checked(n: u64) -> Result<BigUint, CombError> {
34    if n > MAX_FACTORIAL_INPUT {
35        return Err(CombError::LimitExceeded(format!(
36            "factorial input {n} exceeds maximum {MAX_FACTORIAL_INPUT}"
37        )));
38    }
39    Ok(factorial(n))
40}
41
42/// The falling factorial `n * (n-1) * ... * (n-k+1)` (`0` when `k > n`).
43pub fn falling_factorial(n: u64, k: u64) -> BigUint {
44    if k > n {
45        return BigUint::from(0u32);
46    }
47    (0..k).fold(BigUint::from(1u32), |acc, i| acc * big(n - i))
48}
49
50/// The number of `k`-permutations of `n`, `nPk` (`0` when `k > n`).
51pub fn permutation_count(n: u64, k: u64) -> BigUint {
52    falling_factorial(n, k)
53}
54
55/// `n choose k` via the multiplicative formula (`0` when `k > n`).
56///
57/// # Examples
58///
59/// ```
60/// use num_bigint::BigUint;
61/// use sim_lib_discrete_comb::binomial;
62///
63/// assert_eq!(binomial(5, 2), BigUint::from(10u32));
64/// assert_eq!(binomial(5, 0), BigUint::from(1u32)); // empty choice
65/// assert_eq!(binomial(2, 5), BigUint::from(0u32)); // k > n
66/// ```
67pub fn binomial(n: u64, k: u64) -> BigUint {
68    if k > n {
69        return BigUint::from(0u32);
70    }
71    let k = k.min(n - k);
72    let mut result = BigUint::from(1u32);
73    for i in 1..=k {
74        // result is always divisible by i at this step, so division is exact.
75        result = result * big(n - k + i) / big(i);
76    }
77    result
78}
79
80/// The multinomial coefficient `(sum parts)! / prod(part!)`.
81pub fn multinomial(parts: &[u64]) -> BigUint {
82    let total: u64 = parts.iter().sum();
83    let mut denom = BigUint::from(1u32);
84    for &p in parts {
85        denom *= factorial(p);
86    }
87    factorial(total) / denom
88}
89
90/// Stirling numbers of the second kind `S(n, k)`: partitions of an `n`-set into
91/// `k` non-empty unlabeled blocks.
92pub fn stirling2(n: u64, k: u64) -> BigUint {
93    let (n, k) = (n as usize, k as usize);
94    let mut dp = vec![BigUint::from(0u32); k + 1];
95    dp[0] = BigUint::from(1u32); // S(0,0) = 1
96    for _ in 1..=n {
97        let mut next = vec![BigUint::from(0u32); k + 1];
98        for j in 1..=k {
99            next[j] = big(j as u64) * &dp[j] + &dp[j - 1];
100        }
101        dp = next;
102    }
103    if k < dp.len() {
104        dp[k].clone()
105    } else {
106        BigUint::from(0u32)
107    }
108}
109
110/// The Bell number `B(n) = sum_k S(n, k)`: total partitions of an `n`-set.
111pub fn bell_number(n: u64) -> BigUint {
112    (0..=n).map(|k| stirling2(n, k)).sum()
113}
114
115/// The partition count `p(n)`: ways to write `n` as a sum of positive integers,
116/// order ignored.
117pub fn integer_partition_count(n: u64) -> BigUint {
118    let n = n as usize;
119    let mut dp = vec![BigUint::from(0u32); n + 1];
120    dp[0] = BigUint::from(1u32);
121    for part in 1..=n {
122        for j in part..=n {
123            dp[j] = &dp[j] + &dp[j - part].clone();
124        }
125    }
126    dp[n].clone()
127}
128
129/// `p(n)` with an explicit input ceiling ([`MAX_PARTITION_INPUT`]).
130///
131/// Returns [`CombError::LimitExceeded`] for `n` beyond the cap, so an untrusted
132/// `n` cannot drive an unbounded allocation or `O(n^2)` recurrence.
133pub fn integer_partition_count_checked(n: u64) -> Result<BigUint, CombError> {
134    if n > MAX_PARTITION_INPUT {
135        return Err(CombError::LimitExceeded(format!(
136            "partition-count input {n} exceeds maximum {MAX_PARTITION_INPUT}"
137        )));
138    }
139    Ok(integer_partition_count(n))
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    fn b(n: u64) -> BigUint {
147        BigUint::from(n)
148    }
149
150    #[test]
151    fn factorial_basics() {
152        assert_eq!(factorial(0), b(1));
153        assert_eq!(factorial(5), b(120));
154    }
155
156    #[test]
157    fn binomial_and_permutations() {
158        assert_eq!(binomial(5, 2), b(10));
159        assert_eq!(binomial(10, 5), b(252));
160        assert_eq!(binomial(5, 7), b(0));
161        assert_eq!(binomial(6, 0), b(1));
162        assert_eq!(permutation_count(5, 3), b(60));
163        assert_eq!(falling_factorial(5, 2), b(20));
164    }
165
166    #[test]
167    fn multinomial_value() {
168        // 4! / (2! 1! 1!) = 12
169        assert_eq!(multinomial(&[2, 1, 1]), b(12));
170    }
171
172    #[test]
173    fn stirling_bell_and_partitions() {
174        assert_eq!(stirling2(4, 2), b(7));
175        assert_eq!(bell_number(4), b(15));
176        assert_eq!(integer_partition_count(5), b(7));
177    }
178
179    #[test]
180    fn checked_counts_accept_small_inputs() {
181        assert_eq!(factorial_checked(5).unwrap(), b(120));
182        assert_eq!(integer_partition_count_checked(5).unwrap(), b(7));
183    }
184
185    #[test]
186    fn checked_counts_reject_huge_inputs() {
187        assert!(matches!(
188            factorial_checked(MAX_FACTORIAL_INPUT + 1),
189            Err(CombError::LimitExceeded(_))
190        ));
191        assert!(matches!(
192            integer_partition_count_checked(u64::MAX),
193            Err(CombError::LimitExceeded(_))
194        ));
195    }
196}