Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Count combinations with replacement #737

35 changes: 34 additions & 1 deletion src/combinations_with_replacement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::fmt;
use std::iter::FusedIterator;

use super::lazy_buffer::LazyBuffer;
use crate::combinations::checked_binomial;

/// An iterator to iterate through all the `n`-length combinations in an iterator, with replacement.
///
Expand All @@ -24,7 +25,7 @@ where
I: Iterator + fmt::Debug,
I::Item: fmt::Debug + Clone,
{
debug_fmt_fields!(Combinations, indices, pool, first);
debug_fmt_fields!(CombinationsWithReplacement, indices, pool, first);
Philippe-Cholet marked this conversation as resolved.
Show resolved Hide resolved
}

impl<I> CombinationsWithReplacement<I>
Expand Down Expand Up @@ -100,10 +101,42 @@ where
None => None,
}
}

fn size_hint(&self) -> (usize, Option<usize>) {
let (mut low, mut upp) = self.pool.size_hint();
low = remaining_for(low, self.first, &self.indices).unwrap_or(usize::MAX);
upp = upp.and_then(|upp| remaining_for(upp, self.first, &self.indices));
(low, upp)
}
Philippe-Cholet marked this conversation as resolved.
Show resolved Hide resolved

fn count(self) -> usize {
let Self { indices, pool, first } = self;
let n = pool.count();
remaining_for(n, first, &indices).unwrap()
}
Philippe-Cholet marked this conversation as resolved.
Show resolved Hide resolved
}

impl<I> FusedIterator for CombinationsWithReplacement<I>
where
I: Iterator,
I::Item: Clone,
{}

/// For a given size `n`, return the count of remaining combinations with replacement or None if it would overflow.
fn remaining_for(n: usize, first: bool, indices: &[usize]) -> Option<usize> {
let count = |n: usize, k: usize| checked_binomial((n + k).saturating_sub(1), k);
let k = indices.len();
if first {
count(n, k)
} else {
indices
.iter()
.enumerate()
// TODO: Once the MSRV hits 1.37.0, we can sum options instead:
// .map(|(i, n0)| count(n - 1 - *n0, k - i))
// .sum()
.fold(Some(0), |sum, (i, n0)| {
sum.and_then(|s| s.checked_add(count(n - 1 - *n0, k - i)?))
})
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe you that this is correct, but I have a hard time comprehending this without comments. Is it the "stars and sticks" thing that leads to binomial(n+k-1, k)?

Could you comment the loop similar to what we did in the other cases?

In addition: Could n+k overflow? And if so, shouldn't we return None here instead of panicking?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I chose the expression "stars and bars" from wikipedia, expression I did not know. I added a comment.
Good catch n+k overflowing, handled.
And I copied/updated the previous comment about the loop.

}
22 changes: 22 additions & 0 deletions tests/test_std.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1019,6 +1019,28 @@ fn combinations_with_replacement() {
);
}

#[test]
fn combinations_with_replacement_range_count() {
for n in 0..=7 {
for k in 0..=7 {
let len = binomial(usize::saturating_sub(n + k, 1), k);
let mut it = (0..n).combinations_with_replacement(k);
assert_eq!(len, it.clone().count());
assert_eq!(len, it.size_hint().0);
assert_eq!(Some(len), it.size_hint().1);
for count in (0..len).rev() {
let elem = it.next();
assert!(elem.is_some());
assert_eq!(count, it.clone().count());
assert_eq!(count, it.size_hint().0);
assert_eq!(Some(count), it.size_hint().1);
}
let should_be_none = it.next();
assert!(should_be_none.is_none());
}
}
}

#[test]
fn powerset() {
it::assert_equal((0..0).powerset(), vec![vec![]]);
Expand Down