洛谷P3195 [HNOI2008]玩具装箱TOY

题面
大意:自己看太复杂我不想解释了


终于,我要开始复习我目前接触到的最毒瘤、思考难度最大的算法了。
斜率优化。
简单介绍一下: 斜率优化在我看来就是利用一个玄学的方法确定一个值,然后用这个值去确定以前的一些状态是否没有用,再结合单调队列或其他数据结构减少DP过程中的继承次数,从而达到加速的目的。 不得不说斜率优化必!须!得!数!形!结!合!

斜率优化是将每一个状态转换成决策点,再用求最小截距等方法加速的

废话少说我们来做题

首先我们可以飞快地想出 O ( n 2 ) O(n^2) O(n2)的算法:
f [ i ] f[i] f[i]表示前 i i i个玩具被装好的最小花费
然后
f [ i ] = min ⁡ j = 0 i − 1 ( f [ j ] + ( i − j − 1 + ∑ k = j + 1 i a [ i ] − L ) 2 ) f[i]=\min\limits^{i-1}_{j=0}(f[j]+(i-j-1+\sum\limits^{i}_{k=j+1}a[i]-L)^2) f[i]=j=0mini1(f[j]+(ij1+k=j+1ia[i]L)2)
即将第 j + 1 j+1 j+1到第 i i i个放到一个盒子中
(请原谅我用了一种玄学的方式表示求最小值)
其中 ∑ k = j + 1 i a [ i ] \sum\limits^{i}_{k=j+1}a[i] k=j+1ia[i]一部分可以用前缀和求

#incLude<cstdio>
#incLude<cstring>
using namespace std;
Long Long min(register Long Long a,register Long Long b)
{return a<b?a:b;}
Long Long sqr(register Long Long x)
{return x*x;}
int a[51000],sum[51000];
Long Long f[51000];
int main()
{
	int n,l;
	scanf("%d%d",&n,&l);
	for(int i=1;i<=n;i++)
		scanf("%d",a+i),sum[i]=sum[i-1]+a[i];
	memset(f,1,sizeof(f));
	f[0]=0;
	for(int i=1;i<=n;i++)
		for(int j=0;j<n;j++)
			f[i]=min(f[i],f[j]+sqr(i-j-1+(sum[i]-sum[j])-l));
	printf("%LLd",f[n]);
	return 0;
}

可以成功地获得20分


接下来优化的时间就到了
首先,我们从状态转移方程入手:

f[i]=min(f[i],f[j]+sqr(i-j-1+(sum[i]-sum[j])-l));

可以发现,记 s [ i ] = s u m [ i ] + i s[i]=sum[i]+i s[i]=sum[i]+i L = L + 1 L=L+1 L=L+1可以简化方程
则有:

f[i]=min(f[i],f[j]+sqr(s[i]-s[j]-l));

然后我们开始数形结合

