题面
大意:自己看太复杂我不想解释了
终于,我要开始复习我目前接触到的最毒瘤、思考难度最大的算法了。
斜率优化。
简单介绍一下: 斜率优化在我看来就是利用一个玄学的方法确定一个值,然后用这个值去确定以前的一些状态是否没有用,再结合单调队列或其他数据结构减少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=0mini−1(f[j]+(i−j−1+k=j+1∑ia[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+1∑ia[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+(si−sj−L)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+si2−2si(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
fi−si2=fj−2si(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
fi−si2+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)+fi−si2
其中:
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=fi−si2
这样,我们就得到了对于
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(qtail−1,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不顶用