【算法入门】动态规划·斜率优化:[HNOI2008]玩具装箱

本文介绍了动态规划在解决[HNOI2008]玩具装箱问题中的应用,通过斜率优化降低算法复杂度。讲解了如何将状态转移方程转化为一次函数,并利用单调队列进行优化,实现O(n)的时间复杂度。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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。但他希望费用最小.

Solution

如果考虑 O ( n 2 ) O(n^2) O(n2)的算法,设f[i]为前i件玩具压缩后形成的最小费用。

则根据分段的规则,对于每一个 f [ i ] f[i] f[i]枚举上一个分段的临街点j,可以得出状态转移方程:

f [ i ] = m i n ( f [ j ] + ( i − j − 1 + s u m [ i ] − s u m [ j ] − L ) 2 ) f[i]=min(f[j]+(i-j-1+sum[i]-sum[j]-L)^2) f[i]=min(f[j]+(ij1+sum[i]sum[j]L)2)

为了方便地求出区间和,我们使用前缀和,其中 s u m [ i ] = ∑ j = 1 i a [ j ] sum[i]=\sum_{j=1}^{i} a[j] sum[i]=j=1ia[j]

显然,这样的复杂度是不可取的,我们应该考虑对转移进行优化。

我们观察到方程较为复杂,需要考虑化简:

a ( i ) = i + s u m [ i ] , b ( i ) = i + s u m [ i ] + 1 + L a(i)=i+sum[i],b(i)=i+sum[i]+1+L a(i)=i+sum[i],b(i)=i+sum[i]+1+L

原式可变形为: f [ i ] = m i n ( f [ j ] + ( a ( i ) − b ( j ) ) 2 ) f[i]=min(f[j]+(a(i)-b(j))^2) f[i]=min(f[j]+(a(i)b(j))2)

接着把平方式展开,把 m i n min min函数去掉: f [ i ] = f [ j ] + a ( i ) 2 + 2 ∗ a ( i ) ∗ b ( j ) + b ( j ) 2 f[i]=f[j]+a(i)^2+2*a(i)*b(j)+b(j)^2 f[i]=f[j]+a(i)2+2a(i)b(j)+b(j)2

移项, f [ j ] + b ( j ) 2 = 2 ∗ a ( i ) ∗ b ( j ) + f [ i ] − a ( i ) 2 f[j]+b(j)^2=2*a(i)*b(j)+f[i]-a(i)^2 f[j]+b(j)2=2a(i)b(j)+f[i]a(i)2

就这样,将方程变形为了一个一次函数 ( y = k x + b , k ≠ 0 ) (y=kx+b,k≠0) (y=kx+b,k̸=0的形式,其中:

f [ j ] + b [ j ] 2 f[j]+b[j]^2 f[j]+b[j]2应变量 y y y 2 ∗ a ( i ) 2*a(i) 2a(i)斜率 k k k b ( j ) b(j) b(j)自变量 x x x f [ i ] − a ( i ) 2 f[i]-a(i)^2 f[i]a(i)2表示截距 b b b

为什么是这样变形的,或通常变形的规则是怎样的?

  • 一般情况下,将仅含有 i i i项的为截距 b b b,仅含有 j j j项的为应变量 y y y,在既含有 i i i项又含有 j j j项的为 k x kx kx,而 k k k则是含有 i i i的一项, x x x则是含有 j j j的一项。

那么这条直线一定有这个特征:过点 ( b ( j ) , f [ j ] + b ( j ) 2 ) (b(j),f[j]+b(j)^2) (b(j),f[j]+b(j)2)

而这条线的由于斜率k是不变的,而截距是变化的,在坐标系上:是一条相同的直线在上下平移。

b = f [ i ] − a ( i ) 2 → f [ i ] = a ( i ) 2 + b b=f[i]-a(i)^2→f[i]=a(i)^2+b b=f[i]a(i)2f[i]=a(i)2+b

当截距最小时, f [ i ] f[i] f[i]有最小值。或许你也可以这么理解:
斜率优化若干个最外面的决策形成了一个"下凸壳",

当j取不同的值时,有若干个点 ( b ( j ) , f [ j ] + b ( j ) 2 ) (b(j),f[j]+b(j)^2) (b(j),f[j]+b(j)2)。当直线自底向上平移以后,遇到的第一个点就是合法的、最优的决策。观察图中的坐标系,发现最优决策的点与前一个点形成的斜率小于 k k k,与下一个点的斜率大于 k k k;我们便可以利用这个关系去找到这个最优的决策进行状态转移。

当状态转移以后,会形成一个新的点,若该点与上一个形成的斜率小于原来最后两点形成的斜率,说明该新加入的点靠外,一定比原来的点更优,故原来的点便可以排除了。

我们如何用程序实现这一算法呢?——单调队列

程序的实现分为4个步骤:
① ① 若队列队首两点的斜率小于当前直线 2 ∗ a ( i ) 2*a(i) 2a(i)的斜率,弹出队首。
② ② 进行状态转移。
③ ③ 若新加入的项与倒数第二个点的斜率比原来最后两点的斜率小,弹出队尾。
④ ④ 加入队列。

具体实现是这样的:

#include<bits/stdc++.h>
using namespace std;
#define LL long long
const LL N=50000*4;
LL n,L,h,t;
LL sum[N],q[N],f[N];
#define a(i) (i+sum[i])
#define b(j) (j+sum[j]+1+L)
#define x(j) (b(j))
#define y(j) (f[j]+b(j)*b(j))
double k(LL a,LL b)
{
	double K=1.0*(y(a)-y(b))/(x(a)-x(b));
	return abs(K);
}
int main(void)
{
	cin>>n>>L;
	for (LL i=1;i<=n;++i) 
	{
		LL w;
		cin>>w;
		sum[i]=sum[i-1]+w;
	}
	h=t=1;
	for (LL i=1;i<=n;++i)
	{
		while (h<t && k(q[h],q[h+1])<=2.0*a(i)) h++;
		f[i]=f[q[h]]+(a(i)-b(q[h]))*(a(i)-b(q[h]));
		while (h<t && k(q[t],q[t-1])>k(q[t-1],i)) t--;
		q[++t]=i;
	}
	cout<<f[n]<<endl;
	return 0;
}

这种方法叫做动态规划的斜率优化

一般用于转移方程展开后,即含有 i i i项,又含有 j j j项,也同时含有 i i i j j j项的状态转移方程。

要根据坐标系理解其优化的具体信息;再利用单调队列优化。

而每一个信息进出队列一次,故时间复杂度为 O ( n ) . O(n). O(n).

其斜率优化的理解方式还有代数的证明方式,请读者自行理解;若本文有疏漏或错误之处也欢迎读者指正。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值