「學習筆記」李超線段樹
background
學這個算法的是因為某天一個題用$ \text{ set } $維護斜率被卡常數了,在某大佬的安利下學了這個科技,聯賽后又思考了很多關於這個算法的問題,於是寫一篇博客來頹廢並調整一下文化課學習以來壓抑的心態。
在平時的一些訓練中往往遇到一些維護斜率的問題,根據 \(x\) 坐標單不單調和斜率單不單調可以大致分成幾種,並用單調線性結構和 \(\text{ set , cdq}\)分治來解決,事實上李超線段樹可以在沒有刪除操作的題目中成為較好的另外一種選擇,其具有常數小和實現簡單細節少等優點。另外還有一些動態維護的題目只能有李超樹來實現,然而后面並不會講到。
a problem 「Heoi2013」Segment
有一個二維平面,你需要支持兩種操作,插入一條線段,查詢一條直線 \(x = k\) 與其相交的最上面的一條線段。在學習之前離線做法可以用 \(\text{cdq}\) 搞定,在線的話用平衡樹也可以實現,李超樹的做法相較於這兩種要巧妙一些,核心的思想在於標記永久化維護覆蓋區間中點的最高的線段。
考慮當前有一個線段樹上的區間,要在這個區間里插入一條線段並維護答案,不妨分以下幾類討論:
- 如果當前這個區間還沒有線段或者新加入的線段完全覆蓋原來的線段,那么新加入的線段一定替換原來的線段
- 如果當前新加入的線段被原來的線段完全覆蓋,那么這條新加入的線段一定不會再有用了。
- 將新加入的線段和原來的線段求交,將交所在的那半邊較劣的線段下放到對應的兒子,更新區間中點的最高的線段。
此時維護的相當於是標記永久化的若干個線段,求答案就是將 \(k\) 對應路徑上的標記取最優線段,復雜度瓶頸所在的第三種情況每次都只會走一個兒子下放,所以從插入一條線段的復雜度是 \(O(logn)\) ,由於要區間修改要在 \(logn\) 個節點上插入線段,所以總復雜度 \(O(nlog^2n)\)
Why?
首先在沒有任何線段的時候正確性顯然,考慮逐步在一個正確的情況下插入線段,分析正確性是否改變。如果插入的線段是情況 \(1, 2\) ,那么正確性也是顯然的,這里就不啰嗦了,直接考慮第 \(3\) 種情況。
考慮此時區間的線段是覆蓋區間中點最高的線段,也就是所謂的優勢線段。其有一個重要的性質就是任何一條線段在這個區間內與它相交,交不在的區間所有點都是新的比原先的優或者原先的比新的優,也就是交不在的區間的所有點的可能的答案還是優勢線段。而考慮交所在的區間可能的答案有一部分是另外一條線段,此時只需要將不是優勢線段的那一條(被替換的那一條)遞歸向下更新貢獻即可,那么這個節點的形態就是一半邊點就是對優勢線段取 \(\text{max}\),另外一半邊對兩者都取 \(\text{max}\) 。(這里以求最上面的線段為例)。而每次插入都重新維護了優勢線段,所以每次插入后的正確性還是對的。
Code
/*program by mangoyang*/
#include<bits/stdc++.h>
#define inf (0x7f7f7f7f)
#define Max(a, b) ((a) > (b) ? (a) : (b))
#define Min(a, b) ((a) < (b) ? (a) : (b))
typedef long long ll;
using namespace std;
template <class T>
inline void read(T &x){
int ch = 0, f = 0; x = 0;
for(; !isdigit(ch); ch = getchar()) if(ch == '-') f = 1;
for(; isdigit(ch); ch = getchar()) x = x * 10 + ch - 48;
if(f) x = -x;
}
const int N = 200005;
const double eps = 1e-6;
#define lson (u << 1)
#define rson (u << 1 | 1)
int n, tot;
struct Seg{ double k, b; int id; };
struct SegmentTree{
Seg s[N<<2]; int hav[N<<2];
inline void pushdown(int u, int l, int r, Seg now){
if(!hav[u]) return (void) (s[u] = now, hav[u] = 1);
double l1 = now.k * l + now.b, r1 = now.k * r + now.b;
double l2 = s[u].k * l + s[u].b, r2 = s[u].k * r + s[u].b;
if(l2 >= l1 && r2 >= r1) return;
if(l1 >= l2 && r1 >= r2) return (void) (s[u] = now);
double pos = (now.b - s[u].b) / (s[u].k - now.k);
int mid = l + r >> 1;
if(pos <= mid) pushdown(lson, l, mid, r1 > r2 ? s[u] : now);
else pushdown(rson, mid + 1, r, l1 > l2 ? s[u] : now);
if((l1 > l2 && pos >= mid) || (r1 > r2 && pos < mid)) s[u] = now;
}
inline void Insert(int u, int l, int r, int L, int R, Seg now){
if(l >= L && r <= R) return (void) (pushdown(u, l, r, now));
int mid = l + r >> 1;
if(L <= mid) Insert(lson, l, mid, L, R, now);
if(mid < R) Insert(rson, mid + 1, r, L, R, now);
}
inline Seg query(int u, int l, int r, int pos){
if(l == r) return hav[u] ? s[u] : (Seg){0, 0, 0};
int mid = l + r >> 1; Seg now;
if(pos <= mid) now = query(lson, l, mid, pos);
else now = query(rson, mid + 1, r, pos);
if(!hav[u]) return now;
double p1 = s[u].k * pos + s[u].b, p2 = now.k * pos + now.b;
if(!now.id || (p1 > p2 || (fabs(p1-p2) < eps && s[u].id < now.id))) now = s[u];
return now;
}
}van;
signed main(){
read(n); int lastans = 0;
for(int i = 1, op, x, X0, Y0, X1, Y1; i <= n; i++){
read(op);
if(op == 1){
read(X0), read(Y0), read(X1), read(Y1);
X0 = (X0 + lastans - 1) % 39989 + 1;
X1 = (X1 + lastans - 1) % 39989 + 1;
Y0 = (Y0 + lastans - 1) % (int) (1e9) + 1;
Y1 = (Y1 + lastans - 1) % (int) (1e9) + 1;
if(X0 > X1) swap(X0, X1), swap(Y0, Y1);
if(X0 == X1){
van.Insert(1, 1, 39989, X0, X1, (Seg){0.0, max(Y0, Y1), ++tot});
continue;
}
double k = (double)(Y1 - Y0) / (double)(X1 - X0), b = (double)Y1 - k * X1;
van.Insert(1, 1, 39989, X0, X1, (Seg){k, b, ++tot});
}
else{
read(x), x = (x + lastans - 1) % 39989 + 1;
printf("%lld\n", lastans = van.query(1, 1, 39989, x).id);
}
}
return 0;
}