【李超树】凸包优化之李超线段树

前言

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

李超树

引入(斜率优化)

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


我们看一个\(dp\)状态转移方程式
\(dp[i]=max\{dp[j]+b_i*a_j+a[i]\},j<i\)

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


假设题目保证\(a_i\)递增

假设\(j<k<i\)且决策点\(j\)优于决策点\(k\),则有

\[dp[j]+b_i*a_j+a[i]>dp[k]+b_i*a_k+b[i]\ \ \ \ \ \ \ ① \]

\[dp[j]-dp[k]>b_i*(a_k-a_j)\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ ② \]

题目保证\(a\)单增,即\(a_k-a_j>0\),继续变形得到

\[\frac{dp[j]-dp[k]}{a_k-a_j}>b_i\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ ③ \]

上述过程其实就是斜率优化的推导过程,这种情况下我们仍然可以吃老本——走斜率优化


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

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

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


此时我们怎么办呢??

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

在这里插入图片描述

什么是李超树?

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

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

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

李超树活着能干点什么?

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

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

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

算法思想(使用手册?)

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

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

插入

分两种情况

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

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

查询

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

模板

判断是否覆盖(优不优)

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

查询

int query( int num, int l, int r, int x ) {
	int 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 ) );
}

例题

板题: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 ) );
} 

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

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

ZZH的旅行

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

solution

在这里插入图片描述

code

有一坨不像我🐎风的快读快输代码,是因为我\(T\)了,真的⛏不动了,加了就能\(A\)
在这里插入图片描述

#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
#define int long long
#define maxn 1000005
vector < pair < int, int > > G[maxn];
int n;
int aa[maxn], bb[maxn], dep[maxn];
int X[maxn], K[maxn], B[maxn], rt[maxn], dp[maxn];
int son[maxn][2];

namespace IO{
	const int sz=1<<22;
	char a[sz+5],b[sz+5],*p1=a,*p2=a,*t=b,p[105];
	inline char gc(){
		return p1==p2?(p2=(p1=a)+fread(a,1,sz,stdin),p1==p2?EOF:*p1++):*p1++;
	}
	template<class T> void read(T& x){
		x=0; char c=gc();
		for(;c<'0'||c>'9';c=gc());
		for(;c>='0'&&c<='9';c=gc())
			x=x*10+(c-'0');
	}
	inline void flush(){fwrite(b,1,t-b,stdout),t=b; }
	inline void pc(char x){*t++=x; if(t-b==sz) flush(); }
	template<class T> void print(T x,char c='\n'){
		if(x==0) pc('0'); int t=0;
		for(;x;x/=10) p[++t]=x%10+'0';
		for(;t;--t) pc(p[t]); pc(c);
	}
	struct F{~F(){flush();}}f; 
}
using IO::read;
using IO::print;

int insert( int x, int l, int r, int k, int b, int id ) {
	if( ! x ) {
		son[id][0] = son[id][1] = 0;
		K[id] = k, B[id] = b;
		return id;
	}
	int mid = ( l + r ) >> 1;
	if( X[mid] * K[x] + B[x] < X[mid] * k + b )
		swap( K[x], k ), swap( B[x], b );
	if( l < r ) {
		if( k >= K[x] ) son[x][1] = insert( son[x][1], mid + 1, r, k, b, id );
		else son[x][0] = insert( son[x][0], l, mid, k, b, id );
	}
	return x;
}

int query( int x, int l, int r, int id ) {
	if( ! x ) return -1e18;
	int ans = K[x] * X[id] + B[x];
	int mid = ( l + r ) >> 1;
	if( id <= mid ) ans = max( ans, query( son[x][0], l, mid, id ) );
	else ans = max( ans, query( son[x][1], mid + 1, r, id ) );
	return ans;
}

int merge( int x, int y, int l, int r ) {
	if( ! x || ! y ) return x + y;
	int mid = ( l + r ) >> 1;
	son[x][0] = merge( son[x][0], son[y][0], l, mid );
	son[x][1] = merge( son[x][1], son[y][1], mid + 1, r );
	if( K[x] * X[mid] + B[x] < K[y] * X[mid] + B[y] )	
		swap( K[x], K[y] ), swap( B[x], B[y] );
	if( l < r ) insert( x, l, r, K[y], B[y], y );
	return x;
}

void dfs1( int u, int fa ) {
	for( int i = 0;i < G[u].size();i ++ ) {
		int v = G[u][i].first, w = G[u][i].second;
		if( v == fa ) continue;
		else dep[v] = dep[u] + w, dfs1( v, u );
	}
}

void dfs2( int u, int fa ) {
	for( int i = 0;i < G[u].size();i ++ ) {
		int v = G[u][i].first;
		if( v == fa ) continue;
		else dfs2( v, u ), rt[u] = merge( rt[u], rt[v], 1, n );
	}
	dp[u] = max( 0ll, query( rt[u], 1, n, lower_bound( X + 1, X + n + 1, aa[u] + dep[u] ) - X ) );
	rt[u] = insert( rt[u], 1, n, bb[u], dp[u] - bb[u] * dep[u], u );
}

signed main() {
	read( n );
	for( int i = 1;i <= n;i ++ )
		read( aa[i] ), read( bb[i] );
	for( int i = 1, u, v, d;i < n;i ++ ) {
		read( u ), read( v ), read( d );
		G[u].push_back( make_pair( v, d ) );
		G[v].push_back( make_pair( u, d ) );
	}
	dfs1( 1, 0 );
	for( int i = 1;i <= n;i ++ )
		X[i] = aa[i] + dep[i];
	sort( X + 1, X + n + 1 );
	dfs2( 1, 0 );
	for( int i = 1;i <= n;i ++ )
		print( dp[i] );
	return 0;
}
posted @ 2020-12-02 16:53  读来过倒字名把才鱼咸  阅读(223)  评论(0编辑  收藏  举报