算法学习笔记(2):与斜率优化共舞

斜率优化 DP

对于像如下这样的 dp 方程,我们可以使用斜率优化解决。

dpi=min/max{ai×bj+ci+dj}


例题:P2900 Land Acquisition G

显然,如果 wiwjlilj,那么土地 j 可以直接被土地 i 并购。

考虑将所有土地按 wi 降序排序,wi 相同的按 li 降序排序,再通过双指针保留不能被其它土地并购的土地。

注意到此时被保留下的土地,wi 满足 单调递减li 满足 单调递增

dpi 表示将第 1i 块土地并购的最小代价,不难列出 dp 方程:

dpi=min0j<i{dpj+wj+1×li}

上面的方程复杂度为 O(n2),然而 n5×104,我们需要想办法优化。

先将 min 去掉,再把 wj+1×li 移到左边,得:

wj+1×li+dpi=dpj

将所有能够转移到 ij 视为一个点 (wj+1,dpj),那么问题就转化成了:

  • 有一条斜率为 li 的直线,这条直线需要经过上述的这些点中的一个或多个,并且希望它的截距最小。

上面这句话有点抽象,不妨将 原方程 与 一次函数 做一个对比,发现它们确实十分相似:

wj+1×li+dpi=dpj

kx+b=y

本质就是我们拿着一条斜率为 li 的直线,从下往上靠(增加它的截距),直到某一时刻这条线经过了我们维护的一个或多个点,停下来,此时截距(dpi)一定是最小的。

仔细观察可以发现,无论斜率 (li) 是多少,有些点一定不会被第一次经过(如下图灰色点)。将这些点去除,剩余的点恰好形成了一个右下凸壳:

不妨先来维护这个凸壳,假设现在加入点 C(wi+1,dpi),考虑凸壳上最新的两个点 AB,只可能有以下两种情况:

  • C 点可以直接加入凸壳。

记点 X 与点 Y 所连成的直线斜率为 slope(X,Y),则 C 能直接加入凸壳当且仅当 slope(A,B)slope(B,C)

  • C 点加入后无法形成凸壳。

这时候一定有 slope(A,B)slope(B,C),故需要将 B 弹出凸壳,在拿剩余凸壳最新的两个点与 C 作比较。

由于这些点的 x 单调递增,可以使用类似单调栈的方法维护,时间复杂度 O(n)

接下来就是要在维护出的凸壳上找到最优决策点,随便画一张图,由于这些直线的斜率一定不断增加,所以最优决策点一定不断向凸壳上方移动:

进一步观察,可以发现,如果凸壳上相邻的两点 AB 满足 slope(A,B)li,那么点 B 永远在 i 以后一定 不会成为最优决策点。不妨把原来维护凸壳的单调栈换成单调队列,通过凸壳最前面两点的斜率判断是否弹出队头。最终,队头一定是当前的最优决策点。

时间复杂度 O(n)(本题由于要排序所以总复杂度应该为 O(nlogn),这里只计算了 dp 的复杂度)。代码如下:

#include <bits/stdc++.h>
using namespace std;
int n, pos, q[50005], h, t;
long long dp[50005];
struct land
{
int w, l;
bool operator<(const land t) const
{
return w == t.w ? l > t.l : w > t.w;
}
} p[50005];
double X(int j)
{
return -p[j + 1].w;
}
double Y(int j)
{
return dp[j];
}
double slope(int x, int y)
{
return (Y(x) - Y(y)) / (X(x) - X(y));
}
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%d%d", &p[i].w, &p[i].l);
sort(p + 1, p + n + 1);
for (int i = 1; i <= n; i++)
if (p[i].l > p[pos].l)
p[++pos] = p[i];
for (int i = 1; i <= pos; i++)
{
while (h < t && slope(q[h], q[h + 1]) <= p[i].l)
h++;
dp[i] = dp[q[h]] + p[q[h] + 1].w * 1LL * p[i].l;
while (h < t && slope(q[t - 1], q[t]) >= slope(q[t], i))
t--;
q[++t] = i;
}
printf("%lld", dp[pos]);
return 0;
}

例题:P3195 玩具装箱

si=j=1iCi,则可以列出 dp 方程:

dpi=min0j<i{dpj+(sisj+ij1L)2}

