斜率优化学习笔记

看到出现了可以写题解的 P10979 任务安排 2,于是写学习笔记了。


斜率优化模板题,有三倍经验,难度逐渐递增,建议从前做到后。

P2365 任务安排P10979 任务安排 2P5785 [SDOI2012] 任务安排

(但是我这种做法 P10979 和 P5785 没有区别。

思路:

fi 表示第 i 个任务加工后所需的最小总费用,那么就有转移式。

fi=minj=0i1{fj+k=j+1i((l=1iTl+num×s)×Ck)}=minj=0i1{fj+(k=1iTk+num×s)×k=j+1iCk}

num 表示在这批任务完成之前分了多少批任务。

但是 num 的值关乎到前面转移的值,要再开一维吗?不需要,这里采用费用前置的思想。由于每分一组,num 就会多一,根据 k=1iTk+num×s,后面的每个任务 k 就会多贡献 s×Ck,那么转移式就变成了:

fi=minj=0i1{fj+k=1iTk×k=j+1iCk+k=j+1nCk×s}

观察到转移式的 都是可以用前缀和维护的,对 T,C 数组进行前缀和(为了方便书写,不加说明的 T,C 数组表示已经前缀和完了的数组),那么转移式为:

fi=minj=0i1{fj+Ti×(CiCj)+(CnCj)×s}

动态规划的转移参照 P2365 任务安排这篇题解


下面讲一个不太常用的斜率优化方法。

我们先把转移式做一些变化:

fi=minj=1i1{fjCj×(Ti+s)}+Ti×Ci+Cn×s

即把常数项提到 min 外,再合并同类项。

上述转移式,时间复杂度瓶颈是找到最优决策点,所以我们考虑两个决策点 j1,j2,考虑若决策点 j1 优于决策点 j2,那么:

fj1Cj1×(Ti+s)<fj2Cj2×(Ti+s)

(Cj2Cj1)(Ti+s)<fj2fj1

现在我们钦定 Cj2>Cj1注意不是 j2>j1,后面再说为什么),那么我们可以把 (Cj2Cj1) 除到不等式的右边,即:

fj2fj1Cj2Cj1>Ti+s

不等式的左边是不是很像斜率式?我们令 Pi 为二维平面直角坐标系的点 (Ci,fi),那么不等式的左边可以表示为 kPj1,Pj2 即点 Pj1 和点 Pj2 之间的斜率。

该不等式可以用文字描述为:若两点之间的斜率大于 Ti+s,那么左边的点更优,否则右边的点更优(等号取到的情况即为两个点一样优秀,选哪个无所谓,不做细分)、

我们现在考虑三个点的情况,若这三个点 A,B,C 围成一个上凸壳(假设左右顺序为 A,B,C),那么直线 AB,直线 BC 和斜率为 Ti+s 的直线有下面三种关系:

  • 直线 AB 和直线 BC 的斜率都大于 Ti+s,此时 A 点优于 B 点,B 点优于 C 点,B 点不是最优的,如图:
  • 直线 AB 的斜率大于 Ti+s,直线 BC 的斜率小于 Ti+s,此时 A 点优于 B 点,C 点优于 B 点,B 点不是最优的,请读者自行画图。
  • 直线 AB 和直线 BC 的斜率都小于 Ti+s,此时 B 点优于 A 点,C 点优于 B 点,B 点不是最优的,请读者自行画图。

综上,若三个点围成一个上凸壳,那么中间的那个点一定不是最优的。

现在考虑转移 i,根据上面的性质,我们可以将 i 之前的所有点维护一个下凸壳,由于两点之间的斜率大于 Ti+s 则左边的点更优,我们可以找到第一个 j,使得 PjPj+1 的斜率大于 Ti+s 的点,若没有这个点,那么凸壳的最后一个点最优,然后用这个点的来更新 i 的 dp 值,这个点可以通过二分找到。

这个点形象化的来说就是用斜率为 Ti+s 的直线来切这个凸包,切到的点就是最优决策点,如图:

其中 E 就是最优决策点。

然后考虑如何维护这个下凸壳,这个还是比较容易想到,假设在此之前已经维护好了之前所有点的下凸壳,需要加上该点。由于 Ci(不是前缀和数组)是非负的,那么 Ci 是单调的,即点的横坐标是单调的,所以我们可以直接在下凸壳的最后加点。若新加上该点,点集不构成下凸壳,那么将最后一个点去掉反复循环,直到新加上该点可以构成下凸壳,或者下凸壳中没有点了结束。根据这个算法,我们可以用一个单调栈维护下凸壳。

这道题就做完了。

对于这种做法来说,P10979 任务安排 2P5785 [SDOI2012] 任务安排没有区别,因为这两道题的 Ci 都是非负整数(不是前缀和数组),这就使每个点的横坐标是单调的,而这个算法没有限制 Ti(不是前缀和数组)的正负,即并不需要要求决策单调性。

做完这道题,我们分析一下什么题可以用斜率优化,首先他要求出一段区间的某个函数值的极值,而这个函数值中包含 ij 的交叉项,这样的题目大概率是斜率优化。


现在来讲为什么我们是要钦定 Cj2>Cj1 而不是钦定 j2>j1,有的同学可能会认为钦定 j2>j1,不就是和钦定 xj2>xj1xi 为点 i 的横坐标)一样的吗?为什么不行。对于比较简单的斜率优化题目来说,这样是可以的包括这道题,但是有一些题目的点的横坐标并不是单调的,就导致每次加点的时候就不能按照编号顺序一个一个加点,而是要按照点的横坐标顺序来加点,否则你的思路会十分混乱。对于这种题目我们可以用 cdq 分治或者用平衡树维护凸壳解决。

