斜率优化DP复习笔记

前言

复习笔记2nd。

Warning:鉴于摆渡车是普及组题目,本文的难度定位在普及+至省选-。

参照洛谷的题目难度评分(不过感觉部分有虚高,提高组建议全部掌握,普及组可以选择性阅读。)

引用部分(如这个文本)为总结性内容,建议即使是跳过部分也进行阅读。

0——P3195[HNOI2008]玩具装箱

题目链接
怎么一上来就是紫题啊

题意

给定 \(C_i\) 表示每个物体长度,把 \(i\sim j\) 的物品放入一个容器中,容器的长度为 \(x=j-i+\sum_{k=i}^j C_k.\) 一个长度为 \(x\) 的容器制作费用为 \((x-L)^2\) ,求装下所有物品的最小费用。

思路

\(S[n]=\sum( C_i+1),f[i]\) 为装好前 \(i\) 个的最小花费,转移方程为 :

\[f[i]=min(f[j]+(S[i]-S[j]-1-L)^2). \]

\(L\) 提前加一,去掉 \(min\) 化简得:

\[f[i]=f[j]+(S[i]-S[j]-L)^2 \]

把平方拆开得到:

\[f[i]=S[i]^2-2S[i]L+f[j]+(S[j]+L)^2-2S[i]S[j] \]

下面将描述如何进行斜率优化。


斜率优化的一般方式

注:此处对应讲解的“线性规划”部分,个人认为比较便于理解。

对于上面那个式子,进行移项,使得变成形如 \(y=kx+b\) 的形式。

移项遵循原则:把含有 \(g(i)\times g(j)\) 的表达式看做斜率 \(k\) 乘以未知数 \(x\) ,含有 \(f[i]\) 的项必须在 \(b\) 的表达式中,含有 \(g(j)\) 的项必须在 \(y\) 的表达式中。为了方便分析,如果 \(x\) 的表达式单调递减,等式两边同乘 \(-1\) 变为单增。

那么原式就可以化为:

\[2(S[i])S[j]+(f[i]-S[i]^2+2S[i]L)=(f[j]+(S[j]+L)^2) \]

其中,一次函数的各个项分别对应:

\[k_i=2S[i],x_i=S[j],b_i=f[i]-S[i]^2+2S[i]L,y_i=f[j]+(S[j]+L)^2 \]

对于这个式子,我们的目的是求出一个 \(j\) 使得 \(f[i]\) 最小,又有 \(b[i]=f[i]-S[i]^2\) ,所以从图像角度看,就是找某个点使得这条直线经过它的时候算出来的 \(b\) 最小。

由上面的式子可以知道,这条直线的斜率是固定的。那么想象在一个平面上,有一些点,一条直线向上移,碰到的第一个点一定是使得 \(b\) 最小的位置。 可以发现,可能的点位于点集的下凸包上。

那么现在只需要考虑如何维护凸包点集即可。

用单调队列维护:

(1) 在凸包上找到最优点 \(j\) ,并用来更新 \(f[i]\)

(2) 将 \(i\) 作为一个决策点加入,并更新凸包(如果点 \(i\) 也是决策点之一,那么交换顺序)。具体操作为,(对于下凸包)如果 \((q[t-1],q[t])\) 的斜率不大于 \((q[t],i)\) 的斜率,那么队尾出队,出队完成后把 \(i\) 加入。
slope(q[t-1],q[t])>=slope(q[t-1],i) (这个地方讲义里面貌似有误,和下凸包的情况不符)


决策单调性再优化

Warning :此部分需要证明,并非通用。

\(j_0[i]\)\(f[i]\) 转移的最优决策点,那么有 \(\forall i\leq i',j_0[i]\leq j_0[i']\) (非严格递增)(证明略,见文末讲义)

