斜率优化

前言

由于某些原因,最近学习了这个优化 \(\text{DP}\) 的算法——斜率优化。

算法简介

我们在做 \(\text{DP}\) 的题目时,常常会遇到这样的一个状态转移方程:

\[dp[i]=\min_{0\leq j<i}\left\{dp[j]+f(i)+g(j)+h(i)\times r(j)\right\} \]

对于其中的 \(f(i),g(j)\) 我们都可以考虑通过单调队列来优化掉。

但是对于 \(h(i)\times r(j)\) 这项,它与 \(i,j\) 同时有关,单调队列便不再适用……

于是,我们引进一个新的优化方式——斜率优化。

斜率优化,指的就是利用一次函数的斜率 \(k\) 相关性质对原来的状态转移方程进行优化。

而这就需要我们运用数形结合的思想来解题。

接下来将通过几道题目来加强对斜率优化的认识。

例题讲解

下面提供几道较为基础的题目:

例题1:[HNOI2008]玩具装箱

\(\large{\text{Description:}}\)

现在有 \(n\) 个玩具,第 \(i\) 个玩具的长度为 \(c[i]\) ,将 \(i\)\(j\) 连续的玩具放入一个容器,其长度为 \(x=j-i+\sum\limits_{k=i}^{j}c[k]\) ,制作一个容器的费用为 \(\left(x-L\right)^{2}\) ,其中 \(L\) 为常数。

求将所有玩具都放入容器中的费用最小值,容器的数量不限。

数据范围: \(n\leq 5\times 10^{4},1\leq L,c[i]\leq 10^{7}\)

\(\large{\text{Solution:}}\)

首先,我们不难想到用 \(\text{DP}\) 来求解。

\(dp[i]\) 表示将 \(1\)\(i\) 的玩具都放入容器中的最小费用,于是可以得到如下的状态转移方程:

\[dp[i]=\min_{0\leq j<i}\left\{dp[j]+\left(\sum_{k=j+1}^{i}c[k]+i-j-1-L\right)^{2}\right\} \]

\(c\) 的前缀和数组为 \(s\) ,即 \(s[i]=\sum\limits_{k=1}^{j}c[k]\) ,于是方程就变为如下形态:

\[dp[i]=\min_{0\leq j<i}\left\{dp[j]+\left(s[i]-s[j]+i-j-1-L\right)^{2}\right\} \]

此时,如果我们暴力去做,时间复杂度为 \(O\left(n^{2}\right)\) ,并不够优秀。

于是我们考虑对其进行斜率优化

显然,状态 \(i\) 是由一个状态 \(j\) 转移而来,而这个 \(j\) 是当前最优决策,于是我们考虑两个决策 \(j_1,j_2\left(1\leq j_1,j_2<i\right)\) ,且决策 \(j_1\) 优于决策 \(j_2\) ,那么有如下式子:

\[dp[j_1]+\left(s[i]-s[j_1]+i-j_1-1-L\right)^{2}<dp[j_2]+\left(s[i]-s[j_2]+i-j_2-1-L\right)^{2} \]

为了方便起见,我们设 \(sum[i]=s[i]+i\left(1\leq i\leq n\right)\) ,此时我们化简上述式子得:

\[2\times sum[i]>\frac{\left(dp[j_1]+\left(sum[j_1]+L+1\right)^{2}\right)-\left(dp[j_2]+\left(sum[j_2]+L+1\right)^{2}\right)}{sum[j_1]-sum[j_2]} \]

在这里,我们把 \(sum[j]\) 看作横坐标 \(x_j\) ,把 \(dp[j]+\left(sum[j]+L+1\right)^2\) 看作纵坐标 \(y_j\) ,那么上述式子的右侧就相当于 \(\begin{aligned}\frac{\Delta y}{\Delta x}=k\end{aligned}\) ,也就是一次函数的斜率!

此时对于只与 \(i\) 有关的项,我们可以把它当成一次函数的截距 \(b\) ,那么所求就变成了 \(b\) 的最小值。

我们是如何想到把式子化简成上面的样子呢?

实际上,我们把所有只与 \(i\) 有关的项合并,当作一次函数的 \(b\) ;再把所有只与 \(j\) 有关的项合并,当作一次函数的 \(y\) ;最后把所有与 \(i,j\) 都有关的项再合并,当作一次函数的 \(kx\)

在计算的过程中,我们发现 \(b\) 消掉了,剩下的就是上面的式子。

当我们在求 \(dp[i]\) 时, \(k=2\times sum[i]\) 显然不变。

也就是说,我们平移一条斜率为定值 \(k\) 的直线,使得该直线过某点 \(\left(x_j,y_j\right)\) ,那么此时的截距就是一个可行解。

由于求的是最小值,故我们从下往上平移该直线,第一次得到的可行解就是最优解。

显然,斜率 \(k\) 是单调递增的,于是我们用单调队列维护一个 \(\left(x_j,y_j\right)\) 的下凸包。(这点容易证明是正确的)

这个时候所得的队首就是对于 \(i\) 而言的最佳决策。

\(\large{\text{Code:}}\)

#include<bits/stdc++.h>
#define Re register
using namespace std;

