李超线段树总结

前言

前几天一场模拟赛T3就是李超线段树,结果完全不会这个科技,新高一的全都会啊。果然我菜得离谱。
理解了蛮久,主要是对“优势线段”的含义网上题解大多不是很清楚,有的甚至一笔带过直接讲实现。可能是我太弱了吧,花了一个下午用自己的方法理解了李超线段树,下面是一点点小总结(可能讲的比较碎)。

解决问题

  • 平面坐标系插入直线
  • 查询\(x=x_0\)与插入直线最高交点的纵坐标。

可以理解为维护了凸包,求\(x=x_0\)与凸包的交点的纵坐标。
当然它还同时支持插入线段\(O(log_n^2)\),查询\(x=x_0\)与插入线段最高交点的纵坐标。
下面以[JSOI2008]Blue Mary开公司为例题进行讲解。

例题

  1. 加入直线
  2. 查询\(x=x_0\)与插入直线最高交点纵坐标\((1\le x_0\le 50000)\)

首先想最暴力的暴力。
把加入的每一条直线记下来。然后对于\(x=x_0\),把所有的直线与它的交点都算出来,取最大值即可。
然后发现这样很不优,因为要把所有直线都记下来,而有的直线很明显是不可能成为最终答案所在直线的。
考虑维护的是凸包,所以每一条直线有贡献的一定是一段连续区间,考虑用线段树存储每个位置最大取值的备选直线。\(l_x\)表示\(x\)节点所代表区间的一条备选直线。\(x=x_0\)与插入直线最高交点可以在包含它的线段树节点储存的备选直线上取到。
说明两个概念:

  1. 位置\(x_0\)的备选直线:储存下来考虑它对\(x=x_0\)最高交点的贡献(即所谓的优势线段)
  2. 位置\(x_0\)的最优直线:\(x=x_0\)最高交点所在的直线

我们就考虑直线\(l':y=k_0x+b_0\)对区间\(x\)的影响。

  • \(x\)节点还没有储存备选直线,直接\(l_x=l'\),因为多一条备选直线没什么关系
  • \(l'\)完全在\(l_x\)上面,直接\(l_x=l'\),因为之前的\(l_x\)不可能是区间内任何位置的最优直线。
  • \(l'\)完全在\(l_x\)下面,直接返回,因为\(l'\)不可能是区间内任何位置的最优直线。
  • 若有交点,交在区间的左半边:那么对于区间右半边的位置来说,一定有一条直线完全在另一条上面,把高的那条直线存在当前节点,用另外一条直线更新左儿子所储存的备选直线。
  • 若交点在右半边,与上面同理,把高的放在当前节点,另一条往右儿子递归

我们可以理解为其类似于剪枝,不储存不需要的备选直线,因为每条直线都是已经考虑了对整个区间的影响之后才返回的(要么作为当前备选直线要么不可能有影响返回),所以每个位置的最优直线一定在包含它的节点所储存的备选直线中。而我们的备选线段保证了每次递归只会走向一个儿子,保证了复杂度。
感觉讲的好碎啊....主要是我也是初学理解不深,并且学的时候真的理解了好久,索性写的详细一点希望能一遍讲懂

代码实现

插入直线的时候我是把斜率和截距传了进去,同时注意一下这道题截距为\(S-P\)

#include<bits/stdc++.h>
#define ri register int
#define ll long long
#define ls (u<<1)
#define rs ((u<<1)|1)
using namespace std;
const int maxn = 2e5 + 10,mt = 5e4;
double k[maxn],b[maxn];
bool fl[maxn];
int n;
char op[10];
inline int rd(){
	int res = 0,f = 0; char ch = getchar();
	for(;!isdigit(ch);ch = getchar()) if(ch == '-') f = 1;
	for(;isdigit(ch);ch = getchar()) res = (res<<3) + (res<<1) + ch - 48;
	return f ? -res : res;
}
void modify(int u,int l,int r,double K,double B){
	if(l > r) return;
	if(!fl[u]){
		k[u] = K; b[u] = B; fl[u] = 1;
		return;
	}
	double l1 = l*k[u]+b[u],l2 = l*K+B,r1 = r*k[u]+b[u],r2 = r*K+B; // l2 & r2 is new
	if(l2 >= l1 && r2 >= r1){
		k[u] = K; b[u] = B;
		return;
	}
	if(l2 <= l1 && r2 <= r1) return;
	int mid = (r+l) >> 1;
	double mid1 = mid*k[u]+b[u],mid2 = mid*K+B; // mid2 is new
	if(l2 > l1){
		if(mid1 > mid2) modify(ls,l,mid,K,B);
		else swap(K,k[u]),swap(B,b[u]),modify(rs,mid + 1,r,K,B);
	}
	else{
		if(mid1 < mid2) swap(K,k[u]),swap(B,b[u]),modify(ls,l,mid,K,B);
		else modify(rs,mid + 1,r,K,B);
	}
}
double query(int u,int l,int r,int p){
	if(l==r) return p*k[u]+b[u];
	int mid = (r+l)>>1; double res = k[u]*p+b[u];
	if(p <= mid) res = max(res,query(ls,l,mid,p));
	else res = max(res,query(rs,mid + 1,r,p));
	return res;
}
int main(){
	n = rd();
	double K,B;
	int p;
	while(n--){
		scanf("%s",op + 1);
		if(op[1] == 'Q'){
			p = rd();
			double ans = query(1,1,mt,p);
			printf("%lld\n",(ll)(ans/100));
		}
		else{
			scanf("%lf%lf",&B,&K);
			modify(1,1,mt,K,B-K);
		}
	}
	return 0;
}

后言

若要插入线段,把线段在线段树上进行区间拆分,然后在区间内进行插入即可。
有的题查询的下标会为负,加上下标最大的绝对值,然后在询问和插入的时候注意一下实现即可
李超线段树还支持懒标记,维护\(tagk\)\(tagb\)可以支持将一个区间所有位置的值都加一个一次函数。
因为也是初学没练几道题,并且跟很多人的理解方式都不同,是理解的比较彻底还是麻烦就仁者见仁智者见智了。如果觉得还可以的话就支持下啦

posted @ 2021-09-10 22:38  Lumos壹玖贰壹  阅读(72)  评论(1编辑  收藏  举报