斜率优化(超详细/看了就会)(基本知识+例题讲解+清晰代码)看了的人rp+++++++++ 烽火传递+玩具分组+特别行动队+征途

斜率优化(超详细/看了就会)(基本知识+例题讲解+清晰代码)看了的人rp+++++++++ 烽火传递+玩具分组+特别行动队+征途

对于斜优啊也是早就有所耳闻

但是本蒟太蒻了前段时间才学

发现很有用诶

所以学的很认真

总结打的的也很认真

希望大家也可以看的认真

是为序。

前置芝士

单调队列

【单调】指元素递增或递减

【队列】指元素只能从队头队尾进行操作

不是优先队列哦

不要傻傻分不清啦

例题-烽火传递
题意

两个城市间有 n n n 个烽火台,每个烽火台 i i i 可以付出 a i a_i ai 的代价发出信号,每 m m m 个烽火台中至少要有一个发出信号,问情报在这两座城市之间传递所需要的代价。

解法

f i f_i fi 表示信号从第一座城市传递到 i i i 所需的最小代价

可得转移方程为
f i = min ⁡ i − m − 1 ≤ j < i f j + a i f_i=\min\limits_{i-m-1 \le j<i} f_j+a_i fi=im1j<iminfj+ai
因为 m m m 座中只要有一座发出信号就好了,所以最后的 m m m 座都可以作为答案

那么最后的答案就是
a n s = min ⁡ n − m < i ≤ n f i ans=\min\limits_{n-m<i\le n} f_i ans=nm<inminfi

代码
#include<cstdio>
#include<iostream>
using namespace std;
const int N=1e5,Inf=2e9;
int a[N+10],f[N+10],q[N+10];
int main()
{
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
	}
	int hd=0,tl=0;
	for(int i=1;i<=n;i++)
	{
		while((hd<tl)&&((q[hd]+m)<i))
		{
			hd++;
		}
		f[i]=f[q[hd]]+a[i];
		while((hd<tl)&&(f[q[tl]]>=f[i]))
		{
			tl--;
		}
		q[++tl]=i;
	}
	int ans=Inf;
	for(int i=n-m+1;i<=n;i++)
	{
		ans=min(ans,f[i]);
	}
	printf("%d",ans);
	return 0;
}

由此我们也可以提炼出单调队列优化的基本操作

即:First,不断将队头后移使得其满足最优决策点的性质,最后剩的队头就是最优决策点,可依此进行转移。Second,将加入新的点 i i i 后,不满足单调递增的队尾弹出,并将 i i i 加入队尾

斜率(slope)

上面的链接是百度百科,有需要的自行 click

不过也不用管那么多,简单解释下

一条直线的解析式可以用 y = k x + b y=kx+b y=kx+b 来表示, k k k 就是这条直线的斜率

或者如果知道直线 A B AB AB 上的两个点 A ( a x , a y ) , B ( b x , b y ) A(a_x,a_y),B(b_x,b_y) A(ax,ay),B(bx,by) ,也可以求出斜率

即: s l o p e ( A B ) = a y − b y a x − b x slope(AB)=\dfrac{a_y-b_y}{a_x-b_x} slope(AB)=axbxayby

这个很重要,后面会用到

凸包

也是别管那么多,只要知道:

凸包上每条直线的斜率一定具有单调性

单调递增的是上凸包,单调递减的是下凸包

了解下就好了。


例题引路

[HNOI2008]玩具装箱

题意

n n n 个玩具,玩具 i i i 的长度为 c i c_i ci ,现要将玩具分组,每组里的编号需连续, i ∼ j i \sim j ij 的玩具组代价为 ( j − i + ∑ k = i j − L ) 2 (j-i+\sum_{k=i}^{j}-L)^2 (ji+k=ijL)2 L L L 为给定的常量。

解法

真真斜率优化板子题了

首先统一一下说法

s m i = ∑ j = 1 i c j sm_i=\sum_{j=1}^{i} c_j smi=j=1icj (即前缀和,以下题目均为此含义)
L = L + 1 L=L+1 L=L+1

f i f_i fi 表示装前 i i i 个所需的最小花费,答案就是 f n f_n fn

