【斜率优化】BZOJ3437 小P的牧场
Description
小P在MC里有n个牧场,自西向东呈一字形排列(自西向东用1…n编号),于是他就烦恼了:为了控制这n个牧场,他需要在某些牧场上面建立控制站,每个牧场上只能建立一个控制站,每个控制站控制的牧场是它所在的牧场一直到它西边第一个控制站的所有牧场(它西边第一个控制站所在的牧场不被控制)(如果它西边不存在控制站,那么它控制西边所有的牧场),每个牧场被控制都需要一定的花费(毕竟在控制站到牧场间修建道路是需要资源的嘛~),而且该花费等于它到控制它的控制站之间的牧场数目(不包括自身,但包括控制站所在牧场)乘上该牧场的放养量,在第i个牧场建立控制站的花费是ai,每个牧场i的放养量是bi,理所当然,小P需要总花费最小,但是小P的智商有点不够用了,所以这个最小总花费就由你来算出啦。
Input
第一行一个整数 n 表示牧场数目
第二行包括n个整数,第i个整数表示ai
第三行包括n个整数,第i个整数表示bi
Output
只有一行,包括一个整数,表示最小花费
Sample Input
42 4 2 4
3 1 4 2
Sample Output
9样例解释
选取牧场1,3,4建立控制站,最小费用为2+(2+1*1)+4=9。
1<=n<=1000000, 0 < a i ,bi < = 10000
Hint
Source
KpmCup#0 By Greens题意
一列农场,从左到右\(1,\dots,n\),一个农场\(i\)被标记的代价为\(a_i\),不标记的代价为\((k-i)b_i\)其中\(k\)为比\(i\)大的第一个被标记的农场。
最后一个农场必须被标记。
求最小花费。
思路
很明显一个dp
也很容易想到状态和转移方程而且不会想歪
设\(dp_{i,0/1}\)表示前\(i\)个牧场中,第\(i\)个牧场标记/不标记的最小花费。
讨论一下如果标记
- 枚举前一个被标记的牧场是\(j\),那么\(i\)打标记就需要统计\((j,i)\)不标记的花费。
就是\(\sum_{k=j+1}^{i-1}{(i-k)b_k}\) - \(i\)是整个序列里面第一个打标记的,没有前一个了。
可以当作第一个标记是第\(0\)个牧场。
那么
看下来\(dp_{i,0}\)似乎没有什么用,而且我们的答案也不需要\(dp_{n,0}\)于是乎我们压掉一维
让\(dp_i\)表示\(i\)强制标记的最小花费,有
时间复杂度\(O(n^3)\)
怎么可能过得了嘛。。。
从最里面开始,考虑先优化\(\sum_{k=j+1}^{i-1}{(i-k)b_k}\)
好像可以用前缀和
定义\(st_i=\sum_{j=1}^ii\cdot b_i,sp_i=\sum_{j=1}^ib_i\)
那么就有
优化了一个\(n\)
\(O(n^2)\)了,再努力一下。
这个\(f(i,j)\)里面拆开括号之后,一共有四项两项仅和\(i\)有关,一项仅和\(j\)有关,一项都有关。
想到斜率优化。
我们可以知道,对于不同的\(j,k<i, j\neq k\)我们得出的\(dp_j+f(i,j)\)和\(dp_k+f(i,k)\)不一定相同,而我们就是要遍历所有的\(\forall 0\leq j<i\)求出最小值。
那么我们假设有两个值\(dp_j+f(i,j),dp_k+f(i,k)\),只有他们之中较小的那个能产生贡献。
不妨设\(j>k\)那么根据前缀和的定义我们可以得知\(sp_j>sp_k\)
如果\(j\)比\(k\)更优,也就是\(dp_j+f(i,j)<dp_k+f(i,k)\)
展开一下,
最后这个式子看上去有点眼熟,他分子和分母都是一个二项式,其中一项只和\(j\)有关,一项只和\(k\)有关。
我们能不能把他们看做两个点\(\displaystyle A_j(sp_j,dp_j+st_j), A_k(sp_k,dp_k+st_k)\)
那么那个分式是什么意思?因为\(sp\)互不相同,所以那个分式是不是可以表示直线\(A_jA_k\)的斜率?
那么我们就清楚了,\(j\)比\(k(k<j)\)优当且仅当\(A_jA_k\)的斜率小于\(i\)
反过来想一下,如果过点\(A_k\)作一条斜率为\(i\)的直线\(l_k\),\(j\)比\(k\)优当且仅当\(j\)在直线\(l_k\)之下
\(\uparrow\)就像这幅图,\(l_k\)的斜率是\(i\),那么只要\(A_j\)在\(l_k\)下面那么\(A_kA_j\)的斜率就小于\(k\)了,那么\(j\)就比\(k\)优了。
然后我们考虑一下这些点,因为我们在求\(dp_i\),所以\(\forall k<j<i\)的点的\(dp_j,dp_k\)都应该已经求出来了。然后剩下的\(ap_k,ap_j,st_k,st_j\)都只是一些前缀和,是常数。
所以我们可以在坐标系里面标出所有的\(A_1,A_2,\dots,A_{i-1}\)
因为他们的横坐标\(sp\)单调递增,所以我们画出来的图可能长这样:
(x轴不是直的就告诉你他们的横坐标并不是线性关系,因此斜率为\(1\)的直线看上去并不像一个象限平分线)
理论上只要\(i\)足够大,那么这些点有朝一日都会比\(A_1\)优。因为只要\(i\)足够大,那么做一条直线,除了\(A_1\)之外所有的点都在直线以下,就都比\(A_1\)优。
但是在某些场合,比如\(i\)不够大的情况下,\(A_1\)就是最有用的。
所以我们才要保存下所有的点\(A\),从里面选取最优的。
但是一旦我们的\(i\)增大到比直线\(A_1A_2\)的斜率大,并且\(i>2\),那么只要能选\(A_1\)就能选\(A_2\),所以\(A_1\)可以舍弃不顾。
那么什么样的一个点是可以舍弃的呢?
"上凸"的点
并不是说所有斜率优化的题都要舍弃上凸包,但是这道题要舍弃的是上凸包。看看下面的分析就知道为什么了:
假如现在有三个点\(B,C,D\in\left\{A_i\right\}\),且\(x_B<x_C<x_D\)
“上凸”只是一个形象的描述,我们把他转化为数学语言。
如果直线\(BC\)的斜率是\(k_1\),直线\(CD\)的斜率是\(k_2\),如果\(k_1>k_2\),我们就会说折线\(BCD\)是上凸的。
如果说\(C\)比\(B\)优,那么就等价于\(i>k_1\),然后由\(k_1>k_2\)可以得出\(i>k_2\), \(D\)也比\(B\)优。
那么\(C\)和\(D\)哪个更优呢?我们可以发现,如果在\(C\)处做一条斜率为\(i\)的直线\(l_c\),由于\(i>k_2\),所以线段\(CD\)是在直线\(l_c\)下面的,也就是说点\(D\)在直线\(l_c\)下面,\(D\)比\(C\)更优。
也就是说,只要\(C\)比\(B\)优,那么一定会有\(D\)比\(C\)优。所以\(C\)点作为一个上凸的结点可以删除。
所以说我们要保存的结点就是一段“下凸”包。直线意味着斜率相同,也就是相同贡献,不保存可以节省空间。
为什么我们要保存一个包呢?因为随着\(i\)的不断增大,原来一些没用的点可能会变成有用了,所以我们要保存下来以后用。
于是乎我们得到了一个由一个现在有用以及一些将来有用和一些过去有用的点组成的下凸包:
大概长成这样子。
怎么维护这个下凸包呢?我们可以用一个栈来储存包中的结点。
现在新来了一个点(就相当于处理完了\(dp_i\),之后\(dp_i\)就可以用来更新后面的点,就需要把他放入栈中备用)
本来是下凸包,包中相邻的点的斜率单调递增。新来了一个点(蓝色),就有可能破坏了这个下凸包的性质
那么怎么办呢?我们根据上面的分析,所有上凸的点都是没用的,所以我们可以理所当然的从栈顶把“上凸”的点删去。
然后再看一下,发现还是上凸。
那就一直弹出到不上凸。
这个过程翻译成数学语言就是:
- 若栈中结点个数小于两个,则直接插入。
- 否则,设最后一个入栈的结点(栈顶)和倒数第二个入栈的结点的斜率为\(k_1\),设新的点和栈顶的斜率为\(k_2\),若\(k_1\geq k_2\)则弹出栈顶,否则进入4
- 重复执行2直到\(k_1<k_2\)或栈中结点个数小于两个
- 将新的点插入到栈顶(压入栈中)。
这样我们就可以维护一个上凸包
于是乎我们得到了一个由一个现在有用以及一些将来有用和一些过去有用的点组成的下凸包:
这里陈述得很仔细,只有一个点是现在有用的。我们知道过一个点作斜率为\(i\)的直线,那么在这条直线以下的点都比这个点优。
那么有一句看上去废话的话:
一个点最优当且仅当不存在另一个点比他更优
然后翻译成几何语言就是
一个点最优当且仅当不存在任何一个点在直线以下, 这里直线就是指过这个点作斜率为\(i\)的直线。
那么现在我们有一个下凸折线,怎么找到这个最优的点呢?
我们可以转化为这段折线和斜率为\(i\)的直线的关系。因为这条直线是过某个折线上的点的,所以这条直线必定和折线有交点。
这就有两种情况:相交和相切。
如果是相交那么就有两个交点,又因为这是一条折线所以一定有一个结点作为“拐点”在直线以下,这就说明这条直线经过的点并不是最优的。
如果是相切,那么这是一个下凸包,而且斜率\(i>0\),我们可以很直观看出这条直线下面没有任何点了。
因此最优的点就是折线和斜率为\(i\)的直线相切的点。
这个点有什么特点呢?我们仔细观察一下上图右边的情况。
不难发现这个点和前面的点的斜率小于\(i\),和右边的点的斜率大于\(i\)
如果碰到等于的情况。。。随便选哪个吧。
然后这个折线的斜率是单调递增的,因此我们可以用二分的方法找到这个点。
总结一下,我们首先要用单调栈来维护一个下凸包。然后求解\(dp_i\)的时候就在栈里面通过二分的方式找到切点\(k\),可以证明\(dp_k+f(i,k)\)是最小的。因此一次求解的时间复杂度是\(O(\log_2n)\)。之后把点\(i\)压入栈中。
这样的时间复杂度就是\(O(n\log_2n)\)
结束了?怎么可能。
我们真的有必要每次都二分一下吗?注意到我们求解\(dp_i\)中\(i\)是单调递增的,也就是那条直线的斜率是单调递增的。
想想我们学平面凸包的时候,有一道题叫做最小矩形覆盖(LGP3187)。这道题给了我们思路,一个下凸包,斜率单调递增。配上一条单调递增的折线,他们的切点的横坐标也一定是单调递增的。
这是因为随着直线的斜率递增,切点和前后两个点的斜率也要对应增加。因此在切点前面的点一定不会成为最优决策,完全就可以删去。
那么就好像上面那个图一样
绿色框住的点是可以删去的,因为他们和下一个点的连线的斜率小于当前\(i\),不可能成为切点了。
当前的切点\(A\)在\(i\)增大到大于他和下一个点\(B\)的连线\(l_{AB}\)的斜率的时候也会被删去。
那么我们每次的切点就变成了这条折线中未被删除的第一个点。
我们稍微修改一下,不用单调栈,用单调队列
用数学语言把这一过程加以表述,当我们处理完\(dp_i\)要将\(i\)加入下凸包时
- 若队列中结点个数小于两个,则直接插入。
- 否则,设最后一个入队的结点(队尾)和倒数第二个入队的结点的斜率为\(k_1\),设新的点和队尾的斜率为\(k_2\),若\(k_1\geq k_2\)则弹出队尾,否则进入4
- 重复执行2直到\(k_1<k_2\)或队中结点个数小于两个
- 将新的点插入到队尾(加入队列)。
这个加入队列的过程和压入栈的过程是一模一样的。但是我们用双端单调队列就可以去除队头一些已经没用的点。当我们要处理\(dp_i\)而要找切点时
- 若队列中结点个数小于两个,则队头结点为切点
- 否则,设队头结点和队列第二个结点的斜率为\(k\),若\(k\leq i\)则弹出队头。否则进入4
- 重复执行2直到\(k>i\)或队列中仅剩一个结点
- 队头结点为所求切点。
(完了,真的完结了)
我们来分析一下时间复杂度。我们处理每个点的时候会弹出一些结点,同时加入一个结点。所以我们最多加入\(n\)个结点,当然最多就只能弹出\(n\)个结点,因此我们的时间复杂度是\(O(2n)\)级别的
非常优越。
注意一个小细节,我们判断更优的条件是\(\frac{(dp_j+st_j)-(dp_k+st_k)}{sp_j-sp_k}<i\),这样就有可能出现浮点数。由于浮点数运算很慢(精度问题暂时不需要考虑,这方面还是可靠的),大概是整数运算的5-20倍。因此我们在日常书写代码的过程中尽量避免出现浮点数。所以我们可以把这个条件改成
同样,我们程序过程中求斜率来比较的时候也最好化成整数相乘的方式来比较,这样整个程序的常数会小很多。
最后说一下一些细节,因为最后一个必须标记,因此答案就是\(dp_n\)。然后只有一个的时候他必须被标记,因此初始状态就是\(dp_0=a_0\),然后\(0\)入队。
代码
施工中...施工完毕
#include <cstdio>
const int N=1<<20;
int a[N],b[N],n;
long long int st[N],sp[N],dp[N];
int deque[N],be,ed;
template <typename T>
T read(T* n=0x0)
{
T x=0;
char ch=getchar();
while(ch<'0' or ch >'9') ch=getchar();
while(ch>='0' and ch <='9')
{
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
if(n) *n=x;
return x;
}
#define x(i) sp[i]
#define y(i) (dp[i]+st[i])
inline int get(const int i)
{
// (y(be+1)-y(be))/(x(be+1)-x(be))<=i
while(ed-be>1 and (y(deque[be+1])-y(deque[be])<=i*(x(deque[be+1])-x(deque[be])))) ++be;
return deque[be];
}
inline void append(const int i)
{
// ((y(ed-1)-y(ed-2))/(x(ed-1)-x(ed-2))>=(y(i)-y(ed-1))/(x(i)-x(ed-1)))
while(ed-be>1 and ((y(deque[ed-1])-y(deque[ed-2]))*(x(i)-x(deque[ed-1]))>=(y(i)-y(deque[ed-1]))*(x(deque[ed-1])-x(deque[ed-2])))) --ed;
deque[ed++]=i;
return;
}
#undef x
#undef y
int main()
{
read(&n);
for(register int i=1;i<=n;++i) read(&a[i]);
for(register int i=1;i<=n;++i) read(&b[i]);
// be careful the i below should be long long int
// or the result of the operation will be int, leading to wrong answer
for(register long long int i=1;i<=n;++i) st[i]=st[i-1]+b[i]*i;
for(register int i=1;i<=n;++i) sp[i]=sp[i-1]+b[i];
deque[ed++]=0;
for(register int i=1;i<=n;++i)
{
#define f(i,j) ((sp[i-1]-sp[j])*i-(st[i-1]-st[j]))
const int k=get(i);
dp[i]=dp[k]+f(i,k)+a[i];
append(i);
#undef f
}
printf("%lld\n",dp[n]);
return 0;
}