关于线段树
线段树合并
需用结构体存线段树
简单的,板子也好理解
inline int merge(int x, int y, int l, int r){ //将 y 树合并到 x 树中
if(!x || !y){
return x | y; //返回节点
}
if(l == r){
tree[x].v += tree[y].v; //叶子节点合并
return x;
}
tree[x].l = merge(tree[x].l, tree[y].l, l, mid);
tree[x].r = merge(tree[x].r, tree[y].r, mid + 1, r); //重新分配节点
pushup(x); //x 树上传更新
return x;
}
动态开点
当到了未建立过的新点时再建立点,一般用结构体来存储线段树。
大致代码:
#define lx tree[x].l
#define rx tree[x].r
#define mid ((l + r) >> 1)
int cnt;
struct node{
int l, r;
int v;
}tree[N << 5];
inline void pushup(int x){
tree[x].v = tree[lx].v + tree[rx].v;
}
inline int update(int x, int l, int r, int k, int s){ //此处以单点修改展示
if(!x) x = ++ cnt;
if(l == r){
tree[x].v = s;
return x;
}
if(k <= mid) tree[x].l = update(lx, l, mid, k);
if(mid < k) tree[x].r = update(rx, mid + 1, r, k);
pushup(x);
return x;
}
关于线段树开的空间……
我的评价是:能开大点就开大点,保险
可持久化
常在多颗线段树差异不大但都需访问时使用
即在动态开点上加个记录时间的标记(也可以是其他标记),以达到节省空间开多颗线段树
用 \(rt_i\) 表示第 \(i\) 颗线段树的根节点
一般来说至少要开 \(N << 5\) 的空间
注意点:
- 一定要先建 \(rt[0]\) 的初始树,即使它可能是颗空树
- 开新点的过程大多为直接复制原节点再修改
- 相比正常线段树需多用一 \(vis\) 数组来表示该点是否开过
- 一定要有回传标号的操作
- 可持久化不能使用线段树合并,会有重复合并的错误(亲身经历)
int rt[N], cnt;
struct node{
int l, r;
int v;
bool vis; //vis 表示当点是否被开过
}tree[N << 5];
inline void pushup(int x){
tree[x].v = tree[lx].v + tree[rx].v;
}
inline int build(int x, int l, int r){
x = ++ cnt;
if(l == r){
return x; //回传标号
}
tree[x].l = build(lx, l, mid);
tree[x].r = build(rx, mid + 1, r);
return x; //同上
}
inline int add(int x){
tree[++ cnt] = tree[x];
tree[cnt].vis = 1;
tree[tree[cnt].l].vis = 0;
tree[tree[cnt].r].vis = 0;
return cnt;
}
inline int update(int x, int l, int r, int k, int s){
if(!tree[x].vis){
x = add(x);
}
if(l == r){
tree[x].v = s;
tree[x].vis = 1;
return x;
}
if(k <= mid) tree[x].l = update(lx, l, mid, k, s);
if(mid < k) tree[x].r = update(rx, mid + 1, r, k, s);
pushup(x);
return x;
}
int main(){
rt[0] = build(1, 1, n);
for(int i = 1; i <= n; ++ i){
rt[i] = add(rt[i - 1]); //第 i 颗树继承(复制)第 i - 1 颗树
rt[i] = update(rt[i], 1, n, i, 1);
}
}
李超线段树
用于处理坐标轴中线段……嘶……我描述不出来,思路什么的就去看上面两篇文章吧,这里只给出本人常用的模板。
#define lx (x << 1)
#define rx (x << 1 | 1)
#define mid ((l + r) >> 1)
int k[N], b[N];
inline int find(int p, int x){
//计算第 p 条线段上横坐标为 x 的值
return k[p] * x + b[p];
}
int tree[N << 2];
//此代码以求最大值为展示,注释掉部分为求最小值写法
inline void update(int x, int l, int r, int p){
//x 表示树上标号,区间 [l, r] 表示横坐标
if(l == r){
//当递归到了单点
if(find(tree[x], l) < find(p, l)){
//比较当前横坐标上的高度
tree[x] = p;
}
/* if(find(tree[x], l) > find(p, l)){
tree[x] = p;
}*/
return ;
}
if(!tree[x]){
//这段区间上没有线段
tree[x] = p;
return ;
}
if(k[tree[x]] == k[p]){
//两条线段斜率相等
if(b[tree[x]] < b[p]){
//比较 b ,即比较相对高度
tree[x] = p;
}
/* if(b[tree[x]] > b[p]){
tree[x] = p;
}*/
return ;
}
//以下画图会更好理解
int lst = find(tree[x], mid);
int now = find(p, mid);
//比较中间点相对位置
if(k[tree[x]] < k[p]){
//原线段斜率小于加入线段斜率
//画图理解罢
if(lst <= now){
update(lx, l, mid, tree[x]);
tree[x] = p;
}
else{
update(rx, mid + 1, r, p);
}
/* if(lst < now){
update(lx, l, mid, p);
}
else{
update(rx, mid + 1, r, tree[x]);
tree[x] = p;
}*/
}
else{
if(lst <= now){
update(rx, mid + 1, r, tree[x]);
tree[x] = p;
}
else{
update(lx, l, mid, p);
}
/* if(lst < now){
update(rx, mid + 1, r, p);
}
else{
update(lx, l, mid, tree[x]);
tree[x] = p;
}*/
}
return ;
}
//同上
inline int query(int x, int l, int r, int p){
//查询横坐标为 p 的点的最大 / 最小值
if(l == r){
return find(tree[x], p);
}
int sum = find(tree[x], p);
//当前区间最优
if(p <= mid) return max(sum, query(lx, l, mid, p));
if(mid < p) return max(sum, query(rx, mid + 1, r, p));
/* if(p <= mid) return min(sum, query(lx, l, mid, p));
if(mid < p) return min(sum, query(rx, mid + 1, r, p));*/
}
这种李超树写法貌似有点长,但常数是优的
实质上,李超线段树中存的只是当前区间最优解,故我们修改和查询时都得遍历到叶子结点为止。
修改复杂度 \(O(log^{2}n)\)
查询复杂度 \(O(log n)\)
在求解斜率优化dp的题目中都可用李超线段树解决,但是要注意单调性和离散化的问题,可以多做做题目参考参考代码
若转移方程式为 dp[i] = k[j] * x[i] + b[j]
则 find 函数应为
inline int find(int p, int i){
return k[p] * x[i] + b[p];
}
这也是一类似于离散化的操作