易得
f i = f j + ( i − j + s m i − s m j − L ) 2 f_i=f_j+(i-j+sm_i-sm_j-L)^2 fi=fj+(ij+smismjL)2
显然这是 O ( n 2 ) \Omicron( n^2 ) O(n2)

我们尝试将其优化至 O ( n ) \Omicron( n ) O(n)

然后就开始愉快的推柿子了

A i = s m i + i A_i=sm_i+i Ai=smi+i
f i = f j + ( A i − A j − L ) 2 f_i=f_j+(A_i-A_j-L)^2 fi=fj+(AiAjL)2
考虑 k < j k<j k<j 并且 j j j 转移到 i i i 优于 k k k 需要满足什么条件
f j + ( A i − A j − L ) 2 ≤ f k + ( A i − A k − L ) 2 f_j+(A_i-A_j-L)^2 \le f_k+(A_i-A_k-L)^2 fj+(AiAjL)2fk+(AiAkL)2
拆开,得
f j + A i 2 + A j 2 + L 2 − 2 A i A j − 2 A i L + 2 A j L ≤ f k + A i 2 + A k 2 + L 2 − 2 A i A k − 2 A i L + 2 A k L f_j+A_i^2+A_j^2+L^2-2A_iA_j-2A_iL+2A_jL \le f_k+A_i^2+A_k^2+L^2-2A_iA_k-2A_iL+2A_kL fj+Ai2+Aj2+L22AiAj2AiL+2AjLfk+Ai2+Ak2+L22AiAk2AiL+2AkL
化简一下
f j + A j 2 − 2 A i A j + 2 A j L ≤ f k + A k 2 − 2 A i A k + 2 A k L f_j+A_j^2-2A_iA_j+2A_jL \le f_k+A_k^2-2A_iA_k+2A_kL fj+Aj22AiAj+2AjLfk+Ak22AiAk+2AkL
移一下项
( f j + A j 2 ) − ( f k + A k 2 ) ≤ 2 A i A j − 2 A i A k + 2 A k L − 2 A j L (f_j+A_j^2)-(f_k+A_k^2) \le 2A_iA_j-2A_iA_k+2A_kL-2A_jL (fj+Aj2)(fk+Ak2)2AiAj2AiAk+2AkL2AjL
B i = f i + A i 2 B_i=f_i+A_i^2 Bi=fi+Ai2
B j − B k ≤ 2 ( A i − L ) ( A j − A k ) B_j-B_k \le 2(A_i-L)(A_j-A_k) BjBk2(AiL)(AjAk)
最后
B j − B k A j − A k ≤ 2 ( A i − L ) \frac{B_j-B_k}{A_j-A_k} \le 2(A_i-L) AjAkBjBk2(AiL)
推柿子环节至此结束

那么柿子的左边就是斜率(忘记的往上滑回去看)

我们基于这个式子来进行单调队列优化

其实跟上面的烽火传递很类似,只是柿子部分改下就好了

不过并不是所有的斜率优化都能依靠单调队列进行,只有当斜率具有单调性才可以,不然我们就要利用别的数据结构了。

代码

Talk is cheap,show you the code.

#include<cstdio>
#define ll long long int 
using namespace std;
const int N=5e4;
ll sm[N+10],f[N+10];
int q[N+10];
ll A(int x)
{
	return sm[x]+x;
}
ll B(int x)
{
	return f[x]+A(x)*A(x);
}
double fnd(int x,int y)
{
	return (double)(B(x)-B(y))/(double)(A(x)-A(y));
}
int main()
{
	int n;
	ll m;
	scanf("%d%lld",&n,&m);
	m++;
	for(int i=1;i<=n;i++)
	{
		scanf("%lld",&sm[i]);
		sm[i]+=sm[i-1];
	}
	int hd=0,tl=0;
	for(int i=1;i<=n;i++)
	{
		while((hd<tl)&&(fnd(q[hd],q[hd+1])<=((sm[i]+i-m)<<1)))
		{
			hd++;
		}
		f[i]=f[q[hd]]+(A(i)-A(q[hd])-m)*(A(i)-A(q[hd])-m);
		while((hd<tl)&&(fnd(q[tl-1],q[tl])>fnd(i,q[tl-1])))
		{
			tl--;
		}
		q[++tl]=i;
	}
	printf("%lld",f[n]);
	return 0;
}

