斜率优化dp

HNOI2008 玩具装箱
这个题很容易写出一维的状态和 \(\mathcal O(n^2)\) 的转移。
\(f[i]\) 表示前 \(i\) 个数里在 \(i\) 这里分一段需要多少费用。
\(f[i] = \displaystyle\max_{j < i}\{f[j] + (sum[i] - sum[j] + i - j - 1 - l)^2\}\)。
其中 \(sum\) 表示 \(c\) 的前缀和。
\(How\ to\) 优化?
我们令 \(s[i] = sum[i] + i, t[j] = sum[j] + j + l + 1\)。
也就是说 \(f[i] = \displaystyle\max_{j < i}\{f[j] + (s[i] - t[j])^2\}\)。
假设我们选取了某一个 \(j\) 进行转移。
那么 \(f[i] = f[j] + s[i]^2 + t[j]^2 - 2s[i]t[j]\)。
也就是 \(f[j] + t[j]^2 = 2s[i]t[j] + f[i] - s[i]^2\)。
也就是说 \((t[j], f[j] + t[j]^2)\) 在 \(y = 2s[i]x + f[i] - s[i]^2\) 上。
\(f[i] - s[i]^2\) 是这条直线的 \(y\) 轴截距。
我们将 \((t[j], f[j] + t[j]^2)\) 称为一个决策点。
而 \(s[i]^2\) 是个定值,所以我们如果想让 \(f[i]\) 最小的话就是让 \(y\) 轴截距最小。
那么可能进行转移的决策点们一定构成一个一个下凸壳。
因为 \(t[j]\) 是单调递增的,所以我们可以用单调队列维护这个下凸壳。
而我们对于一个 \(i\) 要找的就是斜率为 \(2s[i]\) 的直线与该下凸壳的切点。
又因为 \(s[i]\) 也是单调递增的,所以我们的决策具有单调性,直接在单调队列里不断删掉队首直到找到满足条件的决策点即可。
复杂度 \(\mathcal O(n)\)
点击查看代码
#include <iostream>
using std::cin;
using std::cout;
const int N = 5e4 + 10;
typedef long long ll;
typedef long double ld;
int c[N];
int q[N];
ld s[N];
ld t[N];
ld f[N];
ld sum[N];
ld X(int x)
{
return t[x];
}
ld Y(int x)
{
return f[x] + t[x] * t[x];
}
ld slope(int x, int y)
{
return (Y(y) - Y(x)) / (X(y) - X(x));
}
int main()
{
int n;
int l;
cin >> n >> l;
for (int i = 1; i <= n; ++i)
cin >> c[i];
for (int i = 1; i <= n; ++i)
sum[i] = sum[i - 1] + c[i];
for (int i = 0; i <= n; ++i)
{
s[i] = i + sum[i];
t[i] = i + sum[i] + 1 + l;
}
int hd = 1, tl = 0;
q[++tl] = 0;
for (int i = 1; i <= n; ++i)
{
while (hd < tl && slope(q[hd], q[hd + 1]) <= s[i] * 2)
++hd;
f[i] = f[q[hd]] + (s[i] - t[q[hd]]) * (s[i] - t[q[hd]]);
while (hd < tl && slope(q[tl - 1], q[tl]) >= slope(q[tl - 1], i))
tl--;
q[++tl] = i;
}
cout << (ll)f[n] << '\n';
return 0;
}
注意到我这里是用的斜率来维护下凸壳,但是实际上用向量的叉积可以避免损失精度。
这种情况称为 \(k\) 单调, \(x\) 单调 的斜率优化。
对于 \(x\) 单调,\(k\) 不单调,如这道题:
SDOI2012 任务安排
我们依旧用单调队列维护下凸壳,但是我们不删点,而是每一次要转移的时候在下凸壳上二分即可。
时间复杂度 \(\mathcal O(n\log n)\)。
点击查看代码
#include <iostream>
using std::cin;
using std::cout;
const int N = 3e5 + 10;
typedef long long ll;
typedef __int128 i1;
int s;
int q[N];
ll f[N];
ll sumt[N];
ll sumc[N];
i1 X(int x)
{
return sumc[x];
}
i1 Y(int x)
{
return f[x] - sumc[x] * s;
}
i1 get(int a, int b, int c)
{
// a -> b, a -> c
// x2 * y1 - x1 * y2;
return (X(c) - X(a)) * (Y(b) - Y(a)) - (X(b) - X(a)) * (Y(c) - Y(a));
}
int qie(int l, int r, ll k)
{
int ans = -1;
while (l <= r)
{
int mid = (l + r) >> 1;
if ((Y(q[mid + 1]) - Y(q[mid])) >= (X(q[mid + 1]) - X(q[mid])) * k)
{
ans = mid;
r = mid - 1;
}
else
l = mid + 1;
}
if (ans == -1)
return r + 1;
else
return ans;
}
int main()
{
int n;
cin >> n >> s;
for (int i = 1; i <= n; ++i)
{
int t, c;
cin >> t >> c;
sumt[i] = sumt[i - 1] + t;
sumc[i] = sumc[i - 1] + c;
}
int hd = 1, tl = 0;
q[++tl] = 0;
for (int i = 1; i <= n; ++i)
{
int j = q[qie(hd, tl - 1, sumt[i])];
f[i] = f[j] + (sumc[i] - sumc[j]) * sumt[i] + (sumc[n] - sumc[j]) * s;
while (hd < tl && get(q[tl - 1], q[tl], i) >= 0)
tl--;
q[++tl] = i;
}
cout << f[n] << '\n';
return 0;
}
注意到我写的并不是向量的叉积,而是其相反数,不过也没有问题。
还有就是,我写的是判断 \(q[tl - 1]\to i\) 和 \(q[tl - 1]\to q[tl]\) 的关系,而不是 \(q[tl - 1]\to q[tl]\) 和 \(q[tl]\to i\) 的关系。
我这么写也是可以的。
再有就是 \(x\) 不单调,如这个题:
CEOI 2017 Building Bridges
这类题我们直接搞个类似cdq分治的东西即可。
时间复杂度 \(\mathcal O(n\log n)\)。
具体看代码。
点击查看代码
#include <iostream>
#include <algorithm>
using std::cin;
using std::cout;
const int N = 1e5 + 10;
typedef long long ll;
typedef __int128 i1;
const ll oo = 1e18;
struct Node
{
ll x, y;
int id, k;
friend bool operator<(const Node &a, const Node &b)
{
return a.k < b.k;
}
} p[N], t[N];
int q[N];
int h[N], w[N];
ll f[N];
ll sum[N];
i1 X(int x)
{
return p[x].x;
}
i1 Y(int x)
{
return p[x].y;
}
i1 get(int a, int b, int c)
{
// a -> b, a -> c
// x2 * y1 - x1 * y2
return (X(c) - X(a)) * (Y(b) - Y(a)) - (X(b) - X(a)) * (Y(c) - Y(a));
}
void merge(int l, int r)
{
if (l == r)
{
if (l == 1)
f[l] = 0;
p[l].x = h[l];
p[l].y = f[l] - sum[l] + 1ll * h[l] * h[l];
return;
}
int mid = (l + r) >> 1;
int p1 = l, p2 = mid + 1;
for (int i = l; i <= r; ++i)
{
if (p[i].id <= mid)
t[p1++] = p[i];
else
t[p2++] = p[i];
}
for (int i = l; i <= r; ++i)
p[i] = t[i];
merge(l, mid);
int hd = 1, tl = 0;
for (int i = l; i <= mid; ++i)
{
while (hd < tl && get(q[tl - 1], q[tl], i) >= 0)
tl--;
q[++tl] = i;
}
for (int i = mid + 1; i <= r; ++i)
{
while (hd < tl && (Y(q[hd + 1]) - Y(q[hd])) <= p[i].k * (X(q[hd + 1]) - X(q[hd])))
hd++;
int nw = p[i].id;
int j = p[q[hd]].id;
f[nw] = std::min(f[nw], f[j] + 1ll * (h[nw] - h[j]) * (h[nw] - h[j]) + sum[nw - 1] - sum[j]);
}
merge(mid + 1, r);
std::inplace_merge(p + l, p + mid + 1, p + r + 1, [&](const Node &a, const Node &b){return (a.x ^ b.x ? a.x < b.x : a.y < b.y);});
}
int main()
{
int n;
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> h[i], p[i].k = (h[i] << 1), p[i].id = i;
for (int i = 1; i <= n; ++i)
cin >> w[i], sum[i] = sum[i - 1] + w[i];
for (int i = 1; i <= n; ++i)
f[i] = oo;
std::sort(p + 1, p + n + 1);
merge(1, n);
cout << f[n] << '\n';
return 0;
}
注意到我在 inplace_merge 函数中的lambda函数里以 \(y\) 为排序的第二关键字,这里很重要的。
如果不写的话,就有可能出现一个点正上方的点把它给替换掉的错误。
练习题
注意到已经有大佬整理了斜率优化的题单:https://www.luogu.com.cn/training/5352#problems

浙公网安备 33010602011771号