Loading

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\) 更优的充要条件是:

\[dp_j+w_{j+1}\times l_i \leq dp_k + w_{k+1} \times l_i \]

把所有带 \(i\) 的移到右边:

\[dp_j-dp_k \leq l_i \times (w_{k+1} - w_{j+1}) \]

一边只留下带 \(i\) 的:

\[l_i \ge \frac {dp_j - dp_k} {w_{k+1} - w_{j+1}} \]

这个就是斜率式


斜率优化代码

先按照斜率式写出计算两点斜率的函数:

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\)

斜率式:

\[2 \times a_i \ge \frac {(dp_j+b_j^2)-(dp_k+b_k^2)} {b_j-b_k} \]

代码

#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}\}\)

斜率式:

\[\frac {dp_k-dp_j} {mina_{u_{j+1}-1}-mina_{u_{k+1}-1}} \lt minb_{v_i+1} \]

点的坐标:\((-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\)

还是维护下凸壳。

斜率式:

\[\frac {dp_j-sumC_j \times s - dp_k + sumC_x \times s} {sumC_j - sumC_k} \leq sumT_i \]

但是由于 \(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

posted @ 2025-07-23 16:51  lajishift  阅读(14)  评论(0)    收藏  举报