考虑如何证明。此题中 \(k_0[i]=2S[i]\) 显然单增。详细一点就是:\(k_0[i]\) 单增,最优决策点单增(看下凸包的图)。当然如果不敢肯定的话还是老老实实二分栈吧qwq

由于最优决策点递增,那么可以用单调队列维护。在原先找 \(j\) 的过程中,是从队头一个一个找(或者二分)的,现在就改为:
如果队头线段斜率 \(\leq k_0[i]\) 直接出队,停止时即为最优决策点。

复杂度 \(O(nlogn)=>O(n)\)


代码

#include <bits/stdc++.h>
#define ll long long
#define lb long double
using namespace std;
const int N=5e4+10;
ll n,L,h=1,t=0,q[N],s[N],f[N];
ll X( ll num ) { return s[num]; }
ll Y( ll num ) { return f[num]+(s[num]+L)*(s[num]+L); }
lb slope( ll n1,ll n2 ) { return (lb)(Y(n2)-Y(n1))/(X(n2)-X(n1)); }

int main()
{
        scanf( "%lld%lld",&n,&L ); L++; s[0]=0;
        for ( int i=1; i<=n ;i++ )
                scanf( "%lld",&s[i] ),s[i]+=s[i-1]+1;
        
        q[++t]=0;
        for ( int i=1; i<=n; i++ )
        {
                while ( h<t && slope(q[h],q[h+1])<=2*s[i] ) h++;
                f[i]=f[q[h]]+(s[i]-s[q[h]]-L)*(s[i]-s[q[h]]-L);
                while ( h<t && slope(q[t-1],q[t])>=slope(q[t-1],i) ) t--;
                q[++t]=i;
        }

        printf( "%lld",f[n] );
}

1——关于单调性的研究

\(X(j)\) 单增单减

将方程变为 \(\dfrac{Y(j_2)-Y(j1)}{X(j_2)-X(j_1)}\leq k_0[i]\) (或者大于等于)或者 \(kx+b=y\) 的形式。

注意遵循之前的原则。

决策点横坐标 \(X(j)\) 不单调

假设此时 \(k_0[i]\) 仍然单调。

那么维护凸包不能用单调队列了(因为会插入到点集中间某一个位置)。需要用到 平衡树维护 或者 CDQ分治 。

斜率 \(k_0[i]\) 不单调

仍然可以队列维护,但是不再满足之前的决策单调性。那么只能使用优化前的方法,二分队列找最优决策点。

以上二者均不单调

考虑在第二种情况上进行改进。

由于平衡树支持查询前驱后继,直接 \(k_0[i]\) 扔进去即可。

再看看 CDQ;在 第二种情况上再加一维偏序,人为排出单调性,普通单调队列维护即可。

好了。东西讲完了。上菜——

2——[NOIP2018PJ] P5017 摆渡车

题目链接

题意

\(n\) 名同学要乘坐摆渡车从 \(A\)\(B\),第 \(i\) 位同学在第 \(t_i\) 分钟去等车。有一辆摆渡车,容量无限大,从 \(A\) 出发、 把车上的同学送到 \(B\)、再回到 \(A\)(去接其他同学),往返一趟总共花费 \(m\) 分钟(上下车时间忽略不计)。要将所有同学都送到 \(B\).

已知你可以任意安排摆渡车出发的时间,求所有人等车时间之和的最小值。摆渡车回到 \(A\) 后可以即刻出发。

\(n\leq 500,m\leq 100,0\leq t\leq 4e6\)

思路

\(f_i\) 表示到了时间点 \(i\),所有同学等待时间最小和。可以写出方程:

\[f_i=min( f_j+\sum_{j<t_k<\leq i} (i-t_k) ),j\leq i-m \]

前缀优化。设 \(cnt_i\)\(i\) 时刻到达车站的学生个数,\(sum_i\) 表示已经到达车站的学生到达时间总和。有方程:

\[f_i=min( f_j+(cnt_i-cnt_j)\times i-(sum_i-sum_j)),j\leq i-m \]

