1
- /// A structure for answering minimum queries on a set of linear functions. Supports two
2
- /// operations: inserting a linear function and querying for minimum at a given point. Unlike the
1
+ /// A structure for answering maximum queries on a set of linear functions. Supports two
2
+ /// operations: inserting a linear function and querying for maximum at a given point. Unlike the
3
3
/// simplest convex hull trick implementation, the queries can be done in any order, and we can do
4
4
/// all the calculations using integers.
5
+ /// https://cp-algorithms.com/geometry/convex_hull_trick.html#li-chao-tree
6
+ /// Compared to the code in the above link, this implementation further improves the algorithm by
7
+ /// reducing the number of nodes to (right - left). This is done by removing the midpoint of a
8
+ /// segment from both children. Even better, this allows the index of a node to just be the
9
+ /// midpoint of the interval!
5
10
6
- /// This data structure builds a static segment tree over the interval of x-coordinates
7
- /// [left,right), so requires O(right-left) memory. Just like normal segment trees, this could be
8
- /// modified to a dynamic tree when the range is huge (it can also be made persistent!).
9
- /// It can also probably be done as an AlCash style iterative segment tree in the static case.
11
+ /// Just like normal segment trees, this could be modified to a dynamic tree when the range is huge
12
+ /// (it can also be made persistent!).
10
13
11
14
pub struct LiChaoTree {
12
15
left : i64 ,
@@ -20,54 +23,60 @@ impl LiChaoTree {
20
23
Self {
21
24
left,
22
25
right,
23
- lines : vec ! [ ( 0 , i64 :: MIN ) ; 4 * ( right - left + 1 ) as usize ] ,
26
+ lines : vec ! [ ( 0 , std :: i64 :: MIN ) ; ( right - left) as usize ] ,
24
27
}
25
28
}
26
29
27
- /// Every node in the tree has the property that the line that minimizes its midpoint is found
30
+ /// Every node in the tree has the property that the line that maximizes its midpoint is found
28
31
/// either in the node or one of its ancestors. When we visit a node, we compute the winner at
29
32
/// the midpoint of the node. The winner is stored in the node. The loser can still possibly
30
33
/// beat the winner on some segment, either to the left or to the right of the current
31
34
/// midpoint, so we propagate it to that segment. This sequence ensures that the invariant is
32
35
/// kept.
33
- fn add_line_impl ( & mut self , mut m : i64 , mut b : i64 , ix : usize , l : i64 , r : i64 ) {
34
- let x = ( l + r) / 2 ;
36
+ fn add_line_impl ( & mut self , mut m : i64 , mut b : i64 , l : i64 , r : i64 ) {
37
+ if r <= l {
38
+ return ;
39
+ }
40
+ let ix = ( ( r - self . left + l - self . left ) / 2 ) as usize ;
41
+ let x = self . left + ( ix as i64 ) ;
35
42
let ( ref mut m_ix, ref mut b_ix) = self . lines [ ix] ;
36
43
if m * x + b > * m_ix * x + * b_ix {
37
44
std:: mem:: swap ( & mut m, m_ix) ;
38
45
std:: mem:: swap ( & mut b, b_ix) ;
39
46
}
40
- if r - l > 1 {
41
- if m < self . lines [ ix] . 0 {
42
- self . add_line_impl ( m, b, 2 * ix, l, x) ;
43
- } else {
44
- self . add_line_impl ( m, b, 2 * ix + 1 , x, r) ;
45
- }
47
+ if m < self . lines [ ix] . 0 {
48
+ self . add_line_impl ( m, b, l, x) ;
49
+ } else {
50
+ self . add_line_impl ( m, b, x + 1 , r) ;
46
51
}
47
52
}
48
53
49
54
/// Adds the line with slope m and intercept b. O(log N) complexity.
50
55
pub fn add_line ( & mut self , m : i64 , b : i64 ) {
51
- self . add_line_impl ( m, b, 1 , self . left , self . right ) ;
56
+ self . add_line_impl ( m, b, self . left , self . right ) ;
52
57
}
53
58
54
59
/// Because of the invariant established by add_line, we know that the best line for a given
55
- /// point is stored in one of the ancestors of its node. So we accumulate the minimum answer as
60
+ /// point is stored in one of the ancestors of its node. So we accumulate the maximum answer as
56
61
/// we go back up the tree.
57
- fn query_impl ( & self , x : i64 , ix : usize , l : i64 , r : i64 ) -> i64 {
62
+ fn query_impl ( & self , x : i64 , l : i64 , r : i64 ) -> i64 {
63
+ if r == l {
64
+ return std:: i64:: MIN ;
65
+ }
66
+ let ix = ( ( r - self . left + l - self . left ) / 2 ) as usize ;
58
67
let y = self . lines [ ix] . 0 * x + self . lines [ ix] . 1 ;
59
68
if r - l == 1 {
60
69
y
61
70
} else if x < ( l + r) / 2 {
62
- self . query_impl ( x, 2 * ix , l, ( l + r ) / 2 ) . max ( y)
71
+ self . query_impl ( x, l, self . left + ix as i64 ) . max ( y)
63
72
} else {
64
- self . query_impl ( x, 2 * ix + 1 , ( l + r ) / 2 , r) . max ( y)
73
+ self . query_impl ( x, self . left + 1 + ix as i64 , r) . max ( y)
65
74
}
66
75
}
67
76
68
- /// Finds the minimum mx+b among all lines in the structure. O(log N) complexity.
77
+ /// Finds the maximum mx+b among all lines in the structure. O(log N) complexity.
69
78
pub fn query ( & self , x : i64 ) -> i64 {
70
- self . query_impl ( x, 1 , self . left , self . right )
79
+ self . query_impl ( x, self . left , self . right )
71
80
}
72
81
}
73
82
@@ -90,7 +99,7 @@ mod test {
90
99
] ;
91
100
let mut li_chao = LiChaoTree :: new ( 0 , 6 ) ;
92
101
93
- assert_eq ! ( li_chao. query( 0 ) , i64 :: MIN ) ;
102
+ assert_eq ! ( li_chao. query( 0 ) , std :: i64 :: MIN ) ;
94
103
for ( & ( slope, intercept) , expected) in lines. iter ( ) . zip ( results. iter ( ) ) {
95
104
li_chao. add_line ( slope, intercept) ;
96
105
let ys: Vec < i64 > = xs. iter ( ) . map ( |& x| li_chao. query ( x) ) . collect ( ) ;
0 commit comments