李超线段树学习笔记

李超线段树

解决的经典问题是每次可以在平面内添加一条 \(y=kx+b,x\in[l,r]\) 的线段,或者询问当 \(x=x_0\) 时,各个线段中最大的 \(y\)

其实可以对每个线段树区间维护动态凸包,然后标记永久化就行了。这样做复杂度不变,并且支持实数查询,但是很难写……需要对每个线段树节点维护极角序,二分后像半平面交那样弹出无用的。

因为 \(x_0\) 的取值范围是整数,所以李超线段树有更优的做法。李超线段树维护的是各段区间内优势线段优势线段是指这段区间内能成为最优线段的长度最大的那条线段。

往区间 \([l,r]\) 插入一条线段 \(c\) 时,就与当前区间 \((l,r)\) 内维护的优势线段 \(s\) 比较。

  • 若这两条线段在 \([l,r]\) 内满足一条完全覆盖了另一条,就直接更新优势线段并返回。
  • 否则用 \(c,s\) 中的较优者更新这个区间的答案,然后递归下去,用 \(c,s\) 中的较劣者去更新两个子区间中的一个。
    因为至少在 \([l,mid]\)\([mid+1,r]\) 这两个区间中的一者,\(c\) 完全覆盖了 \(s\),或 \(s\) 完全覆盖了 \(c\)

更新复杂度是定位区间和递归复杂度的乘积,为 \(O(\log^2 n)\)。查询复杂度还是 \(O(\log n)\)。注意若插入的是直线,那么定位区间的复杂度就省了。

JSOI2008 Blue Mary开公司

第一行 :一个整数 N ,表示方案和询问的总数。

接下来 N 行,每行开头一个单词QueryProject

若单词为Query,则后接一个整数 T,表示 Blue Mary 询问第 T 天的最大收益。

若单词为Project,则后接两个实数 SP,表示该种设计方案第一天的收益 S,以及以后每天比上一天多出的收益 P

1≤N≤100000,1≤T≤50000,0<P<100,∣S∣≤105

题解

这傻逼题就是裸题了。不过这种上古时期的题目真的烦,什么狗屁题目描述、输出格式,做题毫无用户体验。

co int N=50000;
struct func{
	LD k,b;
	il LD operator()(int x)co{
		return k*x+b;
	}
}s[N<<2];
#define lc (x<<1)
#define rc (x<<1|1)
LD query(int x,int l,int r,int p){
	if(l==r) return s[x](p);
	int mid=(l+r)>>1;
	LD ans=s[x](p);
	if(p<=mid) ans=max(ans,query(lc,l,mid,p));
	else ans=max(ans,query(rc,mid+1,r,p));
	return ans;
}
void change(int x,int l,int r,co func&f){
	if(l==r){
		if(f(l)>s[x](l)) s[x]=f;
		return;
	}
	int mid=(l+r)>>1;
	if(f.k>s[x].k){
		if(f(mid)>s[x](mid)) change(lc,l,mid,s[x]),s[x]=f;
		else change(rc,mid+1,r,f);
	}
	else{
		if(f(mid)>s[x](mid)) change(rc,mid+1,r,s[x]),s[x]=f;
		else change(lc,l,mid,f);
	}
}
int main(){
	for(int n=read<int>();n--;){
		char opt[10];scanf("%s",opt);
		if(opt[0]=='Q'){
			int T=read<int>();
			LD ans=query(1,1,N,T);
			printf("%.0Lf\n",floor(ans/100)); // edit 1:floor
		}
		else{
			LD S,P;scanf("%Lf%Lf",&S,&P);
			change(1,1,N,(func){P,S-P});
		}
	}
	return 0;
}

这道题我没有写直接返回的情况,但没什么影响。注意到我定义了一个operator(),这个很好用。

HEOI2013 Segment

要求在平面直角坐标系下维护两个操作:

  1. 在平面上加入一条线段。记第 i 条被插入的线段的标号为 i
  2. 给定一个数 k,询问与直线 x = k 相交的线段中,交点最靠上的线段的编号。