代码:

#include <bits/stdc++.h>

using namespace std;

const int kMaxN = 3e5 + 5;

int n, s, t[kMaxN], c[kMaxN], top, L, R, M;
long long f[kMaxN];

struct P {
  int x;
  long long y;
} stk[kMaxN];

long double slope(P i, P j) { return i.x == j.x ? (i.y > j.y ? -4e18 : 4e18) : (long double)(i.y - j.y) / (i.x - j.x); }

int main() {
  ios::sync_with_stdio(0), cin.tie(0);
  cin >> n >> s;
  for (int i = 1; i <= n; i++) {
    cin >> t[i] >> c[i], t[i] += t[i - 1], c[i] += c[i - 1];
  }
  for (int i = 1; i <= n; i++) {
    for (; top > 1 && slope(stk[top - 1], stk[top]) > slope(stk[top], {c[i - 1], f[i - 1]}); top--) {
    }
    stk[++top] = {c[i - 1], f[i - 1]}, L = 1, R = top, M = L + R >> 1;  // 加入点,维护凸壳
    for (; L < R; M = L + R >> 1) {
      (M == top ? 4e18 : slope(stk[M], stk[M + 1])) > s + t[i] ? R = M : L = M + 1;
    }  // 二分最优决策点
    f[i] = stk[L].y - 1LL * (s + t[i]) * stk[L].x + 1LL * s * c[n] + 1LL * c[i] * t[i];  // 转移
  }
  cout << f[n];
  return 0;
}

作为一篇斜率优化的学习笔记,还是讲一道点的横坐标不单调的例题吧(毕竟这种方法比较常见),例题:P4655 [CEOI2017] Building Bridges

思路:

fi 为建造了一座桥在 i 点的最小代价,siwi 的前缀和数组,那么 dp 式很显然:

fi=minj=0i1{fj+(hihj)2+si1sj}

同样将式子转化一下:

fi=minj=0i1{fj+hj22hihjsj}+hi2+si1

若决策点 j1 优于 j2,那么:

fj1+hj122hihj1sj1<fj2+hj222hihj2sj2

2hi(hj2hj1)<(fj2+hj22sj2)(fj1+hj12sj1)

我们令 gi=fi+hi2si,再钦定 hj2>hj1注意这里),则不等式化为:

gj2gj1hj2hj1>2hi

我们设点 Pi(hi,gi),这里的点的横坐标不单调,所以不能像上一题一样更新一次加一个点,这里考虑使用 cdq 分治。

我们先将所有的点按照横坐标排序,然后将所有的点分成两半(设中点为 mid),第一半的编号为 1mid,第二半的编号为 mid+1n,两半中间的点的横坐标是单调的,假设我们已经更新好了第一半的所有点的 dp 值,我们用前面的点构成的凸壳来更新第二半的 dp 值,然后再递归执行第二半即可。

简单讲一下这种做法的正确性,不需要用 cdq 分治的做法其实是用一个凸包来更新 dp 值,而用了 cdq 分治的做法其实是用 logn 个凸包来更新 当前的 dp 值,将 1i1 这个区间分成若干个小区间,用这些小区间的点构成的凸包分别更新该点的 dp 值,所以这种做法是正确的。

代码:

#include <bits/stdc++.h>

using namespace std;

const int kMaxN = 1e5 + 5;

int n, h[kMaxN], top, L, R, M;
long long s[kMaxN], f[kMaxN];

struct P {
  int i, x;
  long long y;
} stk[kMaxN], a[kMaxN];

long double slope(P i, P j) { return i.x == j.x ? (i.y > j.y ? -4e18 : 4e18) : (long double)(i.y - j.y) / (i.x - j.x); }

void S(int l, int r) {
  if (l == r) {
    a[l].y = f[a[l].i] + 1LL * a[l].x * a[l].x - s[a[l].i];
    return;
  }
  int mid = l + r >> 1;
  stable_partition(a + l, a + r + 1, [&](P p) { return p.i <= mid; });  // 按照编号分成两半
  S(l, mid), top = 0;                                                   // 算出前一半的 dp 值
  for (int i = l; i <= mid; i++) {
    for (; top > 1 && slope(stk[top - 1], stk[top]) >= slope(stk[top], a[i]); top--) {
    }
    stk[++top] = a[i];
  }  // 构成凸壳
  for (int i = mid + 1; i <= r; i++) {
    L = 1, R = top, M = L + R >> 1;
    for (; L < R; M = L + R >> 1) {
      (M == top ? 4e18 : slope(stk[M], stk[M + 1])) > 2 * h[a[i].i] ? R = M : L = M + 1;
    }
    f[a[i].i] = min(f[a[i].i], stk[L].y - 2LL * h[a[i].i] * stk[L].x + 1LL * h[a[i].i] * h[a[i].i] + s[a[i].i - 1]);
  }  // 转移
  S(mid + 1, r);                                                                     // 继续递归
  inplace_merge(a + l, a + mid + 1, a + r + 1, [](P i, P j) { return i.x < j.x; });  // 合并
}

int main() {
  ios::sync_with_stdio(0), cin.tie(0);
  cin >> n;
  for (int i = 1; i <= n; i++) {
    cin >> h[i];
  }
  memset(f, 0x3f, sizeof(f)), f[1] = 0;
  for (int i = 1; i <= n; i++) {
    cin >> s[i], s[i] += s[i - 1], a[i] = {i, h[i], 0};
  }
  sort(a + 1, a + 1 + n, [](P i, P j) { return i.x < j.x; });
  S(1, n), cout << f[n];
  return 0;
}
posted @   liruixiong0101  阅读(17)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示