斜率优化dp总结
Part0
本篇博客主要讲的是斜率优化一些最基础的定义以及做法,高级版可以见 这篇博客
Part1
在某些情况下,\(dp\) 的时间复杂度仍然超出了题目的限制,这时我们就要考虑对其进行优化
斜率优化是对决策进行优化的一种方法
它适用于类似 \(f[i]=min/max(a[i]×b[j]+c[i]+d[j])\) 的方程
斜率优化由一个单调队列进行维护,维护满足要求的最优下标,在更新当前的 \(f\) 也就是 \(dp\) 数组时,直接取队首即可。
接下来以几道简单的例题讲解
例一 [APIO2010]特别行动队
分析
题意很简单,让我们任意分组,使得分出来组的那一段 \(sum\) 在进行那个式子的运算之后最大,于是我们可以推转移方程。
定义 \(f[i][j]\) 表示前 \(i\) 个人,分成 \(j\) 组的最大值。
容易得到 \(f[i][j] = \max_{1\leqslant k \leqslant i}(f[i][j],f[k][j-1] + a\times (sum[i]-sum[k])^2+b\times (sum[i]-sum[k]) + c)\)
我们可以发现每次的最大值都是从之前转移来,并且第二维是没有意义的,所以可以转化为一维。
要使当前被最优地更新,我们可以得到如下不等式:
\(f_{k1}+a\times (sum_i-sum_{k1})^2+b\times (sum_i-sum_{k1}) + c > f_{k2}+a\times (sum_i-sum_{k2})^2+b\times (sum_i-sum_{k2}) + c\)
然后移项使不等号一边只剩下常数,得到:
于是我们可以得到斜率为 \(2\times a\times sum_i\)。
由于 \(a<0\) ,斜率单调减,那么维护一个上凸包即可。
代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define gc() (p1 == p2 ? (p2 = buf + fread(p1 = buf, 1, 1 << 20, stdin), p1 == p2 ? EOF : *p1++) : *p1++)
#define read() ({ register int x = 0, f = 1; register char c = gc(); while(c < '0' || c > '9') { if (c == '-') f = -1; c = gc();} while(c >= '0' && c <= '9') x = x * 10 + (c & 15), c = gc(); f * x; })
char buf[1 << 20], *p1, *p2;
#define db double
const int maxn = 1e6+10;
int x[maxn];
int f[maxn];
int sum[maxn];
int q[maxn];
int a,b,c;
inline db calc(int k1,int k2){
return 1.0 * (db)(f[k1] - f[k2] - a * sum[k2] * sum[k2] + a * sum[k1] * sum[k1] + b * sum[k2] - b * sum[k1]) / (db)(sum[k1] - sum[k2]);
}
signed main(){
int n = read();
a = read(),b = read(),c = read();
for (int i = 1; i <= n; i++) {
x[i] = read();
sum[i] = sum[i-1] + x[i];
}
int head = 1,tail = 1;
for(int i = 1;i <= n;++i){
while(head < tail && calc(q[head],q[head+1]) >= 2.0 * a * sum[i])head++;
f[i] = f[q[head]] + a * (sum[i] - sum[q[head]]) * (sum[i] - sum[q[head]]) + b * (sum[i] - sum[q[head]]) + c;
while(head < tail && calc(q[tail],q[tail-1]) <= calc(q[tail],i))tail--;
q[++tail] = i;
}
printf("%lld\n",f[n]);
return 0;
}
例二 军训队列
题目描述
有 \(n\) 名学生参加军训,军训的一大重要内容就是走队列,而一个队列的不规整程度是该队中最高的学生的身高与最矮的学生的身高差值的平方。现在要将 \(n\) 名参加军训的学生重新分成 \(k\) 个队列,每个队列的人数不限,请求出所有队列的不规整程度之和的最小值。
输入格式
第一行两个整数 \(n,k\) ,表示学生人数和队列数。
第二行 \(n\) 个实数,表示每名学生的身高。身高范围在 \(140∼200cm\) 之间,保留两位小数。
输出格式
一个实数表示答案,保留 \(2\) 位小数。
样例
样例输入1:
3 2
170.00 180.00 168.00
样例输出1:
4.00
样例输入2:
5 2
170.00 180.00 168.00 140.59 199.99
样例输出2:
1023.36
数据范围与提示
数据点 | \(n\) | \(k\) |
---|---|---|
\(1-2\) | \(\leqslant 10\) | \(\leqslant 5\) |
\(3-6\) | \(\leqslant 100\) | \(\leqslant 10\) |
\(7,8\) | \(\leqslant 10^5\) | \(1\) |
\(9,10\) | \(\leqslant 10^5\) | \(2\) |
\(11,12\) | \(\leqslant 10^5\) | \(3\) |
\(13,14\) | \(\leqslant 10^5\) | \(4\) |
\(15-20\) | \(\leqslant 10^5\) | \(\leqslant 20\) |
分析
首先最简单的方法就是离散化一下,直接暴力 \(dp\) ,显然是可以过的。
但是我们作为新时代的青年当然要搞优秀的算法,所以考虑把暴力 \(dp\) 改成斜率优化。
与上一个题类似,状态转移方程仍然是二维的,转移也很容易写出来,即:
这里我们需要保留第二维 \(j\) ,具体没有什么大影响,只需要在计算斜率的时候多传一个参就行了。
然后我们推不等式,容易得到:
然后跟上一个题一样单调队列维护斜率即可。由于 \(x\) 是单调的,所以我们只需要看斜率,维护一个下凸包即可。
代码
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#define db double
#define rint register int
#define max(a,b) (a > b ? a : b)
#define min(a,b) (a < b ? a : b)
const int maxn = 1e5+10;
int n,k;
db a[maxn];
db f[maxn][25];
int q[maxn],head,tail;
inline db calc(int k1,int k2,int j){
return (f[k1][j-1] - f[k2][j-1] + a[k1+1] * a[k1+1] - a[k2+1] * a[k2+1]) / (a[k1+1] - a[k2+1]);
}
int main(){
scanf("%d%d",&n,&k);
if(k > n)return puts("0.00"),0;
for(rint i = 1;i <= n;++i){
scanf("%lf",&a[i]);
}
std::sort(a+1,a+n+1);
rint tot = std::unique(a+1,a+n+1) - a - 1;
head = 1,tail = 0;
for(rint i = 1;i <= tot;++i)f[i][1] = (a[1] - a[i]) * (a[1] - a[i]);
for(rint j = 2;j <= k;++j){
head = 1,tail = 0;
for(rint i = j;i <= tot;++i){
while(head<tail&&calc(q[head],q[head+1],j)<=2*a[i])head++;
f[i][j]=f[q[head]][j-1]+(a[q[head]+1]-a[i])*(a[q[head]+1]-a[i]);
while(head<tail&&calc(q[tail],q[tail-1],j)>calc(q[tail],i,j))tail--;
q[++tail]=i;
}
}
printf("%.2lf\n",f[tot][k]);
return 0;
}
例三 [APIO2014]序列分割
题目
分析
一般的套路题,分组的暴力 \(dp\) 应该非常好想,这里不再一点一点推,然后我们会发现,每次分组是把当前组的两部分乘起来,然后就瞬间没了思路。
由于一个一个区间进行处理不现实,所以一定有什么结论,然后开始推:
假设有 \(4\) 个数 \(a,b,c,d\) 组成一个序列,给出一个固定的分组 \({a},{b,c},{d}\) 。
我们会发现分组的先后顺序对答案没有影响(建议自己多举几个例子),然后我们就可以跟一般的分组 \(dp\) 一样转移了。
容易得出状态转移:
然后我们又开始写斜率不等式:
维护下凸包。
代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define gc() (p1 == p2 ? (p2 = buf + fread(p1 = buf, 1, 1 << 20, stdin), p1 == p2 ? EOF : *p1++) : *p1++)
#define read() ({ register int x = 0, f = 1; register char c = gc(); while(c < '0' || c > '9') { if (c == '-') f = -1; c = gc();} while(c >= '0' && c <= '9') x = x * 10 + (c & 15), c = gc(); f * x; })
char buf[1 << 20], *p1, *p2;
const int maxn = 1e5+10;
#define db double
int n,k;
int f[maxn][210],pre[maxn][210];
int sum[maxn];
int q[maxn];
inline db calc(int k1,int k2,int j){
if(sum[k1] == sum[k2])return -1e18;
return (db)(f[k1][j-1] - f[k2][j-1] + sum[k2] * sum[k2] - sum[k1] * sum[k1]) / (db)(sum[k2] - sum[k1]);
}
inline void print(int x,int gs){
if(gs == 0)return;
print(pre[x][gs],gs-1);
printf("%lld ",x);
}
signed main(){
n = read(),k = read();
for (int i = 1; i <= n; i++)sum[i] = sum[i-1] + read();
int head = 1,tail = 1;
for(int j = 1;j <= k;++j){
head = tail = 0;
for(int i = j;i <= n;++i){
while(head < tail && calc(q[head],q[head+1],j) <= sum[i])head++;
pre[i][j] = q[head];
f[i][j] = f[q[head]][j-1] + sum[q[head]] * (sum[i] - sum[q[head]]);
while(head < tail && calc(q[tail],q[tail-1],j) >= calc(q[tail],i,j))tail--;
q[++tail] = i;
}
}
printf("%lld\n",f[n][k]);
int nn = n;
for(int i = k;i >= 1;--i){
nn = pre[nn][i];
printf("%lld ",nn);
}
return 0;
}
Part2
高级一些的斜率优化 \(dp\) ,先咕咕咕(逃