【笔记】斜率优化(待更新)

斜率优化

听说斜率优化是个很套路的东西 ?

\(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\) 的时候最少的花费。

\[f[i] = \min\{f[j] + (\sum_{k = j - 1} ^ i C_k) ^ 2 + M \ \ | \ \ j < i\} \]

我们再统计个前缀和,\(S[i]\) 表示从 \(a[1]\)\(a[i-1]\) 的数字和,得到 :

\[f[i] = \min\{f[j]+(S[i+1]−S[j])^2+M \ \ | \ \ j < i\} \]

注意这里为了方便起见前缀和与一般的有区别,就是让式子看起来更好看,没别的特殊意义。

之后两重循环枚举即可。

之后我们就发现这是个 \(O(N ^ 2)\) 的复杂度。

考虑优化 :

那么我们想,能否在 \(O(1)\) 时间内找到所有转移里最优的那个呢?

我们假设在求解 \(f[i]\) 时,存在 \(j,k (j > k)\) 使得从 \(j\) 转移比从 \(k\) 转移更优,那么需要满足条件:

\[f[j] + (S[i + 1] - S[j]) ^ 2 + M < f[k] + (S[i + 1] - S[k]) ^ 2 + M \]

展开 :

\[f[j] + (S[i + 1]) ^ 2 - 2 \times S[i + 1] \times S[j] + (S[j]) ^ 2 + M < f[k] + (S[i + 1]) ^ 2 - 2 \times S[i + 1] \times S[k] + (S[k]) ^ 2 + M \]

移项并消去再合并同类项得 :

\[(f[j] + (S[j]) ^ 2) - (f[k] + (S[k]) ^ 2) < 2 \times S[i + 1](S[j] - S[k]) \]

继续推导 :

我们把 \(S[j] - S[k]\) 除过去,得到 :

\[\frac{(f[j] + (S[j]) ^ 2) - (f[k] + (S[k]) ^ 2)}{S[j] - S[k]} < s2 \times S[i + 1] \]

我们可以设 \(dp[x] = f[x] + (S[x]) ^ 2\),就化成了 :

\[\frac{dp[j] - dp[k]}{S[j] - S[k]} < 2 \times S[i + 1] \]

即当 \((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]\) 的关系有三种:

\[\dfrac{dp[j] - dp[k]}{S[j] - S[k]} > \dfrac{dp[i] - dp[j]}{S[i] - S[j]} > 2 \times S[t + 1] \]

那么 \(j\)\(i\) 优,\(k\)\(j\) 优。

\[\dfrac{dp[j] - dp[k]}{S[j] - S[k]} > 2 \times S[t + 1] > \dfrac{dp[i] - dp[j]}{S[i] - S[j]} \]

那么 \(i\)\(j\) 优,\(k\)\(j\) 优。