看到了 \(i,j\) 乘积!好,上斜优。首先来整理一下式子:

\[f_i=min( f_j+sum_j-cnt_j\times i )+cnt_i\times i-sum_i \]

\(min\) 去掉,转化成 \(kx+b=y\) 的形式:

\[f_j+sum_j=cnt_j\times i+f_i-cnt_i\times i+sum_i \]

\(i\) 作为 \(k\)\(cnt_j\) 作为 \(x\),剩余为 \(b\) ,得到了直线方程。决策单调性转移即可。

但是这题还有一些问题需要处理:

  1. \(j\leq i-m\) 。采用分层入队,每当完成了 \(f_i\) 的转移,就把 \(i-m+1\) 这个决策点入队即可。
  2. 有重点。也就是横坐标相等纵坐标不等,那么按位置判断 \(inf/-inf\) 即可。
  3. \(f[]\) 需要初始化。 是第一个问题的延伸,因为前面 \(m\) 个点没有初始值。暴力更新即可。

代码

#include <bits/stdc++.h>
#define ll long long
#define lb long double
using namespace std;
const ll T=9e6+10,inf=1e18;
ll n,m,mxt,cnt[T],sum[T],f[T],q[T],h,t,ans=inf;

lb Y( int num ) { return f[num]+sum[num]; }
lb X( int num ) { return cnt[num]; }
lb slope( int x,int y )
{
        if ( X(x)==X(y) ) return Y(y)>Y(x) ? inf*1.0 : inf*-1.0;
        return ( Y(y)-Y(x)) / ( X(y)-X(x) );
}

int main()
{
        scanf( "%lld%lld",&n,&m );
        for ( ll i=1,ti; i<=n; i++ )
        {
                scanf( "%lld",&ti ),mxt=max( ti,mxt );
                sum[ti]+=ti; cnt[ti]++;
        }
        for ( ll i=1; i<=mxt+m; i++ )
                sum[i]+=sum[i-1],cnt[i]+=cnt[i-1];

        h=t=1; q[t]=0;
        for ( int i=1; i<m; i++ ) f[i]=cnt[i]*i-sum[i];
        for ( int i=m; i<=mxt+m; i++ )
        {
                while ( h<t && slope(q[h],q[h+1])<=1.0*i ) h++;
                f[i]=f[q[h]]+(cnt[i]-cnt[q[h]])*i-(sum[i]-sum[q[h]]);
                while ( h<t && slope(q[t-1],q[t])>=slope(q[t],i-m+1) ) t--;
                q[++t]=i-m+1;
                if ( i>=mxt ) ans=min( ans,f[i] ); 
        }

        printf( "%lld",ans );
}

3——任务安排(套餐)

题目链接

LOJ10184
AcWing300
P2365
任务安排1

LOJ10185
AcWing301
任务安排2

LOJ10186
AcWing302
P5785
任务安排3

注:此处范围、AC代码均以 AcWing 为准。

题意

\(N\) 个任务等待完成(顺序不改变),这 \(N\) 个任务被分成若干批,每批包含相邻的若干任务。从时刻 \(0\) 开始,这些任务被分批加工,第 \(i\) 个任务单独完成所需的时间是 \(Ti\) 。只有一台机器,在每批任务开始前,机器需要启动时间 \(S\),完成这批任务所需的时间是各个任务需要时间的总和(同一批任务将在同一时刻完成)。每个任务的费用是它的完成时刻乘以它的费用系数 \(Ci\)。请确定一个分组方案,使得总费用最小。

T1

\(1⩽N⩽5000,0⩽S⩽50,1⩽T_i,C_i⩽100\)

\(sumT[i]=\sum_{j=1}^i T[j],sumC[i]=\sum_{j=1}^i F[j]\),那么

\[f[p][i]=min( f[p-1][j]+(sumT[i]+p\times S)\times (sumC[i]-sumC[j]) ) \]

