洛谷P1848 [USACO12OPEN]书架Bookshelf

https://www.luogu.org/problem/show?pid=1848
这道题目的线段树简直就是水题,然后用set去做,日了狗了,搞了1个小时左右结果只过4个点,标算又看不懂,先把坑挖好;
首先这道题目的暴力dp那是显然的
f[i]可以从f[j-1]+max(h[j]……h[i])推得
不难发现无论是f[]还是当j确定的max(h[j]……h[i]),都是单调的;
那么我们大力线段树;
对于一个新加入的i点
简单来说就是维护h[j]=max(h[j]……h[i]),顺便吧答案维护了;
然后单调队列获得一段答案的可行区间last~i
那么在这段区间里取答案;
最后把答案放到f[i+1]
换句话说一开始的f[1]是0
这样的话每一个叶子节点的答案刚刚好就是f[j]+h[j]

#include<bits/stdc++.h>
#define Ll long long
using namespace std;
const int N=1e5+5;
struct tree{int l,r,h,H,lazy;Ll f,v;}T[N*4];
int q[N],last;
int n,L,x,y;
Ll sum,ans;
void make(int id,int l,int r){
    T[id].l=l;T[id].r=r;
    if(l==r)return;
    int mid=l+r>>1;
    make(id*2  ,l,mid  );
    make(id*2+1,mid+1,r);
}
void push(int id){
    if(T[id].lazy==0||T[id].l==T[id].r)return;
    int x=id*2,y=x+1;
    T[x].H=T[x].h=T[y].H=T[y].h=T[x].lazy=T[y].lazy=T[id].lazy;
    T[x].v=T[x].f+T[x].H;T[y].v=T[y].f+T[y].H;T[id].lazy=0;
}
void change(int id,int l,int r,int x){
    push(id);
    if(l<=T[id].l&&T[id].r<=r){
        if(T[id].H<x){
            T[id].H=T[id].h=T[id].lazy=x;
            T[id].v=T[id].f+x;
        }else{
            if(T[id*2  ].h<x)change(id*2  ,l,r,x);
            if(T[id*2+1].h<x)change(id*2+1,l,r,x);
            T[id].H=max(T[id*2].H,T[id*2+1].H);
            T[id].h=min(T[id*2].h,T[id*2+1].h);
            T[id].v=min(T[id*2].v,T[id*2+1].v);
        }return;
    }
    if(T[id*2  ].r>=l)change(id*2  ,l,r,x);
    if(T[id*2+1].l<=r)change(id*2+1,l,r,x);   
    T[id].H=max(T[id*2].H,T[id*2+1].H);
    T[id].h=min(T[id*2].h,T[id*2+1].h);
    T[id].v=min(T[id*2].v,T[id*2+1].v);
}
Ll out(int id,int l,int r){
    push(id);
    if(l<=T[id].l&&T[id].r<=r){
//        if(l==4&&r==5)cout<<id<<' '<<T[id].h<<endl;
        return T[id].v;
    }
    Ll ans=1e18;
    if(T[id*2  ].r>=l)ans=min(ans,out(id*2  ,l,r));
    if(T[id*2+1].l<=r)ans=min(ans,out(id*2+1,l,r));
    return ans;
}
void add(int id,int x,Ll f){
    push(id);
    if(T[id].l==T[id].r){T[id].f=f;return;}
    if(T[id*2].r>=x)add(id*2,x,f);else add(id*2+1,x,f);
    T[id].f=min(T[id*2].f,T[id*2+1].f);
}
int main()
{
    scanf("%d%d",&n,&L);
    last=1;
    make(1,1,n);
    for(int i=1;i<=n;i++){
        scanf("%d%d",&x,&q[i]);
        sum+=q[i];
        while(sum>L)sum-=q[last++];
        change(1,1,i,x);
        ans=out(1,last,i);
        if(i!=n)add(1,i+1,ans);
    }printf("%lld",ans);
}

那什么单调队列那我明天学习吧,真的是日了狗了


单调队列调出来了哈哈;
感觉单调队列并没有数据结构那么暴力那么好打;
这种做法完全依赖于这道题目的单调性;
首先先说几点
我的写法貌似并不是很快,而且在不开O2的情况下是比线段树还要慢点;
开了o2就是很快的啦;
然后对于这个set我每进进行一次插入删除,那么就需要一个log的时间,我的程序是带有很大的常数的,但感觉也是基本的一种做法;

对于基本的n^2的dp我就不在赘述了;
那么我们对于每一个读入i
首先就是需要确定可向左边扩展的最远点last
然后根据h[i]去更新这段区间里的h[j];
然后把last~i这段区间的答案全放到一个set里面去
最后去一个最小值产生一个新的f[i];
于是我开了一个队列q
代表现在last~i这段区间里,高度为h[j]最靠前的下标
因为当i确定的时候,h[j]>=h[j+1]
并且h[j]有很多相同的,那么我们要把h[j]合并起来
那么我们对于一个i,先把i放到q里面,然后不断找h[q[top]]<=h[i]把他弹出
之后再把h[j]<=h[i]的最靠前的重新插入,这样的话我们就可以维护关于h[j]了
然后我们考虑如何删除到不了的点,就是更新last值
首先我们判断当前的点是否在q里面,如果在的话就从队首弹出,不在的话,我们把h[j]跟新为h[j-1]
为什么呢,因为我们维护h[j]相同的一块的时候我们只是更新了h[j]值相同的最前面的一点,并且把这个点放到了q里面,那么之后的点的h[j]值其实就是h[j-1],
最后我们要看看现在的last值是不是在队列里面,如果不在的话要从队首压入;
如果没听懂那么很抱歉,我的语文水平貌似太差了
其实就是找到单调性的h[j]规律而已
至于那个in[j]就代表j有没有在q里面

#include<bits/stdc++.h>
#define Ll long long
using namespace std;
const Ll N=1e5+5;
Ll h[N],w[N],f[N],q[N],top;
bool in[N];
multiset<Ll>S;
Ll n,m,last,sum,l;
int main()
{
    scanf("%lld%lld",&n,&m);
    l=last=1;in[1]=1;
    for(Ll i=1;i<=n;i++){
        scanf("%lld%lld",&h[i],&w[i]);
        sum+=w[i];
        while(sum>m){
            if(in[last])S.erase(f[last-1]+h[last]),l++;else h[last]=h[last-1];
            in[last]=0;sum-=w[last++];    
        }
        if(!in[last]){
            in[last]=1;q[--l]=last;h[last]=h[last-1];
            S.insert(f[last-1]+h[last]);
        }
        q[++top]=i;S.insert(f[i-1]+h[i]);in[i]=1;
        while(top>=l&&h[q[top]]<=h[i]){
            in[q[top]]=0;
            S.erase(f[q[top]-1]+h[q[top]]);
            top--;        
        }
        in[q[++top]]=1;
        h[q[top]]=h[i];
        S.insert(f[q[top]-1]+h[q[top]]);
        f[i]=*S.begin();
    }
    printf("%lld",f[n]);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值