斜率优化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\) 次,那么有:
顺序无关,所以我先在 \(i\) 切,然后再切前面的。
考虑斜率优化,对于 \(j<l\) 但 \(j\) 比 \(l\) 更优,并设 \(f_{x,j-1}=f_{j-1}\)。
有:
那么点集就是 \((-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\)。
显然有:
那么我们有 \(\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\) 菜。
有:
移项得到:
这里我要提一嘴:当我们左边的那个 \(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\) 排序):
套路搞一下(\(j<l\) 且 \(j\) 更优,列一下式子但是发现有 \(y\) 的干扰,于是我们打算只弄同一列的转移,那么 \(y\) 的那一项就是 \(0\) 了,于是)得到:
点集就是 \((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}\)。
那么可以得到方差公式:
首先立马得到:
代入定义式化简得到:
乘上 \(m^2\) 就可以得到:
wc,死去的回忆冲撞了我,这好像是斜率优化dp!
令 \(s=\sum x_i\),那么我们就是要最小化:
最小化 \(\sum x_i^2\) 即可。
设 \(f_{i,j}\) 表示前 \(i\) 天到了第 \(j\) 个休息点的目前最小的 \(\sum_{i=1}^j x_i^2\)。
答案即为 \(f_{m,n}\)。
考虑如何转移:
令 \(sum_i=\sum_{j=1}^i a_i\),得到:
于是转移就变成了:
典型的斜率优化dp模式。
化简得到:
\(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\),也就是说:
移项得到:
也就是说我们需要维护这个 \((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];

浙公网安备 33010602011771号