#bzoj3380#小Q的新玩具(DP + set优化 / 线段树优化)

27 篇文章 0 订阅
13 篇文章 0 订阅

3380: 小Q的新玩具

时间限制: 1 Sec  内存限制: 128 MB

题目描述

期末考试完了,小Q得到了一件套新玩具,总共有N个零件。

现在小Q想把新玩具搬回家里,可是他遇到了新的问题:

每个零件有自己的重量Wi,小Q要租车把它们带回家。车每次只能运总重量和小于Lim的玩具,按照其中最重的玩具的重量收费。

零件不能拆分成更小的部分。为了不打乱零件的顺序,增加自己拼装的难度,每次装车只能装连续的部分。

现在想请你帮助小Q计算把玩具全部搬回家的最小费用。

输入

第一行两个整数N和Limit。 

接下来的N行,每行一个整数,代表第i个零件的重量。

输出

第一行一个数字,表示答案。

样例输入

8 17 2 2 2 8 1 8 2 1

样例输出

12

提示

N<=300000


Wi小于int范围


定义Dp[i]表示前搬运前i个玩具回家所需要的最小花费

Dp[i] = min(Dp[j] + w[j + 1][i])

w[j + 1][i]表示从j + 1 到 i 的最大玩具重量。

N很大,300000,w[j + 1][i]肯定不能预处理,而且N^2的DP肯定超时。


暴力的话可以用线段树维护w,然后N ^ 2,有30分。


正解,考虑优化

对于当前的i,它的转移来自于它前面的点,而且这些转移点的重量将呈不上升状:


图中蓝色表示决策点的对应需要加上的重量w[][],那么此时可以知道,对于每一种重量,它的最靠左点将是它的最优(因为靠右可能会引起Dp值增大,而靠左一定不会)

我们将上面的点放入set中维护Dp[j - 1] + w[j]的最小值,在满足lim的情况下,用set.begin()更新Dp[i],然后得到Dp[i]值。

单调队列维护一个不上升的w,此时红色代表w[i],单调队列弹出时,应同时弹出set中的Dp[j - 1] + w[j],换成Dp[j - 1] + w[i]再次放入,直到满足单调队列中不上升,放入。


线段树也可以实现,维护一个最小值。


Code:

一(可能二更容易理解):学的耿神的Set优化

#include<iostream> 
#include<cstdio> 
#include<cstdlib> 
#include<cstring> 
#include<algorithm> 
#include<set> 
using namespace std; 
  
const int Max = 300000; 
  
typedef long long LL; 
  
struct node{ 
    multiset<LL>Set; 
    void push(LL v){    Set.insert(v);} 
    void del(LL v){     Set.erase(Set.lower_bound(v));} 
    LL top(){   return *Set.begin();} 
}P; 
  
int N, Lim; 
int Q[Max + 5], A[Max + 5]; 
LL sum[Max + 5], Dp[Max + 5]; 
  
bool getint(int & num){ 
    char c; int flg = 1;    num = 0; 
    while((c = getchar()) < '0' || c > '9'){ 
        if(c == '-')    flg = -1; 
        if(c == -1) return 0; 
    } 
    while(c >= '0' && c <= '9'){ 
        num = num * 10 + c - 48; 
        if((c = getchar()) == -1)   return 0; 
    } 
    num *= flg; 
    return 1; 
} 
  
int main(){ 
    //freopen("toy.in", "r", stdin); 
    //freopen("toy.out","w",stdout); 
    getint(N), getint(Lim); 
    for(int i = 1; i <= N; ++ i) getint(A[i]), sum[i] = sum[i - 1] + A[i]; 
    int fro = 1, back = 0, lf = 0; 
    Q[++ back] = 1; 
    Dp[1] = A[1]; 
    P.push(Dp[1]); 
    for(int i = 2; i <= N; ++ i){ 
        while(sum[i] - sum[lf] > Lim && lf < i){ 
            P.del(A[Q[fro]] + Dp[lf]); 
            if(Q[fro] == lf + 1)    ++ fro, ++ lf; 
            else ++ lf, P.push(A[Q[fro]] + Dp[lf]); 
        } 
        while(fro <= back && A[Q[back]] <= A[i]){ 
            if(fro == back) P.del(A[Q[back]] + Dp[lf]); 
            else P.del(A[Q[back]] + Dp[Q[back - 1]]); 
            -- back; 
        } 
        Q[++ back] = i; 
        if(fro == back) P.push(A[i] + Dp[lf]); 
        else P.push(A[i] + Dp[Q[back - 1]]); 
        Dp[i] = P.top(); 
    } 
    printf("%lld\n", Dp[N]); 
    return 0; 
} 

