首页 > 其他分享 >DP优化——斜率优化

DP优化——斜率优化

时间:2024-09-04 19:03:06浏览次数:10  
标签:F4 S2 times 斜率 DP 凸壳 优化 dp

引言

在学数据结构优化dp,单调队列优化dp时都很快就懂了,四边形不等式优化dp看一看也懂了,只有斜率优化理解了一个月还不懂,最后在其他大佬和资料的帮助下成功学懂了,于是争取这篇题解在以后又不会的时候一遍就懂。


前置数学知识

1.一次函数

初中数学知识,见八年级数学课本。

2.凸包(凸壳)

  • 定义:
    意思是点集的边界,是一组连接相邻两点的线段斜率单调的一组点集,其中斜率单调递增的叫下凸壳,单调递减的叫上凸壳(摘自<<算法竞赛进阶指南>>)。
    注意:对于正的斜率是越陡越大,对于负的斜率是越平越大
    下面的是两个上凸壳:


    同理,下面是两个下凸壳:

    其实上凸壳还是下凸壳直接看形状就可以了。
  • 性质:
    我们以上凸壳为例,尝试用一条斜率固定为 \(k\) 的直线去切凸壳点集中的那些点,会得到这么多条平行线:

    这个也是上凸壳哦。
    会发现有且仅有过 \(C\) 点的那条直线与这个凸壳相切,我们称这个 \(C\) 点为斜率为 \(k\) 的直线与这个上凸壳的切点
    切点的性质是:他所确定的直线(众所周知一点和斜率可以确定一条直线)是所有直线中截距最大的。
    下凸壳我们也有类似的结论,把截距最大改成最小即可。

适用场景

斜率优化用于解决dp转移方程中涉及到 \(i,j\) 的乘积项的转移,先讲它的一般形式以及解决方法再放例题。


假设我们现在有这么一个转移方程:

\[dp[i]=min_{L(i)\le j\le R(j)}(dp[j] + F1(j) + F2(i) + F3(i)\times F4(j) + A) \]

其中,\(L,R,F1,F2,F3,F4\)是一些函数,\(A\) 是一个常量。
\(L,R\)的实际意义是对于每个 \(i\) 能转移到他的 \(j\) 是一段随着 \(i\) 变化而变化的区间。
\(F1,F2,F3,F4\) 则是一些关于 \(i,j\) 的表达式,因题目而异,注意 \(F1(j),F4(j)\) 只跟 \(j\) 有关,\(F2(i),F3(i)\) 只跟 \(i\) 有关。


我们可以先把与 \(j\) 无关的拉出来,放到 min 外面。

\[dp[i]=min_{L(i)\le j\le R(j)}(dp[j] + F1(j) + F3(i)\times F4(j) ) + F2(i) + A \]

会变得更加美观,这样下面我们就不去管后面那一坨东西了。


然后就是斜率优化的精髓,对于一个决策点 \(j\),当用他去转移 \(i\) 时,我们可以把 min 去掉,会得到:

\[dp[i]=dp[j] + F1(j) + F3(i)\times F4(j) \]

移项:

\[- F3(i)\times F4(j) + dp[i]=dp[j] + F1(j) \]

我们把 \(-F3(i)\) 当作直线的斜率 \(k\),把 \(dp[i]\) 当作直线的截距 \(b\),把 \((F4(j),dp[j] + F1(j))\) 当作直角坐标系的一个点,那相当于这条直线 \(y=kx+b\) 要经过这个点。
因为我们要求的是 \(dp[i]\),也就是 \(b\), 移项得到 \(b = y - kx\),所以我们如果知道斜率和这条直线经过的点就可以知道截距了。
所以问题变成平面上有若干形如 \((F4(j),dp[j] + F1(j))\) 的点,现在要去用一条斜率为 \(k=-F3(i)\) 的直线去切这些点,求最小的截距。

也就是需要将这条直线从下往上平移,第一个切到的点就是我们要的点,比如上图中是 \(G\) 点。


那怎么快速求出第一个切到的点呢,我们来看上图中 \(B,C,D\) 这三个点:

会发现不管斜率是多少,都切不到 \(C\) 点,所以这种上凸的形状中间的点是没用的,也就是说一个点 \(j_2\) 要成为决策点,假设他前面和后面的点是 \(j_1,j_3\),他的必要不充分条件是:

\[ \frac{(dp[j_2] + F1(j_2)) - (dp[j_1] + F1(j_1))}{F4(j_2) -F4(j_1)} < \frac{(dp[j_3] + F1(j_3)) - (dp[j_2] + F1(j_2))}{F4(j_3) -F4(j_3)} \]

