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]+(i−j−1+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+2∗a(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=2∗a(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) 2∗a(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)2→f[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)
2∗a(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).
其斜率优化的理解方式还有代数的证明方式,请读者自行理解;若本文有疏漏或错误之处也欢迎读者指正。