《关于我上冬令营网课时,听说普及组考过斜率优化DP这件事》
题意理解
摆渡车往返一次要
m
m
m 分钟,但摆渡车可以在起点等人,故可以将往返一次的时间
T
∈
[
m
,
∞
)
T \in [m,\infty )
T∈[m,∞)。(但是个人都不会让他等于正无穷吧?)
求这
n
n
n 个人等待时间之和最小值。
设置状态
首先,观察数据范围
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-afPsUkMw-1612343952443)(https://imgchr.com/i/yuYSwq#pic_center)]
小数据就不说了吧
显然,可以根据时间设置状态。
设 f i f_{i} fi 表示前 i i i 个时间单位的最小花费。
状态转移
题上说了,摆渡车容量可以视为无限大,那么我们可以知道,摆渡车是印度产的,在时间
i
i
i 出发的车可以将在
i
i
i 及之前到站所有人都接走,花费为
∑
(
i
−
t
k
)
(
t
k
≤
i
)
\sum (i-t_{k}) \ \ \ (t_{k}\le i)
∑(i−tk) (tk≤i)。
如果在 i i i之前的某时刻 j j j,摆渡车接过一次人呢?(根据题意 j j j 应当满足 0 ≤ j ≤ i − m 0 \le j \le i-m 0≤j≤i−m )。
那么就有 f i = m i n ( f i , f j + ∑ ( i − t k ) ) ( j < t k ≤ i ) f_{i}=min(f_{i},f_{j}+\sum (i-t_{k}))\ \ (j < t_{k}\le i) fi=min(fi,fj+∑(i−tk)) (j<tk≤i) 。
这就是状态转移方程,可以发现遍历一段时间是 O ( T ) O(T) O(T) 的,枚举断点 j j j 也是 O ( T ) O(T) O(T) 的,而计算 ∑ ( i − t k ) \sum (i-t_{k}) ∑(i−tk) 是 O ( n ) O(n) O(n) 的。
这个DP时间复杂度为
O
(
n
T
2
)
O(nT^{2})
O(nT2)。(恭喜你拿到我没截上屏的30分)
DP优化
优化计算
∑
(
i
−
t
k
)
\sum (i-t_{k})
∑(i−tk) 是显然能用前缀和搞一下的:
设
c
n
t
i
cnt_{i}
cnti 表示时间
i
i
i 及之前有多少人在到达。
s
u
m
i
sum_{i}
sumi 表示若在
i
i
i 之前来的人一直在等,那么这些人一共等了多久,
s
u
m
i
sum_{i}
sumi 可以通过
c
n
t
i
cnt_{i}
cnti 累加得到。
n=qr();//读入和处理cnt和sum部分
m=qr();
for(register int i=1;i<=n;i++)
{
cnt[t=qr()]++;
ed=max(ed,t);//ed表示最后来的人的时间
}
for(register int i=1;i<ed+m;i++)
{
cnt[i]+=cnt[i-1];//根据前面的人数累加一下
sum[i]+=sum[i-1]+cnt[i-1];//时间累加就是i-1~i中等待的人数
}
那么怎么通过 c n t cnt cnt 和 s u m sum sum 计算出 ∑ ( i − t k ) ( j < t k ≤ i ) \sum (i-t_{k})\ \ \ (j < t_{k}\le i) ∑(i−tk) (j<tk≤i) 呢?
请看图。
用水平线长短及端点来表示人的等待情况,我们要求的是蓝色线的总长度。
根据我画的圈圈,
1
=
s
u
m
i
1=sum_{i}
1=sumi ,
2
=
s
u
m
j
2=sum_{j}
2=sumj ,
3
=
c
n
t
j
∗
(
i
−
j
)
3=cnt_{j}*(i-j)
3=cntj∗(i−j)。
那么就可以轻而易举地得到
∑
(
i
−
t
k
)
=
s
u
m
i
−
s
u
m
j
−
c
n
t
j
∗
(
i
−
j
)
(
j
<
t
k
≤
i
)
\sum (i-t_{k}) = sum_{i}-sum_{j} - cnt_{j}*(i-j)\ \ \ (j < t_{k}\le i)
∑(i−tk)=sumi−sumj−cntj∗(i−j) (j<tk≤i)。
状态转移方程就能转化为
f i = m i n ( f i , f j + s u m i − s u m j − c n t j ∗ ( i − j ) ) ( j ≤ i − m ) f_{i}=min(f_{i},f_{j}+sum_{i}-sum_{j} - cnt_{j}*(i-j))\ \ (j\le i-m) fi=min(fi,fj+sumi−sumj−cntj∗(i−j)) (j≤i−m) 。
于是复杂度愉快地变成了 O ( T 2 ) O(T^{2}) O(T2) 。(现在有50pts了!)。
Code
#include<bits/stdc++.h>
#define N 1100006
#define LL long long
using namespace std;
int n,m;
LL f[N];
int t,cnt[N],sum[N],ed;
inline int qr()
{
char a=0;int w=1,x=0;
while(a<'0'||a>'9'){if(a=='-')w=-1;a=getchar();}
while(a<='9'&&a>='0'){x=(x<<3)+(x<<1)+(a^48);a=getchar();}
return x*w;
}
int main()
{
n=qr();//读入和处理cnt和sum部分
m=qr();
for(register int i=1;i<=n;i++)
{
cnt[t=qr()]++;
ed=max(ed,t);//ed表示最后来的人的时间
}
for(register int i=1;i<ed+m;i++)//最后一班车最晚在ed+m-1时走
{
cnt[i]+=cnt[i-1];//根据前面的人数累加一下
sum[i]+=sum[i-1]+cnt[i-1];//时间累加就是i-1~i中等待的人数
}
for(register int i=1;i<ed+m;i++)
{
f[i]=sum[i];//前面没有发车
for(register int j=0;j<=i-m;j++)
f[i]=min(f[i],f[j]+sum[i]-sum[j]-cnt[j]*(i-j));//状态转移
}
LL ans=0x3f3f3f3f3f3f3f3f;
for(register int i=ed;i<ed+m;i++)//统计答案
ans=min(ans,f[i]);
printf("%lld\n",ans);
return 0;
}
规避无用转移
显然,每次摆渡车的往返加等待时间不会超过
2
m
2m
2m ,因为该时间超过
2
m
2m
2m 时,在
i
−
(
m
,
2
m
)
i-(m,2m)
i−(m,2m)可以来一辆车,答案必定不会更劣。
所以,枚举 j j j 时只用枚举 [ m a x ( 0 , i − 2 m ) , i − m ] [max(0,i-2m),i-m] [max(0,i−2m),i−m]就可以了,复杂度变成了 O ( T m ) O(Tm) O(Tm)。
把转移改一下就有70pts了!(开O2就过了)。
Code
for(register int i=1;i<ed+m;i++)
{
f[i]=sum[i];//前面没有发车
for(register int j=max(i-(m<<1),0);j<=i-m;j++)
f[i]=min(f[i],f[j]+sum[i]-sum[j]-cnt[j]*(i-j));//状态转移
}
尖端科技——斜率优化(雾)
重新看一下式子:
f i = m i n ( f i , f j + s u m i − s u m j − c n t j ∗ ( i − j ) ) f_{i}=min(f_{i},f_{j}+sum_{i}-sum_{j} - cnt_{j}*(i-j)) fi=min(fi,fj+sumi−sumj−cntj∗(i−j))
把min去掉,然后移下项,可以得到:
f j + c n t j ∗ j − s u m j = c n t j ∗ i + f i − s u m i f_{j}+cnt_{j}*j-sum_{j}=cnt_{j}*i+f_{i}-sum_{i} fj+cntj∗j−sumj=cntj∗i+fi−sumi
使 y = f j + c n t j ∗ j − s u m j y=f_{j}+cnt_{j}*j-sum_{j} y=fj+cntj∗j−sumj , k = i k=i k=i , x = c n t j x=cnt_{j} x=cntj , b = f i − s u m i b=f_{i}-sum_{i} b=fi−sumi
于是转移方程转化愉快地为 y = k x + b y=kx+b y=kx+b 的点斜式形式。
这里 x x x 和 k k k 具有点调性,可以用优先队列维护下凸包。
当遍历到时间 i i i 时将 i − m i-m i−m 入队,如果队列非空进行转移,如果队列是空的话,直接当做之前没有发过车对 f i f_{i} fi 赋值就行了。
Code
#include<bits/stdc++.h>
#define N 9100006
#define LL long long
#define LB long double
using namespace std;
int n,m;
LL f[N],t,cnt[N],sum[N],q[N],ed;
inline int qr()
{
char a=0;int w=1,x=0;
while(a<'0'||a>'9'){if(a=='-')w=-1;a=getchar();}
while(a<='9'&&a>='0'){x=(x<<3)+(x<<1)+(a^48);a=getchar();}
return x*w;
}
inline LB K(LL x,LL y)//计算两点间斜率
{
return (LB) (f[y]+cnt[y]*y-sum[y]-f[x]-cnt[x]*x+sum[x])/((cnt[y]==cnt[x])?(long double)1e-9:cnt[y]-cnt[x]);
}
int main()
{
n=qr();//读入和处理cnt和sum部分
m=qr();
for(register int i=1;i<=n;i++)
{
cnt[t=qr()]++;
ed=max(ed,t);//ed表示最后来的人的时间
}
for(register int i=1;i<ed+m;i++)//最后一班车最晚在ed+m-1时走
{
cnt[i]+=cnt[i-1];//根据前面的人数累加一下
sum[i]+=sum[i-1]+cnt[i-1];//时间累加就是i-1~i中等待的人数
}
int l=1,r=0;
for(register int i=0;i<ed+m;i++)
{
if(i>=m)
{
int op=i-m;//将时间i-m入队
while(l<r&&K(q[r],op)<=K(q[r-1],q[r]))//维护下凸包
r--;
q[++r]=op;
}
while(l<r&&K(q[l],q[l+1])<=(LB)i)//找到最优转移的位置
l++;
f[i]=sum[i];//前面没有发车
if(l<=r)
f[i]=min(f[i],f[q[l]]+sum[i]-sum[q[l]]-cnt[q[l]]*(i-q[l]));
}
LL ans=0x3f3f3f3f3f3f3f3f;
for(register int i=ed;i<ed+m;i++)//统计答案
ans=min(ans,f[i]);
printf("%lld\n",ans);
return 0;
}