「SDOI2012」任务安排 3

知识点:斜率优化,二分

原题面:LojLuogu

四 步 走 战 略

简述

给定一列 \(n\) 个有序的物品,每个物品有两个属性 \((t_i, g_i)\),给定参数 \(s\)
要求将物品分为任意段,第 \(i\)\([l_i,r_i]\) 的代价为 \(\left(is + \sum_{j=l_i}^{r_i} t_j\right)\cdot \sum_{k=l_i}^{r_i}\),要求最小化分段的代价之和。
\(1\le n\le 3\times 10^5\)\(1\le s\le 2^8\)\(|t_i|\le 2^8\)\(0\le g_i\le 2^8\)
1S,512MB。

分析

发现分到第几段对答案有影响,设 \(f_{i,j}\) 表示将前 \(i\) 个任务分为 \(j\) 段的最小费用和,转移时枚举段数 \(k\) 和最后一段,则有:

\[f_{i,k} = \min_{j=0}^{i - 1} \left\{ f_{j,k-1} + \left(ks + \sum_{k=j+1}^{i} t_k \right)\sum_{l=j + 1}^{i} g_l \right\} \]

预处理前缀和,暴力转移时间复杂度 \(O(n^3)\),空间复杂度 \(O(n^2)\)。空间和时间都菜爆了。


发现在上述算法中必须枚举分到第几段,考虑能否优化掉状态的这一维,并优化转移。
这里用到了一种叫做「费用提前计算」的思想。发现每次转移将 \([j + 1,i]\) 这段分出后,后续元素的代价里都会加上 \(k\cdot g\),考虑在状态转移中加上这部分的影响。具体地,将状态删去一维,方程改写为如下所示:

\[f_i = \min_{j=0}^{i-1}\left\{ f_j + \sum_{k=1}^{i} t_k\sum_{l=j + 1}^{i} g_l + s\left( \sum_{k = j + 1}^{n} g_k\right)\right\} \]

状态转移方程很容易理解。此时已经无法准确定义 \(f\) 的含义了,但 \(f_n\) 一定表示将所有物品划分为某几段的最小代价和,且这样转移一定可以保证 \(f_n\) 的正确性。

预处理前缀和后暴力转移即可,时间复杂度 \(O(n^2)\),空间复杂度 \(O(n)\)


但上述 \(O(n^2)\) 算法不足以通过本题,发现出现了乘积项,考虑斜率优化。记 \(st_x = \sum_{i=1}^{x} t_i\)\(sg_x = \sum_{i=1}^x g_i\),代入转移方程并略作变换:

\[\begin{aligned} f_i &= \min_{j=0}^{i-1}\left\{ f_j + st_i\left( sg_i - sg_j \right) + s\left( sg_n - sg_j\right)\right\}\\ f_i - st_i\cdot sg_i - s\cdot sg_n&= \min_{j=0}^{i-1} \left\{ \left(f_j - s\cdot sg_j \right) - st_i\cdot sg_j \right\} \end{aligned}\]

这是一个显然的斜率优化的形式,设:

\[\begin{aligned} x_i &= sg_i\\ y_i &= f_ i- s\cdot sg_i\\ k_i &= st_i\\ b_i &= f_i - st_i\cdot sg_i - s\cdot sg_n \end{aligned}\]

如果 \(k_i\)\(x_i\) 均单调递增,套路地单调队列维护下凸包即可。总时空复杂度均为 \(O(n)\)
但本题中可能出现 \(t_i< 0\)\(k_i\) 是不单调的,这影响了最优决策点的选择,无法使用单调队列选择最优决策点。但 \(x_i\) 单调,使用单调队列维护下凸包的做法是正确的。

仍考虑使用单调队列维护下凸包,每次查询最优决策点时在凸包上二分,找到第一个使得左侧斜率小于 \(k_i\),右侧斜率不小于 \(k_i\) 的位置即为最优决策点,不从队首弹出元素。可以发现此时的“单调队列”实际上是一个单调栈。我们实际上是在用 Andrew 算法 维护凸包。

总时间复杂度变为 \(O(n\log n)\)

代码

 //知识点:斜率优化
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
#define LD long double
const int kN = 3e5 + 10;
const LL kInf = 9e18 + 2077;
//=============================================================
int n, h = 1, t, q[kN];
LL s, ans = kInf, f[kN], sumt[kN], sumg[kN];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmin(LL &fir, LL sec) {
  if (sec < fir) fir = sec;
}
LD X(int x_) {
  return sumg[x_];
}
LD Y(int x_) {
  return f[x_] - s * sumg[x_];
}
LD K(int x_, int y_) {
  if (X(x_) == X(y_)) return (Y(y_) > Y(x_) ? 1e18 : -1e18);
  return (LD) ((Y(y_) - Y(x_)) / (X(y_) - X(x_)));
}
bool Check(LD know_, int mid_) {
  return know_ <= K(q[mid_], q[mid_ + 1]);
}
int Query(int now_) {
  LD know = sumt[now_];
  int ret = t;
  for (int l = h, r = t - 1; l <= r; ) {
    int mid = (l + r) >> 1;
    if (Check(know, mid)) {
      ret = mid; //q[ret] 是最靠右的使得 check 为 true 的位置,即 q[ret] 左侧斜率小于 k,右侧斜率不小于 k。 
      r = mid - 1;
    } else {
      l = mid + 1;
    }
  }
  return q[ret];
}
void Insert(int now_) {
  while (h < t && K(q[t - 1], q[t]) >= K(q[t - 1], now_)) -- t;
  q[++ t] = now_;
}
//=============================================================
int main() { 
  n = read(), s = read();
  for (int i = 1; i <= n; ++ i) {
    sumt[i] = sumt[i - 1] + read();
    sumg[i] = sumg[i - 1] + read();
  }
  Insert(0);
  for (int i = 1; i <= n; ++ i) {
    int j = Query(i);
    f[i] = f[j] + sumt[i] * (sumg[i] - sumg[j]) + 
                  s * (sumg[n] - sumg[j]);
    Insert(i);
  }
  printf("%lld\n", f[n]);
  return 0; 
}
posted @ 2021-02-02 06:53  Luckyblock  阅读(51)  评论(1编辑  收藏  举报