线段树相关
一、基础线段树
二、扫描线
就是维护矩形的面积/周长并。
1. 面积并
用一条线从下往上扫,将所有矩形变成一片一片的(感性理解),容易知道最多 \(2n-1\) 片,每片的贡献是 \(当前线段总长度\times 这片的厚度\)。
最多 \(2n\) 条竖直的线,所以最多 \(2n\) 个端点,最多 \(2n-1\) 个区间。
离散化后计算出不同端点的个数 \(cnt\)。线段树上 \([l,r]\) 的区间代表实际上第 \(l\) 个端点到第 \(r+1\) 个端点。维护每一个区间的长度。
时间复杂度:\(O(n\log n)\)。
查看代码
点击查看代码
ll ans=0;//一般要开 long long
int n,t[2000010],p[2000010],q[2000010],u[2000010],cnt;
//数组含义:t[x]:编号为 x 的区间当前的线段长度之和
//p[x]:编号为 x 的区间被完全覆盖的次数
//q[x]:离散化数组,编号为 x 的端点所对应的实际横坐标
//区间 [l,r] 对应端点 [l,r+1]
struct scl{//区间的左端点、右端点、纵坐标、这条边是矩形的下边(h=1)还是上边(h=-1)
int l,r,x,h;
}s[500010];
bool cmp(scl _1,scl _2){//按照纵坐标排好序
return _1.x<_2.x;
}
void modify(int x,int l,int r,int ql,int qr,int v){
if(ql<=l&&r<=qr){
p[x]+=v;//区间被覆盖的次数
t[x]=p[x]?(q[r+1]-q[l]):(t[x<<1]+t[x<<1|1]);
//如果区间被完全覆盖了>=1次,该区间的线段长度就是区间总长;
//否则 p[x]=0,v 一定是 -1,因为 p[x] 一定大于等于零,
//如果 v 是 1 的话 ,p[x] 一定大于零,矛盾!
//所以这条边是矩形的上边,它的对应的下边在修改时一定也是搜索到 x 就返回了,
//对 x 的子节点没有影响,所以去掉该矩形影响后 t[x] 直接用两个子节点更新即可
//不需要懒标记
return;
}
int mid=(l+r)>>1;
if(ql<=mid)modify(x<<1,l,mid,ql,qr,v);
if(qr>=mid)modify(x<<1|1,mid+1,r,ql,qr,v);
t[x]=p[x]?(q[r+1]-q[l]):(t[x<<1]+t[x<<1|1]);//更新同上
}
int main(){
n=read();
for(int i=1,a,b,c,d;i<=n;i++){
a=read();b=read();c=read();d=read();
s[(i<<1)-1]=scl{a,c,b,1};//存储扫描线
s[i<<1]=scl{a,c,d,-1};
q[(i<<1)-1]=a;q[i<<1]=c;
}
sort(q+1,q+2*n+1);
cnt=unique(q+1,q+2*n+1)-q-1;
sort(s+1,s+2*n+1,cmp);
for(int i=1,x,y;i<=2*n;i++){
x=lower_bound(q+1,q+cnt+1,s[i].l)-q;//离散化
y=lower_bound(q+1,q+cnt+1,s[i].r)-q;
modify(1,1,cnt-1,x,y-1,s[i].h);//注意是 cnt-1,因为 cnt 个端点就 cnt-1 个区间
ans+=1ll*t[1]*(s[i+1].x-s[i].x);//更新答案,t[1] 就是全局线段长度之和
}
cout<<ans<<'\n';
return 0;
}
2. 周长并
就是要额外维护一个区间里当前有多少条竖直线段。
每片的贡献是 \(这一片的厚度\times这一片的竖直线段数\)。
每两片之间的贡献是 修改前后横向线段长度和的差的绝对值。
还有为了避免重复计算,在水平方向上处理线段,排序时纵坐标相同时要先加再减。
时间复杂度:\(O(n\log n)\)
点击查看代码
int n,t[400010],tp[400010],p[400010],q[400010],cnt;
//数组含义:t[x]:编号为 x 的区间当前的线段长度之和
//p[x]:编号为 x 的区间被完全覆盖的次数
//q[x]:离散化数组,编号为 x 的端点所对应的实际横坐标
//tp[x]:编号为 x 的区间中端点的对数,即 编号为x的区间中端点的个数除以 2
//区间 [l,r] 对应端点 [l,r+1]
ll ans=0;//一般要开 long long
bool tl[400010],tr[400010];
//tl[x]:编号为 x 的区间的左端点有没有被覆盖
//tr[x]:编号为 x 的区间的右端点有没有被覆盖
struct scl{
//区间的左端点、右端点、纵坐标、这条边是矩形的下边 (h=1) 还是上边 (h=-1)
int l,r,x,h;
}s[500010];
bool cmp(scl _1,scl _2){//按照纵坐标排好序,纵坐标相同时先加再减
return _1.x==_2.x?_1.h>_2.h:_1.x<_2.x;
}
void van(int x,int l,int r){//更新
if(p[x]){//如果区间x被完全覆盖
tl[x]=tr[x]=tp[x]=1;t[x]=q[r+1]-q[l];
}else if(l==r)t[x]=tl[x]=tr[x]=tp[x]=0;//如果是叶子节点
else{//由两个子节点更新,不用懒标记,理由同上
tl[x]=tl[x<<1];tr[x]=tr[x<<1|1];
tp[x]=tp[x<<1]+tp[x<<1|1]-(tr[x<<1]&&tl[x<<1|1]);
//如果 左子区间的右端点 和 右子区间的左端点 都被覆盖,那么要减去1对重复的
t[x]=t[x<<1]+t[x<<1|1];
}
}
void modify(int x,int l,int r,int ql,int qr,int v){//修改
if(ql<=l&&r<=qr){p[x]+=v;van(x,l,r);return;}
int mid=(l+r)>>1;
if(ql<=mid)modify(x<<1,l,mid,ql,qr,v);
if(qr>mid)modify(x<<1|1,mid+1,r,ql,qr,v);
van(x,l,r);
}
int main(){
n=read();
for(int i=1,a,b,c,d;i<=n;i++){
a=read();b=read();c=read();d=read();
s[(i<<1)-1]=scl{a,c,b,1};//存储扫描线
s[i<<1]=scl{a,c,d,-1};
q[(i<<1)-1]=a;q[i<<1]=c;//q数组用于存储区间端点以方便进行离散化
}
sort(q+1,q+2*n+1);
cnt=unique(q+1,q+2*n+1)-q-1;
sort(s+1,s+2*n+1,cmp);
for(int i=1,x,y,pre=0;i<=2*n;i++){
x=lower_bound(q+1,q+cnt+1,s[i].l)-q;//离散化
y=lower_bound(q+1,q+cnt+1,s[i].r)-q;
modify(1,1,cnt-1,x,y-1,s[i].h);//注意是cnt-1,因为cnt个端点就cnt-1个区间
ans+=1ll*abs(t[1]-pre)+2ll*tp[1]*(s[i+1].x-s[i].x);
//更新答案,t[1]就是全局线段长度之和,tp[1]就是全局端点对数之和,所以要×2
pre=t[1];
}
cout<<ans<<'\n';
return 0;
}
二、李超线段树
1.概述:
要求在平面直角坐标系下维护两个操作共 \(n\) 次:
修改操作:在平面上加入一条线段。记第 \(i\) 条被插入的线段的标号为 \(i\),该线段的斜率 \(k_i\) 和截距 \(b_i\)。
查询操作:给定一个数 \(p\),询问与直线 \(x=p\) 相交的线段中,交点纵坐标最大的线段的编号。
\(n\le 10^6,p\le 10^6\)。
2.经典做法:
加入线段:
用 \(upd\) 函数递归地插入。对于一个完全被第 \(u\) 号线段覆盖的区间 \([l,r]\),假设当前区间懒标记为 \(0\),则直接标为 \(u\) 即可。
如果当前区间已经有标记,为 \(v\),不妨设在区间中点处线段 \(u\) 的值不大于 \(v\)(如果 \(u\) 比 \(v\) 大,交换 \(u,v\) 即可)。
然后比较 \(u\) 和 \(v\) 在左端点的值。如果 \(u\) 在左端点比 \(v\) 大,那么 \(u\) 和 \(v\) 交在左区间,递归处理左区间即可。
然后比较 \(u\) 和 \(v\) 在右端点的值。如果 \(u\) 在右端点比 \(v\) 大,那么 \(u\) 和 \(v\) 交在右区间,递归处理右区间即可。
易知中点属于左区间,不额外考虑 \(u,v\) 在中点处相等的情况。但是在端点相等有时需要额外考虑,比如说要考虑编号大小。
易知以上两个条件至多同时满足一个,所以至多递归 \(O(\log p)\) 次。
注意这并不意味着懒标记的值就是在该区间中点处取到最大值的线段的编号,因为也许该区间被更新了,但是由于修改的区间有限,只修改了它的祖先,但是并不会影响每次查询的答案的正确性。这是运用了标记永久化的思想。
时间复杂度:\(O(\log p)\),因为每个区间至多递归 \(O(\log p)\) 次。常数很小。
void upd(int x,int l,int r,int u){
int &v=s[x],mid=(l+r)>>1;
if(cmp(cal(u,mid),cal(v,mid))>0)swap(u,v);
int bl=cmp(cal(u,l),cal(v,l)),br=cmp(cal(u,r),cal(v,r));
if(bl==1)upd(x<<1,l,mid,u);
if(br==1)upd(x<<1|1,mid+1,r,u);
}
查询:
同理,递归查询即可。与普通线段树类似。
double query(int x,int l,int r,int v){
if(l==r)return cal(s[x],v);
int mid=(l+r)>>1;
if(v<=mid)return max(cal(s[x],v),query(x<<1,l,mid,v));
else return max(cal(s[x],v),query(x<<1|1,mid+1,r,v));
}
时间复杂度:\(O(\log p)\),因为每个区间至多递归 \(O(\log p)\) 次。常数很小。
3.例题
例题 1:P4254 [JSOI2008] Blue Mary 开公司
李超线段树模板题,但是要注意计算第 \(v\) 天的值时要算 \((v-1)\times k+b\)。时间复杂度 \(O(n\log n)\)。
点击查看代码
struct _{double k,b;}p[100010];
inline int cmp(double x,double y){
if(x-y>eps)return 1;
if(y-x>eps)return -1;
return 0;
}
inline double cal(int x,double v){return p[x].k*(v-1)+p[x].b;}
int n,c=0,s[400010];
void upd(int x,int l,int r,int u){
int &v=s[x],mid=(l+r)>>1;
if(cmp(cal(u,mid),cal(v,mid))>0)swap(u,v);
int bl=cmp(cal(u,l),cal(v,l)),br=cmp(cal(u,r),cal(v,r));
if(bl==1)upd(x<<1,l,mid,u);
if(br==1)upd(x<<1|1,mid+1,r,u);
}
double query(int x,int l,int r,int v){
if(l==r)return cal(s[x],v);
int mid=(l+r)>>1;
if(v<=mid)return max(cal(s[x],v),query(x<<1,l,mid,v));
else return max(cal(s[x],v),query(x<<1|1,mid+1,r,v));
}
int main(){
n=read();
for(int i=1;i<=n;i++){
char op[15];
scanf("%s",op);
if(op[0]=='P'){
c++;
scanf("%lf%lf",&p[c].b,&p[c].k);
upd(1,1,50002,c);
}else printf("%d\n",int(query(1,1,50002,read())/100.0));
}
return 0;
}
例题 2:P4097 [HEOI2013] Segment
这一题其实和上一题本质类似,但是有 2 个改变:
- 这一题输入的是线段的 2 个端点。
- 我们要求的是在纵坐标最大的线段里编号最小的。
对于第二个改变,我们可以将维护的最大值改成值和编号的二元组。
对于第一个改变,我们也要将 \((x_0,y_0)\) 与 \((x_1,y_1)\) 转换成 \(y=k\times x+b\) 的形式(注意特判 \(k\) 不存在的情况),然后加入李超线段树。但是我们发现每一个线段都有一个左右端点的限制:\([x_0,x_1]\)。所以我们可以采用类似树套树的思想,先用一个 \(update\) 确定被线段完全覆盖的区间,然后再用 \(upd\) 维护区间最大值。
时间复杂度 \(O(n\log^2 n)\)。
点击查看代码
struct _{double k,b;}p[100010];
inline int cmp(double x,double y){
if(x-y>eps)return 1;
if(y-x>eps)return -1;
return 0;
}
int c=0,n,s[160010];
inline double cal(int x,double v){return p[x].k*v+p[x].b;}
inline void add(double _x,double _y,double x_,double y_){
if(_x==x_)p[++c]=_{0,max(_y,y_)};
else p[++c].k=(y_-_y)/(x_-_x),p[c].b=y_-p[c].k*x_;
}
inline pair<double,int> pmax(pair<double,int>qwq,pair<double,int>qaq){
int tat=cmp(qwq.first,qaq.first);
if(tat==1)return qwq;
else if(tat==-1)return qaq;
return qwq.second>qaq.second?qaq:qwq;
}
void upd(int x,int l,int r,int u){
int &v=s[x],mid=(l+r)>>1;
if(cmp(cal(u,mid),cal(v,mid))>0)swap(u,v);
int bl=cmp(cal(u,l),cal(v,l)),br=cmp(cal(u,r),cal(v,r));
if(bl==1||(!bl&&u<v))upd(x<<1,l,mid,u);
if(br==1||(!br&&u<v))upd(x<<1|1,mid+1,r,u);
}
void update(int x,int l,int r,int ql,int qr,int u){
if(ql<=l&&r<=qr){upd(x,l,r,u);return;}
int mid=(l+r)>>1;
if(ql<=mid)update(x<<1,l,mid,ql,qr,u);
if(qr>mid)update(x<<1|1,mid+1,r,ql,qr,u);
}
pair<double,int> query(int x,int l,int r,int v){
if(l==r)return make_pair(cal(s[x],v),s[x]);
int mid=(l+r)>>1;
if(v<=mid)return pmax(make_pair(cal(s[x],v),s[x]),query(x<<1,l,mid,v));
else return pmax(make_pair(cal(s[x],v),s[x]),query(x<<1|1,mid+1,r,v));
}
int main(){
n=read();
for(int la=0,i=1,xx,cc;i<=n;i++)
if(read()){
ll v=read(),b=read(),o=read(),p=read();
add((xx=(v+la-1)%mod+1),(b+la-1)%awa+1,(cc=(o+la-1)%mod+1),(p+la-1)%awa+1);
update(1,1,mod,min(xx,cc),max(xx,cc),c);
}else {write(la=query(1,1,mod,(read()+la-1)%mod+1).second);putchar('\n');}
return 0;
}
4. 其它的一些常用技巧:
如果 \(p\le 10^9\),或者 \(p\in R\) 怎么办呢?
我们发现李超线段树求值不一定要在整点求值,其实任意点都可以求,所以直接离散化即可。
但是如果加上强制在线的条件呢?
那我们可以动态开点。
三、吉老师线段树
主要是历史操作和最值操作。
1. 最值操作
给定一个 \(n\) 个数的序列 \(a\)