这个判断条件如果想简便可以直接用 long double 存,也可以十字相乘,这样可以避免精度损失带来的误判,因为斜率优化的题基本都会满足 \(j\) 的横坐标的函数 \(F4(j)\) 是单调的(不然点出现的顺序就不是按照 \(j\) 从小到大了),所以不用担心分母出现负数的情况。
也就是说我们要维护的是一个下凸壳,这可以单调栈来维护,也就是每次加入一个点就判断一下栈顶的两个点和新加的这个点是否满足上述式子(新加的点是 \(j_3\) ),不满足就不断弹栈,直到满足为止。


那维护了下凸壳之后怎么求那个切点呢?
进一步发现性质,如果一个点满足他之前线段的斜率都小于 \(k=-F3(i)\),后面线段的斜率都大于 \(k=-F3(i)\),那么这个点就是切点,如下图:

因为下凸壳的点之间的线段的斜率满足单调性,所以这显然是可以二分的(这也就意味着你的栈要手写,因为要查询栈中元素)。这是最普遍的解法,时间复杂度 \(O(n \log n)\)。
那有些时候不能带 \(log\) 怎么办呢?下面的例题会有不带 \(log\) 的解法(\(O(n)\) 解法并不是每种斜率优化的题都适用的)。

当然对于 dp 转移方程里是 max 的,维护上凸壳即可,结论类似。


例题

所有初学斜率优化的应该都是从"任务安排" 这道题开始的吧。


任务安排

弱化版是不用斜率优化的。
设 \(f[i][j]\) 表示前 \(i\) 个任务分成 \(j\) 段的最小花费。
\(f[i][j]=min_{0\le j \le i-1}(f[k][j-1] + s + (S1[i]+s\times j)\times (S2[i]-S2[k]))\)。
\(S1\) 是 \(t\) 的前缀和,\(S2\) 是 \(F\) 的前缀和。
这样是 \(O(n^3)\) 的。

回顾"我的动态规划题单2"中"关路灯"那题,
考虑将 \(s\) 的费用提前计算,只考虑当前这批任务对后面的影响,这样就可以不去记录段数 \(j\) 了。
\(f[i]=min_{0\le j \le i-1}(f[j] + S1[i]\times(S2[i]-S2[j]) + s\times (S2[n]-S2[j]))\)。

时间复杂度\(O(n^2)\)。

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=1e5+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,s,t[N],F[N],f[N],S1[N],S2[N];
signed main(){
	n=read(),s=read();
	for(int i=1;i<=n;i++){
		t[i]=read(),F[i]=read();	
		S1[i]=S1[i-1]+t[i];
		S2[i]=S2[i-1]+F[i];
	} 
	memset(f,0x3f,sizeof f);
	f[0]=0;
	for(int i=1;i<=n;i++){
		for(int j=0;j<i;j++){
			f[i]=min(f[i],f[j] + S1[i]*(S2[i]-S2[j]) + s*(S2[n]-S2[j]));   
		}
	}
	printf("%lld\n",f[n]);
	return 0;
}

任务安排2

\(f[i]=min_{0\le j \le i-1}(f[j] + S1[i]\times(S2[i]-S2[j]) + s\times (S2[n]-S2[j]))\)。

