Skip to content

Commit bf762c9

Browse files
committed
Remove merge_threshold parameter and add proof
1 parent 5ca0294 commit bf762c9

File tree

1 file changed

+28
-29
lines changed

1 file changed

+28
-29
lines changed

src/order.rs

Lines changed: 28 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,13 @@ pub fn slice_upper_bound<T: PartialOrd>(slice: &[T], key: &T) -> usize {
2121
.unwrap_err()
2222
}
2323

24-
/// Merge two sorted and totally ordered collections into one
24+
/// Stably merges two sorted and totally ordered collections into one
2525
pub fn merge_sorted<T: PartialOrd>(
2626
i1: impl IntoIterator<Item = T>,
2727
i2: impl IntoIterator<Item = T>,
2828
) -> Vec<T> {
29-
let (mut i1, mut i2) = (i1.into_iter().peekable(), i2.into_iter().peekable());
29+
let mut i1 = i1.into_iter().peekable();
30+
let mut i2 = i2.into_iter().peekable();
3031
let mut merged = Vec::with_capacity(i1.size_hint().0 + i2.size_hint().0);
3132
while let (Some(a), Some(b)) = (i1.peek(), i2.peek()) {
3233
merged.push(if a <= b { i1.next() } else { i2.next() }.unwrap());
@@ -51,15 +52,15 @@ pub struct SparseIndex {
5152
}
5253

5354
impl SparseIndex {
54-
/// Build an index, given the full set of coordinates to compress.
55+
/// Builds an index, given the full set of coordinates to compress.
5556
pub fn new(mut coords: Vec<i64>) -> Self {
5657
coords.sort_unstable();
5758
coords.dedup();
5859
Self { coords }
5960
}
6061

61-
/// Return Ok(i) if the coordinate q appears at index i
62-
/// Return Err(i) if q appears between indices i-1 and i
62+
/// Returns Ok(i) if the coordinate q appears at index i
63+
/// Returns Err(i) if q appears between indices i-1 and i
6364
pub fn compress(&self, q: i64) -> Result<usize, usize> {
6465
self.coords.binary_search(&q)
6566
}
@@ -68,28 +69,26 @@ impl SparseIndex {
6869
/// Represents a maximum (upper envelope) of a collection of linear functions of one
6970
/// variable, evaluated using an online version of the convex hull trick.
7071
/// It combines the offline algorithm with square root decomposition, resulting in an
71-
/// asymptotically suboptimal but simple algorithm with good amortized performnce:
72-
/// For N inserts interleaved with Q queries, a threshold of N/sqrt(Q) yields
73-
/// O(N sqrt Q + Q log N) time complexity. If all queries come after all inserts,
74-
/// any threshold less than N (e.g., 0) yields O(N + Q log N) time complexity.
72+
/// asymptotically suboptimal but simple algorithm with good amortized performance:
73+
/// N inserts interleaved with Q queries yields O(N sqrt Q + Q log N) time complexity
74+
/// in general, or O(N + Q log N) if all queries come after all inserts.
75+
// Proof: the Q log N term comes from calls to slice_lower_bound(). As for the N sqrt Q,
76+
// note that between successive times when the hull is rebuilt, O(N) work is done,
77+
// and the running totals of insertions and queries satisfy del_N (del_Q + 1) > N.
78+
// Now, either del_Q >= sqrt Q, or else del_Q <= 2 sqrt Q - 1
79+
// => del_N > N / (2 sqrt Q).
80+
// Since del(N sqrt Q) >= max(N del(sqrt Q), del_N sqrt Q)
81+
// >= max(N del_Q / (2 sqrt Q), del_N sqrt Q),
82+
// we conclude that del(N sqrt Q) >= N / 2.
83+
#[derive(Default)]
7584
pub struct PiecewiseLinearConvexFn {
7685
recent_lines: Vec<(f64, f64)>,
7786
sorted_lines: Vec<(f64, f64)>,
7887
intersections: Vec<f64>,
79-
merge_threshold: usize,
88+
amortized_work: usize,
8089
}
8190

8291
impl PiecewiseLinearConvexFn {
83-
/// Initializes with a given threshold for re-running the convex hull algorithm
84-
pub fn with_merge_threshold(merge_threshold: usize) -> Self {
85-
Self {
86-
recent_lines: vec![],
87-
sorted_lines: vec![],
88-
intersections: vec![],
89-
merge_threshold,
90-
}
91-
}
92-
9392
/// Replaces the represented function with the maximum of itself and a provided line
9493
pub fn max_with(&mut self, new_m: f64, new_b: f64) {
9594
self.recent_lines.push((new_m, new_b));
@@ -125,7 +124,9 @@ impl PiecewiseLinearConvexFn {
125124

126125
/// Evaluates the function at x with good amortized runtime
127126
pub fn evaluate(&mut self, x: f64) -> f64 {
128-
if self.recent_lines.len() > self.merge_threshold {
127+
self.amortized_work += self.recent_lines.len();
128+
if self.amortized_work > self.sorted_lines.len() {
129+
self.amortized_work = 0;
129130
self.recent_lines.sort_unstable_by(asserting_cmp);
130131
self.intersections.clear();
131132
let all_lines = merge_sorted(self.recent_lines.drain(..), self.sorted_lines.drain(..));
@@ -212,14 +213,12 @@ mod test {
212213
[1, -1, -2, -3, -3, -3],
213214
[1, -1, -2, -1, 0, 1],
214215
];
215-
for threshold in 0..=lines.len() {
216-
let mut func = PiecewiseLinearConvexFn::with_merge_threshold(threshold);
217-
assert_eq!(func.evaluate(0.0), -1e18);
218-
for (&(slope, intercept), expected) in lines.iter().zip(results.iter()) {
219-
func.max_with(slope as f64, intercept as f64);
220-
let ys: Vec<i64> = xs.iter().map(|&x| func.evaluate(x as f64) as i64).collect();
221-
assert_eq!(expected, &ys[..]);
222-
}
216+
let mut func = PiecewiseLinearConvexFn::default();
217+
assert_eq!(func.evaluate(0.0), -1e18);
218+
for (&(slope, intercept), expected) in lines.iter().zip(results.iter()) {
219+
func.max_with(slope as f64, intercept as f64);
220+
let ys: Vec<i64> = xs.iter().map(|&x| func.evaluate(x as f64) as i64).collect();
221+
assert_eq!(expected, &ys[..]);
223222
}
224223
}
225224
}

0 commit comments

Comments
 (0)