关于线段树
线段树合并
需用结构体存线段树
简单的,板子也好理解
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;
}
关于线段树开的空间……
我的评价是:能开大点就开大点,保险
可持久化
常在多颗线段树差异不大但都需访问时使用
即在动态开点上加个记录时间的标记(也可以是其他标记),以达到节省空间开多颗线段树
用
一般来说至少要开
注意点:
- 一定要先建
的初始树,即使它可能是颗空树 - 开新点的过程大多为直接复制原节点再修改
- 相比正常线段树需多用一
数组来表示该点是否开过 - 一定要有回传标号的操作
- 可持久化不能使用线段树合并,会有重复合并的错误(亲身经历)
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));*/
}
这种李超树写法貌似有点长,但常数是优的
实质上,李超线段树中存的只是当前区间最优解,故我们修改和查询时都得遍历到叶子结点为止。
修改复杂度
查询复杂度
在求解斜率优化dp的题目中都可用李超线段树解决,但是要注意单调性和离散化的问题,可以多做做题目参考参考代码
若转移方程式为 dp[i] = k[j] * x[i] + b[j]
则 find 函数应为
inline int find(int p, int i){
return k[p] * x[i] + b[p];
}
这也是一类似于离散化的操作
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】