把博客园图标替换成自己的图标
把博客园图标替换成自己的图标end

斜率优化dp复习笔记

注意,有些斜率优化的题目由于双递增可以用决策单调性过掉(最优决策点单调)如玩具装箱

P3648 [APIO2014] 序列分割

将序列分割成 \(k+1\) 个部分,每个部分的元素 \(\geq 1\),然后假设一个块你要分成两个块,代价为分成后两个块里面的元素和的乘积,问你怎么才能使代价最大,并输出方案(SPJ)。

题目分析

注意到顺序是无关紧要的:

若一个块 \(sum\) 要从 \(i,j\) 这里下手(分成三段的和 \(a,b,c\)),顺序所导致的有:

  • 先从 \(i\) 砍,代价为 \(a(b+c)+bc=ab+ac+bc\)
  • 先从 \(j\) 砍,代价为 \((a+b)c+ab=ab+ac+bc\)

两者相等,顺序无用。

故设 \(f_{i,j}\) 表示前 \(i\) 个部分切 \(j\) 次,那么有:

\[f_{i,j}=\max_{l\in[0,i-1]} f_{l,j-1}+sum_l\times(sum_i-sum_l) \]

顺序无关,所以我先在 \(i\) 切,然后再切前面的。

考虑斜率优化,对于 \(j<l\)\(j\)\(l\) 更优,并设 \(f_{x,j-1}=f_{j-1}\)

有:

\[f_j+sum_isum_j-sum_j^2>f_l+sum_isum_l-sum_l^2 \Rightarrow \frac{(f_j-sum_j^2)-(f_l-sum_l^2)}{-sum_j-(-sum_l)}>sum_i \]

那么点集就是 \((-sum_x,f_x-sum_x^2)\)

那么对于当前点 \(i\),所有斜率 \(\leq sum_i\) 的都不可行。

然后队头就是最优决策点,为什么呢?因为对于 \(j<l\) 满足即可,显然是第一个。

也就是直接去维护斜率递增。

现在要把 \(i\) 也添加进来。那么我们维护斜率递增即可。

代码

时间复杂度 \(\mathcal{O}(nk)\),代码如下:

#include <iostream>
#include <cstdio>
#include <stdlib.h>
#include <cstring>
#include <stdlib.h>
#include <vector>
#include <deque>
#define int long long
#define N 100005
#define K 205
using namespace std;
int pre[N][K],a[N],sum[N],f[N],g[N];
int x(int id) {return -sum[id];}
int y(int id) {return g[id] - sum[id] * sum[id];}
double slope(int fir,int sec) {
    if (x(fir) == x(sec)) return -1e18;
    return 1.0 * (y(fir) - y(sec)) / (1.0 * (x(fir) - x(sec)));
}
int q[N],head,tail;
signed main(){
    int n,k;
    cin >> n >> k;
    // k ++;
    for (int i = 1;i <= n;i ++) scanf("%lld",&a[i]),sum[i] = sum[i - 1] + a[i];
    // for (int i = 0;i <= n;i ++)
    //     for (int j = 0;j <= n;j ++) f[i][j] = -1e18;
    // f[0][0] = 0;
    // for (int i = 1;i <= n;i ++)
    //     for (int j = 1;j <= min(i,k);j ++)
    //         for (int t = 0;t < i;t ++)
    //             if (f[t][j - 1] + sum[t] * (sum[i] - sum[t]) > f[i][j])
    //                 f[i][j] = f[t][j - 1] + sum[t] * (sum[i] - sum[t]),pre[i][j] = t;
    // printf("%lld\n",f[n][k]);
    // int now = n;
    // for (int j = k;j > 1;j --) printf("%lld ",pre[now][j]),now = pre[now][j];
    for (int t = 1;t <= k;t ++) {
        for (int i = 1;i <= n;i ++) g[i] = f[i];
        tail = 0;
        head = 1;
        // q[++tail] = 0;
        for (int i = 1;i <= n;i ++) {
            while(tail > head && slope(q[head],q[head + 1]) <= sum[i]) head ++;
            f[i] = 0;
            if (head <= tail) {
                pre[i][t] = q[head];
                f[i] = g[q[head]] + sum[q[head]] * (sum[i] - sum[q[head]]);
            }
            while(tail > head && slope(q[tail - 1],q[tail]) >= slope(q[tail],i)) tail --;
            q[++tail] = i;
        }
    }
    printf("%lld\n",f[n]);
    int now = n;
    for (int j = k;j >= 1;j --) printf("%lld ",pre[now][j]),now = pre[now][j];
    return 0;
}