\[2 \times S[t + 1] > \dfrac{dp[j] - dp[k]}{S[j] - S[k]} > \dfrac{dp[i] - dp[j]}{S[i] - S[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\) 个的最小代价。

那么就可以得到 :

\[f[i] = \min\{f[j] + \sum_{k = j + 1} ^ i (i - k) + a[i] \} \\ = f[j] + i \times (i - j) - \sum_{k = j + 1} ^ i k + a[i] \\ = f[j] + i \times (i - j) - (sum[i] - sum[j]) + a[i] \]

其中 \(sum[i]\)\(1\)\(i\) 的和。

我们假设在求解 \(f[i]\) 时,存在 \(j,k (j > k)\) 使得从 \(j\) 转移比从 \(k\) 转移更优,那么需要满足条件:

\[f[j] + i \times (i - j) - (sum[i] - sum[j]) + a[i] < f[k] + i \times (i - k) - (sum[i] - sum[k]) + a[i] \]

我们把式子化简 :

\[f[j] - f[k] + sum[j] - sum[k] < i \times (j - k) \]

\[(f[j] + sum[j]) - (f[k] + sum[k]) < i \times (j - k) \]

\[\dfrac{(f[j] + sum[j]) - (f[k] + sum[k])}{(j - k)} < i \]

即当 \((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\) 件玩具放在一个盒子里的最小费用。

我们很容易得到转移方程 :

\[f[i] = \min\{f[j] + (i - j - L + \sum_{k = j + 1} ^ i C_k - 1) ^ 2 \ \ | \ \ j < i\} \]

我们设 \(sum[i] = \sum_{j = 1} ^ i C_j\),得到 :

\[f[i] = \min\{f[j] + (sum[i] + i - sum[j] - j - L - 1) ^ 2\} \]

我们发现这样很那化简,谁想算一个五项的平方?我们发现 \(sum[i] + i\) 可以预处理求出,所以我们用 \(t[i]\) 代替 \(sum[i] + i\),得到 :

\[f[i] = \min\{f[j] + (t[i] - t[j] - L - 1) ^ 2\} \]

我们设 \(j,k(j > k)\),使从 \(j\) 转移比从 \(k\),转移更优,所以 :

\[f[j] + (t[i] - t[j] - L - 1) ^ 2 < f[k] + (t[i] - t[k] - L - 1) ^ 2 \]

\[f[j] + (t[j] + L + 1) ^ 2 + t[i] ^ 2 - 2 \times t[i] \times (t[j] + L + 1) < f[k] + (t[k] + L + 1) ^ 2 + t[i] ^ 2 - 2 \times t[i] \times (t[k] + L + 1) \]

\[f[j] - f[k] + (t[j] + L + 1) ^ 2 - (t[k] + L + 1) ^ 2 < 2 \times t[i] \times (t[j] + L + 1) - 2 \times t[i] \times (t[k] + L + 1) \]

我们用 \(G[i] = (t[i] + L + 1) ^ 2\)

\[(f[j] + G[j]) - (f[k] + G[k]) < 2 \times t[i] \times (t[j] - t[k]) \]

\[\frac{(f[j] + G[j]) - (f[k] + G[k])}{t[j] - t[k]} < 2 \times t[i] \]

所以这个毒瘤简单的式子就被化简出来了。

发现是个小于号,下凸包。

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

综上所述,我们推得状态转移方程为 :

\[f_i = \min\{f_j + S \times \sum_{k = j + 1} ^ n C_{k} + \sum_{k = 1} ^ i T_k \times \sum_{k = j + 1} ^ i C_k \ \ | \ \ j < i\} \]

很显然,这是一个 \(O(\frac{n ^ 2}{2})\) ,的算法,只能得到 \(20pts\),或者您可以右转进入弱化版

\(t_i = \sum_{k = 1} ^ i T_i,w_i = \sum_{k = 1} ^ i C_i\)

\[f_i = \min\{f_j + S \times (w_n - w_j) + t_i \times (w_i - w_j)\} \]

我们设 \(j,k(j > k)\),使从 \(j\) 转移比从 \(k\),转移更优,所以 :

\[f_j + S \times (w_n - w_j) + t_i \times (w_i - w_j) \leq f_k + S \times (w_n - w_k) + t_i \times (w_i - w_k) \]

\[f_j - f_k + S \times (w_k - w_j) + t_i \times (w_k - w_j) \leq 0 \]

\[f_j - f_k + S \times (w_k - w_j) \leq t_i \times (w_j - w_k) \]

\[\frac{f_j - f_k + S \times (w_k - w_j)}{w_j - w_k} \leq t_i \]

\[\frac{f_j - f_k + S \times (w_k - w_j)}{w_j - w_k} \leq t_i \]

终于化简完了,但是 !!!

三种情况 :

\[\dfrac{f_j - f_k + S \times (w_k - w_j)}{w_j - w_k} > \dfrac{f_i - f_j + S \times (w_j - w_i)}{w_i - w_j} > t_i \]

\[\dfrac{f_j - f_k + S \times (w_k - w_j)}{w_j - w_k} > t_i > \dfrac{f_i - f_j + S \times (w_j - w_i)}{w_i - w_j} \]

\[t_i > \dfrac{f_j - f_k + S \times (w_k - w_j)}{w_j - w_k} > \dfrac{f_i - f_j + S \times (w_j - w_i)}{w_i - w_j} \]

我们发现这个题目不能直接进行维护了,因为我们的单调性不知道,我们发现无论哪一种情况,我们均不能判断大小。

这个题目 : \(\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\) 都是题目中给的常量。

\[f_i = \max\{f_j + a \times (\sum_{k = j + 1} ^ i x_k) + b \times \sum_{k = j + 1} ^ i x_k + c\} \]

考虑优化 :

当然是上斜率优化了

先用 \(sum_i = \sum_{j = 1} ^ i x_k\)

我们设 \(j,k(j > k)\),使从 \(j\) 转移比从 \(k\),转移更优,所以 :

\[f_j + a \times (sum_i - sum_j) ^ 2 + b \times (sum_i - sum_j) + c \geq f_k + a \times (sum_i - sum_k) ^ 2 + b \times (sum_i - sum_k) + c \]

\[f_j - f_k \geq a \times (sum_k ^ 2 - sum_j ^ 2 + 2 \times sum_i \times (sum_j - sum_k)) + b \times (sum_j - sum_k) \]

\[f_j - f_k + a \times sum_j ^ 2 - a \times sum_k ^ 2 \geq (2 \times a \times sum_i + b) \times (sum_j - sum_k) \]

因为 \(sum_j > sum_k\),所以不用变号。

\[\frac{f_j - f_k + a \times sum_j ^ 2 - a \times sum_k ^ 2}{sum_j - sum_k} \geq 2 \times a \times sum_i + b \]

我们用 \(G_i\) 表示 \(a \times sum_j ^ 2\)

\[\frac{(f_j + G_j) - (f_k + G_k)}{sum_j - sum_k} \geq 2 \times a \times sum_i + b \]

这后发现是个小于号,下凸包。

\(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;
}

posted @ 2021-03-17 15:49  Ti_Despairy  阅读(88)  评论(0)    收藏  举报