【斜率优化】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

4
2 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,,n,一个农场i被标记的代价为ai,不标记的代价为(ki)bi其中k为比i大的第一个被标记的农场。
最后一个农场必须被标记。
求最小花费。

思路

很明显一个dp
也很容易想到状态和转移方程而且不会想歪
dpi,0/1表示前i个牧场中,第i个牧场标记/不标记的最小花费。
讨论一下如果标记

  • 枚举前一个被标记的牧场是j,那么i打标记就需要统计(j,i)不标记的花费。
    就是k=j+1i1(ik)bk
  • i是整个序列里面第一个打标记的,没有前一个了。
    可以当作第一个标记是第0个牧场。

那么

dpi,1=min0j<i{dpj,1+k=j+1i1(ik)bk}+ai

看下来dpi,0似乎没有什么用,而且我们的答案也不需要dpn,0于是乎我们压掉一维
dpi表示i强制标记的最小花费,有

dpi=min0j<i{dpj+k=j+1i1(ik)bk}+ai

时间复杂度O(n3)
怎么可能过得了嘛。。。
从最里面开始,考虑先优化k=j+1i1(ik)bk

k=j+1i1(ik)bk=k=j+1i1(ibkkbk)=ik=j+1j1bkk=j+1i1kbk

好像可以用前缀和
定义sti=j=1iibi,spi=j=1ibi
那么就有

k=j+1i1(ik)bk=(spi1spj)i(sti1stj)

优化了一个n

dpi=min0j<i{dpj+f(i,j)}+aif(i,j)=(spi1spj)i(sti1stj)

O(n2)了,再努力一下。
这个f(i,j)里面拆开括号之后,一共有四项两项仅和i有关,一项仅和j有关,一项都有关。
想到斜率优化。
我们可以知道,对于不同的j,k<i,jk我们得出的dpj+f(i,j)dpk+f(i,k)不一定相同,而我们就是要遍历所有的0j<i求出最小值。
那么我们假设有两个值dpj+f(i,j),dpk+f(i,k),只有他们之中较小的那个能产生贡献。
不妨设j>k那么根据前缀和的定义我们可以得知spj>spk
如果jk更优,也就是dpj+f(i,j)<dpk+f(i,k)
展开一下,

dpj+spi1ispjisti1+stj<dpk+spi1ispkisti1+stkdpj+stjspji<dpk+stkspki(dpj+stj)(dpk+stk)<i(spjspk)(dpj+stj)(dpk+stk)spjspk<i

最后这个式子看上去有点眼熟,他分子和分母都是一个二项式,其中一项只和j有关,一项只和k有关。
我们能不能把他们看做两个点Aj(spj,dpj+stj),Ak(spk,dpk+stk)
那么那个分式是什么意思?因为sp互不相同,所以那个分式是不是可以表示直线AjAk的斜率?
那么我们就清楚了,jk(k<j)优当且仅当AjAk的斜率小于i
反过来想一下,如果过点Ak作一条斜率为i的直线lkjk优当且仅当j在直线lk之下

就像这幅图,lk的斜率是i,那么只要Ajlk下面那么AkAj的斜率就小于k了,那么j就比k优了。
然后我们考虑一下这些点,因为我们在求dpi,所以k<j<i的点的dpj,dpk都应该已经求出来了。然后剩下的apk,apj,stk,stj都只是一些前缀和,是常数。
所以我们可以在坐标系里面标出所有的A1,A2,,Ai1
因为他们的横坐标sp单调递增,所以我们画出来的图可能长这样:

(x轴不是直的就告诉你他们的横坐标并不是线性关系,因此斜率为1的直线看上去并不像一个象限平分线)
理论上只要i足够大,那么这些点有朝一日都会比A1优。因为只要i足够大,那么做一条直线,除了A1之外所有的点都在直线以下,就都比A1优。
但是在某些场合,比如i不够大的情况下,A1就是最有用的。
所以我们才要保存下所有的点A,从里面选取最优的。
但是一旦我们的i增大到比直线A1A2的斜率大,并且i>2,那么只要能选A1就能选A2,所以A1可以舍弃不顾。
那么什么样的一个点是可以舍弃的呢?

"上凸"的点

并不是说所有斜率优化的题都要舍弃上凸包,但是这道题要舍弃的是上凸包。看看下面的分析就知道为什么了:
假如现在有三个点B,C,D{Ai},且xB<xC<xD
“上凸”只是一个形象的描述,我们把他转化为数学语言。
如果直线BC的斜率是k1,直线CD的斜率是k2,如果k1>k2,我们就会说折线BCD是上凸的。

如果说CB优,那么就等价于i>k1,然后由k1>k2可以得出i>k2, D也比B优。
那么CD哪个更优呢?我们可以发现,如果在C处做一条斜率为i的直线lc,由于i>k2,所以线段CD是在直线lc下面的,也就是说点D在直线lc下面,DC更优。
也就是说,只要CB优,那么一定会有DC优。所以C点作为一个上凸的结点可以删除。
所以说我们要保存的结点就是一段“下凸”包。直线意味着斜率相同,也就是相同贡献,不保存可以节省空间。
为什么我们要保存一个包呢?因为随着i的不断增大,原来一些没用的点可能会变成有用了,所以我们要保存下来以后用。
于是乎我们得到了一个由一个现在有用以及一些将来有用和一些过去有用的点组成的下凸包:

大概长成这样子。
怎么维护这个下凸包呢?我们可以用一个栈来储存包中的结点。
现在新来了一个点(就相当于处理完了dpi,之后dpi就可以用来更新后面的点,就需要把他放入栈中备用)
本来是下凸包,包中相邻的点的斜率单调递增。新来了一个点(蓝色),就有可能破坏了这个下凸包的性质