P4655 [CEOI 2017] Building Bridges

题目概述

\(n\) 根柱子依次排列,每根柱子都有一个高度。第 \(i\) 根柱子的高度为 \(h_i\)

现在想要建造若干座桥,如果一座桥架在第 \(i\) 根柱子和第 \(j\) 根柱子之间,那么需要 \((h_i-h_j)^2\)​​ 的代价。

在造桥前,所有用不到的柱子都会被拆除,因为他们会干扰造桥进程。第 \(i\) 根柱子被拆除的代价为 \(w_i\),注意 \(w_i\) 不一定非负,因为可能政府希望拆除某些柱子。

现在政府想要知道,通过桥梁把第 \(1\) 根柱子和第 \(n\) 根柱子连接的最小代价。注意桥梁不能在端点以外的任何地方相交。

对于 \(100\%\) 的数据,有 \(2\le n\le 10^5;0\le h_i,\vert w_i\vert\le 10^6\)

分析

\(f_i\) 表示前 \(i\) 个的最小代价。

那么 \(f_1=0\)

显然有:

\[f_i=\min_{j\in[1,i-1]}f_j+(h_i-h_j)^2-sumw_{i-1}-sumw_j \]

那么我们有 \(\mathcal{O}(n^2)\) 代码,期望可得 \(25\) 分:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <stdlib.h>
#include <algorithm>
#include <vector>
#define int long long
#define sqr(x) (x) * (x)
#define N 100005//完了,上瘾了
using namespace std;
int n,h[N],w[N],f[N],sumh[N],sumw[N];
signed main(){
    cin >> n;
    for (int i = 1;i <= n;i ++) scanf("%lld",&h[i]),sumh[i] = sumh[i - 1] + h[i];
    for (int i = 1;i <= n;i ++) scanf("%lld",&w[i]),sumw[i] = sumw[i - 1] + w[i];
    for (int i = 1;i <= n;i ++) f[i] = 1e18;
    f[1] = 0;
    for (int i = 1;i <= n;i ++)
        for (int j = 1;j < i;j ++)
            f[i] = min(f[i],f[j] + sqr(h[i] - h[j]) + sumw[i - 1] - sumw[j]);
    cout << f[n];
    return 0;
}

开始斜率优化。

\(j<l\) 但是 \(j\)\(l\) 菜。

有:

\[f_j+h_j^2-2h_ihj-sumw_j>f_l+h_l^2-2h_ih_l-sumw_l \]

移项得到:

\[(f_j+h_j^2-sumw_j)-(f_l+h_l^2-sumw_l)>2h_i(h_j-h_l) \]

这里我要提一嘴:当我们左边的那个 \(h_j-h_l\) 不确定是正是负可以通过二分确定继续用单调队列(不删除队头),但是如果连 \(h_i\) 都不是单调的,要用 \(cdq\) 分治或者李超线段树去解决了。

李超线段树你只需要考虑插入一个直线然后在 \(x=h_i\) 时取最小值就行了,有时候挺好写的(这道题也是)。

考虑 \(cdq\) 分治的过程:

解决完 \([l,mid]\)

解决完 \([mid + 1,r]\)

通过 \([l,mid]\)\([mid + 1,r]\) 的一些带来的信息来解决 \([l,r]\)

如果 \(cdq\) 分治是搞 \(dp\) 的话,那就是分治完左区间然后对右区间进行转移,最后再分治右区间。

我们先按照原顺序排序处理贡献,最后再按照 \((x,y)\) 进行排序即可。

代码

时间复杂度 \(\mathcal{O}(n\log n)\)

