【总结】斜率优化DP


于是,XSC062开始写总结。

斜率优化DP

前置芝士

(夹带私货)

正文

我们以一道题为例。

打印文章

双倍经验 三倍经验

Solution

明显DP。

那么DP式就是:

\[\begin{aligned} f_i&=\min\{f_j+(s_i-s_j)^2+M\} \\ &=\min\{f_j+{s_i}^2-2\times s_i\times s_j+{s_j}^2+M\} \\ &=\min\{f_j-2\times s_i\times s_j+{s_j}^2\}+{s_i}^2+M \end{aligned} \]

其中 \(s\)\(c\) 的前缀和。

时间复杂度 \(\Theta(n^2)\),明显爆炸,所以我们需要优化。

在上一篇的 中,我们提到过,只有DP式中的与 \(i\) 有关的项能直接提出来时,我们才能使用单调队列优化,而这里的 \(s_i\)\(s_j\) 相乘,无法使用单调队列优化。

我们思考,对于 \(f_i\) 来说,无非就是选出最优的 \(j\) 来构造它。

假设有 \(j\)\(k\),如何判断 \(j\)\(k\) 谁更优呢?

我们先钦定 \(j\) 优于 \(k\),且 \(j

那么可以得到:

\[f_j-2\times s_i\times s_j+{s_j}^2+{s_i}^2+M

化简得:

\[f_j-2\times s_i\times s_j+{s_j}^2

再将\(j,k\) 有关的项放到左边,与 \(i\) 有关的项放到右边:

\[f_j-f_k+{s_j}^2-{s_k}^2<2\times s_i\times s_j-2\times s_i\times s_k \\ f_j-f_k+{s_j}^2-{s_k}^2<2\times s_i\times(s_j-s_k) \]

左右两边同时 \(\div\)\(i\) 无关的项 \(2\times(s_j-s_k)\)

\[\dfrac{(f_j+{s_j}^2)-(f_k+{s_k}^2)}{(2\times s_j)-(2\times s_k)}

如果满足上式 ,则 \(j\(j\) 优于 \(k\)

记住上面推式子的步骤,现在记不住等会儿再重复这个步骤推十几个式子也能记住的OwO

充要条件是什么鬼呀,我不会证呀QAQ

好了,接下来是斜率优化的重点部分——

在义务教育阶段,学生学习了一次函数,它的几何意义表示为一条直线,一次项的系数就是直线的斜率,只不过当直线与 \(x\) 轴垂直的时候无法表示。虽然没有明确给出斜率这个名词,但实际上思想已经渗透到其中。

直线对 \(x\) 轴的倾斜角 \(\alpha\) 的正切值 \(\tan\alpha\) 称为该直线的“斜率”,并记作 \(k\) ,公式为 \(k=\tan\alpha\)

\(k=\tan\alpha=\dfrac{\Delta y}{\Delta x}=\dfrac{y_2-y_1}{x_2-x_1}\)\(\dfrac{y_1-y_2}{x_1-x_2}\)

——选自 斜率_百度百科

XSC062看完了百度百科表示你TM在说些啥是不是欺负我六年义务教育的小学生恍然大悟

上面推出来的那个关于 \(j\)\(k\) 的DP式,不就是求两个点 \((f_j+{s_j}^2,2\times s_j)\)\((f_j+{s_k}^2,2\times s_k)\) 连成一条线之后的坡度吗!

本文后面的部分,\(x\) 的含义会在“点\((f_x+{s_x}^2,2\times s_x)\)”和“\(x\)这个下标”之间漂浮,请根据语境识别~

随后XSC062边打瞌睡边听GM讲课(特异功能),勉强算是明白了中心意思:

如图,假设有三个点 \(A,B,C\),以及 \(B,A\) 的斜率 \(k_1\)\(C,B\) 的斜率 \(k_2\)

我们暂且把这个向外凸起的奇怪玩意儿称为一个上凸。

回到前面的我们得到的那个结论:

\[\dfrac{(f_j+{s_j}^2)-(f_k+{s_k}^2)}{(2\times s_j)-(2\times s_k)}

如果满足上式 ,则 \(j\(j\) 优于 \(k\)

简单记忆为:

  • 若点 \(x,y\) 的斜率比 \(s_i\) 小,则 \(x\(x\) 优于 \(y\)
  • 反之,若点 \(x,y\) 的斜率比 \(s_i\) 大,则 \(y\(y\) 优于 \(x\)

\(A,B\) 代入,我们得知:

  1. \(k_1\leqslant s_i\),则 \(B\) 优于 \(A\)
  2. \(k_1>s_i\),则 \(A\) 优于 \(B\)
  3. \(k_2\leqslant s_i\),则 \(C\) 优于 \(B\)
  4. \(k_2>s_i\),则 \(B\) 优于 \(C\)。(人类的本质……

因为斜率=倾斜度=竖得有多高,明显 \(k_1>k_2\)。所以针对一个大小关系,\(k_1\)\(k_2\) 之间只有可能有:

  1. \(s_i,此时 \(B\) 优于 \(C\)\(A\) 优于 \(B\)\(A\) 为最优解。
  2. \(k_2,此时 \(C\) 优于 \(B\)\(A\) 优于 \(B\)\(A,C\) 为最优解。
  3. \(k_2,此时 \(C\) 优于 \(B\)\(B\) 优于 \(A\)\(C\) 为最优解。

发现了吗?\(B\) 永远都不可能是最优解。(可怜。。。

所以如果我们要维护一个最优解的序列,就可以不要 \(B\) 了。

那么,这个最优解序列里肯定已经不会存在“上凸”了。

如果是这样的“下凸”呢?

\(k_1\)\(B,A\) 的斜率,\(k_2\)\(C,B\) 的斜率。

\(A,B\) 代入,我们得知:

  1. \(k_1\leqslant s_i\),则 \(B\) 优于 \(A\)
  2. \(k_1>s_i\),则 \(A\) 优于 \(B\)
  3. \(k_2\leqslant s_i\),则 \(C\) 优于 \(B\)
  4. \(k_2>s_i\),则 \(B\) 优于 \(C\)

明显 \(k_1。所以 \(k_1\)\(k_2\) 之间只有可能有:

  1. \(s_i,此时 \(B\) 优于 \(C\)\(A\) 优于 \(B\)\(A\) 为最优解。
  2. \(k_1,此时 \(B\) 优于 \(C\)\(B\) 优于 \(A\)\(B\) 为最优解。
  3. \(k_1,此时 \(C\) 优于 \(B\)\(B\) 优于 \(A\)\(C\) 为最优解。

所以,在下凸的情况中,三个点都有可能是最优解,都需要保留。

现在呢,所有上凸都被去掉了,只剩下凸,所以大概最后的最优解序列就长这个样子:

好丑

反过来看就是lifan的脑袋了

观察发现,斜率是从左往右递增的。

所以,我们考虑用单调队列来当这个“最优解序列”。

维护队头

即保证队头元素为最优解。

设队头为 \(q_l\)

如果 \(q_{l+1}\)\(q_l\) 形成的斜率 \(\leqslant s_i\),根据上面推出来的玩意儿,得到 \(q_{l+1}\) 优于 \(q_l\)

那还要 \(q_l\) 干啥,直接 l++

更新DP值

\(f_i=f_{q_l}-2\times s_i\times s_{q_l}+{s_{q_l}}^2+{s_i}^2+M\)

维护队尾

即保证里面塞的点相邻两个的斜率递增。(听起来好怪。。。

设队尾为 \(q_r\),我们要往最优解队列里push一个 \(i\)

若队尾两个点 \(q_r,q_{r-1}\) 形成的斜率比 \(i,q_r\) 形成的斜率大,那么push(i)后,整个队列的斜率就不再单调递增,所以此时要将r--。(因为中讲到的 \(i\) 必须入队,只能委屈一下 \(q_r\) 了)

注意事项

众所周知,斜率是个浮点数。为了避免损失精度造成的一些惨案,我们交叉相乘,将分子、分母分开处理。

以及时刻都要保证队列中至少有两个点,因为要访问 \(q_l,q_{l+1}\)\(q_r,q_{r-1}\)

Code

#include
const int maxn=5e5+5;
int n,m,l,r;
int f[maxn],c[maxn],q[maxn];
int getDP(int i,int j){
	return f[j]+m+(c[i]-c[j])*(c[i]-c[j]);
}
int getup(int j,int k){
	//计算分子的值
	return f[j]+c[j]*c[j]-f[k]-c[k]*c[k];
}
int getdown(int j,int k){
	//计算分母的值
	return (c[j]-c[k])*2;
}
int main(){
	while(~scanf("%d%d",&n,&m)){
		l=r=1;
		//经验表明,凡是涉及到前缀和的单调队列都必须先在队列中push(0),因为sum[0]这个玩意
		for(int i=1;i<=n;++i){
			scanf("%d",&c[i]);
			//懒得开前缀和数组,c自给自足
			c[i]+=c[i-1];
			//维护队头
			//l

在对DP式变形时,我们最好将其化为 \(\dfrac{(j)-(k)}{(j)-(k)}x\)

这个板子只适用于维护下凸包的情况。

当中间的符号为 \(>\) 时,我们会在这份代码上稍作改动,维护一个上凸包,后文会提到有关内容。


玩具装箱

双倍经验 三倍经验

噢噢噢,紫的,紫的!

Solution

从今往后我们就只讲怎么推式子,不再去证明下凸等性质了。

\(f_i\) 表示第 \(i\) 个玩具放完后的最小费用。

\[f_i=\min\{f_j+(i-j-1+\sum\limits_{k=i}^jC_k-L)^2\} \]

为了让这个式子好拆,我们在一开始让 L++,并且 \(C\) 再次自给自足,为输入的 \(C\) 的前缀和数组。

于是式子就变成:

\[f_i=\min\{f_j+(i-j-L+C_i-C_j)^2 \]

明显硬拆会死人。(反正我试过,比较适合用来发泄

所以我们把式子变成酱紫:

\[f_i=\min\{f_j+((C_i+i)-(C_j+j)-L)^2\} \]

既然 \(C_i\)\(i\)\(C_j\)\(j\) 是对应的,那么直接预处理,给 \(C_i\) 加上 \(i\) 不就行了?

现在这个 \(C_i\) 的含义和实现就变得有点曲折难懂了。

具体实现如下:

for(int i=1;i<=n;++i){
	scanf("%lld",&c[i]);
	c[i]+=c[i-1];
}
for(int i=1;i<=n;++i)
	c[i]+=i;

也就是说,\(C_i\) 是在前缀和的基础上加了一个 \(i\),注意不能把 \(i\) 也一起前缀和了。

然后式子就变成了这样:

\[\begin{aligned} f_i&=\min\{f_j+(C_i-C_j-L)^2\} \\ &=\min\{f_j+{C_i}^2+{C_j}^2+L^2-2\times C_i\times C_j-2\times C_i\times L+2\times C_j\times L\} \\ &=\min\{f_j+{C_j}^2-2\times C_i\times C_j+2\times C_j\times L\}+{C_i}^2+L^2-2\times C_i\times L \end{aligned} \]

\(j\) 优于 \(k\)\(j

得:

\[f_j+{C_j}^2-2\times C_i\times C_j+2\times C_j\times L

Code

把上一题的代码中getup,getdown,getDP以及循环条件换一换就行了。

#include
#define int long long
const int maxn=5e4+5;
const int inf=0x3f3f3f3f;
int n,L,l,r;
int f[maxn],c[maxn],q[maxn];
int getDP(int i,int j){
	return f[j]+(c[i]-c[j]-L)*(c[i]-c[j]-L);
}
int getup(int j,int k){
	return f[j]-f[k]+2*c[j]*L-2*c[k]*L+c[j]*c[j]-c[k]*c[k];
}
int getdown(int j,int k){
	return c[j]-c[k]<<1;
}
signed main(){
	scanf("%lld%lld",&n,&L);
	++L;
	l=r=1;
	for(int i=1;i<=n;++i){
		scanf("%lld",&c[i]);
		c[i]+=c[i-1];
	}
	for(int i=1;i<=n;++i){
		c[i]+=i;
		while(l

任务安排 1

双倍经验 三倍经验

Solution

感谢蓝书。这里按着蓝书上的思维走。

解法一

暴力。

此处的 \(t,c\) 为输入的 \(t,c\) 的前缀和数组。

\(f_{i,j}\) 为前 \(i\) 个任务分成 \(j\) 批的最小费用。

\(S\times j+t_i\) 为第 \(i\) 个任务的完成时间。

得出状态转移方程(\(k\) 枚举上一批任务结束位置):

\[f_{i,j}=\min\limits_{0\leqslant k

时间复杂度 \(\Theta(n^3)\)

代码

#include
#include
const int maxn=5005;
int n,s,ans=1<<30;
int f[maxn][maxn];
int t[maxn],c[maxn];
int min(int x,int y){return x

亲测不开long long边T边WA拢共60,开了long long全MLE没分

解法二

脑子炸了,想了好久才想明白这个优化的正确性。

思考,以上代码需要 \(j\) 这一维的根本原因是什么?

因为我们无法确定之前已经划分了多少批,也就是无法确定 \(S\) 的个数。

换个角度思考,我们无法确定之前,却可以确定之后。

什么意思呢?如果我们在任务 \(i\) 处划分,那么任务 \(i\) 以及任务 \(i\) 以后的所有任务的执行时间都会延后 \(S\)

因为 \(i\) 以后的状态也会使用 \(f_i\) 的值,我们在计算 \(f_i\) 时就将 \(S\) 提出来,提前把后面的 \(c\) 乘上不就行了?

中间的结果不对劲也无所谓,只要最后的答案是对的就行惹。

也就是说,我们没有直接求出每批任务的完成时刻,而是在一批任务“开始”对后续任务产生影响时,就先把费用累加到答案中。这是一种名为”费用提前计算“的经典思想。

——李煜东《算法竞赛进阶指南》

状态转移方程:

\[f_i=\min\limits_{0\leqslant j

此处,\(f_i\) 没有具体含义。

代码

lyd貌似已经给了。。。算了,再写一遍巩固一下

#include
#include
#define int long long
const int maxn=5005;
int n,s;
int t[maxn],c[maxn],f[maxn];
int min(int x,int y){return x

任务安排 2

双倍经验

\(n\) 的范围变大了,\(n^2\) 过不了。

Solution

这不随手加个斜率优化的事儿吗。

我们继续瞎搞这个式子。

\[\begin{aligned} f_i&=\min\{f_j+(c_i-c_j)\times t_i+s\times(c_n-c_j)\} \\ &=\min\{f_j+c_i\times t_i-c_j\times t_i+s\times c_n-s\times c_j\} \\ &=\min\{f_j-c_j\times t_i-s\times c_j\}+c_i\times t_i+s\times c_n \end{aligned} \]

\(j\) 优于 \(k\)\(j

则有:

\[f_j-c_j\times t_i-s\times c_j

然后就是老套路。

时间复杂度 \(\Theta(n)\)

Code

lyd怎么又给了。。。毫无挑战性。。。

#include
#define int long long
const int maxn=300005;
int n,s,l,r;
int t[maxn],c[maxn],q[maxn],f[maxn];
int getDP(int i,int j){
	return f[j]+t[i]*(c[i]-c[j])+s*(c[n]-c[j]);
}
int getup(int j,int k){
	return f[j]-f[k]-s*c[j]+s*c[k];
}
int getdown(int j,int k){
	return c[j]-c[k];
}
signed main(){
	scanf("%lld%lld",&n,&s);
	l=r=1;
	for(int i=1;i<=n;++i){
		scanf("%lld%lld",&t[i],&c[i]);
		t[i]+=t[i-1];
		c[i]+=c[i-1];
	}
	for(int i=1;i<=n;++i){
		while(l

任务安排 3

双倍经验 三倍经验

啊,又是刷水紫的一天

\(t\) 有可能是负数。

Solution

\(t\) (输入)有可能为负,代表着 \(t\)(前缀和)不再单调递增,用不单调的玩意作为单调队列的条件一看就十分不讲武德,这样维护出来的队头显然不是正确答案。

后面的 \(t,c\) 默认为前缀和。

好吧,我们感性证明一下为什么不能酱紫玩。(XSC062瞎想的,有错误请指出QwQ

就一个普通的例子,假设输入了一个负数,导致 \(t_i,且有一斜率 \(>t_i\)\(

那么在 \(i-1\) 时,它就被维护队头的操作剔掉了,但也许它凑巧就是 \(i\) 的最优解,呜呼哀哉。

于是我们不能删队头元素了。那怎么查询最优解呢?

单调队列里装的始终还是个具有单调性的下凸包,于是我们可以在队列中二分一个 \(pos\)\(pos\)\(pos-1\) 形成的斜率比 \(t_i\) 小,\(pos+1\)\(pos\) 形成的斜率比 \(t_i\) 大。

然后把 \(pos\) 当成 \(q_l\) 来处理就好惹。

队尾还是维护一个下凸。

时间复杂度 \(\Theta(n\log_2n)\)

Code

这边强烈建议不要去动AcWing版本的那道。

最后两组数据堪称毒瘤。

long long乘法,卡__int128时限,卡double精度,非T即WA。

y总不想要他的horse了

反正我从凌晨1:00到1:30硬是没有搞出来。

#include
#include
#define int long long
const int maxn=300005;
int n,s,l,r;
int t[maxn],c[maxn],q[maxn],f[maxn];
int getDP(int i,int j){
	return f[j]+t[i]*(c[i]-c[j])+s*(c[n]-c[j]);
}
int getup(int j,int k){
	return f[j]-f[k]-s*c[j]+s*c[k];
}
int getdown(int j,int k){
	return c[j]-c[k];
}
int BinarySearch(int val){
    if(l==r)return q[l];
    int L=l,R=r;
    //二分整个单调队列
    while(L>1;
        if(getup(q[mid+1],q[mid])<=val*getdown(q[mid+1],q[mid]))
            L=mid+1;
        else R=mid;
    }
    return q[L];
}
signed main(){
	scanf("%lld%lld",&n,&s);
	l=r=1;
	for(int i=1;i<=n;++i){
		scanf("%lld%lld",&t[i],&c[i]);
		t[i]+=t[i-1];
		c[i]+=c[i-1];
	}
	for(int i=1;i<=n;++i){
		int pos=BinarySearch(t[i]);
		f[i]=getDP(i,pos);
		while(l

土地购买

Solution

首先我们想明白一件事情:如果一块土地,有另一块土地的长和宽都比它大,那就不用再理它了,直接从总序列里剔除。

struct land{
    int w,l;
    bool operator<(const land q)const{
        return w==q.w?l>q.l:w>q.w;
    }
}a[maxn];

...

sort(a+1,a+n+1);
for(int i=1;i<=n;++i){
    if(a[i].l>a[cnt].l)
        a[++cnt]=a[i];
}

然后就推式子。

贪心地想,在前面的操作后,\(a\) 是一个 \(w\) 递减,\(l\) 递增的土地序列。

所以我们选择将连续的一段区间分为一组,这样的话,一个组里的一段连续的土地 \([x,y]\) 就只有 \(w_x\)\(l_y\) 起了作用,又没有中间那一段的事了。

\(f_i\) 表示分配完第 \(i\) 块土地后的最小花费。

则有:

\[f_i=\min\{f_j+w_{j+1}\times l_i\} \]

\(j\) 优于 \(k\)

则有:

\[f_j+w_{j+1}\times l_i

Code

#include
#include
using std::sort;
#define int long long
const int maxn=5e4+5;
struct land{
    int w,l;
    bool operator<(const land q)const{
        return w==q.w?l>q.l:w>q.w;
    }
}a[maxn];
int n,cnt,l,r;
int q[maxn],f[maxn];
int getup(int j,int k){return f[j]-f[k];}
int getdown(int j,int k){return a[k+1].w-a[j+1].w;}
int getDP(int i,int j){return f[j]+a[j+1].w*a[i].l;}
signed main(){
    scanf("%lld",&n);
    for(int i=1;i<=n;++i)
        scanf("%lld%lld",&a[i].w,&a[i].l);
    sort(a+1,a+n+1);
    for(int i=1;i<=n;++i){
        if(a[i].l>a[cnt].l)
            a[++cnt]=a[i];
    }
    n=cnt;
    l=r=1;
    for(int i=1;i<=n;++i){
		while(l

仓库建设

双倍经验 三倍经验

Solution

辣鸡AcWing,毁我青春

\(f_i\) 表示在 \(i\) 工厂建立仓库的最小花费。

则有:

\[\begin{aligned} f_i&=\min\{f_j+\sum\limits_{k=j+1}^{i-1}[(x_i-x_k)\times p_k]+c_i\} \\ &=\min\{f_j+\sum\limits_{k=j+1}^{i-1}(x_i\times p_k)-\sum\limits_{k=j+1}^{i-1}(x_k\times p_k)\} \\ &=\min\{f_j+x_i\times\sum\limits_{k=j+1}^{i-1}p_k-\sum\limits_{k=j+1}^{i-1}(x_k\times p_k)\} \end{aligned} \]

利用前缀和优化:设 \(a\)\(p\) 的前缀和数组,\(b\)\(x_i\times p_i\) 的前缀和数组。

则原式可化为:

\[\begin{aligned} f_i&=\min\{f_j+x_i\times(a_{i-1}-a_j)-(b_{i-1}-b_j)\} \\ &=\min\{f_j+x_i\times a_{i-1}-x_i\times a_j-b_{i-1}+b_j\} \\ &=\min\{f_j-x_i\times a_j+b_j\}+x_i\times a_{i-1}-b_{i-1} \end{aligned} \]

\(j\) 优于 \(k\)

则有:

\[f_j-x_i\times a_j+b_j

Code

想尝试自闭的同学们可以尝试做一下AcWing那个链接,非MLE即WA,爽到炸。

#include
#define int long long
const int maxn=1e6+5;
int n,l,r;
int a[maxn],b[maxn];
int f[maxn],x[maxn],p[maxn],c[maxn],q[maxn];
int getDP(int i,int j){
	return f[j]+(a[i]-a[j])*x[i]-(b[i]-b[j])+c[i];
}
int getup(int j,int k){
	return f[j]-f[k]+b[j]-b[k];
}
int getdown(int j,int k){
	return a[j]-a[k];
}
signed main(){
	scanf("%lld",&n);
	l=r=1;
	for(int i=1;i<=n;++i){
		scanf("%lld%lld%lld",&x[i],&p[i],&c[i]);
		a[i]=a[i-1]+p[i];
		b[i]=b[i-1]+x[i]*p[i];
		while(l

Cats Transport

双倍经验 三倍经验

经验:洛谷的题面比蓝书和AcWing上的标准多了,蓝书的题意不清。

Solution

研究表明,边猛灌养乐多边看蓝书有助于理解。

这道题难在推式子。(不然还能难在哪儿

\(A_i\) 表示要接到第 \(i\) 只猫的最早出发时间,也就是说,在此时出发,猫 \(i\) 的等待时间为 \(0\)

\(A_i=T_i-\sum\limits_{j=1}^{H_i}D_j\),也就是出发时间=到达时间-经过时间。

此时我们可以把其他所有因素去掉,题意转换为:

已知在 \(\geqslant A_i\) 的时刻出发可以接到猫 \(i\) ,在 \(P\) 次出发次数的限制内接到所有猫,猫的等待时间之和最小是多少?

假设饲养员在 \(t\) 时刻出发,猫 \(i\) 的等待时间就是 \(t-A_i\)

\(A_i\) 从小到大排序。

有脑子就能想明白,一次接一段连续的猫,花费自然是最小的。

若我们要接 \([k+1,j]\) 范围内的猫,它们的等待时间之和就是:

\[\begin{aligned} \sum\limits_{p=k+1}^{j}(A_j-A_p)&=[j-(k+1)+1]A_j-\sum\limits_{p=k+1}^{j}A_p \\ &=(j-k)\times A_j-(S_j-S_k) \\ &=j\times A_j-k\times A_j-S_j+S_k \end{aligned} \]

其中 \(S\)\(A\) 的前缀和。

\(f_{i,j}\) 表示前 \(i\) 个饲养员带走前 \(j\) 只猫的最小花费。

则有:

\[\begin{aligned} f_{i,j}&=\min\{f_{i-1,k}+j\times A_j-k\times A_j-S_j+S_k\} \\ &=\min\{f_{i-1,k}-A_j\times k+S_k\}+A_j\times j-S_j \end{aligned} \]

将循环地枚举每个饲养员的循环变量 \(i\) 看做常量。

\(x\) 优于 \(y\),则有:

\[f_{i-1,x}-A_j\times x+S_x

注意事项

\(f\) 初始化为极大值。

Code

#include
#include
#include
using std::sort;
#define int long long
const int maxn=1e5+5;
int n,m,p,h,t,l,r;
int f[105][maxn];
int a[maxn],s[maxn],d[maxn],q[maxn];
int getDP(int i,int j,int k){
	return f[i-1][k]+j*a[j]-k*a[j]-s[j]+s[k];
}
int getup(int i,int x,int y){
	return f[i-1][x]+s[x]-f[i-1][y]-s[y];
}
int getdown(int x,int y){
	return x-y;
}
signed main(){
    memset(f,0x3f,sizeof(f));
	scanf("%lld%lld%lld",&n,&m,&p);
	for(int i=2;i<=n;++i){
		scanf("%lld",&d[i]);
        d[i]+=d[i-1];
    }
	for(int i=1;i<=m;++i){
        scanf("%lld%lld",&h,&t);
		a[i]=t-d[h];
	}
    sort(a+1,a+m+1);
    for(int i=1;i<=m;++i)
        s[i]=s[i-1]+a[i];
    f[0][0]=0;
    for(int i=1;i<=p;++i){
	    l=r=1;
        for(int j=1;j<=m;++j){
            while(l

征途

双倍经验 三倍经验

Solution

那么就直接化掉 \(v\times m^2\) 叭awa

\(x\) 为当前休息站点与上一休息站点的距离, \(x_0\)\(x_1\sim x_n\) 的平均数,\(S\)\(x\) 的前缀和。

\[\begin{aligned} V\times m^2&=[(x_1-x_0)^2+(x_2-x_0)^2+\cdots+(x_m-x_0)^2]\times m \\ &=m\times \sum\limits_{i=1}^{m}{x_i}^2+(m\times {x_0})^2-2\times (x_0\times m)\times \sum\limits_{i=1}^{m}x_i \\ &=m\times \sum\limits_{i=1}^{m}{x_i}^2+{S_m}^2-2\times{S_m}^2 \\ &=m\times \sum\limits_{i=1}^{m}{x_i}^2-{S_m}^2 \end{aligned} \]

锵锵!平均数什么的,不就无了喵?

其中 \(S_m\) 是一个定值。(即输入所有路的长度和)

唯一需要计算的,就是 \(\sum\limits_{i=1}^{m}{x_i}^{2}\),所以我们就来DP这个东西。

\(a\) 为输入道路长度的前缀和数组,\(f_{i,j}\) 表示第 \(i\) 次休息在 \(j\) 处时 \(\min\{\sum\limits_{k=1}^{i}{x_k}^2\}\) 的值,则有:

\[\begin{aligned} f_{i,j}&=\min\{f_{i-1,j}+(a_i-a_j)^2\} \\ &=\min\{f_{i-1,j}+{a_i}^2-2\times a_i\times a_j+{a_j}^2\} \end{aligned} \]

\(j\) 优于 \(k\),则有:

\[f_{i-1,j}-2\times a_i\times a_j+{a_j}^2

注意事项

不注意初始化还能注意哪儿。

\(f\) 初始化为极大值。

Code

在破烂 AcWing 上将 memset 改为初始化 f[0][i]=a[i]*a[i] 就可以过了www

不然会MLE,也许是因为没开滚动。(本来也用不着滚

#include
#include
#define int long long
const int maxn=3005;
int f[maxn][maxn];
int a[maxn],q[maxn];
int n,m,l,r,ans=1e18;
int getDP(int i,int j,int k){
	return f[k-1][j]+(a[i]-a[j])*(a[i]-a[j]);
}
int getup(int j,int k,int i){
	return f[i-1][j]-f[i-1][k]+a[j]*a[j]-a[k]*a[k];
}
int getdown(int j,int k){
	return a[j]-a[k]<<1;
}
int min(int x,int y){return x

扩展题目-柠檬

双倍经验

Solution

是边写这篇边做的呢。

题面翻译成人话就是:

有一个含有 \(n\) 个元素的序列 \(s\)。将这个序列分成连续的若干段,定义每一段的价值为"在这一段当中任选某个元素的个数的平方再乘上这个元素"的最大值。求将 \(s\) 划分后的最大价值。

不难想到分成的每一段首尾元素必须相等。

比如有这样一个序列 \(x,a_1,a_2,a_3,\cdots,a_k,x,y\) 。假设我们求的是这一段包含 \(x\) 的个数的平方,那么完全可以将 \(y\) 从这一段中分离,单独为一段,明显更优。

而其他所有情况都是这种情况的拓展。

于是得到式子:

\[f_i=\max\{f_{j-1}+s_i\times(cnt_i-cnt_j+1)^2\} \]

其中 \(cnt_i\)\(s_i\) 已经出现了多少次。

然后就是套路。

\[\begin{aligned} f_i&=\max\{f_{j-1}+s_i\times({cnt_i}^2+{cnt_j}^2-2\times cnt_i\times cnt_j+2\times cnt_i-2\times cnt_j+1)\} \\ &=\max\{f_{j-1}+s_i\times{cnt_i}^2+s_i\times{cnt_j}^2-2\times s_i\times cnt_i\times cnt_j+2\times s_i\times cnt_i-2\times s_i\times cnt_j+s_i\} \\ &=\max\{f_{j-1}+s_i\times {cnt_j}^2-2\times s_i\times cnt_i\times cnt_j-2\times s_i\times cnt_j\}+s_i\times {cnt_i}^2+2\times s_i\times cnt_i+s_i \end{aligned} \]

这次不是 \(\min\) 了,而是 \(\max\),还能这样玩吗?

不急,我们先按以前的方法试试,见机行事。

\(j\) 优于 \(k\),则有:

\[f_{j-1}+s_i\times {cnt_j}^2-2\times s_i\times cnt_i\times cnt_j-2\times s_i\times cnt_j>f_{k-1}+s_i\times {cnt_k}^2-2\times s_i\times cnt_i\times cnt_k-2\times s_i\times cnt_k \]

经试验,硬搞搞不出来,因为那个 \(s_i\) 乘上的平方项。

那我们想一个办法,把 \(s_i\) 给搞掉就行了。

既然我们已知一段的首尾元素必须相等,那不就说明 \(s_i,s_j,s_k\) 可以相互替换吗?

继续搞。

\[f_{j-1}+s_j\times {cnt_j}^2-2\times s_i\times cnt_i\times cnt_j-2\times s_j\times cnt_j>f_{k-1}+s_k\times {cnt_k}^2-2\times s_i\times cnt_i\times cnt_k-2\times s_k\times cnt_k \\ f_{j-1}-f_{k-1}+s_j\times {cnt_j}^2-s_k\times {cnt_k}^2-2\times s_j\times cnt_j+2\times s_k\times cnt_k>2\times s_i\times cnt_i\times cnt_j-2\times s_i\times cnt_i\times cnt_k \\ \dfrac{f_{j-1}-f_{k-1}+s_j\times {cnt_j}^2-s_k\times {cnt_k}^2-2\times s_j\times cnt_j+2\times s_k\times cnt_k}{cnt_j-cnt_k}>2\times s_i\times cnt_i \]

这里的符号变成 \(>\) 了。

证明得知,这里要维护的是一个斜率单调递减的上凸包最优解序列。

由于篇幅有限(实际上是因为懒),在这里就不把有关证明过程贴上来了,读者可以自行证明。

因为 \(s_i\) 必须与 \(s_j,s_k\) 相同,明显要针对每个 \(s_i\) 维护不同的最优解序列,在每次对应的序列中计算。

维护的斜率单调递减,而对于每一个相等的 \(s_i\)\(2\times s_i\times cnt_i\) 一定是单调递增的——

导致了这个序列大概长这个瓜样:斜率单调递减,但末尾的最小的斜率仍大于 \(2\times s_i\times cnt_i\)

单调栈可以自行研究,因为作者瞄了一眼时间发现已经是明天了。(什么鬼

Code

#include
#include
//不用vector会MLE(但vector难调到像吃了shit一样
using namespace std;
#define int long long
#define top q[x].size()-1
const int maxn=1e5+5;
const int maxm=1e4+5;
int n;
vectorq[maxm];
int f[maxn],s[maxn];
int cnt[maxn],tot[maxm];
int getDP(int i,int j){
	return f[j-1]+(cnt[i]-cnt[j]+1)*(cnt[i]-cnt[j]+1)*s[i];
}
int getup(int j,int k){
	return f[j-1]-f[k-1]+s[j]*cnt[j]*cnt[j]-s[k]*cnt[k]*cnt[k]-2*s[j]*cnt[j]+2*s[k]*cnt[k];
}
int getdown(int j,int k){
	return cnt[j]-cnt[k];
}
signed main(){
	scanf("%lld",&n);
	for(int i=1;i<=n;++i){
		scanf("%lld",&s[i]);
		cnt[i]=++tot[s[i]];
    }
    for(int i=1;i<=n;++i){
        int x=s[i];
		while(q[x].size()>=2&&getup(q[x][top-1],i)*getdown(q[x][top-1],q[x][top])>=getup(q[x][top-1],q[x][top])*getdown(q[x][top-1],i))
            q[x].pop_back();
		q[x].push_back(i);
		while(q[x].size()>=2&&getDP(i,q[x][top])<=getDP(i,q[x][top-1]))
            q[x].pop_back();
		f[i]=getDP(i,q[x][top]);
	}
	printf("%lld",f[n]);
	return 0;
}

总结

总结搞斜优题的步骤就是:

  1. 推DP式子
  2. 看看这个式子是不是人能化的
  3. 假设 \(j\) 由于 \(k\),将式子化成 \(\dfrac{(j)-(k)}{(j)-(k)}> \text{or} 的形式
  4. 单调队列/单调栈优化。
  5. 然后你就又多了一道紫色起步的题目。

end.