2025.2.21的模拟赛题“糖果”题解

2025.2.21的模拟赛题“糖果”题解

一道很神奇的李超线段树题目。

这道题难在模糊的性质,可能的思路,潜在的做法太多了,引人误入歧途。但像 nalemy 这样的巨佬又可以一眼看出正确的方向。看出只能说菜就多练吧。

其实很多东西比如“没有糖果的空白段”,“每个位置出现的时间”,“有糖果的段”似乎等等都是线段最值的问题,可以维护的东西太多了,但好像又怎么都不行,这时我们从答案看起。

(前言终)


我们不难发现答案是一个分段一次函数且分的不超过 n 段,而根据暴力我们不难看出,答案的转折是本质是因为每个段的右端点的函数解析式发生变化,因为最终答案的函数就是所有右端点函数之和。

可以发现每个右端点的位置是一个一次函数即一条直线,而每段真正的右端点是去最大值,在不考虑两个糖果中途重叠在一起的情况下,一个糖果只会成为最大值一次,于是我们合理猜测加上上合并后最大值的切换次数也 O(n) 的,事实上确实如此。

为了维护答案解析式的变化,我们应该在不断维护出那些右端点最早的失效时间并将其更新,这可以用一个堆维护。失效时间用二分求出。

为了求每段每个位置 r 的最大值,我们应该用李超线段树。

两个糖果连续段接触在一起,也会使解析式变化,我们应当用李超线段树合并解决。

复杂度 O(nlog2n)

