[学习笔记] 斜率优化DP - DP
这个真的好容易啊 ——wzw
斜率优化dP
例题
[SDOI2012] 任务安排
毒瘤题,让我惨淡经营了两天。这道题luogu有简单版,可以先去看简单版。
显然这是一只DP题,直接开始推狮子。令 dp[i]
表示以第 \(i\) 个任务为终止时的最小花费。\(t\) 和 \(w\) 都表示的是前缀和,那么有 \(dp[i]=min\{dp[j]+(t[i]+S*k)(w[i]-w[j])\}\) 。观察狮子,\(k\) 是完全不知道的,也就是转移所依赖的 dp[j]
是否有分组是完全不知道的,所以无法转移。不妨继续思考,假如在第 \(1\sim i\) 分了一组,那么 \(i+1\sim n\) 中必然也会分组,也就是 \(s+1\sim n\) 的时间必然会增加一个 \(S\)。于是有:
观察min中的狮子,把它们单独拎出来:\(dp[j]=dp[i]+t[i]*w[j]\)。其中 \(dp[i]\) 为截距,\(t[i]\) 为斜率,只需要用单调队列维护一个下凸包即可。但考虑到 \(Ti\) 存在负数,那么斜率就不再具有单调性,用二分维护即可。复杂度 \(O(nlogn)\)。
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 3e5 + 10;
int n, s, t[N], c[N], p[N], dp[N], tail;
inline int up(int a, int b){ return dp[b] - dp[a]; }
inline int dn(int a, int b){ return c[b] - c[a]; }
inline int get(int l, int r, int slp){
while(l < r){
int mid = (l + r) >> 1;
if(up(p[mid], p[mid+1]) <= slp*dn(p[mid], p[mid+1])) l = mid + 1;
else r = mid;
} return l;
}
signed main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin>>n>>s;
for(int i=1, j; i<=n; ++i){
cin>>t[i]>>c[i];
t[i] += t[i-1], c[i] += c[i-1];
}
for(int i=0, j; i<=n; ++i){
j = get(1, tail, t[i]);
j = p[j];
dp[i] = dp[j] + t[i]*(c[i]-c[j]) + s*(c[n]-c[i]);
while(tail > 1 && up(p[tail-1], p[tail])*dn(p[tail], i) >= up(p[tail], i)*dn(p[tail-1], p[tail])) --tail;
p[++tail] = i;
} return cout<<dp[n], 0;
}
注意几点:
- 维护时应严格递增,不要留有斜率相等的点。
- 维护时应看准 \(tail\) 的范围,如果涉及到 \(tail-1\),那么 \(tail>1\)
- 因为涉及double,尽量不要直接把斜率算出来,而是使用up和down。
[APIO2010] 特别行动队
当你打完任务安排后就会发现这道题简单的跟1+1似的。
一只小地痞。令 dp[i]
表示以第 \(i\) 名士兵为界限分组时的最大战斗力值。\(w\) 表示前缀和,令 \(W=w[i]-w[j]\)。于是有:
展开后得:
观察到只有max里面的狮子和 \(j\) 有关,于是提出来并变形得:
显然 \(2a*w[i]\) 为斜率。因为需要求max,所以维护一个上凸包(斜率严格递减)。接着就是斜率地痞板子了。
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e6 + 1;
int n, a, b, c, w[N], dp[N], p[N], tail, head = 1;
inline int up(int x, int y){ return dp[y] + a*w[y]*w[y] - b*w[y] - dp[x] - a*w[x]*w[x] + b*w[x]; }
inline int dn(int x, int y){ return w[y] - w[x]; }
signed main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin>>n>>a>>b>>c;
for(int i=1; i<=n; ++i) cin>>w[i], w[i] += w[i-1];
for(int i=0; i<=n; ++i){
while(head < tail && up(p[head], p[head+1]) >= 2*a*w[i]*dn(p[head], p[head+1])) ++head;
dp[i] = dp[p[head]] + a*(w[i]-w[p[head]])*(w[i]-w[p[head]]) + b*(w[i]-w[p[head]]) + c;
while(tail > head && up(p[tail-1], p[tail])*dn(p[tail], i) <= up(p[tail], i)*dn(p[tail-1], p[tail])) --tail;
p[++tail] = i;
dp[0] = 0;
// 因为可以从dp[0]转移,所以要把0添加到队列里去
// 但是这样计算的话dp[0]=c,所以要把dp[0]设为0
} return cout<<dp[n], 0;
}
[ZJOI2007] 仓库建设
当你打完任务安排后就会发现这道题简单的跟1+1似的。
一道简单坑人的小地痞。令 dp[i]
表示在第 \(i\) 个工厂建仓库的最大价格。那么在这个工厂和上一个工厂之间的所有工厂的存货都要运送到仓库 \(i\) 里。那么对于一个在此之间工厂 \(k\),它的送货价值就为 \(p[k]*(x[i]-x[k])=p[k]*x[i]-p[k]*x[k]\),所以我们可以预处理出所有 \(p[k]\) 的前缀和 \(P\) 和所有 \(p[k]*x[k]\) 的前缀和 \(T\)。于是地痞方程为:
调整得:
发现min里的狮子都和 \(j\) 有关,于是提出来:\(dp[j]-T[j]=dp[i]+x[i]*P[j]\)。显然 \(x[i]\) 为斜率。因为求min,所以维护下凸包。
hack 数据中末尾会有一连串 \(p=0\) 的工厂,不会对答案产生任何贡献(他们无需运送货物,也不会被作为仓库),但是 dp 过程中会把他们算进去,因此答案应当取最后一个有货物的工厂作为仓库时的最小花费。—— Ithea686
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e6 + 10;
int n, x[N], c[N], t[N], w[N], dp[N], p[N], tail, head = 1, ans = LONG_LONG_MAX;
inline int up(int a, int b){ return dp[b] + t[b] - dp[a] - t[a]; }
inline int dn(int a, int b){ return w[b] - w[a]; }
signed main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin>>n;
for(int i=2; i<=n+1; ++i){
cin>>x[i]>>w[i]>>c[i];
t[i] = t[i-1] + x[i]*w[i];
w[i] += w[i-1];
}
for(int i=1; i<=n+1; ++i){
while(head < tail && up(p[head], p[head+1]) <= x[i]*dn(p[head], p[head+1])) ++head;
dp[i] = dp[p[head]] + x[i]*(w[i-1]-w[p[head]]) - t[i-1] + t[p[head]] + c[i];
while(head < tail && up(p[tail-1], p[tail])*dn(p[tail], i) >= up(p[tail], i)*dn(p[tail-1], p[tail])) --tail;
p[++tail] = i;
}
for(int i=n+1; i>=1; --i){
ans = min(ans, dp[i]);
if(w[i]-w[i-1]) break;
}
return cout<<ans, 0;
}