use:https://imgse.com/
引言(
瞎扯),听_rqy大佬说斜率优化更应该叫做截距优化
rqy大佬说的好像很有道理qwq。
斜率优化,概括的来讲就是优化形如(f_i=a_i+Max^{i-1}_{j=0}(y_j-k_ix_j))其中(k_i)是递减的(或(Max)换成(Min),(k_i)是递增的)的一种方法
其中(a_i)是一个和(j)无关的常数,然后我们看(Max)中,就只有(k_i)是与(j)无关的常数
如果暴力(dp)的话,时间复杂度就是(O(N^2)),我们还需考虑优化
状态是(O(N))的,已经够优了。考虑优化转移到(O(1))。
我们将方程中的(y_j,x_j)拿出来,看做点((x_j,y_j)),然后放到笛卡尔坐标系中(嘿,真洋呼)
像这样
然后将直线(y=kx+b)带入
然后我们选取一点,算出将直线(y=kx+b)平移至改点,然后算出平移后直线的截距
(y-y_i=k(x-x_i))
(y=kx+y_i-kx_i)截距就是(y_i-kx_i)。发现就是所需的数值
然后我们揭下来就是利用其加速决策.
假设我们在进行(k_i)时的决策,我们,我们可以找到一条(y=kx)的直线,然后在我们1~i点组成的图形中找到一个截距最大的点,就可以进行转移了。很巧的是,这条符合条件的直线只经过这个点,不经过其他点之间之间的连线(或只有多个点)
such as
然后我们还有个性质,(k_i)是递减的,然后我们在进行决策时候,就可以让上图中红线的绕这个图形进行旋转,随着(i)的增加,逐渐转到绿线所到的位置。
就像这样(ps:作图时没考虑好qwq
然后我们就会发现,有些点是我们考虑都不会考虑的,如下图所示
这一部分我们就丢掉不管了,反正他不是最优的。
然后我们发现我们进行完这一步,就好像凸出来一块。然后我们按照如此规则,修整一下整个图形
这个东西叫做凸壳
到此,我们先回忆一下。如何利用凸壳进行决策
然后对于一个(i),我们先取得(k_i),然后再从凸壳(1-i)中找到一条直线(y=kx+b),使得其截距最大。
而所找到的直线不会穿过凸壳内部。
凸壳是怎么来的呢?在(k)递减的情况下,绕所有点所围成的。(就像墙上一堆钉子,拿一根小木棍绕着转,决定小木棍的方向的是最外边的两个点,而不是其内部的点,而最外面的点相邻的,对小木棍方向有决定的点连线,形成的图形就是凸壳(马马虎虎qwq))
好,然后我们再来考虑维护凸壳,以及如何具体使用凸壳
如何维护插入一个点,使用单调队列,在我们改变直线斜率时,也就是旋转直线时,如果是顺时针旋转,则不用干其他的操作。如果是逆时针旋转,则需要进行一些维护操作
如下图
需要我们介入,将(C)点删除,那如何程序实现呢?
先不急,我们再来看一个更极端的栗子。
我们如何操作呢?
贪心的想,我们只比较(E,F)两个点,然后我们分别算一下新增点到(E,F)两条直线的斜率,若发现(F)的斜率比(E)的直线的斜率大,那我们就删除(F),(E)点何时删除呢?
在我们比较(D,E)是删除,所以我们就使用单调队列,里面是斜率单减的。
void push(int i)//将第i个点加入
{
while(tail-head>=1)//队列中至少要留两个元素,另外,因为我比较毒瘤,所以我的队首和队尾都是实指
{
int a=queue[tail],b=queue[tail-1];//去出后两个元素
if((y[i]-y[a])*(x[i]-x[b])>(x[i]-x[a])*(y[i]-y[b]))//比较,听_rqy说是叉积
//好高级的样子,其实就是两个截距式比较(我也不知道叫什么,必修二一点也没学,其实读者将左右对调对调,成为分数的形式,就能看出来为什么了。写成乘法快,还可以避免爆精)
tail--;
else break;
}
queue[++tail]=i;
}
然后就是取队首
暴力的想,每次按遍历所有点,取最大值
然后发现,竟然是单峰呀。然后我们再来改变斜率(递减),发现峰值所在的位置竟然递增
好好好,单调队列qwq。
其实这里你可以想象,一条直线就真的在凸壳上旋转,然后就会有个支点,使得这条直线绕他旋转,这个点就是当时直线截距最大的点。然后总是要离开这个支点的,然后到了下一个支点,下一个支点就是在另一个斜率时的峰顶qwq
void pop(int i)
{
while(tail-head>=1)//需要保存两个点
{
int a=queue[head],b=queue[head+1];//取出前两个值
if(y[a]-k[i]*x[a]<y[b]-k[i]*x[b])//保持单调性,排除掉不是最优解
head++;
else break;
}
return ;
}
end.