「李超线段树」学习笔记

刚接触李超线段树不久,感觉还是很容易掌握的,写一篇简单的博客记录一下
李超线段树解决的是这样一类问题:给定二维平面上的一些线段,求 x = p o s x=pos x=pos与这些线段的交点最高的那个点,例如下面这个图,给定 p o s = 4 pos=4 pos=4,则答案就是那个虚线与红线的交点
在这里插入图片描述
顾名思义,李超线段树首先是棵线段树,其核心是维护每个区间的“最优势线段”,即在每个区间的中点处最高的线段。

为了更好理解李超线段树,先介绍查询步骤更好一点

  • 查询步骤

    设查询的位置为 p o s pos pos,对于线段树上的每一个包含 p o s pos pos的区间,取所有这些区间记录的最优势线段的对应 x = p o s x=pos x=pos下的最高点
  • 插入线段步骤

    设待插入的线段左端点 L L L,右端点 R R R,其函数表达式为 f ( x ) f(x) f(x),对于线段树上的每一个完全被包含在区间 [ L , R ] [L,R] [L,R]内的区间,更新该区间的“最优势线段”,分类讨论如下
  1. 若该区间还没有记录最优势线段,则记录最优势线段并返回

  2. 设该区间记录的最优势线段函数表达式为 f 1 ( x ) f_1(x) f1(x),若 f ( L ) > f 1 ( L ) f(L)>f_1(L) f(L)>f1(L) f ( R ) > f 1 ( R ) f(R)>f_1(R) f(R)>f1(R),则更换最优势线段为 f 1 ( x ) f_1(x) f1(x),并返回
    在这里插入图片描述

  3. 若是两线段交叉的情况,另 m i d = ( L + R ) 2 mid=\frac{(L+R)}{2} mid=2(L+R),若 f ( m i d ) > f 1 ( m i d ) f(mid)>f_1(mid) f(mid)>f1(mid),则交换 f ( x ) f(x) f(x) f 1 ( x ) f_1(x) f1(x),由于交换后必有 f 1 ( m i d ) ≥ f ( m i d ) f_1(mid)\geq f(mid) f1(mid)f(mid),继续讨论 f ( x ) f(x) f(x) f 1 ( x ) f_1(x) f1(x)的交点横坐标 x x x情况

    • x > m i d x>mid x>mid
      这时用劣势线段 ( ( 也就是线段 f ( x ) ) f(x)) f(x) 去继续更新右区间,注意此时的 f ( x ) f(x) f(x)可能是交换后的【如果不是交换后的那么显然要去更新右区间】,其实这并不影响,因为如果是交换后的,那么上一次插入交换前的那个线段的时候,更新完当前区间就返回了,并没有更新右区间,而当交换后,如果不用劣势线段去跟新右区间,将丢失劣势线段的信息,而对于区间 [ x , R ] [x,R] [x,R]内的点来说,这条劣势线段才是取的最高的那条,所以必须用劣势线段去更新右区间
      在这里插入图片描述
    1. x &lt; m i d x&lt;mid x<mid
      同上分析可知需要用劣势线段去更新左区间
    2. x = m i d x=mid x=mid
      当前区间最优势线段可以不更新【因为中点函数值都一样】,但是得更新左区间或者右区间,具体更新哪一个区间根据插入的线段在哪边更高就更新哪边
      在这里插入图片描述
  • 模板

struct line{
	double k,b;
	int l,r,flag;  //l,r表示线段的左端点和右端点,而线段树的左端点和右端点是不显示存储的
	line(double a=0,double c=0,int d=0,int e=0,int f=0){
		k=a;b=c;l=d;r=e;flag=f;
	}
}seg[maxn<<2];

struct segment_tree{
	inline double calc(line l1,int pos) {return l1.k*pos+l1.b;}  //kx+b
	inline int cross(line l1,line l2) {return floor((l1.b-l2.b)/(l2.k-l1.k));}  //l1 l2交点

	void build(int le,int ri,int id){ 
		seg[id].l=1;seg[id].r=50000;seg[id].flag=0;
		if(le==ri) return;
		int mid=(le+ri)>>1;
		build(le,mid,id<<1);build(mid+1,ri,id<<1|1);
	}