那么怎么办呢?我们根据上面的分析,所有上凸的点都是没用的,所以我们可以理所当然的从栈顶把“上凸”的点删去。
然后再看一下,发现还是上凸。
那就一直弹出到不上凸。

这个过程翻译成数学语言就是:

  1. 若栈中结点个数小于两个,则直接插入。
  2. 否则,设最后一个入栈的结点(栈顶)和倒数第二个入栈的结点的斜率为k1,设新的点和栈顶的斜率为k2,若k1k2则弹出栈顶,否则进入4
  3. 重复执行2直到k1<k2或栈中结点个数小于两个
  4. 将新的点插入到栈顶(压入栈中)。

这样我们就可以维护一个上凸包

于是乎我们得到了一个由一个现在有用以及一些将来有用和一些过去有用的点组成的下凸包:

这里陈述得很仔细,只有一个点是现在有用的。我们知道过一个点作斜率为i的直线,那么在这条直线以下的点都比这个点优。
那么有一句看上去废话的话:

一个点最优当且仅当不存在另一个点比他更优

然后翻译成几何语言就是
一个点最优当且仅当不存在任何一个点在直线以下, 这里直线就是指过这个点作斜率为i的直线。
那么现在我们有一个下凸折线,怎么找到这个最优的点呢?
我们可以转化为这段折线和斜率为i的直线的关系。因为这条直线是过某个折线上的点的,所以这条直线必定和折线有交点。
这就有两种情况:相交和相切。

如果是相交那么就有两个交点,又因为这是一条折线所以一定有一个结点作为“拐点”在直线以下,这就说明这条直线经过的点并不是最优的。
如果是相切,那么这是一个下凸包,而且斜率i>0,我们可以很直观看出这条直线下面没有任何点了。
因此最优的点就是折线和斜率为i的直线相切的点。
这个点有什么特点呢?我们仔细观察一下上图右边的情况。
不难发现这个点和前面的点的斜率小于i,和右边的点的斜率大于i
如果碰到等于的情况。。。随便选哪个吧。
然后这个折线的斜率是单调递增的,因此我们可以用二分的方法找到这个点。


总结一下,我们首先要用单调栈来维护一个下凸包。然后求解dpi的时候就在栈里面通过二分的方式找到切点k,可以证明dpk+f(i,k)是最小的。因此一次求解的时间复杂度是O(log2n)。之后把点i压入栈中。
这样的时间复杂度就是O(nlog2n)


结束了?怎么可能。
我们真的有必要每次都二分一下吗?注意到我们求解dpii是单调递增的,也就是那条直线的斜率是单调递增的。
想想我们学平面凸包的时候,有一道题叫做最小矩形覆盖(LGP3187)。这道题给了我们思路,一个下凸包,斜率单调递增。配上一条单调递增的折线,他们的切点的横坐标也一定是单调递增的。
这是因为随着直线的斜率递增,切点和前后两个点的斜率也要对应增加。因此在切点前面的点一定不会成为最优决策,完全就可以删去。
那么就好像上面那个图一样

绿色框住的点是可以删去的,因为他们和下一个点的连线的斜率小于当前i,不可能成为切点了。
当前的切点Ai增大到大于他和下一个点B的连线lAB的斜率的时候也会被删去。
那么我们每次的切点就变成了这条折线中未被删除的第一个点。
我们稍微修改一下,不用单调栈,用单调队列
用数学语言把这一过程加以表述,当我们处理完dpi要将i加入下凸包时

  1. 若队列中结点个数小于两个,则直接插入。
  2. 否则,设最后一个入队的结点(队尾)和倒数第二个入队的结点的斜率为k1,设新的点和队尾的斜率为k2,若k1k2则弹出队尾,否则进入4
  3. 重复执行2直到k1<k2或队中结点个数小于两个
  4. 将新的点插入到队尾(加入队列)。

这个加入队列的过程和压入栈的过程是一模一样的。但是我们用双端单调队列就可以去除队头一些已经没用的点。当我们要处理dpi而要找切点时

  1. 若队列中结点个数小于两个,则队头结点为切点
  2. 否则,设队头结点和队列第二个结点的斜率为k,若ki则弹出队头。否则进入4
  3. 重复执行2直到k>i或队列中仅剩一个结点
  4. 队头结点为所求切点。

(完了,真的完结了)
我们来分析一下时间复杂度。我们处理每个点的时候会弹出一些结点,同时加入一个结点。所以我们最多加入n个结点,当然最多就只能弹出n个结点,因此我们的时间复杂度是O(2n)级别的
非常优越。
注意一个小细节,我们判断更优的条件是(dpj+stj)(dpk+stk)spjspk<i,这样就有可能出现浮点数。由于浮点数运算很慢(精度问题暂时不需要考虑,这方面还是可靠的),大概是整数运算的5-20倍。因此我们在日常书写代码的过程中尽量避免出现浮点数。所以我们可以把这个条件改成

(dpj+stj)(dpk+stk)spjspk<i(dpj+stj)(dpk+stk)<i(spjspk)(spj>spk)

同样,我们程序过程中求斜率来比较的时候也最好化成整数相乘的方式来比较,这样整个程序的常数会小很多。
最后说一下一些细节,因为最后一个必须标记,因此答案就是dpn。然后只有一个的时候他必须被标记,因此初始状态就是dp0=a0,然后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;
}
posted @   IdanSuce  阅读(118)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
点击右上角即可分享
微信分享提示