DP 优化 - 斜率优化(本篇暂废)
思想&定义
这块全几把是错的qwq
斜率优化就是 dp 的一种优化方式,针对转移式含有取 min、max 的 dp。
普通斜率优化的前提是决策单调性,我们先定义决策点。
-
决策点:可以转移给 \(dp_i\) 的点称为 \(i\) 的决策点,如例题中的 \(dp_i=\min\{dp_j+w_{j+1}\times l_i\}\) \((0\leq j \lt i)\),那么满足 \(0\leq j \lt i\) 的都是 \(i\) 的决策点。
-
最优决策点:最终给 \(dp_i\) 最佳值的决策点即为 \(i\) 的最优决策点。
-
决策单调性:当 \(i\) 不断增加时,\(i\) 的最优决策点一定是单调递增的,就称为决策单调性。
拓展的斜率优化只要满足 \(dp_i=dp_j+val(i,j)+k\) 即可,详见题 2。
实现
例题:1. [USACO08MAR] Land Acquisition G
首先将被其他土地完全包含的土地去掉,余下的土地都不被完全包含,然后按照宽递减排序,此时高就是递增的,发现选择在一组的块必定是连续的(简单贪心)。
然后就有 \(O(n^2)\) 的 dp 方程:\(dp_i=\min\{dp_j+w_{j+1}\times l_i\}\)。
这个东西是有决策单调性的,容易证明。
斜率式
考虑 \(0 \leq k \lt j \lt i\),\(j\) 比 \(k\) 更优的充要条件是:
把所有带 \(i\) 的移到右边:
一边只留下带 \(i\) 的:
这个就是斜率式
斜率优化代码
先按照斜率式写出计算两点斜率的函数:
double slope(int i,int j) {
return 1.0 * (f[i] - f[j]) / (a[j + 1].x - a[i + 1].x);
}
我们在一点点插入时维护一个双端队列,表示可能的决策点。
双端队列中两两之间斜率递增。
只要队列第二个比第一个更优,就把第一个弹出。
while(l < r && slope(q[l], q[l + 1]) <= a[i].y) ++l;
然后当前队头就是最优决策点,拿他计算 \(dp_i\)。
f[i] = f[q[l]] + a[q[l] + 1].x * a[i].y;
然后维护队尾的单调性(两两之间斜率递增)。
while(l < r && slope(q[r - 1], q[r]) >= slope(q[r],i)) --r;
最后加入 \(i\)。
q[++r] = i;
完整代码
#include <cstdio>
#include <algorithm>
const int N=5e4+5;
int n,q[N];
long long f[N];
struct Land {
int x,y;
bool operator < (const Land &b) const {
return x==b.x?y>b.y:x>b.x;
}
} a[N];
void init() {
std::sort(a+1,a+n+1);
int m=0;
for(int i=1;i<=n;++i) if(a[i].y>a[m].y) a[++m]=a[i];
n=m;
}
double slope(int i,int j) {
return 1.0*(f[i]-f[j])/(a[j+1].x-a[i+1].x);
}
int main() {
scanf("%d",&n);
for(int i=1;i<=n;++i) scanf("%d%d",&a[i].x,&a[i].y);
init();
int l=1,r=0;
q[++r]=0;
for(int i=1;i<=n;++i) {
while(l<r&&slope(q[l],q[l+1])<=a[i].y) ++l;
f[i]=f[q[l]]+1LL*a[q[l]+1].x*a[i].y;
while(l<r&&slope(q[r-1],q[r])>=slope(q[r],i)) --r;
q[++r]=i;
}
printf("%lld\n",f[n]);
return 0;
}
思路总结
做斜率优化的题目的几步:
-
写朴素 dp 式
-
发现过不了,证明决策单调性
-
推导斜率式
-
把 \(j\) 和 \(k\) 往 dp 式里带,其中 \(0 \leq k \lt j \lt i\)。
-
推导 \(j\) 的 dp 式比 \(k\) 的 dp 式更优的条件
-
把带 \(i\) 的放右边,其他放左边。
-
把 \(i\) 那一边不含 \(i\) 的因式除到左边。
-
-
-
用斜率式写计算两点斜率的函数 \(\text{slope}\)。
-
主函数里枚举 \(i\)
-
把双端队列里,从队首开始,比后一项更劣的弹出(更劣用斜率式判断)
-
用队首代入 dp 式计算 \(dp_i\)。
-
维护队尾的单调性,把原来斜率不对的弹出去。
-
把 \(i\) 放进队列。
-
应用
2. [HNOI2008] 玩具装箱
设 \(dp_i\) 为考虑到第 \(i\) 个玩具的最小费用。
\(dp_i = \min\{dp_j + (i-j+1+sum_i-sum_j-L)^2\}\)
记录 \(a_i=sum_i+i,b_i=sum_i+i+1+L\)。
斜率式:
代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define a(i) (sum[i] + i)
#define b(i) (sum[i] + i + L + 1)
#define D cout << "click\n";
const int maxn = 1e5 + 5;
int n, L;
int top, bot;
int que[maxn];
int sum[maxn], dp[maxn];
double squ(double x){
return x * x;
}
double k(int i, int j){
int x1 = b(i), y1 = dp[i] + squ(b(i));
int x2 = b(j), y2 = dp[j] + squ(b(j));
return (y2 - y1) / (x2 - x1);
}
signed main(){
cin >> n >> L;
sum[0] = 0.00;
for(int i = 1; i <= n; i++){
int c;
cin >> c;
sum[i] = sum[i - 1] + c;
}
top = bot = 1;
// que[++top] = 0;
for(int i = 1; i <= n; i++){
// cout << bot << " " << top << endl;
while(bot < top && k(que[bot], que[bot + 1]) < 2 * a(i)) bot++;
int j = que[bot];
dp[i] = dp[j] + squ(a(i) - b(j));
while(bot < top && k(i, que[top]) < k(que[top], que[top - 1])) top--;
que[++top] = i;
}
// cout << fixed << setprecision(2)
cout << dp[n];
return 0;
}
3. 丝之割
如果一条线是 \((u_1,v_1)\),另一条线是 \((u_2,v_2)\),满足 \(u_1 \leq u_2, v_2 \leq v_1\),则切掉第一条线时可以顺便切掉第二条线。相当于不允许两条线交叉,那我们可以将被其他线完全包含的线全部去掉。
这样,线的左端点单调递增时,右端点也具有单调性。
然后设 \(dp_i\) 为切掉前面 \(i\) 条线(线的下标为删去一些之后的重新编号结果)的最小代价。
\(dp_i=\min\{dp_j+mina_{1,u_{j+1}-1}\times minb_{v_{i}+1,n}\}\)
斜率式:
点的坐标:\((-mina_{1,u_{j+1}-1},dp_j)\)
代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn = 3e5 + 5;
int n, m, prem;
int a[maxn], b[maxn], u[maxn], v[maxn];
//mina:a的前缀最小值; minb:b的后缀最小值
int mina[maxn] = {0x3f3f3f3f}, minb[maxn] = {0x3f3f3f3f};
int q[maxn], l, r;
int dp[maxn];
//pre就是存原来的丝,后面去掉交叉的
struct node{
int u, v;
} pre[maxn];
double slope(int x, int y){
if(mina[u[x + 1] - 1] == mina[u[y + 1] - 1]){
return 1e18;
}
return (double)(dp[x] - dp[y]) / (mina[u[y + 1] - 1] - mina[u[x + 1] - 1]);
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> prem;
for(int i = 1; i <= n; i++) cin >> a[i];
for(int i = 1; i <= n; i++) cin >> b[i];
for(int i = 1; i <= prem; i++) cin >> pre[i].u >> pre[i].v;
sort(pre + 1, pre + prem + 1, [](node x, node y){
if(x.u == y.u) return x.v > y.v;
return x.u < y.u;
});
for(int i = 1, mx = 0; i <= prem; i++){
if(pre[i].v > mx){
m++;
u[m] = pre[i].u;
v[m] = pre[i].v;
}
mx = max(mx, v[m]);
}
u[0] = 1;
mina[0] = 0x3f3f3f3f;
minb[n + 1] = 0x3f3f3f3f;
for(int i = 1; i <= n; i++){
mina[i] = min(mina[i - 1], a[i]);
}
for(int i = n; i >= 1; i--){
minb[i] = min(minb[i + 1], b[i]);
}
for(int i = 1; i <= m; i++){
while(l < r && slope(q[l], q[l + 1]) < minb[v[i] + 1]) l++;
dp[i] = dp[q[l]] + mina[u[q[l] + 1] - 1] * minb[v[i] + 1];
while(l < r && slope(q[r - 1], q[r]) > slope(q[r], i)) r--;
q[++r] = i;
}
cout << dp[m];
return 0;
}
4. [SDOI2012] 任务安排
关键词:拓展斜率优化——单调栈二分
写出 dp 方程 \(dp_i=min\{dp_j+sumT_i \times (sumC_i-sumC_j)+s \times (sumC_n-sumC_j)\}\)。
满足斜率优化 \(dp_i = dp_j + val(i,j) + k\)。
还是维护下凸壳。
斜率式:
但是由于 \(t\) 可能是负数,每次枚举到 \(i\),找斜率最相近的线段时,\(sumT_i\) 不是单调的。
所以把单调队列改成单调栈,维护整个凸壳(相当于去掉从队头弹的操作),每次二分最优决策点即可。
二分代码
int solve(int k){
int L = l, R = r - 1, ans = -1;
while(L <= R){
int mid = (L + R) >> 1;
int a = q[mid], b = q[mid + 1];
int ya = dp[a] - c[a] * s;
int yb = dp[b] - c[b] * s;
int xa = c[a];
int xb = c[b];
if(yb - ya > k * (xb - xa)){
ans = mid;
R = mid - 1;
}else{
L = mid + 1;
}
}
if(ans != -1) return q[ans];
return q[r];
}
完整代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn = 5e5 + 5;
int n, s;
int t[maxn], c[maxn];
int dp[maxn];
int q[maxn], l, r;
int solve(int k){
int L = l, R = r - 1, ans = -1;
while(L <= R){
int mid = (L + R) >> 1;
int a = q[mid], b = q[mid + 1];
int ya = dp[a] - c[a] * s;
int yb = dp[b] - c[b] * s;
int xa = c[a];
int xb = c[b];
if(yb - ya > k * (xb - xa)){
ans = mid;
R = mid - 1;
}else{
L = mid + 1;
}
}
if(ans != -1) return q[ans];
return q[r];
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> s;
for(int i = 1; i <= n; i++){
cin >> t[i] >> c[i];
t[i] += t[i - 1];
c[i] += c[i - 1];
}
memset(dp, 0x7f, sizeof(dp));
dp[0] = q[0] = 0;
#define dy1 (dp[q[r]] - c[q[r]] * s - dp[q[r - 1]] + c[q[r - 1]] * s)
#define dx1 (c[q[r]] - c[q[r - 1]])
#define dy2 (dp[i] - c[i] * s - dp[q[r]] + c[q[r]] * s)
#define dx2 (c[i] - c[q[r]])
for(int i = 1; i <= n; i++){
int j = solve(t[i]);
dp[i] = dp[j] + t[i] * (c[i] - c[j]) + s * (c[n] - c[j]);
while(l < r && dy1 * dx2 >= dy2 * dx1) r--;
q[++r]=i;
}
#undef dy1
#undef dx1
#undef dy2
#undef dx2
cout << dp[n];
return 0;
}
P5504
P4072
P4655