李超线段树 (Li-Chao Segment Tree)
李超线段树,顾名思义,就是线段树的一个变种。说来惭愧,我在ACM生涯第二年才知道这么个东西的存在,所以赶紧写博客交学费。
李超线段树是一种用于维护平面直角坐标系内线段关系的数据结构。它常被用来处理这样一种形式的问题:给定一个平面直角坐标系,支持动态插入一条线段,询问从某一个位置 向下看能看到的最高的一条线段(也就是给一条竖线,问这条竖线与所有线段的最高的交点。
如上图,有三条线段,两条红色竖线代表两个询问,则点 与点 就是询问到的答案。
李超线段树的核心是维护每个区间的“最优势线段”,即在每个区间的中点处最高的线段。询问时我们可以对所有包含横坐标为 的位置的区间上的最优势线段计算答案,最后取个 。
其实这就相当于维护一个记录当前区间最高线段的,不下传标记的线段树。(显然如果我们访问到的区间内只包含询问的横坐标,那么这个区间的最优势线段就是答案线段,所以这样统计包含答案是能保证正确性的)
如上图,对于区间[0,8][0,8],绿色线段是“最优势线段”
对于修改,我们先把线段的值域分割到线段树的区间上,每次访问一个完整的包含在线段值域中的区间时:
1. 若当前区间还没有记录最优势线段,则记录最优势线段并返回。
2. 若当前区间的最优势线段被插入的线段完全覆盖,则把最优势线段修改为被插入线段并返回。
3. 若当前区间的最优势线段把被插入线断完全覆盖,则直接返回。
4. 若当前区间最优势线段与被插入线段有交,则先判断哪条线段在当前区间更优,并把更劣的线段下传到交点所在子区间。(交点两边的部分被这两条线段分别控制,而我们已经让在中点更优的那条线段作为区间最优势线段,因此更劣的那条线段只有可能在交点所在子区间超过当前区间的最优势线段)
下面一起来剖析一下代码。
线段树主体部分:(#define lson curpos<<1 #define rson curpos<<1|1 代码里忘记写了)
1 const int maxn = 1e5 + 10; 2 //思来想去,李超线段树的节点名字还是叫Interval而不是Line 3 //因为这棵线段树以区间为核心,维护的是每一个区间的最优势线段 4 //写得很长是为了方便初学者理解 5 struct Interval { 6 double k, b; //区间最优势线段的k,b 7 int l, r, flag; //l,r为区间。flag为该区间已经记录最优势线段标记。 8 Interval() {} 9 Interval(int a, int b, double c, double d) { 10 this->l = a, this->r = b, this->k = c, this->b = d; 11 } 12 double calc(const int pos)const { //返回线段在x处的y值 13 return k * pos + b; 14 } 15 int cross(const Interval &rhs)const { //求两线段交点的横坐标 16 return floor((b - rhs.b) / (rhs.k - k)); 17 } 18 } segt[maxn << 2];
建树:
1 //因为李超线段树以区间为核心,所以建树时main函数调用build(xOy平面最左端,xOy平面最右端,1); 2 void build(int curpos, int curl, int curr) { 3 segt[curpos].k = segt[curpos].b = 0; 4 segt[curpos].l = 1; segt[curpos].r = (int)5e4; 5 if (curl == curr) return; 6 int mid = curl + curr >> 1; 7 build(lson, curl, mid); build(rson, mid + 1, curr); 8 }
插入线段:
1 //插入新线段并维护所有区间最优势线段信息 2 void update(int curpos, int curl, int curr, Interval k) { 3 if (k.l <= curl && curr <= k.r) { //被插入线段完全覆盖了当前区间 4 //如果当前区间还没有最优势线段,直接标记 5 if (!segt[curpos].flag) segt[curpos] = k, segt[curpos].flag = 1; 6 //如果被插入线段优于区间最优势线段,更新之 7 else if (k.calc(curl) - segt[curpos].calc(curl) > eps && k.calc(curr) - segt[curpos].calc(curr) > eps) segt[curpos] = k; 8 //如果被插入线段只有一端优于最优势线段,则检查 9 else if (k.calc(curl) - segt[curpos].calc(curl) > eps || k.calc(curr) - segt[curpos].calc(curr) > eps) { 10 int mid = curl + curr >> 1; 11 if (k.calc(mid) - segt[curpos].calc(mid) > eps) { //这里很关键,要看区间中点哪条线段更优 12 Interval tmp = k; k = segt[curpos]; segt[curpos] = tmp; 13 } 14 if (k.cross(segt[curpos]) - mid < -eps) update(lson, curl, mid, k); 15 else update(rson, mid + 1, curr, k); 16 } 17 } else { //若不优于最优势线段,则检查左右区间 18 int mid = curl + curr >> 1; 19 if (k.l <= mid) update(lson, curl, mid, k); 20 if (k.r > mid) update(rson, mid + 1, curr, k); 21 } 22 }
查询:
1 //查询区间最优势线段在x处取值 2 double query(int curpos, int curl, int curr, int x) { 3 if (curl == curr) return segt[curpos].calc(x); 4 int mid = curl + curr >> 1; 5 double ans = segt[curpos].calc(x); 6 if (x <= mid) return max(ans, query(lson, curl, mid, x)); 7 else return max(ans, query(rson, mid + 1, curr, x)); 8 }
复杂度分析:
修改部分:我们每次把一条线段的值域分隔到O(logn)个区间,每个区间最多把标记下传O(logn)层,因此修改的时间复杂度为O(log2n)。(但实测常数很小)
查询部分:每次在线段树上从上到下扫一遍,时间复杂度为O(logn)。
给出一道李超线段树裸题,bzoj1568 (https://lydsy.com/JudgeOnline/problem.php?id=1568)
1 #include <bits/stdc++.h> 2 #define ll long long 3 #define pb push_back 4 #define mp make_pair 5 #define sot(a,b) sort(a+1,a+1+b) 6 #define rep1(i,a,b) for (int i=a;i<=b;i++) 7 #define eps 1e-12 8 #define int_inf (1<<30)-1 9 #define ll_inf (1LL<<62)-1 10 #define lson curpos<<1 11 #define rson curpos<<1|1 12 13 using namespace std; 14 15 const int maxn = 1e5 + 10; 16 struct Line 17 { 18 double k, b; 19 int l, r, flag; 20 Line() {} 21 Line(int a, int b, double c, double d) 22 { 23 this->l = a, this->r = b, this->k = c, this->b = d; 24 } 25 double calc(const int pos)const 26 { 27 return k * pos + b; 28 } 29 int cross(const Line &rhs)const 30 { 31 return floor((b - rhs.b) / (rhs.k - k)); 32 } 33 } segt[maxn << 2]; 34 35 int n; char op[10]; 36 37 void build(int curpos, int curl, int curr) 38 { 39 segt[curpos].k = segt[curpos].b = 0; 40 segt[curpos].l = 1; segt[curpos].r = (int)5e4; 41 if (curl == curr) return; 42 int mid = curl + curr >> 1; 43 build(lson, curl, mid); build(rson, mid + 1, curr); 44 } 45 46 void update(int curpos, int curl, int curr, Line k) 47 { 48 if (k.l <= curl && curr <= k.r) 49 { 50 if (k.calc(curl) - segt[curpos].calc(curl) > eps && k.calc(curr) - segt[curpos].calc(curr) > eps) segt[curpos] = k; 51 else if (k.calc(curl) - segt[curpos].calc(curl) > eps || k.calc(curr) - segt[curpos].calc(curr) > eps) 52 { 53 int mid = curl + curr >> 1; 54 if (k.calc(mid) - segt[curpos].calc(mid) > eps) 55 { 56 Line tmp = k; k = segt[curpos]; segt[curpos] = tmp; 57 } 58 if (k.cross(segt[curpos]) - mid < -eps) update(lson, curl, mid, k); 59 else update(rson, mid + 1, curr, k); 60 } 61 } 62 else 63 { 64 int mid = curl + curr >> 1; 65 if (k.l <= mid) update(lson, curl, mid, k); 66 if (k.r > mid) update(rson, mid + 1, curr, k); 67 } 68 } 69 70 double query(int curpos, int curl, int curr, int x) 71 { 72 if (curl == curr) return segt[curpos].calc(x); 73 int mid = curl + curr >> 1; 74 double ans = segt[curpos].calc(x); 75 if (x <= mid) return max(ans, query(lson, curl, mid, x)); 76 else return max(ans, query(rson, mid + 1, curr, x)); 77 } 78 79 int main() 80 { 81 scanf("%d", &n); 82 build(1, 1, (int)5e4); 83 rep1(i, 1, n) 84 { 85 scanf("%s", op); 86 if (op[0] == 'P') 87 { 88 double s, p; 89 scanf("%lf%lf", &s, &p); 90 Line now = Line(1, 50000, p, s - p); 91 update(1, 1, (int)5e4, now); 92 } 93 else 94 { 95 int x; scanf("%d", &x); 96 int ans = (int)floor(query(1, 1, (int)5e4, x) / 100); 97 printf("%d\n", ans); 98 } 99 } 100 return 0; 101 }
其他相关题目:
bzoj 3938 4515
Reference:
算法|李超线段树初步(算法讲解+例题):https://ac.nowcoder.com/discuss/180365