考虑费用提前计算。每次分出一批任务,对后面的每个任务的用时都会产生 \(S\) 的贡献,那么可以提前计算。

\[f[i]=min(f[j]+sumT[i]\times ( sumC[i]-sumC[j] )+S\times (sumC[n]-sumC[j]) ) \]

代码


#include <bits/stdc++.h>
using namespace std;
const int N=5010;
int n,s,t[N],c[N],f[N];

int main() 
{
	scanf( "%d%d",&n,&s );
	for ( int i=1; i<=n; i++ )
		scanf( "%d%d",t+i,c+i );
    
    memset( f,0x3f,sizeof f );
    f[0]=0;
    for ( int i=1; i<=n; i++ )
    	t[i]+=t[i-1],c[i]+=c[i-1];
    for ( int i=1; i<=n; i++ )
     for ( int j=0; j<i; j++ )
     	f[i]=min( f[i],f[j]+(c[i]-c[j])*t[i]+s*(c[n]-c[j]) );
    
    printf( "%d\n",f[n] );
} 

T2

\(1\leq N\leq 3e5\) \(1\leq T_i,C_i\leq 512,0\leq S\leq 512\)

\(N\) 变大了,需要加上斜率优化。

\[f[i]=min(f[j]+sumT[i]\times (sumC[i]-sumC[j])+S\times (sumC[n]-sumC[j]) ) \]

考虑转化成斜率式子。

\[f[j]=(sumT[i]+S)\times sumC[j] + f[i]-sumC[i]\times sumT[i]-sumC[n]\times S \]

代码

#include <bits/stdc++.h>
#define ll long long
#define lb long double
using namespace std;
const int N=3e5+10;
int n,s,q[N],h=0,t=0;
ll sc[N],st[N],f[N];

lb slope( int x,int y ) { return (lb)(f[y]-f[x])/(sc[y]-sc[x]); }

int main()
{
	scanf( "%d%d",&n,&s );
	for ( int i=1; i<=n; i++ )
	{
		scanf( "%lld%lld",&st[i],&sc[i] );
		st[i]+=st[i-1]; sc[i]+=sc[i-1];
	}
	
	for ( int i=1; i<=n; i++ )
	{
		while ( h<t && slope(q[h],q[h+1])<=( st[i]+s ) ) h++;
		f[i]=f[q[h]]-( st[i]+s )*sc[q[h]]+sc[i]*st[i]+s*sc[n];
		while ( h<t && slope(q[t-1],q[t])>=slope(q[t],i) ) t--;
		q[++t]=i;
	}
	
	printf( "%lld",f[n] );
}

T3

\(1\leq N\leq 3e5,0\leq S,C_i\leq 512,|T_i|\leq 512\)

任务的执行时间 \(t\) 可能是负数,那么斜率不具有单调性,
就不能只保留大于 \(S+sumT[i]\) 的部分,而应该维护整个凸壳
此时队头不一定是最优决策,需要进行二分查找,求出一个位置,
使左侧的斜率小于 \(S+sumT[i]\) ,右侧斜率大于 \(S+sumT[i]\)

注:此题 AcWing 上数据较强,建议把 slope 改为交叉相乘,需要使用 __int128 或者 doulbe .

代码

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N=3e5+10;
int n,s,q[N],h=0,t=0;
ll sc[N],st[N];
double f[N];