	void insert(int le,int ri,int id,line ins){  //le,ri为需要维护的区间,id为线段树区间编号,ins为需要插入的线段
		if(ins.l<=le&&ins.r>=ri){
			if(!seg[id].flag) {seg[id]=ins;seg[id].flag=1;}
			else if((calc(ins,le)-calc(seg[id],le))>eps&&(calc(ins,ri)-calc(seg[id],ri))>eps) {seg[id]=ins;}
			else if((calc(ins,le)-calc(seg[id],le))>eps||(calc(ins,ri)-calc(seg[id],ri))>eps){
				int mid=(le+ri)>>1;
				if(calc(ins,mid)-calc(seg[id],mid)>eps) swap(seg[id],ins);
				if(cross(seg[id],ins)-mid<-eps) insert(le,mid,id<<1,ins);
				else if(cross(seg[id],ins)-mid>eps) insert(mid+1,ri,id<<1|1,ins);
				else{
					if(calc(seg[id],le)<calc(ins,le)) insert(le,mid,id<<1,ins);
					else insert(mid+1,ri,id<<1|1,ins); 
				}
			}
		}else{
			int mid=(le+ri)>>1;
			if(ins.l<=mid) insert(le,mid,id<<1,ins);
			if(ins.r>mid) insert(mid+1,ri,id<<1|1,ins);
		}
	}

	double query(int le,int ri,int id,int pos){ //le,ri表示当前询问的区间,如果x=pos处没有对应的线段,会返回-inf
		if(le==ri) return seg[id].flag?calc(seg[id],pos):-inf;
		int mid=(le+ri)>>1;double res=seg[id].flag?calc(seg[id],pos):-inf;
		if(pos<=mid) res=max(res,query(le,mid,id<<1,pos));
		else res=max(res,query(mid+1,ri,id<<1|1,pos));
		return res;
	}
}tree;
Time Limit: 15 Sec Memory Limit: 162 MB
Submit: 2583 Solved: 965

Description

在这里插入图片描述

Input

第一行 :一个整数 N N N ,表示方案和询问的总数。
接下来 N N N行,每行开头一个单词“ Q u e r y Query Query”或“ P r o j e c t Project Project”。
若单词为 Q u e r y Query Query,则后接一个整数T,表示 B l u e M a r y Blue Mary BlueMary询问第 T T T天的最大收益。
若单词为 P r o j e c t Project Project,则后接两个实数 S , P S,P SP,表示该种设计方案第一天的收益 S S S,以及以后每天比上一天多出的收益 P P P
1 &lt; = N &lt; = 1000001 &lt; = T &lt; = 500000 &lt; P &lt; 100 , ∣ S ∣ &lt; = 1 0 6 1 &lt;= N &lt;= 100000 1 &lt;= T &lt;=50000 0 &lt; P &lt; 100,| S | &lt;= 10^6 1<=N<=1000001<=T<=500000<P<100S<=106
提示:本题读写数据量可能相当巨大,请选手注意选择高效的文件读写方式。

Output

对于每一个 Q u e r y Query Query,输出一个整数,表示询问的答案,并精确到整百元(以百元为单位,
例如:该天最大收益为 210 210 210 290 290 290时,均应该输出 2 2 2)。没有方案时回答询问要输出 0 0 0

Sample Input

10
Project 5.10200 0.65000
Project 2.76200 1.43000
Query 4
Query 2
Project 3.80200 1.17000
Query 2
Query 3
Query 1
Project 4.58200 0.91000
Project 5.36200 0.39000

Sample Output

0
0
0
0
0

题解

  • 每一个方案随时间的变化规律都可以表示成一个直线方程,然后就是模版题了
  • 注意描述中“一开始没有收到任何方案时,你可以认为每天的最大收益值是 0 0 0”这句话还是有点歧义的,并不是说没有插入任何线段之前查询的话输出 0 0 0,插入之后在所有方案中查询一个最高点。而是任何时间查询都可以是 0 0 0,也就是可以不选择任何方案,所以就直接插入一个 k = 0 , b = 0 k=0,b=0 k=0b=0的线段就行了

代码

#include<bits/stdc++.h>

using namespace std;
const int maxn=1e5+10;
#define eps 1e-9
#define inf 0x3f3f3f3f

struct line{
	double k,b;
	int l,r,flag;  //l,r表示线段的左端点和右端点,而线段树的左端点和右端点是不显示存储的
	line(double a=0,double c=0,int d=0,int e=0,int f=0){
		k=a;b=c;l=d;r=e;flag=f;
	}
}seg[maxn<<2];

struct segment_tree{
	inline double calc(line l1,int pos) {return l1.k*pos+l1.b;}  //kx+b
	inline int cross(line l1,line l2) {return floor((l1.b-l2.b)/(l2.k-l1.k));}  //l1 l2交点

