李超线段树学习笔记
李超线段树
解决的经典问题是每次可以在平面内添加一条 \(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 行,每行开头一个单词Query
或Project
。
若单词为Query
,则后接一个整数 T,表示 Blue Mary 询问第 T 天的最大收益。
若单词为Project
,则后接两个实数 S,P,表示该种设计方案第一天的收益 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
要求在平面直角坐标系下维护两个操作:
- 在平面上加入一条线段。记第 i 条被插入的线段的标号为 i
- 给定一个数 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。战斗看作回合制,每回合进程如下:
- 我方选择对方某台人形兵器并攻击,令其护甲值减少ATK,若护甲值<=0则被破坏。
- 敌方每台未被破坏的人形兵器攻击我方基地造成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 的链还得分开做。
看下别人的代码吧。