对于 100%的数据,1 ≤ n ≤ 105, 1 ≤ k, x0, x1 ≤ 39989, 1 ≤ y0 ≤ y1 ≤ 109

题解

正宗的李超线段树。发现只有一种形式的代码最好写。

co double eps=1e-8;
il int dcmp(double x){
	return abs(x)<=eps?0:x<0?-1:1;
}

co int M=100000+1;
double k[M],b[M];
il double f(int x,int p){
	return k[x]*p+b[x];
}
il bool judge(int x,int y,int p){
	double fx=f(x,p),fy=f(y,p);
	return dcmp(fx-fy)?fx<fy:x>y;
}

co int N=39989;
int dat[N<<2];
#define lc (x<<1)
#define rc (x<<1|1)
void update(int x,int l,int r,int ql,int qr,int nw){
	if(ql<=l&&r<=qr){
		if(judge(nw,dat[x],l)&&judge(nw,dat[x],r)) return; // completely worse
		if(judge(dat[x],nw,l)&&judge(dat[x],nw,r)) {dat[x]=nw;return;}
		int mid=(l+r)>>1;
		if(judge(dat[x],nw,mid)) swap(dat[x],nw); // better choice for mid point
		if(judge(dat[x],nw,l)) update(lc,l,mid,ql,qr,nw);
		else update(rc,mid+1,r,ql,qr,nw);
		return;
	}
	int mid=(l+r)>>1;
	if(ql<=mid) update(lc,l,mid,ql,qr,nw);
	if(qr>mid) update(rc,mid+1,r,ql,qr,nw);
}
int query(int x,int l,int r,int p){
	if(l==r) return dat[x];
	int mid=(l+r)>>1;
	int ans=p<=mid?query(lc,l,mid,p):query(rc,mid+1,r,p);
	if(judge(ans,dat[x],p)) ans=dat[x];
	return ans;
}

int main(){
	int tot=0,ans=0;
	for(int n=read<int>();n--;){
		if(read<int>()){ // update
			int x0=(read<int>()+ans-1)%39989+1,y0=(read<int>()+ans-1)%1000000000+1;
			int x1=(read<int>()+ans-1)%39989+1,y1=(read<int>()+ans-1)%1000000000+1;
			if(x0>x1) swap(x0,x1),swap(y0,y1);
			++tot;
			if(x0==x1) k[tot]=0,b[tot]=max(y0,y1);
			else k[tot]=(double)(y1-y0)/(x1-x0),b[tot]=y1-k[tot]*x1;
			update(1,1,N,x0,x1,tot);
		}
		else{ // query
			int p=(read<int>()+ans-1)%39989+1;
			printf("%d\n",ans=query(1,1,N,p));
		}
	}
	return 0;
}

BZOJ4700 适者

敌方有n台人形兵器,每台的攻击力为Ai,护甲值为Di。我方只有一台人形兵器,攻击力为ATK。战斗看作回合制,每回合进程如下:

  1. 我方选择对方某台人形兵器并攻击,令其护甲值减少ATK,若护甲值<=0则被破坏。
  2. 敌方每台未被破坏的人形兵器攻击我方基地造成Ai点损失。

但是,在第一回合开始之前,某两台敌方的人形兵器被干掉了(秒杀)。问最好情况下,我方基地会受到多少点损失。

3<=n<=3×105

题解

先不管秒杀的问题。可以算出破坏每个敌人所需时间 \(T_i=\lceil \frac{D_i}{ATK} \rceil\)

这种花费与时间成正比的问题,先考虑排序法。对于相邻两个敌人\(i,j\ (j=i+1)\)

选择 花费
\(i\)\(j\) \(A_iT_i+A_j(T_i+T_j)\)
\(i\)\(j\) \(A_jT_j+A_i(T_j+T_i)\)

