最近瞎搞了搞斜率优化...
斜率优化DP是一种优化DP的思想,通过高中数学线性规划那套理论加上凸包那套理论单调的优良性质将O(N^2)的DP枚举转移优化成O(N)的转移
先来一道简单题:[APIO2010]特别行动队
我们设\(s_i\)是前缀和数组,设\(f_i\)为前\(i\)个巨佬的战斗力和最大值,不难列出状态转移方程:
\[f_i=\max(f_j+a(s_i-s_j)^2+b(s_i-s_j)+c)\]
把式子拆开得到
\[f_i=f_j+as_i^2-2as_is_j+as_j^2+bs_i-bs_j+c\]
移项、合并项
\[as_j^2+f_j=(2as_i+b)s_j+(f_i-as_i^2-bs_i-c)\]
如果我们将\((s_j,as_j^2+f_j)\)当做笛卡尔平面上的点,那么\(f_i\)的最大值就是以\(2as_i+b\)为斜率、经过这些点截距的最大值加上\(as_i^2+bs_i+c\)
根据题意,\(s_j\)是单调递增的,\(2as_i+b\)是单调递减的,所以我们可以维护上凸壳,根据转移中斜率的单调性来在凸壳上确定转移点。由于是上凸壳,并且斜率单调递减,所以转移点是单调递增的,即我们可以单调地维护赚一点。转移后再将新的点加入上凸壳即可。时间复杂度为\(O(n)\)。由于坐标都是整点,需要通过叉积判凸壳,一般都要开long long,这里偷懒直接define int long long下面是乱七八糟的代码/
#include#define int long longusing namespace std;int n, a, b, c, s[1000010];int f[1000010];int x[1000010], y[1000010], p, tot;signed main(){ scanf("%lld", &n); scanf("%lld%lld%lld", &a, &b, &c); for (int i = 1; i <= n; i++) scanf("%lld", &s[i]), s[i] += s[i - 1]; tot = 1, p = 1; for (int i = 1; i <= n; i++) { int k = 2 * a * s[i] + b; while (p < tot && y[p + 1] - k * x[p + 1] >= y[p] - k * x[p]) p++; f[i] = y[p] - k * x[p] + a * s[i] * s[i] + b * s[i] + c; int newx = s[i], newy = a * s[i] * s[i] + f[i]; while (tot > 1 && (newx - x[tot]) * (y[tot] - y[tot - 1]) - (newy - y[tot]) * (x[tot] - x[tot - 1]) <= 0) tot--; if (p > tot) p = tot; x[++tot] = newx; y[tot] = newy; } printf("%lld\n", f[n]); return 0;}
练习:玩具装箱toy
再来看一道题:
假设土地A的面积可以被土地B的面积完全包含,由于需要购买所有土地,所以我们可以只购买B(A跟着一起购买了)。我们把每个土地面积当做二维平面上的整点(x,y),则对于所有存在偏序关系的点都可以被删除。我们可以按x排序,第二维通过单调栈来清除所有存在偏序关系的点,这样就剩下了满足x单增、y单减的点。
FJ每次选土地一定是选一段连续的区间更优:假设某一段区间内有土地不选,则显然该土地在这段区间内选择不会比不在区间内选择差,所以我们还是要选择连续的土地一起买。
我们可以设状态了,设\(f_i\)为购买前\(i\)块土地最小值,则
\[f_i=\min(f_j+y[j+1]*x[i])\]
显然我们可以通过斜率优化,稍微推一下式子即可,这里就不啰嗦了,下面是乱七八糟的代码
#include#define int long longusing namespace std;struct fuck{ int x, y;} a[50010];int n, tot, f[50010];int x[50010], y[50010];signed main(){ scanf("%lld", &tot); for (int i = 1; i <= tot; i++) scanf("%lld%lld", &a[i].x, &a[i].y); sort(a + 1, a + 1 + tot, [](const fuck &a, const fuck &b) {return a.x < b.x;}); for (int i = 1; i <= tot; i++) { while (n > 0 && a[n].y <= a[i].y) n--; a[++n] = a[i]; } tot = 1; x[1] = a[1].y, y[1] = 0; int p = 1; for (int i = 1; i <= n; i++) { if (p > tot) p = tot; while (p < tot && a[i].x * x[p + 1] + y[p + 1] < a[i].x * x[p] + y[p]) p++; f[i] = a[i].x * x[p] + y[p]; while (tot > 1 && (x[tot] - x[tot - 1]) * (f[i] - y[tot]) - (y[tot] - y[tot - 1]) * (a[i + 1].y - x[tot]) > 0) tot--; y[++tot] = f[i]; x[tot] = a[i + 1].y; } printf("%lld\n", f[n]); return 0;}
时间复杂度\(O(n\log n)\)(因为有个排序),注意DP过程的时间复杂度是\(O(n)\)的。
斜率优化DP最主要就是先写暴力,然后推式子,推出类似一次函数解析式形式,然后稍微分析下凸包情况即可
注意开long long,有必要可以开double。