#include <iostream>
#include <cstdio>
#include <cstring>
#include <stdlib.h>
#include <algorithm>
#include <vector>
#define int long long
#define sqr(x) (x) * (x)
#define N 100005//完了,上瘾了
using namespace std;
int n,h[N],w[N],f[N],sumh[N],sumw[N];
struct node{
    int x,y,id;
    bool operator<(const node&adv) const {
        if (adv.x ^ x) return x < adv.x;
        return y < adv.y;
    }
}a[N],tmp[N];
double X(int id) {
    return h[id];
}
double Y(int id) {
    return f[id] + h[id] * h[id] - sumw[id];
}
// double Slope(int fir,int sec) {
//     if (X(fir) == X(sec)) {
//         return Y(fir) > Y(sec) ? 1e18 : -1e18;
//     }
//     return (Y(fir) - Y(sec)) / (X(fir) - X(sec));
// }
double Slope(int fir,int sec) {
    if (a[fir].x == a[sec].x) return a[fir].y > a[sec].y ? 1e18 : -1e18;
    return (1.0 * a[fir].y - a[sec].y) / (1.0 * a[fir].x - a[sec].x);
}
int q[N];
void solve(int l,int r) {
    if (l == r) {
        a[l].y = Y(l);
        return ;
    }
    int mid = l + r >> 1;
    int lt = l - 1,rt = mid;
    for (int i = l;i <= r;i ++)
        if (a[i].id <= mid) tmp[++lt] = a[i];
        else tmp[++rt] = a[i];
    for (int i = l;i <= r;i ++) a[i] = tmp[i];
    solve(l,mid);
    int head = 1,tail = 0;
    for (int i = l;i <= mid;i ++) {
        if (i > l && a[i].x == a[i - 1].x) continue;
        while(head < tail && Slope(q[tail - 1],q[tail]) > Slope(q[tail],i)) tail --;
        q[++tail] = i;
    }
    for (int i = mid + 1;i <= r;i ++) {
        while(head < tail && (a[q[head]].y - a[q[head + 1]].y) >= 2 * a[i].x * (a[q[head]].x - a[q[head + 1]].x)) head ++;
        int u = a[i].id,v = a[q[head]].id;
        f[u] = min(f[u],f[v] + sumw[u - 1] - sumw[v] + sqr(h[u] - h[v]));
    }
    solve(mid + 1,r);
    int i = l,j = mid + 1;
    int now = l;
    while(i <= mid && j <= r) {
        if (a[i].x < a[j].x || (a[i].x == a[j].x && a[i].y < a[j].y)) tmp[now] = a[i],now ++,i ++;
        else tmp[now] = a[j],now ++,j ++;
    }
    while(i <= mid) tmp[now] = a[i],now ++,i ++;
    while(j <= r) tmp[now] = a[j],now ++,j ++;
    for (int i = l;i <= r;i ++) a[i] = tmp[i];
}
signed main(){
    cin >> n;
    for (int i = 1;i <= n;i ++) scanf("%lld",&h[i]),sumh[i] = sumh[i - 1] + h[i];
    for (int i = 1;i <= n;i ++) scanf("%lld",&w[i]),sumw[i] = sumw[i - 1] + w[i];
    for (int i = 1;i <= n;i ++) f[i] = 1e18,a[i].id = i,a[i].x = h[i];
    f[1] = 0;
    stable_sort(a + 1,a + 1 + n);
    solve(1,n);
    // for (int i = 1;i <= n;i ++)
    //     for (int j = 1;j < i;j ++)
    //         f[i] = min(f[i],f[j] + sqr(h[i] - h[j]) + sumw[i - 1] - sumw[j]);
    cout << f[n];
    return 0;
}

P4056 [JSOI2009] 火星藏宝图

题目概述

在一个 \(M\times M\) 的地图中给你 \(N\) 个岛,保证 \((1,1),(M,M)\) 有岛,然后每个岛上面有价值 \(val_i\),但是你如果从一个地方走到他右下某个地方(可以只右或者下)代价是欧拉距离的平方,求从 \((1,1)\rightarrow(M,M)\) 的最小代价(价值-代价)。

分析

\(f_i\) 表示到了第 \(i\) 个岛的最小代价,时间复杂度 \(\mathcal{O}(n^2)\)(先对 \(x\) 排序):