所以要比较的就是 \(\frac{T}{A}\),按照这个排序即可。这样便处理完了没有秒杀的最优解。

\(T\) 的前缀和为 \(preT\)\(A\) 的后缀和为 \(sufA\)。考虑秒杀一个敌人,损失会减少 \(V_i=preT_{i-1}A_i+T_isufA_i-A_i\)。这个是固定的。而秒杀两个敌人无非是最大化 \(-T_iA_j\)

抽象出来,固定 \(i\) 后,无非就是最大化 \(-k_jT_i+b_j\),而这个用李超线段树维护即可。

时间复杂度 \(O(n\log n)\)

co int N=300000+10;
struct node {int A,T;}p[N];

il bool operator<(co node&a,co node&b){
	return a.T*b.A<a.A*b.T;
}

LL preT[N],sufA[N];
LL k[N],b[N];

LL f(int x,int p){
	return k[x]*p+b[x];
}
bool cover(int x,int y,int p){
	return f(x,p)>=f(y,p);
}

int s[N<<2];
#define lc (x<<1)
#define rc (x<<1|1)
void insert(int x,int l,int r,int i){
	if(l==r){
		if(cover(i,s[x],l)) s[x]=i;
		return;
	}
	int mid=(l+r)>>1;
	if(k[i]>k[s[x]]){
		if(cover(i,s[x],mid)) insert(lc,l,mid,s[x]),s[x]=i;
		else insert(rc,mid+1,r,i);
	}
	else{
		if(cover(i,s[x],mid)) insert(rc,mid+1,r,s[x]),s[x]=i;
		else insert(lc,l,mid,i);
	}
}
LL query(int x,int l,int r,int p){
	LL ans=f(s[x],p);
	if(l==r) return ans;
	int mid=(l+r)>>1;
	if(p<=mid) ans=max(ans,query(lc,l,mid,p));
	else ans=max(ans,query(rc,mid+1,r,p));
	return ans;
}

int main(){
	int n=read<int>(),ATK=read<int>();
	for(int i=1;i<=n;++i) read(p[i].A),p[i].T=(read<int>()+ATK-1)/ATK;
	sort(p+1,p+n+1);
	for(int i=1;i<=n;++i) preT[i]=preT[i-1]+p[i].T;
	for(int i=n;i>=1;--i) sufA[i]=sufA[i+1]+p[i].A;
	LL ans=0;
	for(int i=1;i<=n;++i){
		k[i]=-p[i].A,b[i]=preT[i-1]*p[i].A+p[i].T*sufA[i]-p[i].A;
		ans+=p[i].T*sufA[i]-p[i].A;
	}
	LL del=0;
	insert(1,1,n,n);
	for(int i=n-1;i>=1;--i){
		del=max(del,query(1,1,n,p[i].T)+b[i]);
		insert(1,1,n,i);
	}
	printf("%lld\n",ans-del);
	return 0;
}

SDOI2016 游戏

Alice 和 Bob 在玩一个游戏。

游戏在一棵有 n 个点的树上进行。最初,每个点上都只有一个数字,那个数字是 123456789123456789。

有时,Alice 会选择一条从 s 到 t 的路径,在这条路径上的每一个点上都添加一个数字。对于路径上的一个点 r,若 r 与 s 的距离是 dis,那么 Alice 在点 r 上添加的数字是 a×dis+b。

有时,Bob 会选择一条从 s 到 t 的路径。他需要先从这条路径上选择一个点,再从那个点上选择一个数字。Bob 选择的数字越小越好,但大量的数字让 Bob 眼花缭乱。Bob 需要你帮他找出他能够选择的最小的数字。

n≤100000,m≤100000,∣a∣≤10000,0<=w,|b|<=109

题解

刘老爷:强行上树系列

确实这题除了码农就没别的了。树剖两端到 LCA 的链还得分开做。

看下别人的代码吧。

posted on 2019-08-28 17:04  autoint  阅读(479)  评论(0编辑  收藏  举报

导航