作为数学渣,先复习一下已知两点(x1,y1), (x2,y2),怎么求过两点的一次函数的斜率…
待定系数法代入 y=kx+b 有:
x1k+b=y1
x2k+b=y2
两式相减有:
k=y2−y1x2−x1
故事围绕着《算法竞赛进阶指南》的三一道例题展开:
引子
发现一个关键性质:
假如我们启动了一个任务[l,r],那么它会对后面造成S∗∑ni=r+1Ci的费用。
所以我们可以使用费用提前计算的方式优化算法:
设st为 t 的前缀和,设sum为C的前缀和
f[i] 表示安排完前 i 个任务的最小花费:
f[i]=min(f[j]+(sum[i]−sum[j])∗t[i]+(sum[n]−sum[j])∗S)
时间复杂度O(N2)
情况1. 斜率、横坐标皆单调递增
将上题推出的转移式子得min去掉观察:
f[i]=f[j]+(sum[i]−sum[j])∗t[i]+(sum[n]−sum[j])∗S
发现我们无法优化dp的原因是有与 i,j 两者都有关的乘积项,导致我们没有最优策略:
−sum[j]∗t[i]
斜率优化
考虑把这个式子拆开转换为一次函数:y=kx+b 的形式。
- 将与 i, j 都有关系的乘积项作为 kx,其中与 i 有关的作为 k,与 j 有关的作为 x
- 将只与 j 有关系的值作为 y
- 其余的当做 b
则以上式子可以化成:
f[j]_y=(t[i]+S)_k∗sum[j]_x+f[i]−sum[i]∗t[i]−sum[n]∗S_b
发现当 i 确定后,该一次函数的斜率 k 确定,则截距 b 越小, f[i] 越小。
我们将 (x,y) 即 (sum[j],f[j]) 放在坐标系上。
则形象化可理解为一条直线从下往上平移,所碰到的第一个点即为最优解。
发现一个点如果被另外两个点围起来,永远不可能作为最优解。
删除了这些点后,发现相邻点之间的斜率为单调递增的,即构成一个凸包:
发现一个斜率 k 固定的直线所匹配的最优点满足:
- 其右边的斜率都 >k
- 其左边的斜率都 <k
由于这道题斜率 t[i]+S、横坐标 sum[j] 皆单调递增。
-
由于横坐标递增,所以维护凸包时,每当加入一个点时:
-
若上面两个点构成的斜率大于这个点和上一个点的斜率,即不满足单调性,可以弹出队尾。即:
yq[tt]−yq[tt−1]xq[tt]−xq[tt−1]>=yi−yq[tt]xi−xq[tt]
- 由于斜率递增,所以 i+1 的最优解一定在 i 的右边,所以一旦队头两个点构成的斜率 < 当前的斜率,可以弹出队头。即满足:
yq[hh+1]−yq[hh]xq[hh+1]−xq[hh]<t[i]+S
然后队头的元素即为最优选择。
时间复杂度 O(N)
Tips:
- 由于除法会有精度问题,可以通过交叉相乘的形式比较大小
#include <cstring>
#include <cstdio>
#include <iostream>
#define x(a) (c[a])
#define y(a) (f[a])
#define k(a) (t[a] + S)
using namespace std;
typedef long long LL;
const int N = 300005;
int n, S;
LL t[N], c[N], q[N], f[N];
int main() {
scanf("%d%d", &n, &S);
for (int i = 1; i <= n; i++) scanf("%lld%lld", t + i, c + i);
for (int i = 1; i <= n; i++) t[i] += t[i - 1], c[i] += c[i - 1];
int hh = 0, tt = 0;
q[0] = 0;
for (int i = 1; i <= n; i++) {
while(hh < tt && (y(q[hh + 1]) - y(q[hh])) <= ((t[i] + S) * (x(q[hh + 1]) - x(q[hh])))) hh++;
f[i] = f[q[hh]] + (c[i] - c[q[hh]]) * t[i] + (c[n] - c[q[hh]]) * S;
while(hh < tt && ((y(q[tt]) - y(q[tt - 1])) * (x(i) - x(q[tt])) >= ((y(i) - y(q[tt])) * (x(q[tt]) - x(q[tt - 1]))))) tt--;
q[++tt] = i;
}
printf("%lld\n", f[n]);
return 0;
}
情况2. 横坐标单调递增
此时的斜率不再递增了,也就是我们不能popfront了,不过我们仍可以维护凸包,然后保持单调性,二分。
时间复杂度O(Nlog2N)
#include <cstring>
#include <cstdio>
#include <iostream>
#define x(a) (c[a])
#define y(a) (f[a])
#define k(a) (t[a] + S)
using namespace std;
typedef long long LL;
const int N = 300005;
int n, S, t[N], c[N], q[N];
LL f[N];
int main() {
scanf("%d%d", &n, &S);
for (int i = 1; i <= n; i++) scanf("%lld%lld", t + i, c + i);
for (int i = 1; i <= n; i++) t[i] += t[i - 1], c[i] += c[i - 1];
int hh = 0, tt = 0;
q[0] = 0;
for (int i = 1; i <= n; i++) {
int l = hh, r = tt;
while(l < r) {
int mid = (l + r) >> 1;
if((y(q[mid + 1]) - y(q[mid])) >= ((LL)k(i) * (x(q[mid + 1]) - x(q[mid])))) r = mid;
else l = mid + 1;
}
f[i] = f[q[r]] + (LL)(c[i] - c[q[r]]) * t[i] + (LL)(c[n] - c[q[r]]) * S;
while(hh < tt && ((y(q[tt]) - y(q[tt - 1])) * (x(i) - x(q[tt])) >= ((y(i) - y(q[tt])) * (x(q[tt]) - x(q[tt - 1]))))) tt--;
q[++tt] = i;
}
printf("%lld\n", f[n]);
return 0;
}
例题
设 d[i] 为从 1 走到 i 的距离。
那么每条小猫最佳的出发时间应为 a[i]=t[i]−d[h[i]],如果要接上这只猫,必须大于这个时间出发。
我们将 a 数组排序,那么问题等价转换于把 m 个点划分成 p 个连续区间,使每一段到右端点的距离之和的总和最小。(内心 OS:这不就是摆渡车的变种吗?)
那么朴素 dp 便很好列出了:
f[k][i] 表示将前 i 只小猫分成 k 组的最小总和。
设 sumA 为 a 数组的前缀和。
f[k][i]=min(f[k−1][j]+a[i]∗(i−j)−sumA[i]+sumA[j])(0<=j<i)
由于这里面有一个非常讨厌的 a[i]∗−j,所以我们考虑斜率优化:
f[k−1][j]+sumA[j]_y=a[i]_k∗j_x+f[k][i]−a[i]∗i+sumA[i]_b
发现这里的横坐标、斜率都是单调递增,即情况 1。那么我们可以将不需要的直接踢出即可。
时间复杂度 O(PM)
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long LL;
const int N = 100005, S = 105;
int n, m, P, d[N], a[N], q[N];
LL f[S][N], sum[N];
LL inline y(int i, int k) {
return f[k - 1][i] + sum[i];
}
int main() {
memset(f, 0x3f, sizeof f);
scanf("%d%d%d", &n, &m, &P);
for (int i = 0; i <= P; i++) f[i][0] = 0;
for (int i = 2; i <= n; i++)
scanf("%d", d + i), d[i] += d[i - 1];
for (int i = 1, h, t; i <= m; i++) {
scanf("%d%d", &h, &t); a[i] = t - d[h];
}
sort(a + 1, a + 1 + m);
for (int i = 1; i <= m; i++) sum[i] = sum[i - 1] + a[i];
for (int k = 1; k <= P; k++) {
int hh = 0, tt = 0;
for (int i = 1; i <= m; i++) {
while (hh < tt && (y(q[hh + 1], k) - y(q[hh], k)) <= (LL)a[i] * (q[hh + 1] - q[hh])) hh++;
f[k][i] = f[k - 1][q[hh]] + (LL)a[i] * (i - q[hh]) - (sum[i] - sum[q[hh]]);
while (hh < tt && (y(q[tt], k) - y(q[tt - 1], k)) * (i - q[tt]) >= (y(i, k) - y(q[tt], k)) * (q[tt] - q[tt - 1])) tt--;
q[++tt] = i;
}
}
printf("%lld\n", f[P][m]);
}
Orz%%%
太强了orz
怎么没有y总的评论orz讲的太好了,
不过能不能修一下latexOrz
讲的真的很好
TQL ORZ
斜率和横坐标在T2那里是不是写反了啊qaq
k=sum[j],x=t[i]+S
刚看见,没有吧,不就是把和 j 相关的看成一个点吗 emm
TQL, ORZ
TQL ORZ
STO
stOrz