ai=si+i1L,bi=sj+j,原方程变为

dpi=min0j<i{dpj+(aibj)2}

dpi=min0j<i{dpj2aibj+bj2}+ai2

min 及其以外得东西去掉,得

dpi=dpj2aibj+bj2

化为直线的形式

2aibj+dpi=dpj+bj2

类似与之前的形式,把每个 j 视作点 (bj,dpj+bj2),每次直线的斜率为 2ai,做斜率优化即可。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int n, L, C, h, t, q[50005];
ll s[50005], dp[50005];
double X(int j)
{
return s[j] + j;
}
double Y(int j)
{
return (s[j] + j) * (s[j] + j) + dp[j];
}
double slope(int i, int j)
{
return (Y(i) - Y(j)) / (X(i) - X(j));
}
int main()
{
scanf("%d%d", &n, &L);
for (int i = 1; i <= n; i++)
{
scanf("%d", &C);
s[i] = s[i - 1] + C;
}
h = t = 1;
for (int i = 1; i <= n; i++)
{
while (h < t && slope(q[h], q[h + 1]) <= 2 * (s[i] + i - L - 1))
h++;
dp[i] = dp[q[h]] + sq(s[i] - s[q[h]] + i - q[h] - 1 - L);
while (h < t && slope(q[t - 1], q[t]) >= slope(q[t], i))
t--;
q[++t] = i;
}
printf("%lld", dp[n]);
return 0;
}

例题:P5785 任务安排(弱化版 P2365 任务安排

至于方程是如何提前计算贡献的我就不细说了,记 sumTi=j=1iTi,sumCi=j=1iCi,可列出方程:

dpi=min{dpj+sumTi×(sumCisumCj)+s×(sumCnsumCj)}

展开整理成直线形式,即

(sumTi+s)×sumCj+dpi=dpj

每个 j 所代表的点:(sumCi,dpj),直线斜率 sumTi+s

正准备敲板子的你突然发现了这一条限制:

|Ti|28

也就是说

28Ti28

那么 sumTi+s 就不单调了,就不能一味地弹出队头了!

可凸壳上相邻两点地斜率还是单调的啊!那么只需要在凸壳上二分,找决策点即可。

时间复杂度 O(nlogn)

#include <bits/stdc++.h>
using namespace std;
long long n, s, sumT[300005], sumC[300005], dp[300005], top, stk[300005];
long long X(int j)
{
return sumC[j];
}
long long long Y(int j)
{
return dp[j];
}
int main()
{
scanf("%lld%lld", &n, &s);
for (int i = 1; i <= n; i++)
{
long long t, f;
scanf("%lld%lld", &t, &f);
sumT[i] = sumT[i - 1] + t;
sumC[i] = sumC[i - 1] + f;
}
stk[++top] = 0;
for (int i = 1; i <= n; i++)
{
int l = 1, r = top - 1, pos = stk[top];
while (l <= r)
{
int mid = (l + r) >> 1;
if (Y(stk[mid + 1]) - Y(stk[mid]) > (sumT[i] + s) * (X(stk[mid + 1]) - X(stk[mid])))
pos = stk[mid], r = mid - 1;
else
l = mid + 1;
}
dp[i] = dp[pos] + sumT[i] * (sumC[i] - sumC[pos]) + s * (sumC[n] - sumC[pos]);
while (top > 1 && (Y(stk[top]) - Y(stk[top - 1])) * (X(i) - X(stk[top])) >= (Y(i) - Y(stk[top])) * (X(stk[top]) - X(stk[top - 1])))
top--;
stk[++top] = i;
}
printf("%lld", dp[n]);
return 0;
}

注:斜率优化比较时建议移项,把除法化为乘法;如果使用 double 类型的 slope 很可能会产生精度误差(本题就卡了。


斜率优化小结:

  • 列出方程,先通过一些简单的代换化简成直线形式。

  • 通过 min/max 以及 XY 坐标的单调性判断凸壳方向。

  • 如果直线斜率不单调,使用二分维护。

  • 如果 Y 坐标不单调,可以证明对最终凸壳没有影响。

  • 如果 X 坐标不单调,可以考虑 CDQ 分治解决,时间复杂度 O(nlog2n)

posted @   CodingShark  阅读(224)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
点击右上角即可分享
微信分享提示