【李超树】李超线段树维护凸包(凸壳) (例题:blue mary开公司+线段游戏+ZZH的旅行)
文章目录
- 前言
- 李超树
- 引入(斜率优化)
- 什么是李超树?
- 李超树活着能干点什么?
- 算法思想(使用手册?)
- 插入
- 查询
- 模板
- 判断是否覆盖(优不优)
- 插入
- 查询
- 例题
- 板题: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−ajdp[j]−dp[k]>bi ③
上述过程其实就是斜率优化的推导过程,这种情况下我们仍然可以吃老本——走斜率优化
但是,如果题目不保证aia_iai单调呢??这个时候,斜率优化可以慢走不送了
这个条件去掉后,为什么斜率优化不行了呢??
再回到上面的推导过程,②能推导出③,实际上是把ak−aja_k-a_jak−aj除了过去
也就是说我们是肯定了ak−aja_k-a_jak−aj的符号才敢这样转移
一旦不确定了,大于小于符号就会错乱,无法斜率优化
ps:我刚刚偷换了一下,我把单增变成了题目不保证单调,因为单减也是可以斜率优化的
所以斜率优化是建立在单调的基础上的
此时我们怎么办呢??
- 暴力的气息扑面而来
- 祈祷数据会有单调的(概率:0.00…01%) 除非你是出数据的
- 乱搞
- 其它优秀的做法
- 我们的主角当当当————李超树!!!!yyds!
什么是李超树?
李超树 额(⊙﹏⊙)…按xie教练的话就是——懒标记永久化的线段树
明明就是个懒标记永久化,搞不懂为什么要专门取个名字叫李超树 ·····················——xie教练
管他的,反正我们不需要了解,它能有点用才是我们关注的!
李超树活着能干点什么?
- 维护一段区间的多条直线
- 支持单点查询多条直线的极值,如查询xxx处的多条直线的纵坐标最大值
- 支持区间查询直线极值,如查询区间[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处的最大/小函数值对应的函数
插入
分两种情况
- 完全覆盖
新的红线在[l,r][l,r][l,r]区间上完全优于原来该区间存的直线,那么将直接进行全覆盖
然后就returnreturnreturn,不用去更新左右子树
至于为什么?跟查询写法有关,也跟部分覆盖挂钩
因为此时更新后如果没有出现部分覆盖的情况,查询我们也会查到这条直线的值
如果出现了部分覆盖的情况,此时的红线就会部分下放更新
反正此时的黄线已经是个废物了,只需要知道这点就o了 - 部分覆盖
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的旅行)相关推荐
- 【BZOJ1568】【Tyvj3490】Blue Mary开公司 李超线段树
Time:2016.08.02 Author:xiaoyimi 转载注明出处谢谢 传送门1 传送门2 思路: 题意大致为 维护有斜率和截距的若干直线,并求直线x=T(T∈N)与当前已加入直线交点的ym ...
- P4254-[JSOI2008]Blue Mary开公司【李超树】
正题 题目链接:https://www.luogu.com.cn/problem/P4254 题目大意 要求支持操作 插入一条直线. 询问一个纵坐标最高的在直线上的点. 解题思路 李超树的模板题,大概 ...
- bzoj1568 [JSOI2008]Blue Mary开公司 标记永久化线段树
维护n条直线,保存斜率和截距. 注意维护的时候分类讨论: 1.两端都大于 2.两端都小于 3.交点在中点左 4.交点在中点右 注: 点数不是询问数 初值 码: #include<iostream ...
- 利用OpenCV的convexHull和convexityDefects做凸包(凸壳)检测及凸包(凸壳)的缺陷检测
图像处理开发需求.图像处理接私活挣零花钱,请加微信/QQ 2487872782 图像处理开发资料.图像处理技术交流请加QQ群,群号 271891601 要理解凸包(凸壳)检测,首无要知道什么是凸包(凸 ...
- 斜率优化之凸包优化与李超线段树
文章目录 前言 凸包优化 第一步 第二步 最后一步 例一 转移方程 凸包优化 代码 例二 题目大意 转移方程 凸包优化 代码 李超线段树 思想 插入 查询 代码 例三 代码 例四 转移方程 怎么做 代 ...
- 数据结构专题-学习笔记:李超线段树
数据结构专题 - 学习笔记:李超线段树 1. 前言 2. 详解 3. 应用 4. 总结 5. 参考资料 1. 前言 本篇博文是博主学习李超线段树的学习笔记. 2020/12/21 的时候我在 线段树算 ...
- 时空旅行[线段树分治][维护凸壳]
文章目录 前言 题目 思路 代码 前言 肝了一上午-这是我才学线段树分治的例题-真舒服 题目 温馨提示:首先在UOJ做,LOJ挖数据,BZOJ终极评测... UOJ198 二手剽- 思路 为什么不能用 ...
- P3309-[SDOI2014]向量集【线段树,凸壳】
正题 题目链接:https://www.luogu.com.cn/problem/P3309 题目大意 nnn个操作 在序列末尾加入一个向量(x,y)(x,y)(x,y) 询问加入的第l∼rl\sim ...
- CF536C-Tavas and Pashmaks【凸壳】
正题 题目链接:https://codeforces.com/contest/536/problem/C 题目大意 nnn个人,第iii个人的游泳速度sis_isi,跑步速度是rir_iri.如果 ...
最新文章
- 58 Node.js中操作mongoDB数据库
- 云计算里的安全:警惕云服务被恶意利用
- Scalaz(27)- Inference Unapply :类型的推导和匹配
- 蚁群优化算法 ACO
- 《计算机网络:自顶向下方法(原书第6版)》一第1章
- 开发者如何谈判才能获得更高的薪水?
- 常用CSS优化总结——网络性能与语法性能建议
- c语言pow函数原型_c语言中的pow()函数怎么用
- ROS可以不扫描地图,自己制作地图
- 西北大学本科毕业论文答辩PPT模板
- JAVA 实现语音播报
- python 主力资金_邢不行 | 量化投资中如何计算机构、主力、散户资金流数据【视频】...
- neo4j-ogm-core使用小记
- 章鱼网络进展月报 | 2022.10.1-10.31
- 5 大最常用 C++ 经典算法
- java 多线程的好处_线程多有什么好处?使用多线程的优势
- HDU 2547 无剑无我
- (C语言)BinarySearch二分搜索/折半查找 --- 递归、非递归
- RSA算法理解与实现
- CRSNet: Dilated Convolutional Neural Networks for Underatanding the Highly Congested Scenes
热门文章
- 一起读懂传说中的经典:受限玻尔兹曼机
- 手把手教你java快速过滤关键词
- pca 累积方差贡献率公式_机器学习数据降维方法:PCA主成分分析
- html答题赚钱源码,WTS在线答题系统 v1.0.0
- 计算机语言学考研科目,语言学考研笔记整理(共16页)
- 各纬度气候分布图_欧洲气候特征:以温带气候类型为主,是海洋性气候最显著的大洲...
- python量化常用_Python量化常用函数
- 算法设计与分析——算法思想总结
- Java并发之volatile
- 高等数学下-赵立军-北京大学出版社-题解-练习9.2