文章目录

  • 前言
  • 李超树
    • 引入(斜率优化)
    • 什么是李超树?
    • 李超树活着能干点什么?
  • 算法思想(使用手册?)
    • 插入
    • 查询
  • 模板
    • 判断是否覆盖(优不优)
    • 插入
    • 查询
  • 例题
    • 板题:BlueMary开公司
      • 分析
      • code
    • 线段游戏
      • 分析
      • code
  • 拓展——(动态开点李超树维护凸包)
    • ZZH的旅行
      • solution
      • code

前言

最近两场xie教练的考试都拉到了动态维护多条直线求最值的凸包问题
然后很愉快的一道都做不出来呢
所以学习了一下下,就写了个小博客

李超树

引入(斜率优化)

学习了c++c++c++,就必少不了dpdpdp的花式玩法
也就不会错过各种O(1),O(log)O(1),O(log)O(1),O(log)的优化
前缀和…斜率优化…单调队列…单调栈…


我们看一个dpdpdp状态转移方程式
dp[i]=max{dp[j]+bi∗aj+a[i]},j<idp[i]=max\{dp[j]+b_i*a_j+a[i]\},j<idp[i]=max{dp[j]+bi​∗aj​+a[i]},j<i

一般这种长相,噢不,准确来说,很多dpdpdp的这种长相,都会跟凸包挂钩

因为出题人不想考那么板的dp送分,就只能搞搞单调让你优化
而我们目前已知的解决凸包就是斜率优化


假设题目保证aia_iai​递增

假设j<k<ij<k<ij<k<i且决策点jjj优于决策点kkk,则有
dp[j]+bi∗aj+a[i]>dp[k]+bi∗ak+b[i]①dp[j]+b_i*a_j+a[i]>dp[k]+b_i*a_k+b[i]\ \ \ \ \ \ \ ①dp[j]+bi​∗aj​+a[i]>dp[k]+bi​∗ak​+b[i]       ①dp[j]−dp[k]>bi∗(ak−aj)②dp[j]-dp[k]>b_i*(a_k-a_j)\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ ②dp[j]−dp[k]>bi​∗(ak​−aj​)                                ②题目保证aaa单增,即ak−aj>0a_k-a_j>0ak​−aj​>0,继续变形得到
dp[j]−dp[k]ak−aj>bi③\frac{dp[j]-dp[k]}{a_k-a_j}>b_i\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ ③ak​−aj​dp[j]−dp[k]​>bi​                                                   ③
上述过程其实就是斜率优化的推导过程,这种情况下我们仍然可以吃老本——走斜率优化


但是,如果题目不保证aia_iai​单调呢??这个时候,斜率优化可以慢走不送了

这个条件去掉后,为什么斜率优化不行了呢??
再回到上面的推导过程,②能推导出③,实际上是把ak−aja_k-a_jak​−aj​除了过去
也就是说我们是肯定了ak−aja_k-a_jak​−aj​的符号才敢这样转移
一旦不确定了,大于小于符号就会错乱,无法斜率优化
ps:我刚刚偷换了一下,我把单增变成了题目不保证单调,因为单减也是可以斜率优化的

所以斜率优化是建立在单调的基础上的


此时我们怎么办呢??

  1. 暴力的气息扑面而来
  2. 祈祷数据会有单调的(概率:0.00…01%) 除非你是出数据的
  3. 乱搞
  4. 其它优秀的做法
  5. 我们的主角当当当————李超树!!!!yyds!

什么是李超树?

李超树 额(⊙﹏⊙)…按xie教练的话就是——懒标记永久化的线段树

明明就是个懒标记永久化,搞不懂为什么要专门取个名字叫李超树 ·····················——xie教练

管他的,反正我们不需要了解,它能有点用才是我们关注的!

李超树活着能干点什么?

  1. 维护一段区间的多条直线
  2. 支持单点查询多条直线的极值,如查询xxx处的多条直线的纵坐标最大值
  3. 支持区间查询直线极值,如查询区间[l,r][l,r][l,r]中各直线最值的最大值

本篇博客仅从“单点查询多条直线的极值”入手,因为博主目前只会这一种

