斜率优化dp学习笔记
一年前就看斜率优化dp了…然而一直没有看懂。今天花了一天时间总算了解了个大概。这篇文章将大致分析斜率优化dp的原理和应用。
[HNOI2008]玩具装箱TOY
Description
P教授要去看奥运,但是他舍不下他的玩具,于是他决定把所有的玩具运到北京。他使用自己的压缩器进行压缩,其可以将任意物品变成一堆,再放到一种特殊的一维容器中。P教授有编号为
Input
- 第一行输入两个整数N,L.接下来N行输入
Ci .1≤N≤50000,1≤L,Ci≤107
Output
- 输出最小费用
Sample
Input
5 4
3
4
2
1
4
Output
1
Solution
首先一眼dp方程:
其中:
然而这是个
我们记
1. 证明决策单调性
首先对原方程做一些化简。令
分析可知要证明:
根据
任取
只需证:
设
展开得到:
由于
只需证:
由于
2. 利用单调性解决问题
考虑何时有
就是
展开并分离变量,得到:
这时左边只剩下了和
我们发现这个式子不仅关于
至此我们成功地获得了一个算法:每次加入新节点时,从队列左端扫去不符合要求的点,剩下的第一个点就是所需的最小答案。之后将当前点插入队列右端并维护下凸性。由于每个元素入队一次出队一次,复杂度为:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 50005;
long long s[MAXN], f[MAXN], L;
int n;
long long q[MAXN];
int l = 1, r = 0;
inline long long T(int i)
{ return i+s[i]; }
long long c;
inline double slop(int j, int k)
{ return 0.5*(f[k]-f[j])/(T(k)-T(j))+0.5*(T(k)+T(j))+c; }
int main()
{
scanf("%d%lld", &n, &L);
c = L+1;
for (int i = 1; i <= n; i++)
scanf("%lld", &s[i]), s[i] += s[i-1];
memset(f, 127/3, sizeof f);
f[0] = 0;
q[++r] = 0;
for (int i = 1; i <= n; i++) {
while (l < r && slop(q[l], q[l+1]) <= T(i)) l++;
f[i] = f[q[l]]+(T(i)-T(q[l])-c)*(T(i)-T(q[l])-c);
while (l < r && slop(q[r-1], q[r]) >= slop(q[r-1], i)) r--;
q[++r] = i;
}
cout << f[n] << endl;
return 0;
}
四边形不等式
一维情景
在证明决策单调性的过程中计算量非常大,有时可以用四边形不等式简化运算:记
下面的命题等价:
- w为凸。
w(a→b)+w(a+1→b+1)≤w(a→b+1)+(a+1→b) f 决策单调
二维情景
则下列命题等价:
- w为凸。
w(a→b)+w(a+1→b+1)≤w(a→b+1)+(a+1→b) k(i,j−1)≤k(i,j)≤k(i+1,j)
还记得dp入门题“石子归并”的方程:
由于
CEOI2004 锯木厂选址
Description
从山顶上到山底下沿着一条直线种植了n棵老树。当地的政府决定把他们砍下来。为了不浪费任何一棵木材,树被砍倒后要运送到锯木厂。
木材只能按照一个方向运输:朝山下运。山脚下有一个锯木厂。另外两个锯木厂将新修建在山路上。你必须决定在哪里修建两个锯木厂,使得传输的费用总和最小。假定运输每公斤木材每米需要一分钱。
Input
- 输入的第一行为一个正整数n——树的个数(
2≤n≤20000 )。树从山顶到山脚按照1开始标号。 - 接下来n行,每行有两个正整数(用空格分开)。第
i+1 行含有:wi ——第i棵树的重量(公斤为单位)和di ——第i棵树和第i+1棵树之间的距离,1≤wi≤10000,0≤di≤10000 。最后一个数dn ,表示第n棵树到山脚的锯木厂的距离。保证所有树运到山脚的锯木厂所需要的费用小于2000000000 分。
Output
- 输出只有一行一个数:最小的运输费用。
Sample
Input
9
1 2
2 1
3 3
1 1
3 2
1 6
2 1
1 2
1 1
Output
26
Solution
“两个”不仅让我们浮想联翩。可以确定一个,枚举另一个作为决策。设
化简得到:
其中:
化简得到:
这是显然的。因此可以考虑斜率优化。仿照上面的推导过程可以得到:
大功告成!原式被整理成了斜率的形式,只需要模仿上面不出脑残错误就可以1A了!(事实上我斜率写错调了1.5h…)
#include <bits/stdc++.h>
using namespace std;
int n;
long long s[20005], d[20005], a[20005], b[20005];
long long q[20005];
int l = 0, r = 0;
double slop(int j, int k)
{ return 1.0*(d[k]*s[k]-d[j]*s[j])/(s[k]-s[j]); }
int main()
{
freopen("two.in", "r", stdin);
freopen("two.out", "w", stdout);
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%lld%lld", &a[i], &b[i]);
d[1] = 0;
for (int i = 2; i <= n+1; i++) d[i] = d[i-1]+b[i-1];
s[0] = 0;
for (int i = 1; i <= n; i++) s[i] = s[i-1]+a[i];
///////
long long T = 0;
for (int i = 1; i <= n; i++) T -= a[i]*d[i];
long long ans = 233333333333ll;
for (int i = 1; i <= n; i++) {
while (l < r && slop(q[l], q[l+1]) <= d[i]) l++;
ans = min(ans, T+d[i]*s[i]-d[n+1]*s[i]+d[n+1]*s[n]-d[i]*s[q[l]]+d[q[l]]*s[q[l]]);
while (l < r && slop(q[r-1], q[r]) >= slop(q[r-1], i)) r--;
q[++r] = i;
}
cout << ans << endl;
return 0;
}