int main()
{
	scanf( "%d%d",&n,&s );
	for ( int i=1; i<=n; i++ )
	{
		scanf( "%lld%lld",&st[i],&sc[i] );
		st[i]+=st[i-1]; sc[i]+=sc[i-1];
	}
	
	for ( int i=1; i<=n; i++ )
	{
		int l=h,r=t;
		while ( l<r )
		{
			int mid=(l+r)/2;
			if ( (f[q[mid+1]]-f[q[mid]])>(st[i]+s)*(sc[q[mid+1]]-sc[q[mid]]) ) r=mid;
			else l=mid+1;
		}
		f[i]=f[q[l]]-( st[i]+s )*sc[q[l]]+sc[i]*st[i]+s*sc[n];
		while ( h<t && ( f[q[t]]-f[q[t-1]] )*( sc[i]-sc[q[t]] )>=( f[i]-f[q[t]] )*( sc[q[t]]-sc[q[t-1]]  ) ) t--;
		q[++t]=i;
	}
	
	printf( "%.0lf",f[n] );
}

中场休息

例题完成了qwq。习题部分由于太多,一天之内无法整理完,所以是选做。(是随便挑了几道,不是挑了几道好题emmm)

4——P2900 [USACO08MAR]Land Acquisition G

题目链接

题意

\(n\) 块土地分组,每组的价格是这组土地中最大的长宽乘积,问买下所有土地的最小花费。

思路

没想到随手一点挑到了一道有意思的题。

如果你啥都不干的话,dp方程:

\[f[i]=min( f[j]+calc(j+1,i)) \]

\(5e4\) 的 N,显然 T飞了。但是你又觉得这个区间最大很难维护。怎么办呢?

考虑一块土地到底该怎么算。显然,对于一块地 \(x\) ,如果存在一个 \(y\) 的长宽均大于它,那么这块土地跟没有一样。所以可以把所有土地按长度排序,长度相同宽度排序。维护一个栈,将土地依次加入,每次加入的时候把所有宽度小于等于它的删除即可。

此时留下的土地宽度降序。显然在最优决策下,每组土地是连续的一段。

那么终于可以得到正确的方程:

\[f[i]=min(f[i],f[j]+w[j+1]\times l[i]) \]

\(O(n^2)\) 还是过不去。看到 \(w[j+1]\times l[i]\) 考虑斜优。

\[f[j]=-w[j+1]\times l[i]+f[i] \]

代码

#include <bits/stdc++.h>
#define ll long long
#define lb long double 
using namespace std;
const int N=5e4+10;
struct land
{
	ll x,y;
	bool operator < ( const land tmp ) 
	{ if ( x!=tmp.x ) return x<tmp.x; return y<tmp.y; }
}a[N],sta[N];
int n,top=0,q[N],h,t;
ll f[N];

void init()
{
	sort( a+1,a+1+n ); top++; sta[top]=a[1];
	for ( int i=2; i<=n; i++ )
	{
		while ( top && sta[top].y<=a[i].y ) top--;
		top++; sta[top]=a[i];
	}
	n=top;
}

lb slope( int x,int y ) { return (lb)(f[y]-f[x])/(-sta[y+1].y+sta[x+1].y); }

int main()
{
	scanf( "%d",&n );
	for ( int i=1; i<=n; i++ )
		scanf( "%lld%lld",&a[i].y,&a[i].x );
	
	init();
	h=t=1; q[1]=0;
	for ( int i=1; i<=n; i++ )
	{
		while ( h<t && slope(q[h],q[h+1])<=1.0*sta[i].x ) h++;
		f[i]=f[q[h]]+sta[q[h]+1].y*sta[i].x;
		while ( h<t && slope(q[t-1],q[t])>=slope(q[t],i) ) t--;
		t++; q[t]=i;
	}

	printf( "%lld",f[n] );
}

5——P2120 [ZJOI2007]仓库建设

题目链接: AcWing
luogu

题意

\(n\) 个工厂,由高到低分布在一座山上,工厂 \(1\) 在山顶,工厂 \(n\) 在山脚。第 \(i\) 个工厂目前有成品 \(p_i\) 件,在第 \(i\) 个工厂位置建立仓库的费用是 \(c_i\). 对于没有建立仓库的工厂,其产品被运往其他的仓库,产品只能往山下运(只能运往编号更大的工厂的仓库),一件产品运送一个单位距离的费用是 \(1\).假设建立的仓库容量都足够大。工厂 \(i\)\(1\) 的距离是 \(x_i\),问总费用最小值。