一些练习题

相信通过上面的例题,大家都对斜率优化有了一些基本了解了。所以下面的练习题就只阐述推柿子的过程和放代码啦。

建议大家在看的时候,自己拿纸笔跟着推一下,收获会更大哦。

特别行动队

题意

现有 n n n 名士兵,士兵 i i i 的战斗力为 x i x_i xi ,要将士兵拆分成若干组,每组中士兵编号连续。一组 i ∼ j i \sim j ij 士兵组的初始战斗力 y y y ∑ k = i j x k \sum_{k=i}^{j} x_k k=ijxk ,修正战斗力 z z z a y 2 + b y + c ay^2+by+c ay2+by+c ,其中 a , b , c a,b,c a,b,c 为给定的系数。求划分后修正战斗力之和最大。

解法

照例, s m sm sm 表示前缀和

f i f_i fi 表示前 i i i 个的修正战斗力最大值,那么答案就是 f n f_n fn

易得状态转移方程
f i = max ⁡ j < i f j + a ( s m i − s m j ) 2 + b ( s m i − s m j ) + c f_i=\max\limits_{j<i} f_j+a(sm_i-sm_j)^2+b(sm_i-sm_j)+c fi=j<imaxfj+a(smismj)2+b(smismj)+c
展开,化简,得
f i = f j + a s m i 2 + a s m j 2 − 2 a s m i s m j − b s m j + b s m i + c f_i=f_j+asm_i^2+asm_j^2-2asm_ism_j-bsm_j+bsm_i+c fi=fj+asmi2+asmj22asmismjbsmj+bsmi+c
照例,把与 j j j 无关的项挪到左边
f i − a s m i 2 − b s m i − c = f j + a s m j 2 − b s m j − 2 a s m i s m j f_i-asm_i^2-bsm_i-c=f_j+asm_j^2-bsm_j-2asm_ism_j fiasmi2bsmic=fj+asmj2bsmj2asmismj
j j j 转移至 i i i k k k 更优,需满足(注意,此题求的是最大值,所以实际上维护的是上凸包,式子中也应为 > 或 ≥ \ge
f j + a s m j 2 − b s m j − 2 a s m i s m j ≥ f k + a s m k 2 − b s m k − 2 a s m i s m k f_j+asm_j^2-bsm_j-2asm_ism_j \ge f_k+asm_k^2-bsm_k-2asm_ism_k fj+asmj2bsmj2asmismjfk+asmk2bsmk2asmismk
A i = f i + a s m i 2 − b s m i A_i=f_i+asm_i^2-bsm_i Ai=fi+asmi2bsmi
A j − 2 s m i s m j ≥ A k − 2 s m i s m k A_j-2sm_ism_j \ge A_k-2sm_ism_k Aj2smismjAk2smismk
移项
A j − A k ≥ 2 s m i ( s m j − s m k ) A_j-A_k \ge 2sm_i(sm_j-sm_k) AjAk2smi(smjsmk)
最后
A j − A k s m j − s m k ≥ 2 s m i \frac{A_j-A_k}{sm_j-sm_k} \ge 2sm_i smjsmkAjAk2smi
搞定!

(不过还是要注意细节,尤其是推柿子的时候)

代码
#include<cstdio>
#define ll long long int 
using namespace std;
const int N=1e6;
ll sm[N+10],f[N+10];
int q[N+10];
int n;
ll a,b,c;
ll A(int x)
{
	return f[x]+a*sm[x]*sm[x]-b*sm[x];
}
double fnd(int x,int y)
{
	return (double)(A(x)-A(y))/(double)(sm[x]-sm[y]);
}
int main()
{
	scanf("%d%lld%lld%lld",&n,&a,&b,&c);
	for(int i=1;i<=n;i++)
	{
		scanf("%lld",&sm[i]);
		sm[i]+=sm[i-1];
	}
	int hd=0,tl=0;
	for(int i=1;i<=n;i++)
	{
		while((hd<tl)&&(fnd(q[hd],q[hd+1])>=(double)(2*a*sm[i])))
		{
			hd++;
		}
		f[i]=f[q[hd]]+a*(sm[i]-sm[q[hd]])*(sm[i]-sm[q[hd]])+b*(sm[i]-sm[q[hd]])+c;
		while((hd<tl)&&(fnd(q[tl-1],q[tl])<=fnd(q[tl],i)))
		{
			tl--;
		}
		q[++tl]=i;
	}
	printf("%lld",f[n]);
	return 0;
}

征途

题意

n n n 段路程,每一段的长度是 a i a_i ai ,将其划分为 m m m 段,求最小方差 × m 2 \times m^2 ×m2

解法

设划分后每段的长度为 b i b_i bi b ‾ \overline{b} b 表示 b b b 序列的平均值,即 ∑ i = 1 m b i m \dfrac{\sum_{i=1}^{m} b_i}{m} mi=1mbi

再设最小方差为 v v v

则:
v m 2 = ∑ i = 1 m ( b i − b ‾ ) 2 m m 2 v m 2 = m ( ∑ i = 1 m ( b i 2 + b ‾ 2 − 2 b ‾ b i ) ) v m 2 = m ( ∑ i = 1 m b i 2 + m b ‾ 2 − 2 b ‾ ∑ i = 1 m b i ) v m 2 = m ∑ i = 1 m b i 2 + ( ∑ i = 1 m b i ) 2 − 2 ( ∑ i = 1 m b i ) 2 v m 2 = m ∑ i = 1 m b i 2 − s m n 2 vm^2=\frac{\sum_{i=1}^{m} (b_i-\overline{b})^2}{m}m^2\\ vm^2=m(\sum_{i=1}^{m} (b_i^2+\overline{b}^2-2\overline{b}b_i))\\ vm^2=m(\sum_{i=1}^{m} b_i^2+m\overline{b}^2-2\overline{b}\sum_{i=1}^{m} b_i)\\ vm^2=m\sum_{i=1}^{m} b_i^2+(\sum_{i=1}^{m}b_i)^2-2(\sum_{i=1}^{m} b_i)^2\\ vm^2=m\sum_{i=1}^{m} b_i^2-sm_n^2 vm2=mi=1m(bib)2m2vm2=m(i=1m(bi2+b22bbi))vm2=m(i=1mbi2+mb22bi=1mbi)vm2=mi=1mbi2+(i=1mbi)22(i=1mbi)2vm2=mi=1mbi2smn2

m m m 是已知的, b i b_i bi 也已知,那么当下目标就是使得 ∑ i = 1 m b i 2 \sum_{i=1}^{m} b_i^2 i=1mbi2 最小

表示第 i i i 段的结尾为 j j j 个数的最小平方和

可得转移方程
f i , j = min ⁡ ( f i − 1 , k + ( s m j − s m k ) 2 ) f_{i,j}=\min(f_{i-1,k}+(sm_j-sm_k)^2) fi,j=min(fi1,k+(smjsmk)2)
展开,得
f i , j = f i − 1 , k + s m j 2 + s m k 2 − 2 s m j s m k f_{i,j}=f_{i-1,k}+sm_j^2+sm_k^2-2sm_jsm_k fi,j=fi1,k+smj2+smk22smjsmk
把与 k k k 无关的项移至左边,得
f i , j − s m j 2 = f i − 1 , k + s m k 2 − 2 s m j s m k f_{i,j}-sm_j^2=f_{i-1,k}+sm_k^2-2sm_jsm_k fi,jsmj2=fi1,k+smk22smjsmk
考虑由 k k k 转移比由 l l l 转移更优需要满足的条件
f i − 1 , k + s m k 2 − 2 s m j s m k ≤ f i − 1 , l + s m l 2 − 2 s m j s m l f_{i-1,k}+sm_k^2-2sm_jsm_k \le f_{i-1,l}+sm_l^2-2sm_jsm_l fi1,k+smk22smjsmkfi1,l+sml22smjsml
惯常套路,浅浅移一下项
( f i − 1 , k + s m k 2 ) − ( f i − 1 , l + s m l 2 ) ≤ 2 s m j s m k − 2 s m j s m l (f_{i-1,k}+sm_k^2)-(f_{i-1,l}+sm_l^2) \le 2sm_jsm_k-2sm_jsm_l (fi1,k+smk2)(fi1,l+sml2)2smjsmk2smjsml
A j = f i − 1 , j + s m j 2 A_j=f_{i-1,j}+sm_j^2 Aj=fi1,j+smj2
A k − A l ≤ 2 s m j ( s m k − s m l ) A_k-A_l \le 2sm_j(sm_k-sm_l) AkAl2smj(smksml)
最后
A k − A l s m k − s m l ≤ 2 s m j \frac{A_k-A_l}{sm_k-sm_l} \le 2sm_j smksmlAkAl2smj

然后依此斜率优化即可

代码
#include<cstdio>
#include<iostream>
#define ll long long int 
using namespace std;
const ll Inf=1e18;
const int N=3e3;
ll sm[N+10],f[N+10][N+10];
int q[N+10];
double A(int x,int y)
{
	return (double)f[x][y]+sm[y]*sm[y];
}
double fnd(int x,int y,int z)
{
	return (A(x,y)-A(x,z))/(double)(sm[y]-sm[z]);
}
int main()
{
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=0;i<=m;i++)
	{
		for(int j=0;j<=n;j++)
		{
			f[i][j]=Inf;
		}
	}
	f[0][0]=0;
	for(int i=1;i<=n;i++)
	{
		int x;
		scanf("%d",&x);
		sm[i]=sm[i-1]+x;
	}
	for(int i=1;i<=m;i++)
	{
		int hd=0,tl=0;
		for(int j=1;j<=n;j++)
		{
			while((hd<tl)&&(fnd(i-1,q[tl],q[tl-1])>fnd(i-1,j,q[tl])))
			{
				tl--;
			}
			q[++tl]=j;
			while((hd<tl)&&(fnd(i-1,q[hd+1],q[hd])<(double)(sm[j]<<1)))
			{
				hd++;
			}
			f[i][j]=f[i-1][q[hd]]+(sm[j]-sm[q[hd]])*(sm[j]-sm[q[hd]]);
		}
	}
	printf("%lld",f[m][n]*m-(sm[n]*sm[n]));
	return 0;
}

