@[斜率優化]
Description
P教授要去看奥运,但是他舍不下他的玩具,于是他决定把所有的玩具运到北京。他使用自己的压缩器进行压
缩,其可以将任意物品变成一堆,再放到一种特殊的一维容器中. P教授有编号为(1 .. N)的(N)件玩具,第(i)件玩具经过压缩后变成一维长度为(C_i).为了方便整理, P教授要求在一个一维容器中的玩具编号是连续的。同时如果一个一维容
器中有多个玩具,那么两件玩具之间要加入一个单位长度的填充物,形式地说如果将第i件玩具到第j个玩具放到一个容器中,那么容器的长度将为 $$x = j - i + sum_{k = i}^{j} C_i$$ 制作容器的费用与容器的长度有关,根据教授研究,如果容器长度为(x), 其制作费用为((X-L)^2). 其中(L)是一个常量。P教授不关心容器的数目,他可以制作出任意长度的容器,甚至超过(L). 但他希望费用最小.
Input
第一行输入两个整数N,L.接下来N行输入(C_i). (1 <= N <= 50000, 1 <= L, C_i <= 10^7)
Output
输出最小费用
Sample Input
5 4
3
4
2
1
4
Sample Output
1
Solution
典型的斜率優化DP
令(f[i])記錄標號為從(1)到(i)的玩具全部壓縮好所需要的最小費用, 則可以得到DP递推式$$f[i] = min_{j = 0}^{i - 1}(f[j] + (sum[i] - sum[j] + i - j - 1 - L)^2)$$其中(sum[i])記錄編號從(1)到(i)的玩具的長度總和.
假設在(i > j > k)中, 對於(i)有(j)比(k)優, 則$$f[j] + (sum[i] - sum[j] + i - j - 1 - L)^2 < f[k] + (sum[i] - sum[k] + i - k - 1 - L)^2$$
化簡得到
其中$$a[i] = sum[i] + 1 - L$$$$b[i] = sum[i] + i$$
*Hint: 化簡有一定技巧. 一般來說, 化簡得到的結果要把含(i)的項移至等號右邊, 不含(i)的項移至左邊, 以方便後續運算.
可以將這個除法式子理解為所謂的斜率, 記為(slope(j, k))
然後用隊列來維護DP. 具體過程如下:
- 對於隊頭的兩個元素, 假如有(slope(queue[head + 1], queue[head]) > 2 * a[i])則說明隊列中第二個元素比第一個優, 隊頭出隊.
- 此時可以確保隊頭元素是最優解, 用隊頭元素計算出(f[i])的數值
- 在有了(f[i])的值的情況下, 就可以在隊尾進行維護了. 對於隊尾的兩個元素記為(x, y(x > y)), 假如有(slope(x, y) > slope(i, x))則說明(x)是無用的, 可以出隊. 具體證明如下: (1). 假如(slope(x, y) > slope(i, x) > 2 * a[i]), 則雖然有(x)比(i)優, 但又有(y)比(x)優, 因此(x)可出隊; (2). 假如(slope(x, y) > slope(i, x) < 2 * a[i]), 則有(i)比(x)優, (x)可出隊.
- 將(i)加入隊尾
維護過程結束. 注意每一步的順序, 都是有先後性的, 不要搞反.
然後再說道一個點, 這一題一定要開(long long)
感覺現階段推公式的能力還要加強, 這一坨東西我推錯了好多次QAQ
附上代碼
#include<cstdio>
#include<cctype>
#include<cstring>
using namespace std;
inline long long read()
{
long long x = 0, flag = 1;
char c;
while(! isdigit(c = getchar()))
if(c == '-')
flag *= - 1;
while(isdigit(c))
x = x * 10 + c - '0', c = getchar();
return x * flag;
}
const long long N = 1 << 16;
long long c[N];
long long sum[N];
long long a[N], b[N];
long long queue[N];
long long f[N];
inline long long sqr(long long x)
{
return x * x;
}
inline long double slope(long long x, long long y)
{
return (long double)((f[x] + sqr(b[x])) - (f[y] + sqr(b[y]))) / (long double)(b[x] - b[y]);
}
int main()
{
#ifndef ONLINE_JUDGE
freopen("BZOJ1010.in", "r", stdin);
freopen("BZOJ1010.out", "w", stdout);
#endif
long long n = read(), l = read();
sum[0] = 0;
a[0] = - 1 - l;
b[0] = 0;
for(long long i = 1; i <= n; i ++)
{
c[i] = read();
sum[i] = c[i] + sum[i - 1];
a[i] = sum[i] + i - 1 - l;
b[i] = sum[i] + i;
}
long long head = 0, tail = 1;
queue[head] = 0;
memset(f, 0, sizeof(f));
for(long long i = 1; i <= n; i ++)
{
while(head + 1 < tail && slope(queue[head + 1], queue[head]) < 2 * (float)a[i])
head ++;
f[i] = f[queue[head]] + sqr(a[i] - b[queue[head]]);
while(head + 1 < tail && slope(queue[tail - 1], queue[tail - 2]) > slope(i, queue[tail - 1]))
tail --;
queue[tail ++] = i;
}
printf("%lld
", f[n]);
}
順便, 這裡想借這一題, 寫一點關於斜率優化的深層次理解, 主要圍繞隊列的維護方面. 首先是入隊, 為什麼要通過這種方式確定隊尾元素的保留還是彈出? 為什麼在(slope(i, queue[tail - 1]) > 2 * a[i])的情況下仍然要將(i)入隊? 主要是出於對後效性的考慮. 對於(2 * a[i])不難看出, 它是隨著(i)的增大而遞增的. 因此, 對於一個(i), 雖然在當前不一定是最優的, 但在之後可能成為最優解, 因此要入隊. 因此在隊尾出隊的原則, 應該是與當前的(a[i])無關的. 至於在隊頭出隊的原則, 也是在考慮到(2 * a[i])的單調性以及確保無後效性的情況下才出隊的.
另外一點就是關於判斷一道題是否可以進行斜率优化. 這主要取決於等式右邊的斜率是否滿足單調性. 雖然說從隊尾出隊的元素與斜率無關, 但是隊頭出隊的元素是需要依賴於斜率的單調性的. 因此斜率是否滿足單調性可以作為一題是否可以採用 常規 的斜率优化的判斷依據.(假如不滿足, 還可以採用特殊的方法優化DP)