当前位置:   article > 正文

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

线段树维护凸函数

前言

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

李超树

引入(斜率优化)

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


我们看一个 d p dp dp状态转移方程式
d p [ i ] = m a x { d p [ j ] + b i ∗ a j + a [ i ] } , j < i dp[i]=max\{dp[j]+b_i*a_j+a[i]\},j<i dp[i]=max{dp[j]+biaj+a[i]},j<i

一般这种长相,噢不,准确来说,很多 d p dp dp的这种长相,都会跟凸包挂钩
在这里插入图片描述
因为出题人不想考那么板的dp送分,就只能搞搞单调让你优化
而我们目前已知的解决凸包就是斜率优化


假设题目保证 a i a_i ai递增

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


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

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

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


此时我们怎么办呢??

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

在这里插入图片描述

什么是李超树?

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

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

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

李超树活着能干点什么?

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

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

只要你能写出 f [ i ] = m a x / m i n ( 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 = k x + b y=kx+b y=kx+b
李超树就能硬刚!
在这里插入图片描述

算法思想(使用手册?)

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

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

插入

分两种情况

  1. 完全覆盖
    在这里插入图片描述
    新的红线在 [ l , r ] [l,r] [l,r]区间上完全优于原来该区间存的直线,那么将直接进行全覆盖
    然后就 r e t u r n return return,不用去更新左右子树
    至于为什么?跟查询写法有关,也跟部分覆盖挂钩
    因为此时更新后如果没有出现部分覆盖的情况,查询我们也会查到这条直线的值
    如果出现了部分覆盖的情况,此时的红线就会部分下放更新
    反正此时的黄线已经是个废物了,只需要知道这点就o了
  2. 部分覆盖
    2-1.
    在这里插入图片描述
    首先这个区间 [ l , r ] [l,r] [l,r]存的直线与 m i d mid mid挂钩,此时红线在 m i d mid mid处的函数值优于黄线
    所以李超线段树 [ l , r ] [l,r] [l,r]这一个区间就会存红线的解析式
    那这个黄线呢??它也并不是全无用,我们需要继续递归右子树,去更新蓝色部分的区间
    在这里插入图片描述
    2-2.
    在这里插入图片描述
    此时红线在 m i d mid mid处的函数值劣于黄线,李超线段树 [ l , r ] [l,r] [l,r]这一个区间的解析式仍然是黄线
    但这个红线呢??也并不是全无用,我们需要继续递归做子树,去更新蓝色部分的区间
    在这里插入图片描述

这三种情况怎么判断呢?就是怎么写呢?
很简单——直线是单调的
我们只需要掌握 l , m i d , r l,mid,r l,mid,r三个点的两点函数值就可以了,具体可看模板
在这里插入图片描述

查询

每一个区间都存了一条直线,可能相同也可能不同
一个点被多个区间包含,也就会被多条直线包含
所以我们一路上下来遇到的每一个区间都要算一次 x x x对应的函数值,取最大值
有可能先遇到的直线的函数值比后遇到的直线的函数值还小,这是有可能的
因为那些区间的函数解析式是根据那些区间的中点 m i d i mid_i midi的函数值决定的
谁管你这个小虾皮
举个栗子
在这里插入图片描述
这一路上涉及到的四个点的每一条解析式(黑线)都要把 x x x带进去算出 y 1 , y 2 , y 3 , y 4 y_1,y_2,y_3,y_4 y1,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 ); 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

插入

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 );
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

查询

这个版本有限制!!!
这个版本有限制!!!
这个版本有限制!!!
在这里插入图片描述

也许是我写法问题
此版本查询维护的是直线,也就是说每一条直线都会覆盖到所有的查询点
我们知道 [ l , r ] [l,r] [l,r]区间维护的函数解析式是当 x = m i d x=mid x=mid时,函数值最大的那条解析式
因为每一条线都能覆盖完,所以每一个区间 [ l , r ] [l,r] [l,r]都会维护一条直线
于是乎此版本的优化: x = m i d x=mid x=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 ) );
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

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

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

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 );
	}
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

也就是说 [ l , r ] [l,r] [l,r]区间如果没有被一条直线完全覆盖的话,这个区间是没有存直线的
那么当我们继续沿用之前的优化, x = m i d x=mid x=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 ) );
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

直线也可看作一条无限长的线段,因此此版本适用范围广
在这里插入图片描述
如果不是很理解,下面两道例题就能很好的辨析出来,┏ (゜ω゜)=☞
线段游戏的样例就会发现查询的写法不同会有问题,可以分布调试看看

例题

板题: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;
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71

线段游戏

分析

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

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;
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103

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

ZZH的旅行

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

solution

在这里插入图片描述

code

有一坨不像我

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/盐析白兔/article/detail/479621
推荐阅读
相关标签