\[f_i=val_i+\max_{j\in[1,i-1],y_j\leq y_i}f_j-(x_i-x_j)^2-(y_i-y_j)^2 \]

套路搞一下(\(j<l\)\(j\) 更优,列一下式子但是发现有 \(y\) 的干扰,于是我们打算只弄同一列的转移,那么 \(y\) 的那一项就是 \(0\) 了,于是)得到:

\[\frac{f_l-x_l^2-(f_j-x_j^2)}{x_l-x_j}<-2x_i \]

点集就是 \((x_j,f_j-x_j^2)\),维护这个斜率要 \(<-2x_i\)

由于 \(x_i\) 单调递增,于是维护斜率单调递减。

然后做完了

代码

时间复杂度 \(\mathcal{O}(nm)\)

#include <iostream>
#include <cstring>
#include <algorithm>
#include <stdlib.h>
#include <cstdio>
#include <vector>
#define int long long
#define N 200005
#define M 1005
#define isdigit(ch) ('0' <= ch && ch <= '9') 
using namespace std;
template<typename T>
void read(T&x) {
	x = 0;
	char ch = getchar();
	for (;!isdigit(ch);ch = getchar());
	for (;isdigit(ch);ch = getchar()) x = (x << 1) + (x << 3) + (ch ^ 48);
}
template<typename T>
void write(T x) {
	if (x > 10) write(x / 10);
	putchar(x % 10 + '0');
}
int m,n,f[N];
struct node{
	int x,y,val,id;
}a[N];
int q[M][M],tail[M],head[M];
int dy(int j,int l) {return -(f[j] - a[j].x * a[j].x - f[l] + a[l].x * a[l].x);}
int dx(int j,int l) {return -(a[j].x - a[l].x);}//j<l
signed main(){
	read(n),read(m);
	for (int i = 1;i <= n;i ++) read(a[i].x),read(a[i].y),read(a[i].val),a[i].id = (a[i].x - 1) * m + a[i].y;
	sort(a + 1,a + 1 + n,[](node x,node y) {
		return x.id < y.id;
	});
	memset(f,-0x3f,sizeof f);
	f[1] = a[1].val,q[1][++tail[1]] = 1;
	for (int i = 2;i <= n;i ++) {
		int c = a[i].y;
		for (int j = 1;j <= c;j ++) {//列 
			while(head[j] < tail[j] && dy(q[j][head[j]],q[j][head[j] + 1]) >= (-2ll * a[i].x) * dx(q[j][head[j]],q[j][head[j] + 1])) head[j] ++;
			int k = q[j][head[j]];
			f[i] = max(f[i],f[k] + a[i].val - (a[i].x - a[k].x) * (a[i].x - a[k].x) - (c - j) * (c - j));
		} 
		while(head[c] < tail[c] && dy(q[c][tail[c]],i) * dx(q[c][tail[c] - 1],q[c][tail[c]]) >= dy(q[c][tail[c] - 1],q[c][tail[c]]) * dx(q[c][tail[c]],i)) tail[c] --;
		q[c][++tail[c]] = i;
	}
	cout << f[n];
	return 0;
} 

好像还有 \(\mathcal{O}(m^2)\) 做法。

P4072 [SDOI2016] 征途

题目概述

输入 \(n\) 个数(\(1\leq n\leq 3000\)),你将 \(n\) 个数切成 \(m\) 个部分,使得方差 \(s\) 最小,输出 \(s\times m^2\)

分析

这道题好像远古时期做过,复jian。

首先注意到平均量是固定的 \(\frac{\sum a_i}{m}\),令其为 \(\overline{x}\)

那么可以得到方差公式:

\[\frac{\sum_i(x_i-\overline x)^2}{m} \]

首先立马得到:

\[\frac{\sum_i x_i^2+m\overline x^2-2\overline x\sum x_i}{m} \]

代入定义式化简得到:

\[\frac{\sum_i x_i^2}{m}-\overline x^2 \]

乘上 \(m^2\) 就可以得到:

\[m\sum_i x_i^2-\left(\sum x_i\right)^2 \]

wc,死去的回忆冲撞了我,这好像是斜率优化dp!

