Skip to main content

sirius/solver/
count.rs

1//! Polynomial summation.
2
3use super::poly::{
4    Poly,
5    coef::Coef,
6    mono::{Mono, Pow, Var},
7};
8
9/// Closed-form sum of a [`Poly`] over given bounds. [`Count`]s can themselves be summed, since they are represented by a [`Poly`] divided by a constant.
10///
11/// Given the code:
12/// ```text
13/// for i from 0 to N:
14///     for j from 0 to i^2:
15///         yield 1
16/// ```
17///
18/// The inner `for` body `yield`s $1$ time, outer `for` body `yield`s $\sum_{j=0}^{i^2-1} 1 = i^2$ times, and the full program `yield`s $\sum_{i=0}^{N-1} i^2 = N(N-1)(2N - 1)/6$ times.
19///
20/// To count `yield`s in general, we need an algorithm to count lattice points over [`Poly`] bounds. Treating other [`Var`]s as constants, and summing each term independently, the problem reduces to
21/// $\sum_{x=0}^{P-1} x^{n}$ for variable $x$, constant $n$, and polynomial $P$.
22///
23/// We can convert to [falling factorials](https://en.wikipedia.org/wiki/Falling_and_rising_factorials) $x_{(k)} = (x)(x-1)(x-2)\dots(x-k+1)$ using [Stirling numbers of the second kind](https://en.wikipedia.org/wiki/Stirling_numbers_of_the_second_kind) and the identity
24///
25/// $$x^{n} = \sum_{k=0}^n {S(n, k)} x_{(k)}$$
26///
27/// Therefore, $$\sum_{x=0}^{P-1} x^n = \sum_{x=0}^{P-1} \left( \sum_{k=0}^n S(n, k) x_{(k)} \right)$$
28///
29/// After the inner sum is expanded, each term will be a falling factorial that can be evaluated with the discrete power rule
30///
31/// $$\sum_{x=0}^{K-1} x_{(k)} = \frac{K_{(k+1)}}{k+1}$$
32///
33/// For example, in the case above, $P=N$ and $n=2$, so
34/// $$\sum_{x=0}^{N-1} x^2 = \sum_{x=0}^{N-1} \left( \sum_{k=0}^2 \textcolor{blue}{S(2, k)} \textcolor{red}{x_{(k)}} \right)$$
35/// $$= \textcolor{blue}{0}\sum_{x=0}^{N-1} \textcolor{red}{x_{(0)}} + \textcolor{blue}{1}\sum_{x=0}^{N-1} \textcolor{red}{x_{(1)}} + \textcolor{blue}{1}\sum_{x=0}^{N-1} \textcolor{red}{x_{(2)}}$$
36/// $$= \frac{N\_{(2)}}{2} + \frac{N\_{(3)}}{3}$$
37/// $$= \frac{N(N-1)}{2} + \frac{N(N-1)(N-2)}{3}$$
38/// $$= \frac{3(N^2-N) + 2(N^3-3N^2+2N)}{6}$$
39/// $$= \frac{2N^3 - 3N^2 + N}{6}$$
40/// $$= \frac{N(N−1)(2N−1)}{6}$$
41///
42/// This algorithm is implemented in [`Count::sum_below`]:
43///
44/// ```
45/// use sirius::solver::{poly::poly, count::Count};
46///
47/// let i2 = Count::ratio(poly!(i^2), 1);
48/// let sum = i2.sum_below('i', &poly!(N));
49/// assert_eq!(sum, Count::ratio(poly!(2*N^3 - 3*N^2 + N), 6));
50/// ```
51#[derive(Clone, PartialEq, Eq, Hash, Debug)]
52pub struct Count {
53    num: Poly,
54    den: Coef,
55}
56
57impl Count {
58    pub fn zero() -> Self {
59        Self {
60            num: Poly::zero(),
61            den: 1.into(),
62        }
63    }
64
65    pub fn constant<T: Into<Coef>>(c: T) -> Self {
66        Self {
67            num: Poly::constant(c),
68            den: 1.into(),
69        }
70    }
71
72    pub fn var<T: Into<Var>>(v: T, p: Pow) -> Self {
73        Self {
74            num: Poly::var(v, p),
75            den: 1.into(),
76        }
77    }
78
79    pub fn ratio<T: Into<Coef>>(num: Poly, den: T) -> Self {
80        let den = den.into();
81        assert!(!den.is_zero());
82
83        let (num, den) = if !den.is_positive() {
84            (num.mul_scalar(-1), -den)
85        } else {
86            (num, den)
87        };
88        let g = num.coef_gcd().gcd(&den);
89        let num = num.divide_coefs(g).unwrap();
90
91        Self { num, den: den / g }.debug_checked()
92    }
93
94    pub fn num(&self) -> &Poly {
95        &self.num
96    }
97
98    pub fn den(&self) -> Coef {
99        self.den
100    }
101
102    pub fn is_zero(&self) -> bool {
103        self.num.is_zero()
104    }
105
106    pub fn as_poly(&self) -> Option<&Poly> {
107        (self.den == 1.into()).then_some(&self.num)
108    }
109
110    pub fn as_constant(&self) -> Option<(Coef, Coef)> {
111        self.num.as_constant().map(|c| (c, self.den))
112    }
113
114    pub fn add(&self, rhs: &Self) -> Self {
115        let g = self.den.gcd(&rhs.den);
116        let lcm = self.den / g * rhs.den;
117        let num = self
118            .num
119            .mul_scalar(lcm / self.den)
120            .add(&rhs.num.mul_scalar(lcm / rhs.den));
121        Self::ratio(num, lcm).debug_checked()
122    }
123
124    pub fn neg(&self) -> Self {
125        Self {
126            num: self.num.neg(),
127            den: self.den,
128        }
129        .debug_checked()
130    }
131
132    pub fn sub(&self, rhs: &Self) -> Self {
133        self.add(&rhs.neg())
134    }
135
136    pub fn mul(&self, rhs: &Self) -> Self {
137        Self::ratio(self.num.mul(&rhs.num), self.den * rhs.den)
138    }
139
140    pub fn mul_scalar<T: Into<Coef>>(&self, c: T) -> Self {
141        Self::ratio(self.num.mul_scalar(c), self.den)
142    }
143
144    pub fn div_scalar<T: Into<Coef>>(&self, d: T) -> Self {
145        Self::ratio(self.num.clone(), self.den * d.into())
146    }
147
148    pub fn eval(&self, point: impl FnMut(Var) -> i128) -> (Coef, Coef) {
149        let n = self.num.eval(point);
150        let g = n.gcd(&self.den);
151        (n / g, self.den / g)
152    }
153
154    pub fn eval_int(&self, point: impl FnMut(Var) -> i128) -> Option<Coef> {
155        let (num, den) = self.eval(point);
156        if den == 1.into() { Some(num) } else { None }
157    }
158
159    pub fn substitute(&self, v: Var, q: &Poly) -> Self {
160        Self::ratio(self.num.substitute(v, q), self.den)
161    }
162
163    pub fn assert_canonical(&self) {
164        self.num.assert_canonical();
165        assert!(
166            self.den.is_positive(),
167            "denominator {:?} not positive",
168            self.den
169        );
170        assert_eq!(
171            self.num.coef_gcd().gcd(&self.den),
172            1.into(),
173            "fraction not reduced: content {:?} vs den {:?}",
174            self.num.coef_gcd(),
175            self.den
176        );
177    }
178
179    fn debug_checked(self) -> Self {
180        #[cfg(debug_assertions)]
181        self.assert_canonical();
182        self
183    }
184
185    pub fn sum_below<T: Into<Var>>(&self, v: T, hi: &Poly) -> Self {
186        let v = v.into();
187        assert_eq!(hi.degree_in(v), 0);
188        if self.is_zero() {
189            return Self::zero();
190        }
191
192        // a summand of degree 0 in v still sums to (summand * hi), handled by the k = 0 group
193        let kmax = self.num().degree_in(v) as usize;
194
195        let mut groups: Vec<Vec<(Coef, Mono)>> = vec![Vec::new(); kmax + 1];
196        for (c, m) in self.num().terms().iter().rev() {
197            let k = m.degree_in(v) as usize;
198            let rest = Mono::new(m.exps().iter().copied().filter(|&(vi, _)| vi != v));
199            groups[k].push((*c, rest));
200        }
201
202        let mut falling_factorials: Vec<Poly> = Vec::with_capacity(kmax + 1);
203        falling_factorials.push(hi.clone());
204        for j in 1..=kmax {
205            let factor = hi.sub(&Poly::constant(Coef::from_size(j)));
206            falling_factorials.push(falling_factorials[j - 1].mul(&factor));
207        }
208
209        let s2 = stirling2(kmax);
210        let mut total = Self::zero();
211        for (k, group) in groups.into_iter().enumerate() {
212            let c_k = Poly::from_terms(group);
213            if c_k.is_zero() {
214                continue;
215            }
216
217            let mut a_k = Self::zero();
218            for (j, &s) in s2[k].iter().enumerate() {
219                if !s.is_zero() {
220                    a_k = a_k.add(&Self::ratio(
221                        falling_factorials[j].mul_scalar(s),
222                        Coef::from_size(j + 1),
223                    ));
224                }
225            }
226            total = total.add(&Self::ratio(c_k, 1).mul(&a_k));
227        }
228        total.div_scalar(self.den())
229    }
230
231    pub fn sum_range(&self, var: Var, lo: &Poly, hi: &Poly) -> Self {
232        self.sum_below(var, hi).sub(&self.sum_below(var, lo))
233    }
234}
235
236fn stirling2(kmax: usize) -> Vec<Vec<Coef>> {
237    let mut s2: Vec<Vec<Coef>> = Vec::with_capacity(kmax + 1);
238    s2.push(vec![Coef::new(1)]);
239    for k in 1..=kmax {
240        let mut row = Vec::with_capacity(k + 1);
241        row.push(Coef::new(0));
242        for j in 1..=k {
243            let above = if j < k { s2[k - 1][j] } else { Coef::new(0) };
244            let diag = s2[k - 1][j - 1];
245            row.push((Coef::from_size(j) * above) + diag);
246        }
247        s2.push(row);
248    }
249    s2
250}
251
252#[cfg(test)]
253mod tests {
254    use super::{Count, Var, stirling2};
255    use crate::solver::poly::{coef::Coef, poly};
256
257    #[test]
258    fn ratio_reduce() {
259        let half_n = Count::ratio(poly!(2 * x), 4);
260        assert_eq!(half_n.num(), &poly!(x));
261        assert_eq!(half_n.den(), 2.into());
262        assert_eq!(
263            Count::ratio(poly!(2 * x + 2), 2),
264            Count::ratio(poly!(x + 1), 1)
265        );
266        assert_eq!(
267            Count::ratio(poly!(2 * x), -1),
268            Count::ratio(poly!(-2 * x), 1)
269        );
270    }
271
272    #[test]
273    fn arithmetic_sanity() {
274        // N/2 * N/3 = N^2/6; N/2 − N/2 = 0
275        let (a, b) = (Count::ratio(poly!(x), 2), Count::ratio(poly!(x), 3));
276        assert_eq!(a.add(&b), Count::ratio(poly!(5 * x), 6));
277        assert_eq!(a.sub(&a), Count::ratio(poly!(), 1))
278    }
279
280    #[test]
281    fn stirling_triangle() {
282        // OEIS A008277
283        assert_eq!(
284            stirling2(5),
285            vec![
286                Coef::vec(&[1]),
287                Coef::vec(&[0, 1]),
288                Coef::vec(&[0, 1, 1]),
289                Coef::vec(&[0, 1, 3, 1]),
290                Coef::vec(&[0, 1, 7, 6, 1]),
291                Coef::vec(&[0, 1, 15, 25, 10, 1]),
292            ]
293        );
294    }
295
296    #[test]
297    fn constant_summand() {
298        // Σ_{0<=i<B} 1 = B
299        let one = Count::ratio(poly!(1), 1);
300        assert_eq!(one.sum_below('i', &poly!(b)), Count::ratio(poly!(b), 1));
301
302        // Σ_{2<=i<B} 3 = 3B − 6
303        let three = Count::ratio(poly!(3), 1);
304        assert_eq!(
305            three.sum_range('i' as Var, &poly!(2), &poly!(b)),
306            Count::ratio(poly!(3 * b - 6), 1)
307        );
308    }
309
310    #[test]
311    fn faulhaber() {
312        // Σ_{0<=i<B} i^3 = B^2(B−1)^2/4
313        let cube = Count::ratio(poly!(i ^ 3), 1);
314        let s = cube.sum_below('i', &poly!(b));
315        let b2 = poly!(b ^ 2);
316        let bm1 = poly!(b - 1);
317        assert_eq!(s, Count::ratio(b2.mul(&bm1.pow(2)), 4));
318
319        // (Σ i)^2 = Σ i^3
320        let triangle = Count::ratio(poly!(i), 1).sum_below('i', &poly!(b));
321        assert_eq!(triangle.mul(&triangle), s);
322    }
323}