浅谈李超线段树
huangzirui
2020-10-24 14:20:46
### 引入:
有时候我们被给定一些直线,需要求 $x=a$ 时最值的问题。
在某些斜率优化的题目中,这个问题经常出现。
一般的斜率优化题都满足一些单调性质,可以支持 $O(1) /O(\log n)$ 查询。
但是在失去了某些单调性后,就需要用数据结构维护。
#### 例:
给定一些柱子的高度 $h_i$ 与拆除它们的代价 $w_i$ ,你需要在柱子之间建立一些桥来连接 $1$ 号和 $n$ 号柱子并拆除没有连接桥的柱子。建立一座从 $i$ 到 $j$ 的桥需要花费 $(h_i-h_j)^2$ 的代价。求代价最小值。
$n \leq 10^5\ \ 0 \leq |w_i|,h_i \leq 10^6$
---
显然可以列出一个状态转移方程,设 $dp_x$ 为转移到 $x$ 号柱子并选择它的最小代价:
$$dp_x=min\{dp_i+(h_i-h_x)^2-W_i+W_x\}$$
其中 $W_x = \sum_{i=1}^{x-1}w_i$
那么可以转化成一个类似斜率优化的式子:
$$dp_x-W_x-{h_x}^2=dp_i+{h_i}^2-W_i-2h_ih_x$$
可以把每个转移点抽象为一条斜率为 $-2h_i$ ,截距为 $dp_i+{h_i}^2-W_i$ 的直线,需要求的就是 $x=h_x$ 时,$y$ 最小的直线。
这个问题是李超线段树的经典问题,要解决它,我们先要了解李超线段树。
---
### 李超线段树
简单来说,李超线段树和线段树类似,都是每个节点维护 $[L,R]$ 的最优值。但是与一般线段树不一样的是,每个节点的“最优线段”是满足在 $x=mid$ 时 $y$ 值最大/最小且覆盖了全区域的线段。
查询某个点 $x=a$ 的最优值时,只需要查询**所有满足 $L \leq a \leq R$ 的节点上的最优线段**,并求出它们的最优值即可。
这里以 [P4097](https://www.luogu.com.cn/problem/P4097) 为例说明。
代码实现如下:
```cpp
#define l(x) tree[x].l
#define ls(x) (tree[x].son[0])
#define rs(x) (tree[x].son[1])
#define ll long long
#define Mid ((L+R)>>1)
const double inf=1e18;
struct line{
double k,b;int id;
double calc(int x){return k*x+b;}
};
struct segment_tree{
int son[2];
line l;
}tree[maxn*8];
```
代码实现了直线和节点,其中 $\text{calc}(x)$ 是计算直线在 $x$ 时的 $y$ 值。
```cpp
void insert(int &root,int L,int R,int l,int r,line g){
if(!root)root=++cnt,tree[root]={0,0,0};
if(L==l && R==r){
if(l(root).calc(Mid)<g.calc(Mid))swap(l(root),g);
if(L!=R){
if(l(root).k<g.k)insert(rs(root),Mid+1,R,Mid+1,r,g);
else if(l(root).k>g.k)insert(ls(root),L,Mid,l,Mid,g);
}return;
}
if(r<=Mid)insert(ls(root),L,Mid,l,r,g);
else if(l>Mid)insert(rs(root),Mid+1,R,l,r,g);
else insert(ls(root),L,Mid,l,Mid,g),insert(rs(root),Mid+1,R,Mid+1,r,g);
}
```
代码实现了插入一条线段,这个操作的复杂度是 $O(\log^2S)$ 的。($S$ 为值域)
首先,将一条线段分割为 $\log S$ 份,且每份都可以在线段树上找到一条对应的可以包含 $[L,R]$ 的节点。
然后判断是否可以取代原最优线段,如果是就把原最优线段下传,否则下传当前的线段,下传时需要分类讨论。
具体来说,如果当前的线段比最优线段差,假设当前线段是$\color{red}\text{红色}$,最优线段是$\text{\text{黑色}}$,那么:
(使用了 OI wiki 的图片)
![](https://oi-wiki.org/ds/images/li-chao-tree3.png)
如果当前线段斜率大于最优线段,那么只可能在右侧区间更优,直接下传。
![](https://oi-wiki.org/ds/images/li-chao-tree4.png)
同理,下传至左侧即可。
因此每条线段最多下传 $\log S$ 层,所以复杂度为 $O(\log^2S)$ 。
**注意:插入直线的复杂度为 $O(\log S)$ !**
```cpp
inline line MAX(line a,line b,int x){
return a.calc(x)>b.calc(x) || (fabs(a.calc(x)-b.calc(x))<1e-9 && a.id<b.id)?a:b;
}
line find(int root,int L,int R,int x){
if(!root)return {0,0,0};
if(L==R)return l(root);
if(x<=Mid)return MAX(find(ls(root),L,Mid,x),l(root),x);
return MAX(find(rs(root),Mid+1,R,x),l(root),x);
}
```
代码实现了寻找一条 $x=a$ 时的最优线段,查找时**不应直接返回节点 $[a,a]$ 的线段**,而要返回所有**节点包含 $a$ 的线段**中最优的那个,才能保证正确性。复杂度显然是 $O(\log S)$ 的。
以上是李超线段树的基本操作。接下来讲一下例题:
---
### 例题
[[HEOI2013]Segment](https://www.luogu.com.cn/problem/P4097)
模板题,需要注意斜率不存在的情况。
[[CEOI2017]Building Bridges](https://www.luogu.com.cn/problem/P4655)
引入题。
这道题提示我们,在解决一些动态规划问题时,可以利用李超线段树维护转移过程。
一些其他的解决方法有 cdq 分治等,但是个人认为李超树更简单一些(
[[SDOI2016]游戏](https://www.luogu.com.cn/problem/P4069)
> “总是有出题人喜欢把序列上用线段树解决的题目出到树上,让选手强行写个树链剖分或树分治或某种动态树数据结构。 这种行为已经很无趣了。 所以我们想让大家知道,不光可以放在静态树上,动态仙人掌也是可以的。”
期待有出题人把李超线段树放到动态仙人掌上(