conjure_cp_core/utils/combinatorics.rs
1use thiserror::Error;
2use ustr::Ustr;
3
4#[derive(Clone, Debug, PartialEq, Eq, Error)]
5pub enum CombinatoricsError {
6 #[error("The operation is not defined for the given input: {0}")]
7 NotDefined(Ustr),
8 #[error("The result is too large to fit into the return type")]
9 Overflow,
10}
11
12impl CombinatoricsError {
13 pub fn not_defined(input: impl Into<Ustr>) -> Self {
14 Self::NotDefined(input.into())
15 }
16}
17
18/// Count *combinations* - the number of ways to pick `n_choose` items from `n_total`,
19/// where order does not matter.
20///
21/// # Formula
22/// C(n, r) = n! / (r! * (n-r)!)
23///
24/// Not defined for r > n.
25pub fn count_combinations(n_total: u64, n_choose: u64) -> Result<u64, CombinatoricsError> {
26 if n_choose > n_total {
27 return Err(CombinatoricsError::not_defined(
28 "n_choose must be <= n_total",
29 ));
30 }
31
32 // Use symmetry C(n, k) == C(n, n-k) to make the loop smaller
33 let k = n_choose.min(n_total - n_choose);
34
35 // Repeatedly multiply / divide as factors get big fast;
36 // return None if we overflow anyway
37 (1u64..=k).try_fold(1u64, |acc, val| {
38 n_total
39 .checked_sub(val)
40 .ok_or(CombinatoricsError::Overflow)? // n_total - val
41 .checked_add(1u64)
42 .ok_or(CombinatoricsError::Overflow)? // + 1
43 .checked_mul(acc)
44 .ok_or(CombinatoricsError::Overflow)? // * acc
45 .checked_div(val)
46 .ok_or(CombinatoricsError::Overflow) // / val
47 })
48}
49
50/// Count *permutations* - the number of ways to pick `n_choose` items from `n_total`,
51/// where order matters.
52///
53/// # Formula
54/// P(n, r) = n! / (n-r)!
55///
56/// Not defined for r > n.
57pub fn count_permutations(n_total: u64, n_choose: u64) -> Result<u64, CombinatoricsError> {
58 if n_choose > n_total {
59 return Err(CombinatoricsError::not_defined(
60 "n_choose must be <= n_total",
61 ));
62 }
63
64 let start = n_total
65 .checked_sub(n_choose)
66 .ok_or(CombinatoricsError::Overflow)?
67 .checked_add(1u64)
68 .ok_or(CombinatoricsError::Overflow)?;
69 (start..=n_total).try_fold(1u64, |acc, val| {
70 acc.checked_mul(val).ok_or(CombinatoricsError::Overflow)
71 })
72}
73
74/// Count *surjections* from an `n`-element set onto a `k`-element set: the Stirling number of the
75/// second kind `S(n, k)`, i.e. the number of ways to partition `n` labelled elements into exactly
76/// `k` non-empty unlabelled blocks (a surjection onto `k` elements is exactly a choice of which
77/// block maps to which target element, and blocks are otherwise interchangeable until that
78/// assignment -- so partitioning first and multiplying by `k!` elsewhere gives the surjection
79/// count; this function returns the partition count alone).
80///
81/// # Formula
82/// `S(n, k) = k * S(n-1, k) + S(n-1, k-1)`, with `S(0, 0) = 1`, `S(n, 0) = 0` for `n > 0`, and
83/// `S(n, k) = 0` for `k > n`.
84pub fn stirling_second_kind(n: u64, k: u64) -> Result<u64, CombinatoricsError> {
85 if k > n {
86 return Ok(0);
87 }
88 // table[i][j] = S(i, j), for i in 0..=n, j in 0..=k.
89 let mut table = vec![vec![0u64; (k + 1) as usize]; (n + 1) as usize];
90 table[0][0] = 1;
91 for i in 1..=n {
92 for j in 1..=k.min(i) {
93 let term1 = j
94 .checked_mul(table[(i - 1) as usize][j as usize])
95 .ok_or(CombinatoricsError::Overflow)?;
96 let term2 = table[(i - 1) as usize][(j - 1) as usize];
97 table[i as usize][j as usize] = term1
98 .checked_add(term2)
99 .ok_or(CombinatoricsError::Overflow)?;
100 }
101 }
102 Ok(table[n as usize][k as usize])
103}
104
105/// Count *derangements* of `n` elements: permutations with no fixed points at all.
106///
107/// # Formula
108/// `D(n) = (n-1) * (D(n-1) + D(n-2))`, with `D(0) = 1`, `D(1) = 0`.
109pub fn derangements(n: u64) -> Result<u64, CombinatoricsError> {
110 if n == 0 {
111 return Ok(1);
112 }
113 let mut prev2 = 1u64; // D(0)
114 let mut prev1 = 0u64; // D(1)
115 if n == 1 {
116 return Ok(prev1);
117 }
118 for i in 2..=n {
119 let sum = prev1
120 .checked_add(prev2)
121 .ok_or(CombinatoricsError::Overflow)?;
122 let current = (i - 1)
123 .checked_mul(sum)
124 .ok_or(CombinatoricsError::Overflow)?;
125 prev2 = prev1;
126 prev1 = current;
127 }
128 Ok(prev1)
129}
130
131/// Count ways to partition `n` labelled elements into exactly `k` unlabelled, non-empty blocks,
132/// each block's size restricted to `[block_min, block_max]`.
133///
134/// Generalises [`stirling_second_kind`] (which is the `block_min = 1, block_max = n` case) to a
135/// bounded block size, needed for a partition domain's own `numParts`/`partSize` attributes.
136///
137/// # Method
138/// Builds each partition by always placing the smallest not-yet-placed element into a fresh
139/// block, then choosing the rest of that block's members from the remaining elements -- this
140/// canonical "root by smallest element" construction counts each unordered partition exactly
141/// once, unlike naively assigning elements to numbered blocks (which overcounts by the blocks'
142/// own arbitrary ordering). Recurrence, with `g(n, k)` counting `n` elements into `k` blocks:
143/// `g(0, 0) = 1`, `g(n, 0) = 0` for `n > 0`, `g(0, k) = 0` for `k > 0`, and for `n, k > 0`:
144/// `g(n, k) = sum_{s=block_min}^{min(block_max, n)} C(n-1, s-1) * g(n-s, k-1)`
145/// (choose `s`, the size of the block containing the smallest remaining element, then its other
146/// `s-1` members from the other `n-1` elements).
147pub fn restricted_partition_count(
148 n: u64,
149 k: u64,
150 block_min: u64,
151 block_max: u64,
152) -> Result<u64, CombinatoricsError> {
153 let block_min = block_min.max(1);
154 if block_max < block_min || k == 0 && n > 0 || k > 0 && n == 0 {
155 return Ok(0);
156 }
157 if n == 0 && k == 0 {
158 return Ok(1);
159 }
160
161 // table[i][j] = g(i, j), for i in 0..=n, j in 0..=k.
162 let mut table = vec![vec![0u64; (k + 1) as usize]; (n + 1) as usize];
163 table[0][0] = 1;
164 for i in 1..=n {
165 for j in 1..=k.min(i) {
166 let mut total = 0u64;
167 let s_max = block_max.min(i);
168 for s in block_min..=s_max {
169 let choose = count_combinations(i - 1, s - 1)?;
170 let rest = table[(i - s) as usize][(j - 1) as usize];
171 let term = choose
172 .checked_mul(rest)
173 .ok_or(CombinatoricsError::Overflow)?;
174 total = total
175 .checked_add(term)
176 .ok_or(CombinatoricsError::Overflow)?;
177 }
178 table[i as usize][j as usize] = total;
179 }
180 }
181 Ok(table[n as usize][k as usize])
182}