	void build(int le,int ri,int id){   //这个操作好像没啥用233
		seg[id].l=1;seg[id].r=50000;seg[id].flag=0;
		if(le==ri) return;
		int mid=(le+ri)>>1;
		build(le,mid,id<<1);build(mid+1,ri,id<<1|1);
	}

	void insert(int le,int ri,int id,line ins){  //le,ri为需要维护的区间,id为线段树区间编号,ins为需要插入的线段
		if(ins.l<=le&&ins.r>=ri){
			if(!seg[id].flag) {seg[id]=ins;seg[id].flag=1;}
			else if((calc(ins,le)-calc(seg[id],le))>eps&&(calc(ins,ri)-calc(seg[id],ri))>eps) {seg[id]=ins;}
			else if((calc(ins,le)-calc(seg[id],le))>eps||(calc(ins,ri)-calc(seg[id],ri))>eps){
				int mid=(le+ri)>>1;
				if(calc(ins,mid)-calc(seg[id],mid)>eps) swap(seg[id],ins);
				if(cross(seg[id],ins)-mid<-eps) insert(le,mid,id<<1,ins);
				else if(cross(seg[id],ins)-mid>eps) insert(mid+1,ri,id<<1|1,ins);
				else{
					if(calc(seg[id],le)<calc(ins,le)) insert(le,mid,id<<1,ins);
					else insert(mid+1,ri,id<<1|1,ins); 
				}
			}
		}else{
			int mid=(le+ri)>>1;
			if(ins.l<=mid) insert(le,mid,id<<1,ins);
			if(ins.r>mid) insert(mid+1,ri,id<<1|1,ins);
		}
	}

	double query(int le,int ri,int id,int pos){ //le,ri表示当前询问的区间
		if(le==ri) return seg[id].flag?calc(seg[id],pos):-inf;
		int mid=(le+ri)>>1;double res=seg[id].flag?calc(seg[id],pos):-inf;
		if(pos<=mid) res=max(res,query(le,mid,id<<1,pos));
		else res=max(res,query(mid+1,ri,id<<1|1,pos));
		return res;
	}
}tree;

int n,pos,l,r;
char opt[100];
double k,b;

int main()
{
	scanf("%d",&n);tree.insert(1,50000,1,line(0,0,1,50000,1));
	while(n--){
		scanf("%s",opt+1);
		if(opt[1]=='P'){
			scanf("%lf %lf",&b,&k);
			tree.insert(1,50000,1,line(k,b-k,1,50000,1));
		}else{
			scanf("%d",&pos);double res=tree.query(1,50000,1,pos);
			printf("%d\n",(int)floor(res/100));
		}
	}
}
Time Limit: 10 Sec Memory Limit: 256 MB
Submit: 493 Solved: 162

Description

q q q n n n只机器人,一开始他把机器人放在了一条数轴上,第 i i i只机器人在ai的位置上静止,而自己站在原点。在这
之后小 q q q会执行一些操作,他想要命令一个机器人向左或者向右移动 x x x格。但是机器人似乎听不清小 q q q的命令,事实
上它们会以每秒 x x x格的速度匀速移动。看着自己的机器人越走越远,小 q q q很着急,他想知道当前离他(原点)最远的
机器人有多远。具体的操作以及询问见输入格式。注意,不同的机器人之间互不影响,即不用考虑两个机器人撞在
了一起的情况。

Input

共有 m m m个事件,输入将会按事件的时间顺序给出。第一行两个正整数 n , m n,m n,m。接下来一行 n n n个整数,第 i i i个数是 a i a_i ai,表示
i i i个机器人初始的位置(初始移动速度为 0 0 0)。接下来 m m m行,每行行首是一个非负整数 t i t_i ti,表示该事件点发生的时
刻(以秒为单位)。第二个是一个字符串 S S S,代表操作的种类。数字与字符串之间用一个空格隔开。接下来的输入
S S S的种类分类。若 S S S是“ c o m m a n d command command”(不带引号),则接下来两个整数 k i , x i k_i,x_i ki,xi,表示小 q q q对第 k i k_i ki个机器人执行了操作
,该机器人的速度将会被重置,变为向数轴正方向每秒移动 x i x_i xi格(若 x i x_i xi为负数就相当于向数轴负方向每秒移动 ∣ x i ∣ ∣x_i ∣ xi格)。保证 1 ≤ k i ≤ n 1\leq k_i\leq n 1kin。若 S S S是“ q u e r y query query”(不带引号),则你需要输出当前离原点最远的机器人有多远。保证 t 1 ≤ t 2 ≤ t 2 ≤ . . . ≤ t m t_1 \leq t_2 \leq t_2 \leq...\leq tm t1t2t2...tm。(注:若同一时间发生多次操作,则按读入顺序依次执行)