typedef long long LL;
typedef double db;

const int N=50005;

int n,L;
LL sum[N];
int h,t; LL q[N];
LL dp[N];

inline LL rd()
{
	char ch=getchar();
	LL x=0,f=1;
	while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}

inline LL X(int x){return sum[x];}
inline LL Y(int x){return dp[x]+(sum[x]+L)*(sum[x]+L);}
inline LL K(int x){return 2*sum[x];}
inline db slope(int x,int y){return double(Y(y)-Y(x))/(X(y)-X(x));}

int main()
{
	scanf("%d%d",&n,&L); L++;
	for(Re int i=1;i<=n;i++)
	{
		sum[i]=sum[i-1]+rd()+1;
	}
	for(Re int i=1;i<=n;i++)
	{
		while(h<t&&slope(q[h],q[h+1])<=K(i)) h++;
		dp[i]=dp[q[h]]+(sum[i]-sum[q[h]]-L)*(sum[i]-sum[q[h]]-L);
		while(h<t&&slope(q[t],q[t-1])>=slope(q[t],i)) t--;
		q[++t]=i;
	}
	printf("%lld",dp[n]);
	return 0;
}

例题2:[CEOI2004]锯木厂选址

\(\large{\text{Description:}}\)

从山顶上到山底下沿着一条直线种植了 \(n\) 棵老树。当地的政府决定把他们砍下来,并运送到锯木厂,而且木材只能朝山下运。

山脚下有一个锯木厂,另外两个锯木厂将新修建在山路上,你必须决定在哪里修建这两个锯木厂,使得运输的费用总和最小。

给定 \(n\) 棵树的重量与位置,计算最小运输费用。

数据范围: \(n\leq 20000\)

\(\large{\text{Solution:}}\)

这不难想到是个 \(\text{DP}\) 题。

\(dp[i]\) 表示第 \(2\) 个工厂修到第 \(i\) 棵树的位置时的最小花费。

于是得出状态转移方程:

\[dp[i]=\min_{0\leq j<i}\left\{res−sd[j]\times sw[j]−sd[i]\times \left(sw[i]−sw[j]\right)\right\} \]

其中, \(res\) 表示所有树一开始全部运送的山脚下的花费, \(sd\) 表示距离后缀和, \(sw\) 表示重量前缀和。

然后我们将状态转移方程变形,令 \(j_1,j_2\) 这两种决策转移到 \(i\) 的时候, \(j_1\) 决策更优,那么我们就可以得到:

\[res−sd[j_1]\times sw[j_1]−sd[i]\times \left(sw[i]−sw[j_1]\right)<res−sd[j_2]\times sw[j_2]−sd[i]\times \left(sw[i]−sw[j_2]\right) \]

化简后得到:

\[sd[i]<\frac{sd[j_1]\times sw[j_1]-sd[j_2]\times sw[j_2]}{sw[j_1]-sw[j_2]} \]

此时令:

\[\begin{aligned}x_j&=sw[j]\\y_j&=sd[j]\times sw[j]\\k_i&=sd[i]\end{aligned} \]

之后套上斜率优化的模板即可。

\(\large{\text{Code:}}\)

#include<bits/stdc++.h>
#define Re register
using namespace std;

const int N=20005;

struct Node {
	int w,d;
}a[N];

int n,sd[N],sw[N],res;

int h,t,q[N],ans;

inline int rd()
{
	char ch=getchar();
	int x=0,f=1;
	while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}

inline int X(int x){return sw[x];}
inline int Y(int x){return sd[x]*sw[x];}
inline int K(int x){return sd[x];}
inline double slope(int x,int y){return 1.0*(Y(y)-Y(x))/((X(y)-X(x)));}

int main()
{
	scanf("%d",&n);
	for(Re int i=1;i<=n;i++)
	{
		a[i].w=rd();
		a[i].d=rd();
	}
	for(Re int i=n;i>0;i--)
	{
		sd[i]=sd[i+1]+a[i].d;
	}
	for(Re int i=1;i<=n;i++)
	{
		sw[i]=sw[i-1]+a[i].w;
	}
	for(Re int i=1;i<=n;i++)
	{
		res+=a[i].w*sd[i];
	}
	ans=0x3f3f3f3f;
	for(Re int i=1;i<=n;i++)
	{
		while(h<t&&slope(q[h],q[h+1])>K(i)) h++;
		ans=min(ans,res-sd[q[h]]*sw[q[h]]-sd[i]*(sw[i]-sw[q[h]]));
		while(h<t&&slope(q[t],q[t-1])<slope(q[t],i)) t--;
		q[++t]=i;
	}
	printf("%d",ans);
	return 0;
}

总结

  1. 斜率单调暴力移指针

  2. 斜率不单调二分找答案

  3. \(x\) 坐标单调开单调队列

  4. \(x\) 坐标不单调用平衡树或 \(\text{cdq}\) 分治

练习题

  1. [SDOI2016]征途

  2. [ZJOI2007]仓库建设

  3. [APIO2014]序列分割

  4. [NOI2016]国王饮水记

  5. [CF311B]Cats Transport

posted @ 2020-12-18 23:42  kebingyi  阅读(572)  评论(0编辑  收藏  举报