noip膜你赛 第三题 小Q的新玩具(dp优化 set)

12 篇文章 0 订阅
3 篇文章 0 订阅

题意:有n个零件,每个零件重量不同,你需要把它们运输回去,你只能运输连续的一段零件,且这连续的一段零件重量之和不能大于limit,而运输某一段的花费是这段重量的最大值。

题解:

这毫无疑问是一道毒瘤题,dp方程很简单,优化却比较复杂。

我们令dp[i]表示前i个零件的最小费用

所以dp[i]=min(dp[j]+max_w(j+1,i)) (sumw[i]-sumw[j]<=limit)

朴素做法O(n^2),会超时,接下来是优化。

对于dp[i],能更新它的j的max_w值在j+1到i这个区间一定是单调不上升的

而且对于j+1到i的某一段区间来说,这个值都是一定的。再来看dp[j],它

也具有类似的单调性,从1到n,dp的值都一定是单调不下降的。

那么我们要求的是min(dp[j-1]+max_w(j,i)),所以对于一段max_w相同的

区间,我们只需取其最左边的dp[j]即可,若再往右取一点点,dp单调不下降

一定不会更优。

分析到这里,我们就可以着手优化了,我们可以使用一个单调队列来记录max_w的

值,当然,根据上面的分析,我们只需要记录max_w相同区间内位置在最左边的那

一个下标,在i更新的时候,可能会出现一个新的max_w,这时,就将单调队列更新

然后对于快速地求出min(dp[j]+max_w(j+1,i)),这个可以使用一个set,来支持插入,

删除,查询三个操作,当然,自己敲一个合适的数据结构也是可以的,当单调队列中

的值需要被更新的时候,set也需要被更新了


虽然理完了思路,但是我仍然对如何实现这个代码感到迷茫,于是对着标程边打边理解

在下面给出的代码中将有我理解时所加上的一些注释,有一些是废话,但有一些是有用的

code:

#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<set>

typedef long long LL;
const int MAXN=300005;
using namespace std;
int n,Q[MAXN]; LL sum[MAXN];
LL limit,w[MAXN],dp[MAXN];
struct ISET{
    multiset<LL> S;
    void Insert(int v){S.insert(v);}
    void Delete(int v){S.erase(S.lower_bound(v));}
    int top(){return *S.begin();}
}iset;

inline void Read(LL &Ret){
    char ch;bool flag=0;
    for(;ch=getchar(),ch<'0'||ch>'9';)if(ch=='-')flag=1;
    for(Ret=ch-'0';ch=getchar(),'0'<=ch&&ch<='9';Ret=Ret*10+ch-'0');
    flag&&(Ret=-Ret);
}
int main()
{
    scanf("%d",&n); Read(limit);
    for(int i=1;i<=n;i++)
        Read(w[i]),sum[i]=sum[i-1]+w[i];
    int h=1,t=0,left=1;
    Q[++t]=1; iset.Insert(dp[1]=w[1]);//定义初始状态,并在set和队列中插入这个状态
    //Q中储存的是max_w相同的一段区间内的最右边的那个值,这和理论推导出来的似乎恰好相反,但事实上,这样做可以简化代码,因为
    //状态转移是dp[j-1],这不就是Q的第二个元素吗
    for(int i=2;i<=n;i++)
    {
        while(sum[i]-sum[left-1]>limit&&left<i)//去掉非法情况
        {
            iset.Delete(dp[left-1]+w[Q[h]]);//此时这一段的重量已超过limit,删除掉最左边的情况
            if(Q[h]==left) left++, h++;//发现队列中也存在这一非法情况,删除队列中的这一情况
            else left++,iset.Insert(dp[left-1]+w[Q[h]]);//Q[h]是这段max_w相同区间的最右边的下标,不是最左边的,如果这里
            //是最左边的话,在Q[h]==left时h++后可能是非法的
            //这句话的大部分操作都会被iset.Delete(dp[left-1]+w[Q[h]]);给抵消掉,他的作用其实只是将第1个合法的值给插入到set中
        }//注意这样一个细节,是否会存在left在初始时就比Q[h]大的情况,导致插入了许多max_w相同的点,其实并不会,注意到left只
        //会在这里更新,初始时与Q[h]相同的,而Q[h]就算被更新,也只会往大了更新
        while(h<=t&&w[i]>=w[Q[t]])//更新单调队列,因为是更新的最右边的,w[i]==w[Q[t]]时也要更新
        {
            if(h==t) iset.Delete(dp[left-1]+w[Q[t]]);
            else iset.Delete(dp[Q[t-1]]+w[Q[t]]);
            t--;
        }
        Q[++t]=i;
        if(h==t) iset.Insert(dp[left-1]+w[i]);
        else iset.Insert(dp[Q[t-1]]+w[i]);
        dp[i]=iset.top();
    }
    printf("%lld",dp[n]);
    return 0;
}//其实这个代码最令人难以理解的地方就是Q 中存储的是一段max_w相同区间内的最右边的下标,这一点卡了我很久


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值