@@ -21,12 +21,13 @@ pub fn slice_upper_bound<T: PartialOrd>(slice: &[T], key: &T) -> usize {
21
21
. unwrap_err ( )
22
22
}
23
23
24
- /// Merge two sorted and totally ordered collections into one
24
+ /// Stably merges two sorted and totally ordered collections into one
25
25
pub fn merge_sorted < T : PartialOrd > (
26
26
i1 : impl IntoIterator < Item = T > ,
27
27
i2 : impl IntoIterator < Item = T > ,
28
28
) -> 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 ( ) ;
30
31
let mut merged = Vec :: with_capacity ( i1. size_hint ( ) . 0 + i2. size_hint ( ) . 0 ) ;
31
32
while let ( Some ( a) , Some ( b) ) = ( i1. peek ( ) , i2. peek ( ) ) {
32
33
merged. push ( if a <= b { i1. next ( ) } else { i2. next ( ) } . unwrap ( ) ) ;
@@ -51,15 +52,15 @@ pub struct SparseIndex {
51
52
}
52
53
53
54
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.
55
56
pub fn new ( mut coords : Vec < i64 > ) -> Self {
56
57
coords. sort_unstable ( ) ;
57
58
coords. dedup ( ) ;
58
59
Self { coords }
59
60
}
60
61
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
63
64
pub fn compress ( & self , q : i64 ) -> Result < usize , usize > {
64
65
self . coords . binary_search ( & q)
65
66
}
@@ -68,28 +69,26 @@ impl SparseIndex {
68
69
/// Represents a maximum (upper envelope) of a collection of linear functions of one
69
70
/// variable, evaluated using an online version of the convex hull trick.
70
71
/// 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 ) ]
75
84
pub struct PiecewiseLinearConvexFn {
76
85
recent_lines : Vec < ( f64 , f64 ) > ,
77
86
sorted_lines : Vec < ( f64 , f64 ) > ,
78
87
intersections : Vec < f64 > ,
79
- merge_threshold : usize ,
88
+ amortized_work : usize ,
80
89
}
81
90
82
91
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
-
93
92
/// Replaces the represented function with the maximum of itself and a provided line
94
93
pub fn max_with ( & mut self , new_m : f64 , new_b : f64 ) {
95
94
self . recent_lines . push ( ( new_m, new_b) ) ;
@@ -125,7 +124,9 @@ impl PiecewiseLinearConvexFn {
125
124
126
125
/// Evaluates the function at x with good amortized runtime
127
126
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 ;
129
130
self . recent_lines . sort_unstable_by ( asserting_cmp) ;
130
131
self . intersections . clear ( ) ;
131
132
let all_lines = merge_sorted ( self . recent_lines . drain ( ..) , self . sorted_lines . drain ( ..) ) ;
@@ -212,14 +213,12 @@ mod test {
212
213
[ 1 , -1 , -2 , -3 , -3 , -3 ] ,
213
214
[ 1 , -1 , -2 , -1 , 0 , 1 ] ,
214
215
] ;
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[ ..] ) ;
223
222
}
224
223
}
225
224
}
0 commit comments