-
Notifications
You must be signed in to change notification settings - Fork 231
Li chao #16
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
Li chao #16
Changes from 1 commit
fdee9b6
22453cd
03068a3
2a86a88
607553e
9ad439d
c37757c
ab7d29d
9b234c6
be2aad5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
This passes the same tests as in the sqrt decomposition implementation, but hasn't been stress tested.
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
/// A structure for answering minimum queries on a set of linear functions. Supports two | ||
/// operations: inserting a linear function and querying for minimum at a given point. Unlike the | ||
/// simplest convex hull trick implementation, the queries can be done in any order, and we can do | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm curious, what's the simple implementation? I don't think the one in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. By the way, it's not necessary for the nodes to be consecutive integers is it? As long as we know all the query locations ( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The simplest version doesn't do any sqrt decomposition or anything like that, it just adds lines in increasing-slope order. I'm not as familiar with your algorithm, but I can change the wording here. |
||
/// all the calculations using integers. | ||
|
||
/// This data structure builds a static segment tree over the interval of x-coordinates | ||
/// [left,right), so requires O(right-left) memory. Just like normal segment trees, this could be | ||
/// modified to a dynamic tree when the range is huge (it can also be made persistent!). | ||
/// It can also probably be done as an AlCash style iterative segment tree in the static case. | ||
|
||
pub struct LiChaoTree { | ||
left: i64, | ||
right: i64, | ||
lines: Vec<(i64, i64)>, | ||
} | ||
|
||
impl LiChaoTree { | ||
/// Creates a new tree, built to handle queries on the interval [left, right). | ||
pub fn new(left: i64, right: i64) -> Self { | ||
Self { | ||
left, | ||
right, | ||
lines: vec![(0, i64::MAX); 4 * (right - left + 1) as usize], | ||
} | ||
} | ||
|
||
/// Every node in the tree has the property that the line that minimizes its midpoint is found | ||
/// either in the node or one of its ancestors. When we visit a node, we compute the winner at | ||
/// the midpoint of the node. The winner is stored in the node. The loser can still possibly | ||
/// beat the winner on some segment, either to the left or to the right of the current | ||
/// midpoint, so we propagate it to that segment. This sequence ensures that the invariant is | ||
/// kept. | ||
fn add_line_impl(&mut self, mut m: i64, mut b: i64, ix: usize, l: i64, r: i64) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would involve some funky transformations, but I believe it's possible to use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, better yet, is it possible to just use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not sure what you mean about including the middle node. Is there a way to get rid of some of the nodes? I think they are all needed. The standard bound for segment tree nodes is 4N; if N is a power of 2 it's 2N but I think we need around 4N if N is slightly higher than a power of 2. That's one of the reasons the AlCash style is more efficient. That one has a bound of 2N regardless of N. But I don't know if it's easy to convert Li Chao trees to that style - they are inherently top-down, moreso than normal segment trees. Converting to use the ArqView interface would be an interesting puzzle... I wonder if it can be done while keeping time/space complexity. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Space optimization added in latest update. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yay! |
||
let x = (l + r) / 2; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm I think you could do something like: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. Would something like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you would have a typing error there because a reference to a pair is not the same as a pair of references. They have to be converted. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Whoa never mind you're absolutely right! I was just confused because although |
||
if m * x + b < self.lines[ix].0 * x + self.lines[ix].1 { | ||
std::mem::swap(&mut m, &mut self.lines[ix].0); | ||
std::mem::swap(&mut b, &mut self.lines[ix].1); | ||
} | ||
if r - l > 1 { | ||
if m < self.lines[ix].0 { | ||
self.add_line_impl(m, b, 2 * ix + 1, x, r); | ||
Fr0benius marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} else { | ||
self.add_line_impl(m, b, 2 * ix, l, x); | ||
} | ||
} | ||
} | ||
|
||
/// Adds the line with slope m and intercept b. O(log N) complexity. | ||
pub fn add_line(&mut self, m: i64, b: i64) { | ||
self.add_line_impl(m, b, 1, self.left, self.right); | ||
} | ||
|
||
/// Because of the invariant established by add_line, we know that the best line for a given | ||
/// point is stored in one of the ancestors of its node. So we accumulate the minimum answer as | ||
/// we go back up the tree. | ||
fn query_impl(&self, x: i64, ix: usize, l: i64, r: i64) -> i64 { | ||
let y = self.lines[ix].0 * x + self.lines[ix].1; | ||
if r - l == 1 { | ||
y | ||
} else if x >= (l + r) / 2 { | ||
Fr0benius marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self.query_impl(x, 2 * ix + 1, (l + r) / 2, r).min(y) | ||
} else { | ||
self.query_impl(x, 2 * ix, l, (l + r) / 2).min(y) | ||
} | ||
} | ||
|
||
/// Finds the minimum mx+b among all lines in the structure. O(log N) complexity. | ||
pub fn query(&self, x: i64) -> i64 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be nice to name the methods consistently with their equivalents in We can discuss what these names should be; back then I settled on |
||
self.query_impl(x, 1, self.left, self.right) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod test { | ||
use super::*; | ||
|
||
#[test] | ||
fn test_li_chao_tree() { | ||
let lines = [(0, 3), (1, 0), (-1, 8), (2, -1), (-1, 4)]; | ||
let xs = [0, 1, 2, 3, 4, 5]; | ||
// results[i] consists of the expected y-coordinates after processing | ||
// the first i+1 lines. | ||
let results = [ | ||
[3, 3, 3, 3, 3, 3], | ||
[0, 1, 2, 3, 3, 3], | ||
[0, 1, 2, 3, 3, 3], | ||
[-1, 1, 2, 3, 3, 3], | ||
[-1, 1, 2, 1, 0, -1], | ||
]; | ||
let mut li_chao = LiChaoTree::new(0, 6); | ||
|
||
assert_eq!(li_chao.query(0), i64::MAX); | ||
for (&(slope, intercept), expected) in lines.iter().zip(results.iter()) { | ||
li_chao.add_line(slope, intercept); | ||
let ys: Vec<i64> = xs.iter().map(|&x| li_chao.query(x)).collect(); | ||
assert_eq!(expected, &ys[..]); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this work without the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, it can't compare an array and a vector if you don't convert to a slice first. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh I see, it's the same as https://doc.rust-lang.org/std/vec/struct.Vec.html#method.as_slice |
||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,3 +8,4 @@ pub mod range_query; | |
pub mod rng; | ||
pub mod scanner; | ||
pub mod string_proc; | ||
pub mod li_chao; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note: our other convex hull trick now searches for the max
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I will flip everything to keep things consistent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks