【笔记】斜率优化(待更新)
斜率优化
听说斜率优化是个很套路的东西 ?
【\(Print \ \ Article\)】
【\(Description\)】
要输出 \(n\) 个数字 \(a_i \to a_n\),每个字符的输出花费 \(C_i\) 元,输出的时候可以连续的输出,每输出一串,它的费用是 : \((\sum_{i = 1} ^ k C_i) ^ 2 + M\),其中 \(M\) 是一个常数。
求最小的费用。
\(0 \leq n \leq 5 \times 10 ^ 6\) , \(0 \leq m \leq 10 ^ 3\)。
【\(Solution\)】
我们看到这个题目,发现 DP 方程很好设计。
我们设 \(f[i]\) 表示输出到 \(i\) 的时候最少的花费。
我们再统计个前缀和,\(S[i]\) 表示从 \(a[1]\) 到 \(a[i-1]\) 的数字和,得到 :
注意这里为了方便起见前缀和与一般的有区别,就是让式子看起来更好看,没别的特殊意义。
之后两重循环枚举即可。
之后我们就发现这是个 \(O(N ^ 2)\) 的复杂度。
考虑优化 :
那么我们想,能否在 \(O(1)\) 时间内找到所有转移里最优的那个呢?
我们假设在求解 \(f[i]\) 时,存在 \(j,k (j > k)\) 使得从 \(j\) 转移比从 \(k\) 转移更优,那么需要满足条件:
展开 :
移项并消去再合并同类项得 :
继续推导 :
我们把 \(S[j] - S[k]\) 除过去,得到 :
我们可以设 \(dp[x] = f[x] + (S[x]) ^ 2\),就化成了 :
即当 \((j > k)\) 时,若 \(\dfrac{dp[j] - dp[k]}{S[j] - S[k]} < 2 \times S[i + 1]\),则 \(j\) 对更新 \(f[i]\) 比 \(k\) 更新 \(f[i]\) 优。
让我们来继续推 :
那把 \((s[i],dp[i])\) 看作一个点,左边就是斜率的形式了。
当一个数的 \(f\) 值求完了,它的 \(dp\) 值也跟着确定,我们就可以在空间中绘制出点 \((s[i],dp[i])\) 。这个点代表已经求出 \(f\) 值的一个点。
当我们要求解 \(f[t]\) 时,如果可用的集合里存在这样三个点,位置关系如图所示:

这时候他们和 \(2 \times S[t + 1]\) 的关系有三种:
那么 \(j\) 比 \(i\) 优,\(k\) 比 \(j\) 优。
那么 \(i\) 比 \(j\) 优,\(k\) 比 \(j\) 优。
那么 \(i\) 比 \(j\) 优,\(j\) 比 \(k\) 优。
综上,不管什么样的 \(S[t+1]\),从 \(j\) 转移都不会是最佳方案。那么用一个数据结构维护一个凸包 (下凸),每加入一个点就删去一些点,使其维持凸包的形态,最优转移一定在这个凸包中。
下凸的凸包边斜率增加,上凸的凸包边斜率减小。
在凸包里,谁又是最最优呢 ? 首先一定数据结构里的凸包一定会是这样的:

我的 \(f\) 和 \(dp\) 又写反了。这里的 \(f\) 应该是 \(dp\)。
假设 \(\overrightarrow{j i}\) 的斜率 \(>2 \times S[t+1]\) 且 \(\overrightarrow{k j}\) 的斜率 \(< 2 \times S[t+1]\) 从图形特点我们可以发现 \(j\) 点比所有比 \(k\) 小的点都优,比所有比 \(i\) 大的也优。
所以对于我们二分查找斜率比 \(2 \times S[t+1]\) 小的编号最大的点,就是最优的转移点。
由于 \(S[i]\) 也满足单调性,那么如果一个点不是 \(i\) 的最优点了,那么肯定也不是 \(i+1\) 的,我们还可以直接维护一个单调队列就能解决这个问题。
单调队列每次从后加时,维护凸包。
每次新计算一个 \(i\) 的 \(f\) 值,从单调队列队首弹出那些不可能再合法的元素。
【\(Summary\)】
看似推导很多,其实是很套路的,并且很多都是在证明和理解,多做两道题就掌握了。
设 \(j>k\) 且 \(j\) 优于 \(k\),以此列个式子。
推式子的时候,把只与 \(j\) 和 \(k\) 有关放在不等号左边,带 \(i\) 有关的项放在不等式右边。
设出点的坐标,根据推出方程的不等号,是大于号,那么上凸,维护斜
率递减。
小于号,下凸,维护斜率递增。


【防御准备】
【\(Description\)】