(PS.:本来这后面有一段两千多字打了我整整一天的推导,最后发现根本**不通——请原谅我因为实在是太生气了)(数形结合万岁!

简单地讲,现在优化的目的就是尽快地找到这个使 f i f_i fi最小化的 j j j
所以,我们设 j j j就是令 f i f_i fi最小化的继承状态
f i = f j + ( s i − s j − L ) 2 f_i=f_j+(s_i-s_j-L)^2 fi=fj+(sisjL)2
将这个方程展开,并将其转化为 y = k x + b y=kx+b y=kx+b的形式
其中, y y y x x x要包含所有与 j j j有关的项,且 x x x要保证随 j j j单调递增(方便构建模型), k , b k,b k,b是与 j j j无关的值, b b b要包含 f i f_i fi
原 式 = f i = f j + s i 2 − 2 s i ( s j + L ) + ( s j + L ) 2 原式=\qquad\qquad f_i=f_j+{s_i}^2-2s_i(s_j+L)+(s_j+L)^2 =fi=fj+si22si(sj+L)+(sj+L)2
f i − s i 2 = f j − 2 s i ( s j + L ) + ( s j + L ) 2 f_i-{s_i}^2=f_j-2s_i(s_j+L)+(s_j+L)^2 fisi2=fj2si(sj+L)+(sj+L)2
f i − s i 2 + 2 s i ( s j + L ) = f j + ( s j + L ) 2 f_i-{s_i}^2+2s_i(s_j+L)=f_j+(s_j+L)^2 fisi2+2si(sj+L)=fj+(sj+L)2
即 f j + ( s j + L ) 2 = 2 s i ( s j + L ) + f i − s i 2 即f_j+(s_j+L)^2=2s_i(s_j+L)+f_i-{s_i}^2 fj+(sj+L)2=2si(sj+L)+fisi2
其中:
y = f j + ( s j + L ) 2 y=f_j+(s_j+L)^2 y=fj+(sj+L)2
k = 2 s i k=2s_i k=2si
x = s j + L x=s_j+L x=sj+L
(由 2 s i ( s j + L ) 2s_i(s_j+L) 2si(sj+L)拆成)
b = f i − s i 2 b=f_i-{s_i}^2 b=fisi2

这样,我们就得到了对于 j j j的坐标描述: ( s j + L , f j + ( s j + L ) 2 ) (s_j+L,f_j+(s_j+L)^2) (sj+L,fj+(sj+L)2)
不逼逼上图

就是这样,我们要找到一个点把那条线挂上去
线的斜率不变,又需要挂在最低的地方
点从左到右依次加入
观察一下,除了图中的那几个折线上的点,其它点想都不用想就可以淘汰:

因为它们被挡住了。
而再观察这条折线,上面任意的三个点 A , B , C A,B,C A,B,C若满足 x A < x B < x C x_A<x_B<x_C xA<xB<xC,就有 A B AB AB的斜率小于 B C BC BC的斜率。(记任意两点 B , C B,C B,C的斜率为 slop ⁡ ( B , C ) \operatorname{slop}(B,C) slop(B,C)
结合点的加入方式和最优化的需求,考虑 O ( 1 ) O(1) O(1)查询最值+末端插入的单调队列
(记单调队列为 q q q,队尾下标为 t a i l tail tail,队头下标为 h e a d head head,新加入的点为 i i i
推出淘汰队列末尾的标准:
slop ⁡ ( q t a i l − 1 , q t a i l ) ≥ slop ⁡ ( q t a i l , i ) \operatorname{slop}(q_{tail-1},q_{tail})\ge\operatorname{slop}(q_{tail},i) slop(qtail1,qtail)slop(qtail,i)时,剔除 q t a i l q_{tail} qtail

而又从单调队列引出一个问题:如何删队头?
这简单:由图可知斜率小于 k k k的就删掉证明很简单但我很懒
如果以后的直线斜率比现在小了怎么办?
k = 2 s i k=2s_i k=2si—— s i s_i si是和 i i i一起递增的。

由此,单调队列所有操作全部推出。

代码:

#include<cstdio>
#define ll long long
using namespace std;
int l;
struct deque
{
	int list[51000];
	int head,tail;//head~tail
	
	deque():head(1),tail(0){}
	int size(){return tail-head+1;}
	
	int front(){return list[head];}
	void push_front(int x){list[--head]=x;}
	void pop_front(){++head;}
	int front_2nd(){return list[head+1];}
	
	int back(){return list[tail];}
	void push_back(int x){list[++tail]=x;}
	void pop_back(){--tail;}
	int back_2nd(){return list[tail-1];}
};
ll sqr(ll x)
{return x*x;}
int a[51000];
ll s[51000];
ll f[51000];
double slop(ll a,ll b)
{return (f[b]+sqr(s[b]+l)-f[a]-sqr(s[a]+l))/(double)(s[b]-s[a]);}
int main()
{
	int n;
	scanf("%d%d",&n,&l);l++;
	for(int i=1;i<=n;i++)
		scanf("%d",a+i),s[i]=s[i-1]+a[i]+1;
	deque q;
	q.push(0);//很重要!!!要初始化!!!
	f[0]=0;
	for(int i=1;i<=n;i++)
	{
		while(q.size()>1&&slop(q.front(),q.front_2nd())<=2*s[i])q.pop_front();
		f[i]=f[q.front()]+sqr(s[i]-s[q.front()]-l);
		while(q.size()>1&&slop(q.back_2nd(),q.back())>=slop(q.back(),i))q.pop_back();
		q.push_back(i);
	}
	printf("%lld",f[n]);
	return 0;
}

参考资料:LB不顶用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值