题目链接
1010: [HNOI2008]玩具装箱toy
Time Limit: 1 Sec
Memory Limit: 162 MB
Submit: 7027
Solved: 2670
[ Submit][ Status][ Discuss]
Description
P教授要去看奥运,但是他舍不下他的玩具,于是他决定把所有的玩具运到北京。他使用自己的压缩器进行压缩,其可以将任意物品变成一堆,再放到一种特殊的一维容器中。P教授有编号为1...N的N件玩具,第i件玩具经过压缩后变成一维长度为Ci.为了方便整理,P教授要求在一个一维容器中的玩具编号是连续的。同时如果一个一维容器中有多个玩具,那么两件玩具之间要加入一个单位长度的填充物,形式地说如果将第i件玩具到第j个玩具放到一个容器中,那么容器的长度将为 x=j-i+Sigma(Ck) i<=K<=j 制作容器的费用与容器的长度有关,根据教授研究,如果容器长度为x,其制作费用为(X-L)^2.其中L是一个常量。P教授不关心容器的数目,他可以制作出任意长度的容器,甚至超过L。但他希望费用最小.
Input
第一行输入两个整数N,L.接下来N行输入Ci.1<=N<=50000,1<=L,Ci<=10^7
Output
Sample Input
5 4
3
4
2
1
4
Sample Output
1
题解:用dp[i]表示装完前i个物品的最小花费,转移就是:
dp[i]=min(dp[k]+(sum[i]-sum[k]+i-k-1-l)*(sum[i]-sum[k]+i-k-1-l))
sum[i]表示前i个物品的前缀和。
方法一:
另w(k,i)=(sum[i]-sum[k]+i-k-1-l)*(sum[i]-sum[k]+i-k-1-l);
可以证明:w(i,j)+w(i+1,j+1)<=w(i,j+1)+w(i+1,j)。
另k(i)表示状态i取到最优值时的决策,则:k(i)<=k(j)。决策单调。
因此我们可以采用论文:《1D/1D动态规划优化初步》中的方法。
O(nlgn)解决此问题。代码如下:
#include<stdio.h>
#include<iostream>
#include<algorithm>
#include<map>
#include<string.h>
#include<vector>
#include<math.h>
typedef long long LL;
typedef unsigned long long LLU;
const double eps=1e-8;
const int nn=110000;
const int inf=0x3fffffff;
using namespace std;
int n;
LL l,c[nn];
LL dp[nn];
LL sum[nn];
struct node
{
int x,y;
int val;
node(){}
node(int xx,int yy,int vall)
{
x=xx,y=yy,val=vall;
}
};
node que[nn];
int head,last;
LL solve(int i,int k)
{
return dp[k]+(sum[i]-sum[k]+i-k-1-l)*(sum[i]-sum[k]+i-k-1-l);
}
bool check(int id,int v1,int v2)
{
return solve(id,v1)<solve(id,v2);
}
void add(int x,int y,int val)
{
node tem=node(x,y,val);
while(last>head)
{
if(check(que[last-1].x,que[last-1].val,val))
{
int l=que[last-1].x+1,r=que[last-1].y+1;
while(l<r)
{
int mid=(l+r)/2;
if(check(mid,que[last-1].val,val))
{
l=mid+1;
}
else
r=mid;
}
tem.x=r;
que[last-1].y=r-1;
break;
}
else
{
tem.x=que[last-1].x;
last--;
}
}
if(tem.x<=n)
{
tem.y=n;
que[last++]=tem;
}
}
int main()
{
int i;
while(scanf("%d%lld",&n,&l)!=EOF)
{
sum[0]=0;
for(i=1;i<=n;i++)
{
scanf("%lld",&c[i]);
sum[i]=sum[i-1]+c[i];
}
dp[0]=0;
head=last=0;
add(1,n,0);
for(i=1;i<=n;i++)
{
while(que[head].y<i)
{
head++;
}
dp[i]=solve(i,que[head].val);
add(n+1,n+1,i);
}
printf("%lld\n",dp[n]);
}
return 0;
}
方法二:再来分析转移方程:
dp[i]=min(dp[k]+(sum[i]-sum[k]+i-k-1-l)*(sum[i]-sum[k]+i-k-1-l))
=min(dp[k]+(sum[i]+i-l-1)*(sum[i]+i-l-1)+(sum[k]+k)*(sum[k]+k)-2*(sum[i]+i-l-1)*(sum[k]+k))
=min( dp[k]+(sum[k]+k)*(sum[k]+k)-2*(sum[i]+i-l-1)*(sum[k]+k) ) + (sum[i]+i-l-1)*(sum[i]+i-l-1)
我们令常量 P=(sum[i]+i-l-1)
令:X(k)=dp[k]+(sum[k]+k)*(sum[k]+k)
令:Y(k)=(sum[k]+k)
则:dp[i]=min(X(k)-2*P*Y(k))+p*p;
令C=X(k)-2*P*Y(k) ,则:
Y(k)=1/2p*X(k) - C/2*p;
我们可以把决策K看成是二维坐标上的一个点( X(k),Y(k) ) ,求解dp[i]可以看成是,一条斜率固定的直线,在一些点中(决策),找一个点,使得纵坐标的截距最大即(C最小)。不难发现,这个点一定在这些点的凸包上。
由于1/2*p随着i的增加单调递减,X(k)随着K的增加单调递增。
因此我们可以用一个双端队列维护凸包上的点,队列中相邻两点的斜率随着x的增加而减少。
每次转移的时候,从队首开始,找到第一个与后一个点的斜率小于当前直线的斜率的点,就是我们要找的决策点。对于不满足条件的点出队。
转移完成以后,从队尾开始添加一个点,保证队列中的点斜率递减,不满足条件的点出队。
这样就可以均摊O(1)复杂度,实现转移。
对于斜率优化的具体内容可看论文:《1D/1D动态规划优化初步》,或者其它关于斜率优化的论文。
代码如下:
#include<stdio.h>
#include<iostream>
#include<algorithm>
#include<math.h>
#include<queue>
#include<stack>
#include<set>
#include<map>
#include<vector>
#include<string.h>
#include<string>
#include<stdlib.h>
typedef long long LL;
typedef unsigned long long LLU;
const int nn=51000;
const int inf=0x3fffffff;
const LL inf64=(LL)inf*inf;
const int mod=1000000007;
using namespace std;
int n;
LL l,c[nn];
LL dp[nn];
LL sum[nn];
pair<LL,LL>que[nn];
int head,last;
void add(LL x,LL y)
{
while(last-1>head)
{
if((que[last-1].second-que[last-2].second)*(x-que[last-1].first)>(que[last-1].first-que[last-2].first)*(y-que[last-1].second))
break;
last--;
}
que[last++]=make_pair(x,y);
}
int main()
{
int i;
while(scanf("%d%lld",&n,&l)!=EOF)
{
sum[0]=0;
for(i=1;i<=n;i++)
{
scanf("%lld",&c[i]);
sum[i]=sum[i-1]+c[i];
}
dp[0]=0;
head=last=0;
add(0,0);
LL ix;
for(i=1;i<=n;i++)
{
ix=sum[i]+i-l-1;
while(head<last-1)
{
if((que[head+1].first-que[head].first)>(que[head+1].second-que[head].second)*ix*2)
{
break;
}
head++;
}
dp[i]=ix*ix+que[head].first-2*ix*que[head].second;
add(dp[i]+(sum[i]+i)*(sum[i]+i),sum[i]+i);
}
printf("%lld\n",dp[n]);
}
return 0;
}