浅谈斜率优化DP
前言#
考试 T2 出题人放了个树上斜率优化 DP,直接被同校 OIER 吊起来锤。
离 NOIP 还有不到一周,赶紧学一点。
update:2023.11.14 : 是的第二天我就被吊锤了,发现自己理解的有点问题,所以改一改。
引入#
斜率#
斜率,数学、几何学名词,是表示一条直线(或曲线的切线)关于(横)坐标轴倾斜程度的量。它通常用直线(或曲线的切线)与(横)坐标轴夹角的正切,或两点的纵坐标之差与横坐标之差的比来表示。
斜率可以用来描述一个坡的倾斜程度,公式
初中学过一元一次函数
解决什么#
在经典 DP 式:
但当
然而不同的情况下要使用不同的数据结构来进行斜率优化,如:单调队列(适用于斜率有单调性),二分查找单调栈(适用于x坐标有单调性),平衡树和CDQ分治(适用于x坐标都不单调,实现动态开点和动态插入以及相关维护)
当
仅当
理解#
下面来以一道题目为例进行讲解。
P3195 [HNOI2008] 玩具装箱#
看完题目应该都可以想出来一个
设
咱尝试把这一堆东西分分类,把只有
得到:
设
那么就是 :
显然的,
这个式子是把只与
那么咱就可以把一个之前转移完成的状态看成是一个
那么咱要求
于图像中#
假设下面的三个点是咱待选的状态:
假设咱当前要求的斜率画出来是下面这样:
咱就从下往上,一点一点向上挪,直到碰到的第一个点,此时的截距一定最大。咱也能看出的确
那么此时的
答案是可以,因为斜率是单调递增的,既然这次第一个碰不到
但是咱如何做到最快找出呢?
队列维护#
观察这张图片,假设里面的点都是之前转移完的状态。
比较
不难发现
这个咱可以用一个队列来维护一个下凸壳,也就是凸包的一部分。
然后根据上面说的,要是队列头的两个元素形成的直线斜率比当前的小,也可以直接弹出。
这样队列的队头元素就是咱要转移的值了。
code:#
/*
* @Author: Aisaka_Taiga
* @Date: 2023-11-13 14:11:27
* @LastEditTime: 2023-11-13 15:09:40
* @LastEditors: Aisaka_Taiga
* @FilePath: \Desktop\P3195.cpp
* The heart is higher than the sky, and life is thinner than paper.
*/
#include <bits/stdc++.h>
#define pf(x) ((x) * (x))
#define int long long
#define DB double
#define N 1000100
using namespace std;
inline int read()
{
int x = 0, f = 1;
char c = getchar();
while(c < '0' || c > '9'){if(c == '-') f = -1; c = getchar();}
while(c <= '9' && c >= '0') x = (x << 1) + (x << 3) + (c ^ 48), c = getchar();
return x * f;
}
int n, L, q[N], c[N], f[N], sum[N], A[N], B[N];
inline int X(int x){return B[x];}
inline int Y(int x){return f[x] + pf(B[x]);}
inline DB xl(int i, int j){return (Y(i) - Y(j)) * 1.0 / (X(i) - X(j));}
signed main()
{
n = read(), L = read();
for(int i = 1; i <= n; i ++) c[i] = read();
for(int i = 1; i <= n; i ++)
{
sum[i] = sum[i - 1] + c[i];
B[i] = sum[i] + i + L + 1;
A[i] = sum[i] + i;
}
B[0] = L + 1;
int h = 1, t = 1;
for(int i = 1; i <= n; i ++)
{
while(h < t && xl(q[h], q[h + 1]) < 2 * A[i]) h ++;
int j = q[h];
f[i] = f[j] + pf(A[i] - B[j]);
while(h < t && xl(q[t - 1], i) < xl(q[t - 1], q[t])) t --;
q[++ t] = i;
}
cout << f[n] << endl;
return 0;
}
一般化#
咱最后得到的一定是类似这样的方程式:
不妨设
同样的咱可以最后表示成
这种形式,然后就可以计算是否舍去了。
实现细节#
-
大部分情况下需要将决策点
入队。 -
保证队列中至少有两个元素(构成直线),单调队列的判断条件要写成
。 -
建议使用乘积判断斜率大小防止被卡精度,具体参考P5785
-
最优决策点在上凸包还是下凸包上根据具体方程具体分析。
-
可能会出现仅满足
单调不降的情况,此时会造成两直线的斜率相同。可以考虑在求斜率时写成if (X(x_) == X(y_)) return (Y(y_) > Y(x_) ? 1e18 : -1e18);
以及判断决策优劣时写成<=
和>=
来解决。
例题#
P2120 [ZJOI2007] 仓库建设#
咱可以设
那么咱就能写出来一个
复杂度肯定受不了,我们先拆一下式子:
咱设
那么咱就可以简化成这样:
假设前面有两个位置
右边的这个东西可以看作是由
开一个队列,设
最后有的工厂可能没有商品,所以此时会出现相邻两个点构成的直线中
所以若
/*
* @Author: Aisaka_Taiga
* @Date: 2023-11-14 16:17:16
* @LastEditTime: 2023-11-14 19:42:44
* @LastEditors: Aisaka_Taiga
* @FilePath: \Desktop\P2120.cpp
* The heart is higher than the sky, and life is thinner than paper.
*/
#include <bits/stdc++.h>
#define int long long
#define DB double
#define N 1000010
using namespace std;
inline int read()
{
int x = 0, f = 1;
char c = getchar();
while(c < '0' || c > '9'){if(c == '-') f = -1; c = getchar();}
while(c <= '9' && c >= '0') x = (x << 1) + (x << 3) + (c ^ 48), c = getchar();
return x * f;
}
int n, c[N], p[N], x[N], s[N], q[N], f[N], ans = 1e18;
/*
f[i] = min(f[j] + \sum_{k = j + 1}^{i} (x[i] - x[k]) * p[k] + c[i]);
f[i] = min(f[j] + \sum_{k = j + 1}^{i} (x[i] * p[k]) - \sum_{k = j + 1}^{i} x[k] * p[k] + c[i]);
s[i] = \sum_{k = 1}^{i} p[k] * x[k], p[i] = \sum_{k = 1}^{i} p[k];
f[i] = min(f[j] + x[i] * (p[i] - p[j]) - s[i] + s[j] + c[i]);
f[i] = min(f[j] + x[i] * p[i] - x[i] * p[j] - s[i] + s[j] + c[i]);
f[j] = x[i] * p[j] + f[i] - x[i] * p[i] + s[i] - s[j] - c[i];???
*/
inline DB xl(int i, int j)
{
DB y = f[j] - f[i] + s[j] - s[i];
if(p[j] == p[i])
{
if(y == 0) return 0;
else
{
if(y > 0) return 1e19;
else return -1e19;
}
}
else return y / (p[j] - p[i]);
return (f[j] - f[i]) * 1.0 / (p[j] - p[i]);
// return(p[j] == p[i] ? (!y ? 0 : (y > 0 ? 1e19 : -1e19)) : y / DB(p[j] - p[i]));
}
signed main()
{
n = read();
for(int i = 1; i <= n; i ++)
{
x[i] = read(), p[i] = read(), c[i] = read();
s[i] = s[i - 1] + p[i] * x[i];
p[i] += p[i - 1];
}
int h = 1, t = 1;
for(int i = 1; i <= n; i ++)
{
while(h < t && xl(q[h], q[h + 1]) <= x[i]) h ++;
f[i] = f[q[h]] + x[i] * (p[i] - p[q[h]]) - s[i] + s[q[h]] + c[i];
// cout << "CAO : " << q[h] << endl;
while(h < t && xl(q[t - 1], i) <= xl(q[t - 1], q[t])) t --;
q[++ t] = i;
}
h = n; ans = f[n];
while(h && p[h] - p[h - 1] == 0) h --, ans = min(ans, f[h]);
cout << ans << endl;
return 0;
}
Frog 3#
第一次自己推出来斜率优化。
设
看到题目咱可以写出一个
然后咱给他拆一下:
咱设两个决策点
因为
注意青蛙在第一块石头上!
/*
* @Author: Aisaka_Taiga
* @Date: 2023-11-15 17:08:33
* @LastEditTime: 2023-11-15 17:38:43
* @LastEditors: Aisaka_Taiga
* @FilePath: \Desktop\ATDPZ.cpp
* The heart is higher than the sky, and life is thinner than paper.
*/
#include <bits/stdc++.h>
#define pf(x) ((x)*(x))
#define int long long
#define DB double
#define N 200010
using namespace std;
inline int read()
{
int x = 0, f = 1;
char c = getchar();
while(c < '0' || c > '9'){if(c == '-') f = -1; c = getchar();}
while(c <= '9' && c >= '0') x = (x << 1) + (x << 3) + (c ^ 48), c = getchar();
return x * f;
}
int n, m, q[N], f[N], h[N];
/*
f[i] = min(f[j] + (h[i] - h[j])^2 + C);
f[i] = f[j] + h[i]^2 + h[j]^2 - 2 * h[i] * h[j] + C;
*/
inline int X(int x){return h[x];}
inline int Y(int x){return f[x] + pf(h[x]);}
inline DB xl(int i, int j){return (Y(j) - Y(i)) * 1.0 / (X(j) - X(i));}
signed main()
{
int n = read(), m = read();
for(int i = 1; i <= n; i ++) h[i] = read();
int H = 1, t = 1;
q[1] = 1;
for(int i = 2; i <= n; i ++)
{
while(H < t && xl(q[H], q[H + 1]) <= 2 * h[i]) H ++;
f[i] = f[q[H]] + pf(h[i] - h[q[H]]) + m;
while(H < t && xl(q[t], i) <= xl(q[t - 1], q[t])) t --;
q[++ t] = i;
}
cout << f[n] << endl;
return 0;
}
P3628 [APIO2010] 特别行动队#
咱先对
那么很容易写出下面的式子:
然后咱设两个点
由于题目给的
/*
* @Author: Aisaka_Taiga
* @Date: 2023-11-15 19:34:12
* @LastEditTime: 2023-11-15 19:47:39
* @LastEditors: Aisaka_Taiga
* @FilePath: \Desktop\P3628.cpp
* The heart is higher than the sky, and life is thinner than paper.
*/
#include <bits/stdc++.h>
#define pf(x) ((x) * (x))
#define int long long
#define DB double
#define N 1000100
using namespace std;
inline int read()
{
int x = 0, f = 1;
char c = getchar();
while(c < '0' || c > '9'){if(c == '-') f = -1; c = getchar();}
while(c <= '9' && c >= '0') x = (x << 1) + (x << 3) + (c ^ 48), c = getchar();
return x * f;
}
int n, a, b, c, A[N], f[N], q[N];
/*
S = A[i] - A[j];
f[i] = max(f[j] + a * S * S + b * S + c);
f[i] = f[j] + a * (A[i]^2 + A[j]^2 - 2 * A[i] * A[j]) + b * A[i] - b * A[j] + c;
f[i] = f[j] + a * A[i]^2 + a * A[j]^2 - 2 * a * A[i] * A[j] + b * A[i] - b * A[j] + c
f[x] + a * A[i]^2 + a * A[x]^2 - 2 * a * A[i] * A[x] + b * A[i] - b * A[x] + c < f[y] + a * A[i]^2 + a * A[y]^2 - 2 * a * A[i] * A[y] + b * A[i] - b * A[y] + c
f[x] - f[y] + a * A[x]^2 - b * A[x] - a * A[y]^2 + b * A[y] < 2 * a * A[i] * (A[x] - A[y])
\frac{f[x] + a * A[x]^2 - b * A[x] - (f[y] + a * A[y]^2 - b * A[y])}{A[x] - A[y]} < 2 * a * A[i]
*/
inline int X(int x){return A[x];}
inline int Y(int x){return f[x] + a * pf(A[x]) - b * A[x];}
inline DB xl(int x, int y){return (Y(y) - Y(x)) * 1.0 / (X(y) - X(x));}
signed main()
{
n = read();
a = read(), b = read(), c = read();
for(int i = 1; i <= n; i ++) A[i] = A[i - 1] + read();
int h = 1, t = 1;
for(int i = 1; i <= n; i ++)
{
while(h < t && xl(q[h], q[h + 1]) > 2 * a * A[i]) h ++;
f[i] = f[q[h]] + a * pf(A[i] - A[q[h]]) + b * (A[i] - A[q[h]]) + c;
while(h < t && xl(q[t], i) > xl(q[t - 1], q[t])) t --;
q[++ t] = i;
}
cout << f[n] << endl;
return 0;
}
参考:
https://www.cnblogs.com/terribleterrible/p/9669614.html
作者: 北烛青澜
出处:https://www.cnblogs.com/Multitree/p/17829216.html
本站使用「CC BY 4.0」创作共享协议,转载请在文章明显位置注明作者及出处。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战