注意,根据我们上面推出的柿子,答案是 m f m , n − s m n 2 mf_{m,n}-sm_n^2 mfm,nsmn2

一些疑惑

Q1:什么时候可以用斜优啊?

A1:一般来讲,如果这道题你推出的 dp 式形为 f i = min ⁡ / max ⁡ ( f j + a j + b i c j ) f_i=\min / \max(f_j+a_j+b_ic_j) fi=min/max(fj+aj+bicj) ,也就是说同时含有 i , j i,j i,j 的项,此时很难找到最优决策点,多半是斜率优化。

Q2:可以讲下一般怎么推柿子吗?

A2:当然。以作者的个人习惯,会分为一下几个步骤:

  1. 一般只把含有转移项的项留在右边,其余的挪去左边(比如说,用 j j j 来转移 i i i ,就只把含有 j j j 的项留在右边)
  2. 考虑怎样转移更优(例:考虑由 j j j 转移到 i i i k k k 转移更优),列出不等式
  3. 然后无所不用其极努力来化简这个不等式,使柿子的左边化成类似斜率的形式 a y − b y a x − b x \dfrac{a_y-b_y}{a_x-b_x} axbxayby 。在过程中可以适当引入一些表示方式来使柿子更简洁。

Q2:刚说只有斜率具有单调性的时候才能单调队列优化,那如果不具有单调性呢?

A2:这个时候就需要用一些恶心复杂的数据结构,比如平衡树之类的来寻找最优决策点,或者使用 CDQ 分治实现。

Q3+:为什么这篇文章不写呢?

A3+:这就非常惭愧了,因为作者本身是一名初中 OIer。啊不过这不是重点,重点是作者实在是太蒻了,这一部分也还在学习中,学会了再来和大家分享。

后记

这是我在 csdn 的第二篇博客啦

希望讲的能让大家看的清楚

博客中有错误或是遗漏的话也请各位不吝指出

有不太能理解的地方也可以在评论区发出来,我会解答的。

这篇真的打的超久超用心的

大家多多支持,一键三连哦。

谢谢啦🌹

  • 19
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值