思路

\[f_i=min(f_j+x_i\times \sum_{l=j+1}^i(p_l)-\sum_{l=j+1}^i(x_l\times p_l) )+c_i,0\leq j<i \]

前缀和,设 \(sp_i=\sum_{j=1}^i p_j,s_i=\sum_{j=1}^i p_j\times x_j\)
得到新的式子:

\[f_i=f_j+x_i\times (sp_i-sp_{j})-(s_i-s_j)+c_i \]

然后斜率优化:

\[f_j+s_j=x_i\times sp_j+(f_i+s_i-c_i-x_i\times sp_i) \]

代码

#include <bits/stdc++.h>
#define ll long long
#define lb long double
using namespace std;
const int N=1e6+10;
int n,q[N],h,t;
ll x[N],p[N],c[N],f[N],sp[N],s[N];

ll X( int num ){ return sp[num]; }
ll Y( int num ){ return f[num]+s[num]; }
lb slope( int n1,int n2 ) { return (lb)(Y(n2)-Y(n1))/(X(n2)-X(n1)); }

int main()
{
        scanf( "%d",&n );
        for ( int i=1; i<=n; i++ )
                scanf( "%lld%lld%lld",&x[i],&p[i],&c[i] );
        sp[0]=s[0]=0;
        for ( int i=1; i<=n; i++ )
                sp[i]=sp[i-1]+p[i],s[i]=s[i-1]+p[i]*x[i];
        
        h=t=1; q[1]=0;
        for ( int i=1; i<=n; i++ )
        {
                while ( h<t && slope(q[h],q[h+1])<=(lb)x[i] ) h++;
                f[i]=f[q[h]]+x[i]*(sp[i]-sp[q[h]])-(s[i]-s[q[h]])+c[i];
                while ( h<t && slope(q[t-1],q[t])>=slope(q[t],i) ) t--;
                t++; q[t]=i;
        }

        printf( "%lld",f[n] );
}

6——P3628 [APIO2010]特别行动队

题目链接:AcWing luogu

题意

有一支由 \(n\) 名士兵组成的部队,士兵从 \(1\)\(n\) 编号,要将他们拆分成若干个特别行动队调入战场。同一支行动队的队员的编号应该连续。

