斜率优化DP
一,概述
针对一类DP递推式中出现多项式交叉的多项式的DP
一定要求具有求解单调性
1,OI Wiki 的描述:
- 将初始状态入队
- 每次使用一条和 i i i 相关的直线 f i f_i fi 去切维护的凸包,找到最优决策,更新 d p i dp_i dpi
- 加入状态 d p i dp_i dpi。如果一个状态(即凸包上的一个点)在 d p i dp_i dpi加入后不再是凸包上的点,需要在 d p i dp_i dpi加入之前剔除
2,思考过程
1,把原始DP方程做出来
2,把常量提出,在其余需要转移的量中选取一个 和转移位置下标有关的数组值 ,作为自变量(一般希望它在X轴方向上具有单调性)
3, 剩余的未知量(一般是前面的Dp值),作为纵坐标,建立坐标系
4,在转移的过程中,一般通过选取的横坐标的系数(斜率)不变
5,建模线性规划,考虑定斜率直线切点集凸包
6,维护凸包,线性或LOG选取进行转移
- 最终我们希望转移方程为以下形式: f [ i ] = m i n { f [ j ] + a [ i ] ∗ b [ j ] + c [ j ] + d [ i ] } f[i] = min\{f[j]+a[i]*b[j] +c[j]+d[i]\} f[i]=min{f[j]+a[i]∗b[j]+c[j]+d[i]}
- 进一步的: f [ j ] + c [ j ] = a [ i ] ∗ b [ j ] + d [ i ] + f [ i ] f[j]+c[j] = a[i]*b[j]+d[i]+f[i] f[j]+c[j]=a[i]∗b[j]+d[i]+f[i]
- 我们要最小化或者最大化的一般是斜率,考虑适当换元使得符合这样一个一次形式
- k = a [ i ] k=a[i] k=a[i] 记作待决策值值
- ( b [ j ] , f [ j ] + c [ j ] ) (b[j]~,~f[j]+c[j]) (b[j] , f[j]+c[j]) 在函数散点图中出现,作为凸包的维护对象记作决策值
- 整个DP 的过程就是在散点图中拟合待择取值并优化
3,具体构造
1,通过四边形不等式或者数学归纳证明求解单调性
2,根据单调性设出两个决策对象 j1,j2 并根据求解单调性对函数值列出不等式
3,提出不变量(
a
[
i
]
a[i]
a[i]) 并得到不等关系,以此择取值
4,结合线性规划做成下凸或者上凸
3.5,维护下凸壳的方法
- 择取队尾和队尾之后一个元素,和要入队的元素比较,如果是这种构型,直接删去
细节:
1,初始队列不为空,注意转移的起点需要赋初值
2,斜率是绑定在靠左的点上求的,我们需要第一个斜率大于目标的直线的左端点,注意弹栈的限制
3,队列至少两个元素!
4,一般建模:
1,数组有序,分组选取
2,在min,max择取的时候考虑通过排序保证择取的是端点值
3,等待问题考虑细分过程,明确时间底线使用前缀和择取
二,用法
以下的手法只是维护凸包的一个手段,我们不做lim,只是提出一个引子和启发
1,单调队列选取(k和b[i]单调)
有 n n n 个 任务 排成一个 序列,顺序不得改变,其中第 i i i 个 任务 的 耗时 为 t i t_i ti, 费用系数 为 c i c_i ci
现需要把该 n n n 个 任务 分成 若干批 进行加工处理
每批次的 段头,需要 额外消耗 S S S 的时间启动机器。每一个任务的 完成时间 是所在 批次 的 结束时间。
完成一个任务的 费用 为:从 0 0 0 时刻 到该任务 所在批次结束 的时间 t t t乘以 该任务 费用系数 c c c
int main()
{
cin >> n >> s;
for (int i = 1; i <= n; i ++ )
{
cin >> t[i] >> c[i];
c[i] += c[i-1];
t[i] += t[i-1];
}
int hh = 0;
int tt = 0;
int q[N];
q[0] = 0;
for (int i = 1; i <= n; i ++ )
{
while(hh < tt && f[q[hh+1]]-f[q[hh]] <= (s+t[i])*(c[q[hh+1]]-c[q[hh]]) )hh++;
f[i] = f[q[hh]]-(s+t[i])*c[q[hh]]+s*c[n]+t[i]*c[i];
while (hh < tt && (f[i]-f[q[tt]])*(c[q[tt]]-c[q[tt-1]]) <= (f[q[tt]]-f[q[tt-1]])*(c[i]-c[q[tt]]))tt--;
q[++tt]=i;
}
cout << f[n];
}
2,二分择取(k不单调,b[i]单调)
- 如果目标斜率不单调了,我们就不能单调栈直接择取了
- 但是凸包还是要维护,凸包的性质是一定的
- 二分,斜率有序的凸包中查找第一个斜率大于目标的位置并转移
- 二分的位置就是单调队列内部,注意左右边界的择取
int main()
{
cin >> n >> s;
for (int i = 1; i <= n; i ++ )
{
cin >> t[i] >> c[i];
c[i] += c[i-1];
t[i] += t[i-1];
}
int hh = 0;
int tt = 0;
q[0] = 0;
for (int i = 1; i <= n; i ++ )
{
int l = hh;
int r = tt;
while (l<r)
{
int mid = l+r>>1;
if(f[q[mid+1]]-f[q[mid]] > (s+t[i])*(c[q[mid+1]]-c[q[mid]]))r=mid;
else l = mid+1;
}
f[i] = f[q[l]]-(s+t[i])*c[q[l]]+s*c[n]+t[i]*c[i];
while (hh < tt && (s64)(f[i]-f[q[tt]])*(c[q[tt]]-c[q[tt-1]]) <= (s64)(f[q[tt]]-f[q[tt-1]])*(c[i]-c[q[tt]]))tt--;
q[++tt]=i;
}
cout << f[n];
}