只要你能写出 f[i]=max/min(f[j]∗h(i)+g[j])+t(i)f[i]=max/min(f[j]*h(i)+g[j])+t(i)f[i]=max/min(f[j]∗h(i)+g[j])+t(i)
搞那么复杂干什么, 其实就是你能写出一条一次函数的解析式 y=kx+by=kx+by=kx+b
李超树就能硬刚!

算法思想(使用手册?)

维护上凸包和下凸包差不多,我们以维护最大值为例

李超树每个区间记录的是该区间中点midmidmid处的最大/小函数值对应的函数

插入

分两种情况

  1. 完全覆盖

    新的红线在[l,r][l,r][l,r]区间上完全优于原来该区间存的直线,那么将直接进行全覆盖
    然后就returnreturnreturn,不用去更新左右子树
    至于为什么?跟查询写法有关,也跟部分覆盖挂钩
    因为此时更新后如果没有出现部分覆盖的情况,查询我们也会查到这条直线的值
    如果出现了部分覆盖的情况,此时的红线就会部分下放更新
    反正此时的黄线已经是个废物了,只需要知道这点就o了
  2. 部分覆盖
    2-1.

    首先这个区间[l,r][l,r][l,r]存的直线与midmidmid挂钩,此时红线在midmidmid处的函数值优于黄线
    所以李超线段树[l,r][l,r][l,r]这一个区间就会存红线的解析式
    那这个黄线呢??它也并不是全无用,我们需要继续递归右子树,去更新蓝色部分的区间

    2-2.

    此时红线在midmidmid处的函数值劣于黄线,李超线段树[l,r][l,r][l,r]这一个区间的解析式仍然是黄线
    但这个红线呢??也并不是全无用,我们需要继续递归做子树,去更新蓝色部分的区间

这三种情况怎么判断呢?就是怎么写呢?
很简单——直线是单调的
我们只需要掌握l,mid,rl,mid,rl,mid,r三个点的两点函数值就可以了,具体可看模板

查询

每一个区间都存了一条直线,可能相同也可能不同
一个点被多个区间包含,也就会被多条直线包含
所以我们一路上下来遇到的每一个区间都要算一次xxx对应的函数值,取最大值
有可能先遇到的直线的函数值比后遇到的直线的函数值还小,这是有可能的
因为那些区间的函数解析式是根据那些区间的中点midimid_imidi​的函数值决定的
谁管你这个小虾皮
举个栗子

这一路上涉及到的四个点的每一条解析式(黑线)都要把xxx带进去算出y1,y2,y3,y4y_1,y_2,y_3,y_4y1​,y2​,y3​,y4​

模板

判断是否覆盖(优不优)

double calc( node num, int x ) {return num.k * x + num.b;
}bool cover( node old, node New, int x ) {return calc( old, x ) <= calc( New, x );
}

插入