Output

对于每个 q u e r y query query询问,输出一行,包含一个整数表示正确的答案。 C / C + + C/C++ C/C++输入输出 l o n g   l o n g long\ long long long时请用 % l l d \%lld %lld。由于本题数
据量较大,建议不要使用 c i n / c o u t cin/cout cin/cout进行输入输出。

Sample Input

4 5
-20 0 20 100
10 command 1 10
20 command 3 -10
30 query
40 command 1 -30
50 query

Sample Output

180
280

HINT

第一个命令执行时,各个机器人的位置为: − 20 , 0 , 20 , 100 −20,0,20,100 20,0,20,100
第二个命令执行时,各个机器人的位置为: 80 , 0 , 20 , 100 80,0,20,100 80,0,20,100
第一个询问时,各个机器人的位置为: 180 , 0 , − 80 , 100 180,0,−80,100 180,0,80,100
第三个命令执行时,各个机器人的位置为: 280 , 0 , − 180 , 100 280,0,−180,100 280,0,180,100
第二个询问时,各个机器人的位置为: − 20 , 0 , − 280 , 100 −20,0,−280,100 20,0,280,100

限制与约定

c o m m a n d command command 的个数为 C C C q u e r y query query 的个数为 Q Q Q。(所以 C + Q = m C+Q=m C+Q=m
对于所有的事件满足 0 ≤ t i ≤ 1 0 9 0 \leq ti \leq 10^9 0ti109,对于所有的 c o m m a n d command command 满足 ∣ x i ∣ ≤ 1 0 4 ∣x_i∣\leq 10^4 xi104
对于所有的机器人满足 ∣ a i ∣ ≤ 1 0 9 ∣ai∣\leq 10^9 ai109
N , C ≤ 1 0 5 N,C \leq 10^5 N,C105
Q ≤ 5 ∗ 1 0 5 Q \leq 5*10^5 Q5105

Source

2015年集训队互测 By zhj

题意:

  • 就是n个机器人在x轴上,给你所有机器人初始(0时刻)位置静止,然后给定两种操作,一个是让某个机器人以一定的速度开始运动,另一个操作是查询某个时刻距离原点最远的机器人的距离

题解

  • 网上很多大神都是写的李超线段树+离散化,个人更喜欢动态开点
  • 很容易看出来位置-时间是一个一次函数,所以就转化为李超线段树解决,首先需要明确的是后面的修改不会对前面的查询造成影响,因为 t 1 ≤ t 2 ≤ . . . ≤ t m t_1\leq t_2\leq... \leq t_m t1t2...tm。所以考虑离线处理,即将所有线段插入到线段树中,然后再一起查询所有答案,注意有一个小的 t r i k trik trik就是同一个时间对同一个点可能修改很多次,这时要注意保留最后一次修改的那个,这个 d e b u g debug debug数据我放在了代码后面,然后还有一个小技巧就是不用去维护最低的点,每次插入的时候同时插入一条关于 x x x轴对称的线段就行了,这样就只用维护最大值了,分析一下时间复杂度,首先查询是 O ( l o g 1 0 9 ) O(log 10^9) O(log109),修改过程也是 O ( l o g 1 0 9 ) O(log 10^9) O(log109),总时间复杂度 O ( m l o g 1 0 9 ) O(m log 10^9) O(mlog109),空间复杂度 O ( m l o g 1 0 9 ) O(mlog 10^9) O(mlog109)
  • 同时 g e t get get到一个新操作
    vector::insert()
    iterator insert ( iterator position, const T& x );
    void insert ( iterator position, size_type n, const T& x );
    
    template <class InputIterator>
    void insert ( iterator position, InputIterator first, InputIterator last );
    
    插入新的元素,
    
    第一个函数,在迭代器指定的位置前插入值为x的元素
    
    第二个函数,在迭代器指定的位置前插入n个值为x的元素
    
    第三个函数,在迭代器指定的位置前插入另外一个容器的一段序列迭代器first到last
    
    若插入新的元素后总得元素个数大于capacity,则又一次分配空间
    

代码

#include<bits/stdc++.h>

using namespace std;
const int maxn=2e5+10;
#define eps 1e-9
#define inf 2e18
#define L 0
#define R 1e9

struct line{
	long long k,b;
	int l,r,flag,ls,rs;  //l,r表示线段的左端点和右端点,而线段树的左端点和右端点是不显示存储的,ls左儿子,rs右儿子
	line(long long a=0,long long c=0,int d=0,int e=0,int f=0,int g=0,int h=0){
		k=a;b=c;l=d;r=e;flag=f;ls=g;rs=h;
	}
}seg[maxn*30];

struct segment_tree{
	int tot;
	segment_tree() {tot=1;}
	inline long long calc(line l1,int pos) {return l1.k*pos+l1.b;}  //kx+b
	inline long long cross(line l1,line l2) {return (long long)((l1.b-l2.b)/(l2.k-l1.k));}  //l1 l2交点

	void insert(int le,int ri,int id,line ins){  //le,ri为需要维护的区间,id为线段树区间编号,ins为需要插入的线段
		if(ins.l>ins.r) return;
		if(ins.l<=le&&ins.r>=ri){
			if(!seg[id].flag) {
				swap(seg[id],ins);seg[id].ls=ins.ls;seg[id].rs=ins.rs;
			}
			else if((calc(ins,le)-calc(seg[id],le))>eps&&(calc(ins,ri)-calc(seg[id],ri))>eps) {
				swap(seg[id],ins);seg[id].ls=ins.ls;seg[id].rs=ins.rs;
			}
			else if((calc(ins,le)-calc(seg[id],le))>eps||(calc(ins,ri)-calc(seg[id],ri))>eps){
				int mid=(le+ri)>>1;
				if(calc(ins,mid)-calc(seg[id],mid)>eps) {
					swap(seg[id],ins);seg[id].ls=ins.ls;seg[id].rs=ins.rs;
				}
				if(cross(seg[id],ins)-mid<-eps) {
					if(!seg[id].ls) seg[id].ls=++tot;
					insert(le,mid,seg[id].ls,ins);
				}
				else if(cross(seg[id],ins)-mid>eps) {
					if(!seg[id].rs) seg[id].rs=++tot;
					insert(mid+1,ri,seg[id].rs,ins);
				}
				else{
					if(calc(seg[id],le)<calc(ins,le)) {
						if(!seg[id].ls) seg[id].ls=++tot;
						insert(le,mid,seg[id].ls,ins);
					}
					else {
						if(!seg[id].rs) seg[id].rs=++tot;
						insert(mid+1,ri,seg[id].rs,ins); 
					}
				}
			}
		}else{
			int mid=(le+ri)>>1;
			if(ins.l<=mid) {
				if(!seg[id].ls) seg[id].ls=++tot;
				insert(le,mid,seg[id].ls,ins);
			}
			if(ins.r>mid) {
				if(!seg[id].rs) seg[id].rs=++tot;
				insert(mid+1,ri,seg[id].rs,ins);
			}
		}
	}

	long long query(int le,int ri,int id,int pos){ //le,ri表示当前询问的区间
		if(le==ri) return calc(seg[id],pos);
		int mid=(le+ri)>>1;long long res=calc(seg[id],pos);
		if(pos<=mid&&seg[id].ls)  res=max(res,query(le,mid,seg[id].ls,pos));
		else if(pos>mid&&seg[id].rs) res=max(res,query(mid+1,ri,seg[id].rs,pos));
		return res;
	}
}tree;

char opt[100];
vector<pair<int,long long> > pos[maxn];
int a[maxn],n,m,x,t,que[3*maxn],cnt=0;long long k;


int main()
{
	scanf("%d %d",&n,&m);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	for(int i=1;i<=m;i++){
		scanf("%d %s",&t,opt+1);
		if(opt[1]=='q') que[++cnt]=t;
		else{
			scanf("%d %lld",&x,&k);
			pos[x].push_back(make_pair(t,k));
		}
	}
	for(int i=1;i<=n;i++) pos[i].insert(pos[i].begin(),make_pair(0,0)),pos[i].insert(pos[i].end(),make_pair(1e9+1,0));
	for(int i=1;i<=n;i++){
		long long last=a[i];
		for(int j=0;j<pos[i].size()-1;j++){
			long long b=last-pos[i][j].first*pos[i][j].second,k=pos[i][j].second;
			tree.insert(L,R,1,line(pos[i][j].second,b,pos[i][j].first,pos[i][j+1].first-1,1));
			tree.insert(L,R,1,line(-pos[i][j].second,-b,pos[i][j].first,pos[i][j+1].first-1,1));
			last=pos[i][j].second*pos[i][j+1].first+b;
		}
	}
	for(int i=1;i<=cnt;i++) printf("%lld\n",tree.query(L,R,1,que[i]));
}

/* 答案48
1 3
0
0 command 1 -3
0 command 1 -6
8 query
*/
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值