[洛谷2900][USACO08MAR]土地征用Land Acquisition(斜率优化dp)
Solution
每块土地的长和宽分别用\(l\)和\(h\)数组表示。
因为一组土地购买的价格 \(=\) 最大的长 \(*\) 最大的宽,所以对于一块土地\(x\),如果存在一块土地\(y\),满足\(l[y]>=l[x]\)且\(h[y]>=h[x]\),那么它只要把土地\(x\)和土地\(y\)合为一组,最大的长可以不取\(l[x]\),最大的宽可以不取\(h[x]\),所以土地\(x\)的存在与否对答案是没有任何影响的。
因此,可以把所有土地按长度从小到大排序,对于长度相同的按宽度从小到大排序。然后维护一个栈,将排序后的土地一个一个加入,每次加入之前,先把当前栈中宽小于等于它的土地全部删除(因为这些土地长肯定小于等于它),再加入它。最后留在栈里的土地就是有存在必要的土地。
可以发现,此时留下的土地的宽度是从大到小排好的。所有还有一个结论:此时在最优决策下,每一组土地都是连续的一段。为什么呢?举个例子:假设这时有三个土地\(x\),\(y\),\(z\),是连续的三块土地,那么有:\(l[x]<l[y]<l[z]\)且\(h[x]>h[y]>h[z]\)。考虑把\(x\)和\(z\)分为一组,\(y\)单独一组,要花费\(h[x]*l[z]+h[y]*l[y]\)的费用。如果把\(y\)也分进\(x\),\(z\)那一组呢?只要\(h[x]*l[z]\)的费用,省去了\(y\)单独一组的费用。
因此,考虑\(dp\)。设\(f[i]\)表示前\(i\)个土地的最小费用。易得转移方程:\(f[i]=min(f[i],f[j]+h[j+1]*l[i])\),然而这是\(O(n^2)\)的,过不去。
考虑优化。求出\(f[j]\)之后,设\(a=h[j+1]\),\(b=f[j]\),就可以表示一条直线:\(y=ax+b\),求\(f[i]\)等价于在已经表示出的所有直线中,找一个当\(x=l[i]\)时\(y\)最大的。注意到\(l[i]\)是递增的,而\(h[i]\)是递减的。如图1:
可以发现,一旦\(l[i]>p\)的横坐标,较优的永远是直线\(p2\),\(p1\)已经没有用了。因此维护一个单调队列,对于每一个\(i\),比较单调队列队头两个\(j\)生成的直线,只要当\(x=l[i]\)时,队头第一个不如队头第二个优,就把队头第一个删除了,直到队头第一个比第二个优。
然而还有这样一种情况,如图2:
按照之前的那种做法,过点\(A\)之前,都会选择直线\(p1\)最优,但是在过\(B\)点之后,\(p1\)已经不如\(p3\)优了。所以每次把直线\(i\)加入队列作为以后备选的\(j\)之前,先判断队尾两条直线和直线\(i\)是否有如上图的关系,有的话删除队尾第一条,直到不存在上图情况,加入直线\(i\)。
判断即:\(p1\)与\(p2\)交点横坐标是否大于\(p1\)与\(p3\)交点横坐标。需要用到除法,可以通过移项时它变为乘法,避免因交点横坐标不为整数带来的精度问题。
总结一下就是,先将土地排序,去掉没有存在必要的土地,然后进行\(dp\),先删除队头不优的,删完之后队头就是此时最优的\(j\),然后算出\(f[i]\),生成直线\(i\),然后删除队尾不优的,再将直线\(i\)放入队列,作为以后的\(j\)。
Code
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cstdlib>
using namespace std;
const int e = 5e4 + 5;
int n, q[e], t = 1, w = 1, tot;
long long a[e], b[e], f[e], h[e], l[e];
struct point
{
long long w, l;
}c[e];
inline int read()
{
char ch; int res;
while (ch = getchar(), ch < '0' || ch > '9');
res = ch - 48;
while (ch = getchar(), ch >= '0' && ch <= '9')
res = res * 10 + ch - 48;
return res;
}
inline bool cmp(const point &c, const point &d)
{
if (c.l == d.l) return c.w < d.w;
else return c.l < d.l;
}
inline double calc(int i, int j)
{
return f[j] + h[j + 1] * l[i];
}
inline bool slope(int p1, int p2, int p3)
{
return (b[p3] - b[p1]) * (a[p2] - a[p1])
- (b[p2] - b[p1]) * (a[p3] - a[p1]) >= 0;
}
int main()
{
int i;
n = read();
for (i = 1; i <= n; i++)
{
c[i].w = read();
c[i].l = read();
}
sort(c + 1, c + n + 1, cmp);
for (i = 1; i <= n; i++)
{
while (tot && c[i].w >= h[tot]) tot--;
h[++tot] = c[i].w; l[tot] = c[i].l;
}
a[0] = h[1];
for (i = 1; i <= tot; i++)
{
while (t < w && calc(i, q[t]) >= calc(i, q[t + 1])) t++;
f[i] = calc(i, q[t]);
a[i] = h[i + 1];
b[i] = f[i];
while(t < w && slope(q[w - 1], q[w], i)) w--;
q[++w] = i;
}
cout << f[tot] << endl;
return 0;
}