按照上述所说的套路,把式子改写一下,只跟 \(j\) 有关和只跟 \(i\) 有关的拎出来:
\(f[i]=min_{0\le j \le i-1}(f[j] - (s+S1[i])\times S2[j]) + s\times S2[n] + S1[i]\times S2[i]\)。
不管后面那一坨,去掉 \(min\) 写成 \(y=kx+b\) 的形式:
\((s+S1[i])\times S2[j] + f[i] = f[j]\)。
所以就用一条斜率为 \((s+S1[i])\) 的直线去切这些形如 \((S2[j] , f[j])\) 的点。
维护出下凸壳之后,注意到因为 \(t_i\) 都是正数,所以斜率 \((s+S1[i])\) 随着 \(i\) 的增大而增大,那根据切点的判定条件,切点也是一定右移的,所以只保留斜率大于 \((s+S1[i])\) 的部分即可。
而且注意到这里的 \(L(i)\) 函数始终是 \(0\),所以就有了一下 O(n) 的做法:

  1. 用单调队列替换掉单调栈。
  2. 对于 \(i\),把队头那些斜率 \(\le (s+S1[i])\) 的线段 pop 掉,这样队头的点就是要求的切点。
  3. 取出队头转移。
  4. 加入 \(i\),并维护下凸壳。

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=3e5+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,s,t[N],c[N],f[N],S1[N],S2[N];
int dq[N],l,r;  //单调队列 
int x(int j){return S2[j];}  //横坐标
int y(int j){return f[j];}  //纵坐标 
signed main(){
	n=read(),s=read();
	for(int i=1;i<=n;i++){
		t[i]=read(),c[i]=read();	
		S1[i]=S1[i-1]+t[i];
		S2[i]=S2[i-1]+c[i];
	}
	memset(f,0x3f,sizeof f);
	f[0]=0;
	l=1,r=0; //初始化空的单调队列 
	dq[++r]=0;
	for(int i=1;i<=n;i++){
		while( l<r && ( y(dq[l+1]) - y(dq[l]) ) <= (s + S1[i]) * ( x(dq[l+1]) - x(dq[l]) ) ) l++;    //注意是 l<r 而不是 l<=r,因为起码要有两个点才是线段 
		int j=dq[l];
		f[i]=f[j] + S1[i]*(S2[i]-S2[j]) + s*(S2[n]-S2[j]);  //这里就用原来的式子就好了,新的那个太大便了 
		while( l<r && ( y(dq[r]) - y(dq[r-1]) ) * ( x(i) - x(dq[r]) ) >= ( y(i) - y(dq[r]) ) * ( x(dq[r]) - x(dq[r-1]) ) ) r--;    //维护凸壳 
		dq[++r]=i;
	}
	
	printf("%lld\n",f[n]);
	return 0;
}





[SDOI2012] 任务安排

题面都是一样的。

这里 \(t_i\) 可能是负数,所以 \((s+S1[i])\) 不一定单调。
所以求切点要二分,这里还是用的单调队列,其实单调栈也可以。
时间复杂度 O(n log n)。
要注意的是,这里 \(c_i\) 可以等于 \(0\),也就是说也就是会出现横坐标相同的两个点,那么有可能出现原先的斜率是 \(inf\)(即与 \(x\) 轴垂直,但是后面那个在前面那个上面),加进来一个后变成 \(-inf\)(加进来的在原来队尾的下面),但它们的斜率在比较时是一样,如果不把前面那个弹掉,就不满足斜率单调递增了,所以在维护下凸壳时,\(=\) 的情况也要把队尾弹掉(代码中也有注释)。

code

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=3e5+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,s,t[N],c[N],f[N],S1[N],S2[N];
int dq[N],l,r;  //单调队列 
int x(int j){return S2[j];}  //横坐标
int y(int j){return f[j];}  //纵坐标 
int Binary_search(int K){
	int L=l,R=r,mid,res=r;  //二分找第一个满足它后面的线段的斜率比 s+S1[i] 大的点y 
	while(L<=R){
		mid=(L+R)>>1;
		if(mid<r && y(dq[mid+1]) - y(dq[mid]) > K * ( x(dq[mid+1]) - x(dq[mid]) )) R=mid-1,res=mid;
		else L=mid+1;
	}
	return dq[res];
}
signed main(){
	n=read(),s=read();
	for(int i=1;i<=n;i++){
		t[i]=read(),c[i]=read();	
		S1[i]=S1[i-1]+t[i];
		S2[i]=S2[i-1]+c[i];
	}
	memset(f,0x3f,sizeof f);
	f[0]=0;
	l=1,r=0; //初始化空的单调队列 
	dq[++r]=0;
	for(int i=1;i<=n;i++){
		int j=Binary_search(s+S1[i]);
		f[i]=f[j] + S1[i]*(S2[i]-S2[j]) + s*(S2[n]-S2[j]); 
		while( l<r && ( y(dq[r]) - y(dq[r-1]) ) * ( x(i) - x(dq[r]) ) >= ( y(i) - y(dq[r]) ) * ( x(dq[r]) - x(dq[r-1]) ) ) r--;    //维护凸壳 
		/*
			这边取等号是因为 ti 可以=0,也就是会出现横坐标相同的两个点,那么有可能出现原先的斜率是 inf(即与 x 轴垂直,但是后面那个在前面那个上面),
			加进来一个后变成 -inf,但它们的斜率表示出来是一样,如果不把前面那个弹掉,就不满足斜率单调递增了。 
		*/ 
		dq[++r]=i;
	}
	
	printf("%lld\n",f[n]);
	return 0;
}