void insert( int num, int l, int r, node New ) {if( cover( t[num], New, l ) && cover( t[num], New, r ) ) {//新的直线完全覆盖了原区间的最优直线 t[num] = New;return;}if( l == r ) return;int mid = ( l + r ) >> 1;if( cover( t[num], New, mid ) )//区间[l,r]维护x=mid的最优值 swap( t[num], New );//现在这条直线需要继续往下去更新左右儿子(如果这条直线更优) if( cover( t[num], New, l ) ) insert( num << 1, l, mid, New );if( cover( t[num], New, r ) )insert( num << 1 | 1, mid + 1, r, New );
}

查询

这个版本有限制!!!
这个版本有限制!!!
这个版本有限制!!!

也许是我写法问题
此版本查询维护的是直线,也就是说每一条直线都会覆盖到所有的查询点
我们知道[l,r][l,r][l,r]区间维护的函数解析式是当x=midx=midx=mid时,函数值最大的那条解析式
因为每一条线都能覆盖完,所以每一个区间[l,r][l,r][l,r]都会维护一条直线
于是乎此版本的优化:x=midx=midx=mid时直接返回,不再寻找左右儿子
在这个前提下,这个优化才有正确性保障

double query( int num, int l, int r, int x ) {double ans = -1e9;int mid = ( l + r ) >> 1;if( x < mid ) ans = query( num << 1, l, mid, x );if( mid < x ) ans = query( num << 1 | 1, mid + 1, r, x );return max( ans, calc( t[num], x ) );
}

这一个版本维护的是线段,可能只会覆盖到一部分查询点

对应的插入操作也会发生改变,[l,r][l,r][l,r]能插入当且仅当[l,r][l,r][l,r]被某条直线完全包含,我们要再写一个modifymodifymodify

void modify( int num, int l, int r, int L, int R, node New ) {if( L > R ) return;if( L <= l && r <= R ) {insert( num, l, r, New );return;}int mid = ( l + r ) >> 1;if( R <= mid ) modify( num << 1, l, mid, L, R, New );else if( mid < L ) modify( num << 1 | 1, mid + 1, r, L, R, New );else {modify( num << 1, l, mid, L, mid, New );modify( num << 1 | 1, mid + 1, r, mid + 1, R, New );}
}

也就是说[l,r][l,r][l,r]区间如果没有被一条直线完全覆盖的话,这个区间是没有存直线的
那么当我们继续沿用之前的优化,x=midx=midx=mid就返回,很可能这段区间压根没有直线解析式
所以我们必须继续询问左儿子或者右儿子

打破砂锅问到底

double query( int num, int l, int r, int x ) {double ans;if( l == r ) return calc( t[num], x - 1 );int mid = ( l + r ) >> 1;if( x <= mid ) ans = query( num << 1, l, mid, x );else ans = query( num << 1 | 1, mid + 1, r, x );return max( ans, calc( t[num], x - 1 ) );
}

直线也可看作一条无限长的线段,因此此版本适用范围广

如果不是很理解,下面两道例题就能很好的辨析出来,┏ (゜ω゜)=☞
线段游戏的样例就会发现查询的写法不同会有问题,可以分布调试看看

例题

板题:BlueMary开公司

分析

每一个项目看作一条直线,而某一天就是查询点
依题意可得,每一个项目可以覆盖每一天,也就是直线覆盖所有点情况
两种查询模板都可以直接上

code

#include <cstdio>
#include <iostream>
using namespace std;
#define maxn 50005
struct node {double k, b;node(){}node( double K, double B ) {k = K, b = B;}
}t[maxn << 2];
int n;double calc( node num, int x ) {return num.k * x + num.b;
}bool cover( node old, node New, int x ) {return calc( old, x - 1 ) <= calc( New, x - 1 ); //题目原因 符合要求的x实际上是需要-1才算得出正确收益
}void insert( int num, int l, int r, node New ) {if( cover( t[num], New, l ) && cover( t[num], New, r ) ) {t[num] = New;return;}if( l == r ) return;int mid = ( l + r ) >> 1;if( cover( t[num], New, mid ) )swap( t[num], New );if( cover( t[num], New, l ) ) insert( num << 1, l, mid, New );if( cover( t[num], New, r ) )insert( num << 1 | 1, mid + 1, r, New );
}double query( int num, int l, int r, int x ) {double ans = -1e9;int mid = ( l + r ) >> 1;if( x < mid ) ans = query( num << 1, l, mid, x );if( mid < x ) ans = query( num << 1 | 1, mid + 1, r, x );return max( ans, calc( t[num], x - 1 ) );
}
/*
已测试,此写法依然可过
double query( int num, int l, int r, int x ) {double ans;if( l == r ) return calc( t[num], x - 1 );int mid = ( l + r ) >> 1;if( x <= mid ) ans = query( num << 1, l, mid, x );else ans = query( num << 1 | 1, mid + 1, r, x );return max( ans, calc( t[num], x - 1 ) );
}
*/
int main() {scanf( "%d", &n );while( n -- ) {char opt[10]; int k, b, t;scanf( "%s", opt );if( opt[0] == 'Q' ) {scanf( "%d", &t );printf( "%d\n", int( query( 1, 1, maxn, t ) / 100 ) );}else {double k, b;scanf( "%lf %lf", &b, &k );insert( 1, 1, maxn, node( k, b ) );}}    return 0;
}

线段游戏

分析

这道题就很特别了,我们维护的不再是一条直线,而是一条线段
这条线段只能覆盖有限的查询点,而不能覆盖的点我们是不能进行求解的

code

#include <cstdio>
#include <iostream>
using namespace std;
#define maxn 100000
#define inf 0x7f7f7f7f
struct node {double k, b;node(){}node( double K, double B ) {k = K, b = B;}
}t[( maxn + 5 ) << 2];
int n, m;double calc( node num, int x ) {return num.k * x + num.b;
}bool cover( node old, node New, int x ) {return calc( old, x ) < calc( New, x );
}void insert( int num, int l, int r, node New ) {if( cover( t[num], New, l ) && cover( t[num], New, r ) ) {t[num] = New;return;}if( l == r ) return;int mid = ( l + r ) >> 1;if( cover( t[num], New, mid ) )swap( t[num], New );if( cover( t[num], New, l ) )insert( num << 1, l, mid, New );if( cover( t[num], New, r ) )insert( num << 1 | 1, mid + 1, r, New );
}void modify( int num, int l, int r, int L, int R, node New ) {if( L > R ) return;if( l == L && R == r ) {insert( num, l, r, New );return;}int mid = ( l + r ) >> 1;if( R <= mid ) modify( num << 1, l, mid, L, R, New );else if( mid < L ) modify( num << 1 | 1, mid + 1, r, L, R, New );else {modify( num << 1, l, mid, L, mid, New );modify( num << 1 | 1, mid + 1, r, mid + 1, R, New );}
}double query( int num, int l, int r, int x ) {if( l == r ) return calc( t[num], x );double ans;int mid = ( l + r ) >> 1;if( x <= mid ) ans = query( num << 1, l, mid, x );else ans = query( num << 1 | 1, mid + 1, r, x );return max( ans, calc( t[num], x ) );
/*
这种写法是不对的,原因上面已经分析过了......double ans = -inf;int mid = ( l + r ) >> 1;if( x < mid ) ans = query( num << 1, l, mid, x );if( mid < x ) ans = query( num << 1 | 1, mid + 1, r, x );return max( ans, calc( t[num], x ) );
*/
}void build( int num, int l, int r ) {t[num].k = 0, t[num].b = -inf;if( l == r ) return;int mid = ( l + r ) >> 1;build( num << 1, l, mid ), build( num << 1 | 1, mid + 1, r );
}int main() {scanf( "%d %d", &n, &m );build( 1, 1, maxn );int x0, x1, y1, x2, y2, opt;double k, b;for( int i = 1, x1, y1, x2, y2;i <= n;i ++ ) {scanf( "%d %d %d %d", &x1, &y1, &x2, &y2 );if( x1 == x2 ) k = 0, b = max( y2, y1 );else k = ( y2 - y1 ) * 1.0 / ( x2 - x1 ), b = y1 - k * x1;modify( 1, 1, maxn, max( 1, min( x1, x2 ) ), min( maxn, max( x1, x2 ) ), node( k, b ) );}while( m -- ) {scanf( "%d", &opt );if( opt ) {scanf( "%d", &x0 );double ans = query( 1, 1, maxn, x0 );printf( "%.6f\n", ( ans <= -inf ? 0 : ans ) );}else {scanf( "%d %d %d %d", &x1, &y1, &x2, &y2 );if( x1 == x2 ) k = 0, b = max( y2, y1 );else k = ( y2 - y1 ) * 1.0 / ( x2 - x1 ), b = y1 - k * x1;modify( 1, 1, maxn, max( 1, min( x1, x2 ) ), min( maxn, max( x1, x2 ) ), node( k, b ) );}}return 0;
}

拓展——(动态开点李超树维护凸包)

ZZH的旅行



solution

code

有一坨不像我

【李超树】李超线段树维护凸包(凸壳) (例题:blue mary开公司+线段游戏+ZZH的旅行)相关推荐

  1. 【BZOJ1568】【Tyvj3490】Blue Mary开公司 李超线段树

    Time:2016.08.02 Author:xiaoyimi 转载注明出处谢谢 传送门1 传送门2 思路: 题意大致为 维护有斜率和截距的若干直线,并求直线x=T(T∈N)与当前已加入直线交点的ym ...

  2. P4254-[JSOI2008]Blue Mary开公司【李超树】

    正题 题目链接:https://www.luogu.com.cn/problem/P4254 题目大意 要求支持操作 插入一条直线. 询问一个纵坐标最高的在直线上的点. 解题思路 李超树的模板题,大概 ...

  3. bzoj1568 [JSOI2008]Blue Mary开公司 标记永久化线段树

    维护n条直线,保存斜率和截距. 注意维护的时候分类讨论: 1.两端都大于 2.两端都小于 3.交点在中点左 4.交点在中点右 注: 点数不是询问数 初值 码: #include<iostream ...

  4. 利用OpenCV的convexHull和convexityDefects做凸包(凸壳)检测及凸包(凸壳)的缺陷检测

    图像处理开发需求.图像处理接私活挣零花钱,请加微信/QQ 2487872782 图像处理开发资料.图像处理技术交流请加QQ群,群号 271891601 要理解凸包(凸壳)检测,首无要知道什么是凸包(凸 ...

  5. 斜率优化之凸包优化与李超线段树

    文章目录 前言 凸包优化 第一步 第二步 最后一步 例一 转移方程 凸包优化 代码 例二 题目大意 转移方程 凸包优化 代码 李超线段树 思想 插入 查询 代码 例三 代码 例四 转移方程 怎么做 代 ...

  6. 数据结构专题-学习笔记:李超线段树

    数据结构专题 - 学习笔记:李超线段树 1. 前言 2. 详解 3. 应用 4. 总结 5. 参考资料 1. 前言 本篇博文是博主学习李超线段树的学习笔记. 2020/12/21 的时候我在 线段树算 ...

  7. 时空旅行[线段树分治][维护凸壳]

    文章目录 前言 题目 思路 代码 前言 肝了一上午-这是我才学线段树分治的例题-真舒服 题目 温馨提示:首先在UOJ做,LOJ挖数据,BZOJ终极评测... UOJ198 二手剽- 思路 为什么不能用 ...

  8. P3309-[SDOI2014]向量集【线段树,凸壳】

    正题 题目链接:https://www.luogu.com.cn/problem/P3309 题目大意 nnn个操作 在序列末尾加入一个向量(x,y)(x,y)(x,y) 询问加入的第l∼rl\sim ...

  9. CF536C-Tavas and Pashmaks【凸壳】

    正题 题目链接:https://codeforces.com/contest/536/problem/C 题目大意 nnn个人,第iii个人的游泳速度sis_isi​,跑步速度是rir_iri​.如果 ...

最新文章

  1. 58 Node.js中操作mongoDB数据库
  2. 云计算里的安全:警惕云服务被恶意利用
  3. Scalaz(27)- Inference Unapply :类型的推导和匹配
  4. 蚁群优化算法 ACO
  5. 《计算机网络:自顶向下方法(原书第6版)》一第1章
  6. 开发者如何谈判才能获得更高的薪水?
  7. 常用CSS优化总结——网络性能与语法性能建议
  8. c语言pow函数原型_c语言中的pow()函数怎么用
  9. ROS可以不扫描地图,自己制作地图
  10. 西北大学本科毕业论文答辩PPT模板
  11. JAVA 实现语音播报
  12. python 主力资金_邢不行 | 量化投资中如何计算机构、主力、散户资金流数据【视频】...
  13. neo4j-ogm-core使用小记
  14. 章鱼网络进展月报 | 2022.10.1-10.31
  15. 5 大最常用 C++ 经典算法
  16. java 多线程的好处_线程多有什么好处?使用多线程的优势
  17. HDU 2547 无剑无我
  18. (C语言)BinarySearch二分搜索/折半查找 --- 递归、非递归
  19. RSA算法理解与实现
  20. CRSNet: Dilated Convolutional Neural Networks for Underatanding the Highly Congested Scenes

热门文章

  1. 一起读懂传说中的经典:受限玻尔兹曼机
  2. 手把手教你java快速过滤关键词
  3. pca 累积方差贡献率公式_机器学习数据降维方法:PCA主成分分析
  4. html答题赚钱源码,WTS在线答题系统 v1.0.0
  5. 计算机语言学考研科目,语言学考研笔记整理(共16页)
  6. 各纬度气候分布图_欧洲气候特征:以温带气候类型为主,是海洋性气候最显著的大洲...
  7. python量化常用_Python量化常用函数
  8. 算法设计与分析——算法思想总结
  9. Java并发之volatile
  10. 高等数学下-赵立军-北京大学出版社-题解-练习9.2