李超线段树

用处

插入若干条直线,求 \(x=a\) 这条直线与插入的直线的交点的最小/大纵坐标。

插入

以维护最小值为例。

\(cal_{u,a}\) 表示把 \(x=a\) 带入直线 \(u\) 的值。

插入一条直线,考虑对一个线段树上的区间维护一个 \(tag\) 表示当前区间最优秀的直线是 \(tag_k\)(这里的优秀指的是带入 \(x=mid\) 的值最小的直线)。

插入一条直线 \(u\),这个区间上的 \(tag\)\(v\)

当前区间对应的是 \([l,r]\)

比较一下 \(cal_{u,mid}\)\(cal_{v,mid}\) 的大小。

  1. \(cal_{u,mid}\) 更小,直接把这个区间的 \(tag\) 改成 \(u\)。比较一下 \(cal_{u,l}\)\(cal_{v,l}\) 的大小:如果 \(cal_{v,l}\) 更小,那么说明在区间 \([l,mid]\) 中,直线 \(v\) 仍然有可能成为最优直线,向区间 \([l,mid]\) 递归;否则再比较一下 \(cal_{u,r}\)\(cal_{v,r}\),如果 \(cal_{v,r}\) 小,直线 \(v\) 就有可能在区间 \([mid+1,r]\) 中成为最优直线,向区间 \([mid + 1,r]\) 递归。显然两个条件要么满足一个,要么都不满足,所以最多递归 \(\log\) 次。(这里画一下图就很好理解了)

  2. \(cal_{v,mid}\) 更小,处理方式和上述差不多。

// 存直线的
struct line {
    int k, b;
} l[N];

// 查询值
int cal( int id, int x) {
    return l[id].k * x + l[id].b;
}

// 插入一条直线 u
void insert( int & k, int l, int r, int u) {
    if (! k) return k = ++ tot, tag[k] = u, void();
    
    int & v = tag[k];
    if (cal(v, mid) > cal(u, mid)) swap(v, u);
    if (cal(u, l) < cal(v, l)) insert(ls[k], l, mid, u);
    if (cal(u, r) < cal(v, r)) insert(rs[k], mid + 1, r, u);
}

询问

询问 \(x=a\) 这条直线与插入的直线的交点纵坐标最小值。

因为标记无法下传,所以考虑像标记永久化那样的询问处理方式。

对于包含了 \(x=a\) 这条直线的 \(\log\) 个区间,每个都取一下最小值即可。

int ask( int k, int l, int r, int x) {
    if (! k) return inf;

    return min(cal(tag[k], x), x <= mid ? ask(ls[k], l, mid, x) : ask(rs[k], mid + 1, r, x));
}

合并

如果想和并两棵李超线段树,很容易。

例如把树 \(b\) 合并进树 \(a\)

直接在两颗树对应的区间上,把 \(b\) 的直线全部插入进 \(a\) 即可。

// 把 b 中的所有直线合并进 a
void merge( int & a, int b, int l, int r) {
    if (! a) return a = b, void();
    if (! b) return ;

    insert(a, l, r, tag[b]); // 这里直接插入
    if (l == r) return ;

    merge(ls[a], ls[b], l, mid);
    merge(rs[a], rs[b], mid + 1, r);
}

扩展

如果说插入的不是直线而是一条线段,该怎么办?

插入一条线段 \(((x1,y1),(x2,y2))\),可以看作插入一个有定义域为 \([x1,x2]\) 的直线。

那么在线段树上,如果当前区间 \([l,r]\)\([x1,x2]\) 包含,那么在 \([l,r]\) 插入这条直线即可。

这样的复杂度 \(\log^2\)

posted @ 2025-08-14 10:24  咚咚的锵  阅读(22)  评论(0)    收藏  举报