解释一下这个代码,我也是好久才弄懂。

Q中存放的是各个不同的重量(理论上不能重复元素,但是去掉等号因为数据水也可以过)

但实际上是上图中每个高度最靠右的那一个值(而不是标出来的靠左的粉色!)

因为每次更新set中值的时候仍然是利用Q中的值更新(可以想象Q中存的就是j - 1,j是每条线最靠左的,所以j - 1就是前一条线最靠右)。

假设用A[Q[t]]来更新,那么按转移方程就应该是Dp[Q[t]这个的最靠左的那个值,也就是Q[t - 1]] + A[Q[t]]。

这里很巧妙,值得好好理解。


二:(同学代码)

#include<cstdio>
#include<set>
#include<algorithm>
using namespace std;

int n,lim,a[300055],head,tail,minn;
long long sum[300055],dp[300055];

struct node{
	int num,id,setnum;
	node(){}
	node(int a,int b,int c):num(a),id(b),setnum(c){}
}q[300055];

int main(){
	//freopen("toy.in","r",stdin);
	//freopen("toy.out","w",stdout);
	set<long long>s;
	int tmp;
	scanf("%d%d",&n,&lim);
	for(int i=1;i<=n;i++)
		scanf("%d",&a[i]),
		sum[i]=sum[i-1]+a[i];
		
	head=tail=0;
	for(int i=1;i<=n;i++){
		while(head<tail && q[tail-1].num <= a[i]){
			if(tail-1>head)
				s.erase(q[tail-1].setnum);
			tail--;
		}
		
		tmp=lower_bound(sum,sum+1+n,sum[i]-lim)-sum;
		
		if(tail>head){
			q[tail++]=node(a[i],i,a[i]+dp[q[tail-1].id]);
			s.insert(q[tail-1].setnum);
		}
		else  	q[tail++]=node(a[i],i,0);
		
		while(head<tail && sum[i]-sum[q[head].id]>lim){
			if(head<tail-1)
				s.erase(q[head+1].setnum);
			head++;
		}
		
		dp[i]=dp[tmp]+q[head].num;
		if(head<tail-1 && (!s.empty())) dp[i]=min(dp[i],*s.begin());
	}
	
	printf("%lld",dp[n]);
}

三:耿神给的线段树:

#include<cstdio>
#include<algorithm>
#define ll long long
using namespace std;
int n,lim;
ll s[1010],mx[4010],f[1010];
void build(int x,int l,int r)
{
    if(l==r)
    {
        mx[x]=s[l]-s[l-1];
        return;
    }
    build(x<<1,l,l+r>>1);
    build(x<<1|1,(l+r>>1)+1,r);
    mx[x]=max(mx[x<<1],mx[x<<1|1]);
}
ll query(int x,int l,int r,int L,int R)
{
    if(l==L&&r==R)return mx[x];
    int mid=l+r>>1;
    if(R<=mid)return query(x<<1,l,mid,L,R);
    if(L>mid)return query(x<<1|1,mid+1,r,L,R);
    return max(query(x<<1,l,mid,L,mid),query(x<<1|1,mid+1,r,mid+1,R));
}
int main()
{
    freopen("toy.in","r",stdin);
    freopen("toy.out","w",stdout);
    scanf("%d%d",&n,&lim);
    for(int i=1; i<=n; ++i)scanf("%lld",&s[i]),s[i]+=s[i-1];
    build(1,1,n);
    for(int i=1; i<=n; ++i)
    {
        ll mi=1ll<<60;
        for(int j=i-1; ~j; --j)
        {
            if(s[i]-s[j]>lim)break;
            mi=min(mi,f[j]+query(1,1,n,j+1,i));
        }
        f[i]=mi;
    }
    printf("%lld\n",f[n]);
}







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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值