\(1 \leq n \leq 10 ^ 6,1 \leq A_i \leq 10 ^ 9\)
题目挂了只有图片。
【\(Solution\)】
设 \(f[i]\) 为第 \(i\) 个建检查站时前 \(i\) 个的最小代价。
那么就可以得到 :
其中 \(sum[i]\) 为 \(1\) ∼ \(i\) 的和。
我们假设在求解 \(f[i]\) 时,存在 \(j,k (j > k)\) 使得从 \(j\) 转移比从 \(k\) 转移更优,那么需要满足条件:
我们把式子化简 :
即当 \((j > k)\) 时并且 \(\dfrac{(f[j] + sum[j]) - (f[k] + sum[k])}{(j - k)} < i\) 时,则 \(j\) 对更新 \(f[i]\) 比 \(k\) 更新 \(f[i]\) 优。
感性理解就是,如果两个决策点的斜率小于 \(i\) 那么就是靠后的决策点更优,否则就是靠前的更优。
还有就是我们推出来的是小于号,就说明是下凸,否则就是上凸。(自己的理解)
这两句话很重要 !!!
所以我们可以发现这是一个下凸的函数。
【\(Code\)】
#include <cstdio>
#include <cmath>
#include <iostream>
#include <cstring>
#include <algorithm>
#define int long long
using namespace std;
const int Maxk = 1e6 + 10;
int n,l,r;
int a[Maxk];
int q[Maxk];//单调队列
int f[Maxk];
int sum[Maxk];
inline int read()
{
int s = 0, f = 0;char ch = getchar();
while (!isdigit(ch)) f |= ch == '-', ch = getchar();
while (isdigit(ch)) s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
int Calc(int x,int y) {
return (f[x] + sum[x]) - (f[y] + sum[y]);
}
signed main()
{
n = read();
for(int i = 1;i <= n;i ++) a[i] = read(),sum[i] = sum[i - 1] + i;
for(int i = 1;i <= n;i ++) {
while(l < r && Calc(q[l + 1],q[l]) < i * (q[l + 1] - q[l])) l ++;
f[i] = f[q[l]] + i * (i - q[l]) - (sum[i] - sum[q[l]]) + a[i];
while(l < r && Calc(i,q[r]) * (q[r] - q[r - 1]) < Calc(q[r],q[r - 1]) * (i - q[r])) r --;
q[++ r] = i;
}
printf("%lld\n",f[n]);
return 0;
}
斜率优化其实就是一个优化 \(dp[i]= \max/\min\{f[j]+g[i]*h[j]\}+a[i]\) 式子的一个通用方法。
除了推式子的部分,还要保证,推出来等式的右边要单调,不单调,就要在凸壳上二分。
等式左边抽象出来的点的 \(X\) 坐标也要单调,\(Y\) 坐标不需要保证单调。
当然其实 \(X\) 不单调和等式右边不单调也都能做,只不过难度较大,需要用
到 \(CDQ\) 分治,或平衡树维护凸包的技巧。(很显然我不会)
【玩具装箱】
【\(Description\)】
自己看
【\(Solution\)】
设 \(f[i]\) 表示前 \(i\) 件玩具放在一个盒子里的最小费用。
我们很容易得到转移方程 :
我们设 \(sum[i] = \sum_{j = 1} ^ i C_j\),得到 :
我们发现这样很那化简,谁想算一个五项的平方?我们发现 \(sum[i] + i\) 可以预处理求出,所以我们用 \(t[i]\) 代替 \(sum[i] + i\),得到 :
我们设 \(j,k(j > k)\),使从 \(j\) 转移比从 \(k\),转移更优,所以 :
我们用 \(G[i] = (t[i] + L + 1) ^ 2\)。
所以这个毒瘤简单的式子就被化简出来了。
发现是个小于号,下凸包。
【\(Code\)】
#include <cstdio>
#include <cmath>
#include <iostream>
#include <cstring>
#include <algorithm>
#define int long long
using namespace std;
const int Maxk = 5e4 + 100;
int n,L,tail,head;
int t[Maxk],f[Maxk];
int a[Maxk],G[Maxk];
int q[Maxk];
inline int read()
{
int s = 0, f = 0;char ch = getchar();
while (!isdigit(ch)) f |= ch == '-', ch = getchar();
while (isdigit(ch)) s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
double calc(int x,int y)
{
return (double)((f[x] + G[x]) * 1.0 - (f[y] + G[y]) * 1.0) / (t[x] - t[y]) * 1.0;
}
signed main()
{
n = read(),L = read();
for(int i = 1;i <= n;i ++) {
a[i] = read() + a[i - 1];
t[i] = a[i] + i;
G[i] = (t[i] + L + 1) * (t[i] + L + 1);
}
G[0] = (L + 1) * (L + 1);
for(int i = 1;i <= n;i ++) {
while(head < tail && calc(q[head + 1],q[head]) < 2 * t[i]) head ++;
f[i] = f[q[head]] + (t[i] - t[q[head]] - L - 1) * (t[i] - t[q[head]] - L - 1);
while(head < tail && calc(i,q[tail]) < calc(q[tail],q[tail - 1])) tail --;
q[++ tail] = i;
}
printf("%lld",f[n]);
return 0;
}
【任务安排】
【\(Description\)】
看题面。
【\(Solution\)】
当我们看到这个题目的数据范围的时候,发现这只能是一个一维的 DP。
推转移方程,发现 \(f_i\) 的值与 \(C_i,T_i,S\) 有关,这三个变量为答案做出了贡献。
我们设 \(f[i]\),表示一个任务组到 \(i\) 结束的最小花费。
发现每一次开机都会使得排在后面的任务的花费增加一倍,而且完成第 \(i\) 个任务所花费的时间是 \(T_i \times \sum_{k = j + 1} ^ i C_k\)。
综上所述,我们推得状态转移方程为 :
很显然,这是一个 \(O(\frac{n ^ 2}{2})\) ,的算法,只能得到 \(20pts\),或者您可以右转进入弱化版。
令 \(t_i = \sum_{k = 1} ^ i T_i,w_i = \sum_{k = 1} ^ i C_i\)。
我们设 \(j,k(j > k)\),使从 \(j\) 转移比从 \(k\),转移更优,所以 :
终于化简完了,但是 !!!
三种情况 :
我们发现这个题目不能直接进行维护了,因为我们的单调性不知道,我们发现无论哪一种情况,我们均不能判断大小。
这个题目 : \(\left\vert T_i \right\vert \leq 10 ^ 8\),\(T_i\) 可能是一个负数,不满足单调性,我们不能直接往里面代入了,只能用单调队列储存一下决策点,用二分查找来寻找。
这不 so easy (逃
其实我不会二分/kk
【\(Code\)】
#include <cstdio>
#include <cmath>
#include <iostream>
#include <cstring>
#include <algorithm>
#define int long long
using namespace std;
const int Maxk = 1e6 + 10;
int n,s,head = 1,tail;
int t[Maxk],T[Maxk];
int c[Maxk],w[Maxk];
int f[Maxk],q[Maxk];
inline int read()
{
int s = 0, f = 0;char ch = getchar();
while (!isdigit(ch)) f |= ch == '-', ch = getchar();
while (isdigit(ch)) s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
bool check(int x,int i)
{
if(f[q[x + 1]] - f[q[x]] > (s + t[i]) * (w[q[x + 1]] - w[q[x]])) return true;
else return false;//如果这样成立,说明更优
}
int calc(int l,int r,int i)
{
int id = tail;
while(l <= r) {
int mid = (l + r) >> 1;
if(check(mid,i)) r = mid - 1,id = mid;
else l = mid + 1;
}
return id;
}
signed main()
{
n = read(),s = read();
for(int i = 1;i <= n;i ++) {
T[i] = read();
c[i] = read();
w[i] = w[i - 1] + c[i];
t[i] = t[i - 1] + T[i];
}
q[++ tail] = 0;
for(int i = 1;i <= n;i ++) {
int id = calc(head,tail,i);
f[i] = f[q[id]] + s * (w[n] - w[q[id]]) + t[i] * (w[i] - w[q[id]]);
while(head < tail &&
(f[q[tail]] - f[q[tail - 1]]) * (w[i] - w[q[tail]]) >=
(f[i] - f[q[tail]]) * (w[q[tail]] - w[q[tail - 1]]) ) tail --;
q[++ tail] = i;
}
printf("%lld",f[n]);
return 0;
}
【特别行动队】
【\(Description\)】
见题面。
【\(Solution\)】
我们设到 \(f_i\) 为一个队伍到 \(i\) 结束的最大战斗力。
似乎转移方程很简单,其中 \(a,b,c\) 都是题目中给的常量。
考虑优化 :
当然是上斜率优化了
先用 \(sum_i = \sum_{j = 1} ^ i x_k\)。
我们设 \(j,k(j > k)\),使从 \(j\) 转移比从 \(k\),转移更优,所以 :
因为 \(sum_j > sum_k\),所以不用变号。
我们用 \(G_i\) 表示 \(a \times sum_j ^ 2\)。
这后发现是个小于号,下凸包。
【\(Code\)】
#include <cstdio>
#include <cmath>
#include <iostream>
#include <cstring>
#include <algorithm>
#define int long long
using namespace std;
const int Maxk = 1e6 + 10;
int n,a,b,c,head,tail;
int q[Maxk],sum[Maxk];
int f[Maxk],g[Maxk];
inline int read()
{
int s = 0, f = 0;char ch = getchar();
while (!isdigit(ch)) f |= ch == '-', ch = getchar();
while (isdigit(ch)) s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
int calc(int x,int y)
{
return (f[x] + g[x]) - (f[y] + g[y]);
}
signed main()
{
n = read();
a = read(),b = read(),c = read();
for(int i = 1;i <= n;i ++) {
sum[i] = sum[i - 1] + read();
g[i] = a * sum[i] * sum[i];
}
for(int i = 1;i <= n;i ++) {
while(head < tail && calc(q[head + 1],q[head]) >= (2LL * a * sum[i] + b) * (sum[q[head + 1]] - sum[q[head]])) 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 - 1],i) * (sum[q[tail - 1]] - sum[q[tail]]) >= calc(q[tail - 1],q[tail]) * (sum[q[tail - 1]] - sum[i])) tail --;
q[++ tail] = i;
}
printf("%lld",f[n]);
return 0;
}

浙公网安备 33010602011771号