然而我dp废的一比
用例题开讲:
P2365 任务安排
明显看题解可知,dp方程为:
$f_i=min(f_j+sumt_i*(sumc_i-sumc_j)+s*(sumc_n-sunc_j),f_i)$
故标程如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| #include <bits/stdc++.h> using namespace std; const int N = 5000 + 5; struct Sum { int t, c; } sum[N]; int n, s; int f[N]; int main() { scanf("%d%d", &n, &s); for (int i = 1; i <= n; ++i) { int t, c; scanf("%d%d", &t, &c); sum[i].t = sum[i - 1].t + t; sum[i].c = sum[i - 1].c + c; } memset(f, 0x3f, sizeof f); f[0] = 0; for (int i = 1; i <= n; ++i) for (int j = 0; j < i; ++j) f[i] = min(f[i], f[j] + sum[i].t * (sum[i].c - sum[j].c) + s * (sum[n].c - sum[j].c)); printf("%d", f[n]); return 0; }
|
那么,康康这个——P5785 SDOI2012任务安排
令人惋惜的是,在数据加强后, $O(n^2)$ 的时间复杂度似乎确凿过不了,为此,我们看题解后想到救星——
斜率优化
我们先假设所有的 $t$ 都大于0。
我们来康康转移方程:
$f_i=min(f_j+sumt_i*(sumc_i-sumc_j)+s*(sumc_n-sunc_j),f_i)$
不妨去掉 $min$ 看成:
$f_i=f_j+sumt_i*(sumc_i-sumc_j)+s*(sumc_n-sunc_j)$
它等价于:
$f_i=f_j-(sumt_i+s) * sumc_j+sumt_i* sumc_i+s* sumc_n$
设 $f_j=y,sumc_j=x$ (这是所有的关于 $j$ 的变量),然后化为直线表达式 $y=kx+b$ 的形式:
$f_j=(sumt_i+s)* sumc_j+f_i-sumt_i* sumc_i-s* umc_n$
不难发现 :
$k=sumt_i+s$ ,故 $0<k<\infty$ 。
且 $0 \le j \le i-1,j \in Z$
而直线上的点为:$(f_0,sumc_0)$ 、 $(f_1,sumc_1)$ 、 … 、 $(f_{i-1},sunc_{i-1})$
我们的目标是让 $f_i$ 最小
我们带着目标,看看图像:
对于一个已知的 $i$ , $k=sumt_i+s$ 是固定的,而若点 $(x_0,y_0)$在 $y=kx+b$ 上,那么截距 $b$ 是可以算出来的,又因 $b=f_i-sumt_i* sumc_i-s* umc_n$ ,故 $b$ 最小时 $f_i$ 最小。
那么,我们康康下图:
图中绿色的点是所有可能的 $(x_0,y_0)$ ,红色的线是 $y=kx+b$ (只有 $k$ 确定,所以在从下往上平移)。
不难发现,凸包(绿线)内部的点对于 $b$ 的最小值毫无意义。换句话说,最小值的点只会在凸包上。
那么在凸包上哪一点呢?
还是上图,令构成凸包的三条直线斜率为 $k_1,k_2,k_3$ ,由凸包性质可得 $k_1<k_2<k_3$ 。再看看上图中我们要找的那个点(不妨设它为点 $A$ ), $A$ 所在的两条直线斜率为 $k_2,k_3$ ,且 $k_2<k<k_3$ 也就是说,对于一个给定斜率为的直线 $y=kx+b$ 让它的截距 $b$ 取得最小值的点就是凸包上第一个斜率大于 $k$ 的线段的下端点。
由此,我们想到在单调队列中维护第一个大于某个数的点。
我们又发现,因为 $t$ 大于0,所以:
斜率是单调递增的,且新加的点的横坐标也单调递增。
所以:
在查询的时候,可以将队头小于当前斜率的点全删除掉。
在插入的时候,可以将队尾所有不在凸包上的点全删除掉。
我们便得到以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| #include <bits/stdc++.h> using namespace std; const int N = 300000 + 5; typedef long long ll; struct Sum { ll t, c; } sum[N]; ll n, s; ll f[N]; ll q[N], hh = 0, tt = 0;
bool check_slopeh(ll x, ll y, ll o) { return (f[y] - sum[y].c * s - f[x] + sum[x].c * s) <= sum[o].t * (sum[y].c - sum[x].c); } bool check_slopet(ll x, ll y, ll a, ll b) { return (f[y] - sum[y].c - f[x] + sum[x].c) * (sum[b].c - sum[a].c) >= (f[b] - sum[b].c - f[a] + sum[a].c) * (sum[y].c - sum[x].c); } int main() { scanf("%lld%lld", &n, &s); for (ll i = 1; i <= n; ++i) { ll t, c; scanf("%lld%lld", &t, &c); sum[i].t = sum[i - 1].t + t; sum[i].c = sum[i - 1].c + c; } memset(f, 0x7f, sizeof f); f[0] = 0; q[0] = 0; for (ll i = 1; i <= n; ++i) { while (hh < tt && check_slopeh(q[hh], q[hh + 1], i)) hh++; ll j = q[hh]; f[i] = f[j] + sum[i].t * (sum[i].c - sum[j].c) + s * (sum[n].c - sum[j].c); while (hh < tt && check_slopet(q[tt - 1], q[tt], q[tt], i)) tt--; q[++tt] = i; } printf("%lld", f[n]); return 0; }
|
但是——60分!哦可恶!题目中的 $t$ 可以为负数,咋办?
二分
我们来看看 $t$ 可以为负数的情况:
$k=sumt_i+s$ ,但 $sumt_i$ 可能小于0,故 $ -\infty <k< \infty$ 。
$0 \le j \le i-1,j \in Z$
而直线上的点为:$(f_0,sumc_0)$ 、 $(f_1,sumc_1)$ 、 … 、 $(f_{i-1},sunc_{i-1})$
我们的目标是让 $f_i$ 最小
其实只有 $k$ 的范围变了, $k$ 可以小于0了,所以:
斜率不再具有单调性,但新加的点的横坐标仍然单调递增。
所以:
在查询的时候,只能用二分来查找了。
但在插入的时候,仍可以将队尾所有不在凸包上的点全删除掉。
终于,正解如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| #include <bits/stdc++.h> using namespace std; const int N = 300000 + 5; typedef long long ll; struct Sum { ll t, c; } sum[N]; ll n, s; ll f[N]; ll q[N], hh = 0, tt = 0;
bool check_slopeh(ll x, ll y, ll o) { return (f[y] - sum[y].c * s - f[x] + sum[x].c * s) <= sum[o].t * (sum[y].c - sum[x].c); } bool check_slopet(ll x, ll y, ll a, ll b) { return (f[y] - sum[y].c - f[x] + sum[x].c) * (sum[b].c - sum[a].c) >= (f[b] - sum[b].c - f[a] + sum[a].c) * (sum[y].c - sum[x].c); } ll work(ll o) { ll l = hh, r = tt; int ans = l; while (l <= r) { ll mid = l + r >> 1; if (!check_slopeh(q[mid], q[mid + 1], o)) r = mid - 1, ans = mid; else l = mid + 1; } return q[ans]; } int main() { scanf("%lld%lld", &n, &s); for (ll i = 1; i <= n; ++i) { ll t, c; scanf("%lld%lld", &t, &c); sum[i].t = sum[i - 1].t + t; sum[i].c = sum[i - 1].c + c; } memset(f, 0x7f, sizeof f); f[0] = 0; q[0] = 0; for (ll i = 1; i <= n; ++i) { ll j = work(i); f[i] = f[j] + sum[i].t * (sum[i].c - sum[j].c) + s * (sum[n].c - sum[j].c); while (hh < tt && check_slopet(q[tt - 1], q[tt], q[tt], i)) tt--; q[++tt] = i; } printf("%lld", f[n]); return 0; }
|