代码如下(这是重点):

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int NN=3e5+5,INF=0x3f3f3f3f;
int n,m; 
struct Line{
    int k,b;
}lin[NN];
struct Candy{
    int l,r,v;
    void read(){
        cin>>l>>r>>v;
        return;
    }
    bool operator<(const Candy &b)const{
        return l<b.l;
    }
}candy[NN];
set<int> ms;//连续段的L端点
unordered_map<int,int> rt,nowid;//根据连续段的L端点获取李超线段树的根 
int tl,tr,tk;
struct ElementInQueue{
    int linid,itsL,endtime;
    bool operator<(const ElementInQueue &b)const{
        return endtime>b.endtime;//小根堆 
    }
};
priority_queue<ElementInQueue> q;
struct Query{
    int t,id;
    bool operator<(const Query &b)const{
        return t<b.t;
    }
    void read(int i){
        cin>>t;
        id=i;
        return;
    }
}qry[NN];
int ans[NN];
struct SegTrNode{
    int ls,rs,lid;
    #define ls(x) sgt[x].ls
    #define rs(x) sgt[x].rs
    #define lid(x) sgt[x].lid
}sgt[NN*30];
int segtot;
int Val(int lid,int x){
    if(!lid)return -INF;
    return lin[lid].k*x+lin[lid].b;
}
void Insert(int &p,int lid,int L=0,int R=1e6){//动态开点李超线段树的线段全局插入
    if(!p)p=++segtot;
    if(!lid(p)){
        lid(p)=lid;
        return;
    }
    int mid=L+R>>1;
    if(Val(lid(p),mid)<=Val(lid,mid))swap(lid(p),lid);
    if(L==R)return;
    if(Val(lid(p),L)<Val(lid,L))Insert(ls(p),lid,L,mid);
    if(Val(lid(p),R)<Val(lid,R))Insert(rs(p),lid,mid+1,R);
    return;
}
int Merge(int lt,int rt,int L=0,int R=1e6){
    if(!lt||!rt)return lt|rt;
    Insert(lt,lid(rt),L,R);
    if(L==R)return lt;
    int mid=L+R>>1;
    ls(lt)=Merge(ls(lt),ls(rt),L,mid);
    rs(lt)=Merge(rs(lt),rs(rt),mid+1,R);
    return lt;
}
int Max(int l1,int l2,int pos){
    if(Val(l1,pos)<Val(l2,pos))return l2;
    return l1;
}
int QueryLine(int p,int pos,int L=0,int R=1e6){//查询某个自变量的某个线段树的最大值的那条线段 
    if(!p)return 0;
    if(L==R)return lid(p);
    int mid=L+R>>1;
    if(pos<=mid)return Max(lid(p),QueryLine(ls(p),pos,L,mid),pos);
    else        return Max(lid(p),QueryLine(rs(p),pos,mid+1,R),pos);
}
int TouchTime(int L,int lid){//超过1e6一律按1e6算 
    if(ms.upper_bound(L)==ms.end())return 1e6;//后面没有了,再也不会相撞
    int nxtL=*ms.upper_bound(L);
    return (nxtL-lin[lid].b)/lin[lid].k;//直接整除就好了 
}
int SearchEndTime(int L,int lid,int begintime){//超过1e6一律按1e6算
    int l=begintime-1,r=TouchTime(L,lid);//l<begintime表示没有出现 
    while(l<r){
        int mid=l+r+1>>1;
        if(QueryLine(rt[L],mid)==lid)l=mid;
        else r=mid-1;
    }
    return l;
}
signed main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++)candy[i].read();
    sort(candy+1,candy+1+n);
    int R=0,L=0;
    for(int i=1;i<=n;i++){
        if(candy[i].l<=R){
            R=max(R,candy[i].r);
            lin[i].k=candy[i].v,
            lin[i].b=candy[i].r;
            Insert(rt[L],i);//在这个李超线段树上插入一条线段 
        }else{//一个新的连续段 
            L=candy[i].l;R=candy[i].r;
            lin[i].k=candy[i].v,
            lin[i].b=candy[i].r;
            Insert(rt[L],i);//在这个李超线段树上插入一条线段 
            ms.insert(L);
        }
    }
    //先得到时间t=0时每个部分的右端点 
    for(int o:ms){
        tl+=o;
        int linid=QueryLine(rt[o],0);
        nowid[o]=linid;
        tk+=lin[linid].k;
        tr+=lin[linid].b;
        int endtime=SearchEndTime(o,linid,0);//在写SearchEndTime函数的时候注意包含进TounchTime 
        q.push({linid,o,endtime});
    }
    for(int i=1;i<=m;i++)qry[i].read(i);
    sort(qry+1,qry+1+m);
    for(int i=1;i<=m;i++){
        while(!q.empty()&&q.top().endtime<qry[i].t){//这里是"<"因为我们把实数时间下取整,EndTime的那一瞬间依然是其本身最大 
            ElementInQueue ele=q.top();q.pop();
            if(!ms.count(ele.itsL))continue;//已经被删了就不处理了 
            if(ele.endtime==TouchTime(ele.itsL,ele.linid)){//因为碰撞而消除,此时应该合并两个李超线段树 
                int nxtL=*ms.upper_bound(ele.itsL);
                tr-=lin[nowid[nxtL]].b,
                tk-=lin[nowid[nxtL]].k;
                tl-=nxtL;//后继
                nowid.erase(nxtL);
                ms.erase(nxtL);
                rt[ele.itsL]=Merge(rt[ele.itsL],rt[nxtL]);
                rt.erase(nxtL);
            }//否则只是简单的更换线段,并且这里是更新到下一条而非直接更新到k这瞬间,可能因为是不好处理吧 
            tr-=lin[ele.linid].b,
            tk-=lin[ele.linid].k;
            int newlinid=QueryLine(rt[ele.itsL],ele.endtime+1);
            tr+=lin[newlinid].b,
            tk+=lin[newlinid].k;
            nowid[ele.itsL]=newlinid;
            q.push({newlinid,ele.itsL,SearchEndTime(ele.itsL,newlinid,ele.endtime+1)});
        }
        ans[qry[i].id]=tk*qry[i].t+tr-tl;
    }
    for(int i=1;i<=m;i++)cout<<ans[i]<<"\n";
    return 0;
}/*
这道题根本不是在维护r,而是在维护r的解析式,通过r的解析式和并得到答案的函数
通过处理r直线的变换维护答案函数的变化 
*/

作者:lupengheyyds

出处:https://www.cnblogs.com/lupengheyyds/p/18730116

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   lupengheyyds  阅读(8)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
点击右上角即可分享
微信分享提示