编号为 \(i\) 的士兵的初始战斗力为 \(x_i\) ,一支队伍的初始战斗力为所有队员初始战斗力之和,记为 \(x\) 。最终战斗力为:\(x'=ax^2+bx+c.(a<0)\).

试求出最终战斗力之和的最大值。

思路

\(s[i]=\sum_{j=1}^i x_j.\)

\[f[i]=f[j]+a\times (s[i]-s[j])^2+b\times (s[i]-s[j])+c. \\\\ =f[j]+as[i]^2-2as[i]s[j]+as[j]^2+bs[i]-bs[j]+c \\\\ =(f[j]+as[j]^2-bs[j])+(as[i]^2+bs[i]+c)-2as[i]s[j] \]

所以整合得到:

\[f[j]+as[j]^2-bs[j]=2as[i]s[j]+(f[i]-as[i]^2-bs[i]-c) \]

代码

#include <bits/stdc++.h>
#define ll long long
#define lb long double
using namespace std;
const int N=1e6+10;
int n,q[N],h,t;
ll s[N],a,b,c,f[N];

ll X( int num ){ return s[num]; }
ll Y( int num ){ return f[num]+a*s[num]*s[num]-b*s[num]; }
lb slope( int n1,int n2 ) { return (lb)(Y(n2)-Y(n1))/(X(n2)-X(n1)); }

int main()
{
        scanf( "%d",&n ); scanf( "%lld%lld%lld",&a,&b,&c );
        s[0]=0;
        for ( int i=1; i<=n; i++ )
                scanf( "%lld",&s[i] ),s[i]+=s[i-1];
        
        h=t=1; q[1]=0;
        for ( int i=1; i<=n; i++ )
        {
                while ( h<t && slope(q[h],q[h+1])>=(lb)2.0*a*s[i] ) h++;
                int j=q[h]; f[i]=f[j]+a*s[j]*s[j]-b*s[j]+a*s[i]*s[i]+b*s[i]+c-2*a*s[i]*s[j];
                while ( h<t && slope(q[t-1],q[t])<=slope(q[t],i) ) t--;
                t++; q[t]=i;
        }

        printf( "%lld",f[n] );
}

7——P4027 [NOI2007]货币兑换

题目链接
luogu
AcWing

又是 AcWing 众多恶评题之一(

题意

太长了,自己看题

又是用NOI题作结的一天呢

本题没有部分分,你的程序的输出只有和标准答案相差不超过 \(0.0010.001\) 时,才能获得该测试点的满分,否则不得分。

输入文件可能很大,请采用快速的读入方式。

必然存在一种最优的买卖方案满足:

每次买进操作使用完所有的人民币,每次卖出操作卖出所有的金券。

思路

这道题就是典型的不能单调栈,要平衡树或者 CDQ 的题目 就算是早年NOI也没那么简单呢qaq

\(f[i]\) 表示到第 \(i\) 天最多有多少钱,\(g[i]\) 表示用第 \(i\) 天的钱最多能买多少 \(B\) 券。易知 \(g[i]=\dfrac{f[i]}{r[i]\times a[i]+b[i]}\)

得到转移:

\[f[i]=max( max_{j=1}^{i-1}( g[j]\times \dfrac{b[i]}{a[i]}+r[j]\times g[j] ) \times a[i],f[i-1]) \]

外面的 \(max\) 单独判断即可。主要是斜优里面那个式子。

\[x=\dfrac{b[i]}{a[i]},k=g[j],b=r[j]\times g[j] \]

但是你发现斜率不单调。所以根据前文所述,要使用平衡树或者CDQ分治维护了。

由于平衡树 太长 太板子了,这里就不作介绍,采用 CDQ分治维护。

CDQ分治比较好的总结可以看这里,顺便我的LCT也是这个博主教的

现在 假装你已经会CDQ分治了 考虑分治,对于任意一个 \(f[i]\) ,只需要考虑 \(1\leq j\leq i-1\) 即可,和CDQ的解决方式非常相像。

对于一段区间 \([l,r]\) ,先递归左子区间 \([l,m]\) 保证 \([l,m]\)\(f,g\) 都已经得到;把左子区间按 \(k\) 递增排序,就可以按斜率优化 \(O(n)\) 转移;再把右子区间按在原序列中的位置递增排序,递归。此时左子区间对右子区间的影响已经考虑完毕。注意边界 \(l==r\) 。复杂度是 \(O(nlog^2n).\)

继续优化。考虑 CDQ本身就是类似归并排序的过程,可以用这个去掉排序复杂度。

我们希望得到什么呢?当我们拿到 \([l,r]\) 这个区间的时候,\(x\) 是单调的。于是把外面的原序列按 \(x\) 递增排序;拿到这个序列之后,我们希望在原序列中靠左的东西去左子区间,把 \([l,r]\) 扫一遍,把在原序列中 \(\leq m\) 的东西放左边,\(\ge m\) 的东西放右边,而且左右区间对 \(x\) 的单调性没有影响。在递归右子区间的时候,希望左子区间回来的时候关于 \(k\) 单调递增,所以最后对 \(k\) 做一遍归并。复杂度 \(O(nlogn).\)

代码

写了将近半个晚上啊啊啊啊啊

#include <bits/stdc++.h>
using namespace std;
const int N=1e5+10;
const double eps=1e-8;
int q[N],n;
double f[N], g[N];
struct node
{
        int id; double a,b,r,x;
        node() {}
        node( int id,double a,double b,double r ) : id(id),a(a),b(b),r(r),x(b/a) {}
        bool operator < (node tmp ) { return x!=tmp.x ? x<tmp.x : id<tmp.id; }
}p[N],b[N];

double cross( int u,int v ) 
{
        return (p[u].r*g[p[u].id]-p[v].r*g[p[v].id])/(g[p[v].id]-g[p[u].id]);
}

double calc( int u,int v ) { return g[p[u].id]*(p[v].x+p[u].r); }

void update( int u,double v )
{
        if ( f[p[u].id]<v ) f[p[u].id]=v,g[p[u].id]=f[p[u].id]/(p[u].b+p[u].r*p[u].a);
}

void solve( int l,int r )
{
        if ( l==r ) { update(l,f[p[l].id-1]); return; }
        int m=(l+r)>>1,i,h,t;
        for ( h=l,t=m+1,i=l; i<=r; i++ )
                p[i].id<=m ? b[h++]=p[i] : b[t++]=p[i];
        for ( int i=l; i<=r; i++ )
                p[i]=b[i];
        solve( l,m ); h=1; t=0;
        for ( i=l; i<=m; i++ )
        {
                while ( h<t && cross(q[t],i)<cross(q[t-1],i)+eps ) t--;
                q[++t]=i;
        }
        for ( ; i<=r; i++ )
        {
                while ( h<t && calc( q[h],i )<calc( q[h+1],i )+eps ) h++;
                update( i,calc(q[h],i)*p[i].a );
        }
        solve( m+1,r );
        for ( h=l,t=m+1,i=l; h<=m && t<=r; )
                g[p[h].id]<g[p[t].id] ? b[i++]=p[h++] : b[i++]=p[t++];
        while ( h<=m ) b[i++]=p[h++];
        while ( t<=r ) b[i++]=p[t++];
        for ( i=l; i<=r; i++ ) p[i]=b[i];
}

int main()
{
        scanf( "%d%lf",&n,&f[1] );
        double a,b,r;
        for ( int i=1; i<=n; i++ )
                scanf( "%lf%lf%lf",&a,&b,&r ),p[i]=node(i,a,b,r);
        
        g[1]=f[1]/(p[1].r*p[1].a+p[1].b); sort( p+1,p+1+n );
        solve( 1,n );

        printf( "%.3lf\n",f[n] );
}

8——注意事项

  • 写出DP方程之后要判断能不能用斜优 不要无脑使用啊 ,即是否存在 \(g(i)\times g(j)\) 或者 \(\dfrac{Y(j)-Y(j')}{X(j)-X(j')}\) 的形式。
  • 通过大于小于符号或者 \(b\) 中的 \(f[i]\) 结合要求 \(min/max\) 判断上下凸包,不要盲猜
  • \(X(j)\) 非严格递增时,在求斜率时可能会出现相等的情况,这时候一定要特判(除数不能为0),return Y(j)>=Y(i) ? inf : -inf ,不要直接返回。
  • 比较 \(k_0[i],slope(j_1,j_2)\) 要写规范,可能出现同除负数却没有变号。
  • 队列初始化要塞一个点 \(P(0),j=0\)
  • 出入队列的判断要至少两个元素(不然没有斜率),所以不能写 h<=t ,应该写 h<t .
  • 计算斜率的除法有误差,用 long double
  • 有可能出现部分dp初始值无法转移,需要预处理(如摆渡车)
  • 比较两个斜率的时候尽量写 <=,>= 以避免有重点时分母出问题。但保险起见建议加上第三条中的判断。

Last——鸣谢

和上一篇一样,是对 @[Xing_Ling]
这篇博文
的学习整理。同样适合作为初学教材。

posted @ 2020-11-02 20:05  MontesquieuE  阅读(213)  评论(0编辑  收藏  举报