\(s=\sum x_i\),那么我们就是要最小化:

\[m\sum_i x_i^2-s^2 \]

最小化 \(\sum x_i^2\) 即可。

\(f_{i,j}\) 表示前 \(i\) 天到了第 \(j\) 个休息点的目前最小的 \(\sum_{i=1}^j x_i^2\)

答案即为 \(f_{m,n}\)

考虑如何转移:

\[f_{i,j}=\min\{f_{i-1,k}+val^2(k,j)\} \]

\(sum_i=\sum_{j=1}^i a_i\),得到:

\[val(k,j)=sum_j-sum_k \]

于是转移就变成了:

\[f_{i,j}=\min_{k\in[1,j]}f_{i-1,k}+(sum_j-sum_k)^2 \]

典型的斜率优化dp模式。

化简得到:

\[f_{i,j}=\min_{k\in[1,j]}f_{i-1,k}+sum_j^2-sum_ksum_j+sum_k^2 \]

\(sum\) 无负数单调递增,只需要考虑单调队列即可。

但是我们发现 \(n,m\leq 3000\),所以说这个转移时 \(\mathcal{O}(n^2m)\) 的。

赛时决定先用决策单调性优化一下,然后对拍完之后再写正解以提高对拍效率和解题效率。

是这样的:

auto square = [](int x) {
    return x * x;
};
for (int i = 1;i <= m;i ++)
    for (int j = 1,t = 0;j <= n;j ++)
        for (int k = t;k <= j;k ++)
            if (f[i][j] > f[i - 1][k] + square(sum[j] - sum[k])) f[i][j] = f[i - 1][k] + square(sum[j] - sum[k]),t = k;
cout << f[m][n] * m - square(sum[n]);

如何用单调队列优化斜率dp呢?还是十分套路。

先考虑 \(k,l\) 两个可能的状态转移,其中 \(k<l\leq j\),但是 \(l\) 更优于 \(k\),也就是说:

\[f_{i-1,l}+sum_j^2-2sum_lsum_j+sum_l^2<f_{i-1,k}+sum_j^2-2sum_ksum_j+sum_k^2 \]

移项得到:

\[f_{i-1,l}+sum_l^2-(f_{i-1,k}+sum_k^2)<2(sum_l-sum_k)sum_j\\\Rightarrow \frac{f_{i-1,l}+sum_l^2-(f_{i-1,k}+sum_k^2)}{sum_l-sum_k}<2sum_j \]

也就是说我们需要维护这个 \((sum_x,f_{i-1,x}+sum_x^2)\) 这些点集,首先队头如果满足这个条件,那么就直接退队,维护斜率单调递增即可。

完美用斜率优化dp解决,时间复杂度 \(\mathcal{O}(nm)\)。(别平方的时候忘记中间项 \(\times2\) 啊!)

auto square = [](int x) {
    return x * x;
};
auto dx = [](int i,int k,int l) {
    return sum[l] - sum[k];
};
auto dy = [](int i,int k,int l) {
    return f[i - 1][l] + sum[l] * sum[l] - f[i - 1][k] - sum[k] * sum[k];
};
for (int i = 1;i <= m;i ++) {
    int head = 1,tail = 1;
    q[tail] = 0;
    for (int j = 1;j <= n;j ++) {
        while(head < tail && dy(i,q[tail],j) * dx(i,q[tail - 1],q[tail]) < dx(i,q[tail],j) * dy(i,q[tail - 1],q[tail])) tail --;
        q[++tail] = j;
        while(head < tail && dy(i,q[head],q[head + 1]) <= 2 * sum[j] * dx(i,q[head],q[head + 1])) head ++;
        f[i][j] = min(f[i][j],f[i - 1][q[head]] + square(sum[j] - sum[q[head]]));
//			cout << i << ' ' << j << ' ' << q[head] << ' ' << f[i][j] << '\n';
    }
}
cout << f[m][n] * m - sum[n] * sum[n];
posted @ 2025-10-05 10:44  high_skyy  阅读(27)  评论(0)    收藏  举报
动态线条
动态线条end
浏览器标题切换
浏览器标题切换end
💬 加载中……