拓展

如果对于任务安排这题,\(Ti,Ci\) 都可能是负数怎么办?
这个时候就要更高级的李超树或者平衡树了,这里还不会,就先挖个坑。

标签:F4,S2,times,斜率,DP,凸壳,优化,dp
From: https://www.cnblogs.com/FloatingLife/p/18397198

相关文章

  • 子比主题美化 – 自助售卡/发卡插件 源码 | WordPress插件,完美支持
    插件功能支持自由添加卡密支持查看卡密库存邮箱自动发送卡密信息后台卡密库存不足提醒如何使用:在后台新建一篇文章,然后选择自动售卡。设置相关价格(不支持将价格设置为0)。移动到已编辑文章的底部(添加密码信息)直接发布文章以显示文章销售卡。安装方法:在Wordpress后......
  • 多目标应用:四种多目标优化算法(NSOOA、NSPSO、NSDBO、NSCOA)求解柔性作业车间调度问题(F
    一、柔性作业车间调度问题柔性作业车间调度问题(FlexibleJobSchedulingProblem,FJSP)的描述如下:n个工件{J,J......
  • 基于ABC-BP人工蚁群优化BP神经网络实现数据预测Python实现
    在数据预测领域,传统的统计方法和时间序列分析在面对复杂、非线性的数据时往往力不从心。随着人工智能技术的快速发展,神经网络特别是BP(BackPropagation)神经网络因其强大的非线性映射能力,在预测领域得到了广泛应用。然而,BP神经网络也存在易陷入局部最优、收敛速度慢等问题。为了......
  • 如何优化Oracle数据库的SQL性能?
    优化Oracle数据库的SQL性能可以从以下几个方面入手:‌‌合理的索引设计‌:‌根据查询需求和数据特点选择合适的索引类型,‌如B树索引、‌位图索引等,‌并考虑列的选择性和索引列的顺序。‌同时,‌避免创建过多的索引,‌以免增加维护成本。‌优化查询语句‌:‌避免使用SELECT*......
  • 优化MySQL数据备份的网络策略:技术与实践
    在当今的数据库管理实践中,数据备份是确保数据安全性和业务连续性的关键环节。MySQL作为广泛使用的数据库系统,其数据备份过程往往涉及大量的数据传输,尤其是在分布式或云环境中。因此,网络优化在MySQL数据备份中扮演着至关重要的角色。本文将探讨如何在MySQL中实现数据备份的网......
  • uniapp js 数独小游戏 写死的简单数独demo(优化完成) 数独 4.0
    <template> <viewclass="wrap">  <viewclass="timeGame">   <textclass="time">时间{{gameTime}}</text>  </view>  <viewclass="listWrap">   <view    ......
  • NOIP2024集训Day21 DP常见模型2 - 背包
    NOIP2024集训Day21DP常见模型2-背包A.[BZOJ4987]Tree树形背包dp先考虑几个显而易见的性质:选出的点一定是相邻的对于选出的点,如果从\(a_k\)再走回\(a_1\),那么就相当于每条边经过了两次由于题目没有包含\(dis(a_k,a_1)\),因此就相当于选出的点中的一条链可以只......
  • NOIP2024集训Day20 DP常见模型1 - 序列
    NOIP2024集训Day20DP常见模型1-序列A.[JOI2022Final]Let'sWintheElection贪心+DP。首先,一定是所有协作者同时在同一个州演讲,这样才最优。然后,假设我们已经知道所有州的方案(支持、支持+协作、反对),那我们一定是先按照从小到大的顺序拿下所有“支持+协作”州,这样最优。......
  • NOIP2024集训Day22 DP常见模型3 - 区间
    NOIP2024集训Day22DP常见模型3-区间A.[SCOI2003]字符串折叠因为前面折叠了会对后面产生影响,所以很显然不能贪心。考虑区间DP。定义\(f_{i,j}\)表示\(i\)到\(j\)范围内可以折叠到的最短长度。答案为\(f_{1,n}\)。状态转移:对于\(f_{i,j}\),使用区间DP惯用套路,枚......
  • 基于 Three.js 的 3D 模型加载优化 转载
    一、前言 近段时间,我们使用three.js完成了vivo拟我形象的开发工作,大家可以在vivo账号中拟制属于自己的3D形象,也可以保存作为自己的头像名片。  作为一个3D的项目,从用户打开页面到最终模型的渲染需要经过多个流程,加载的时间也会比普通的H5项目要更长一些。然而过长的等待......