DP优化之斜率优化

文章讲述了如何通过将动态规划问题中的状态转移方程简化,并利用下凸壳数据结构,将时间复杂度从O(n)降低到O(logn),解决给定斜率的最小截距问题。实例以洛谷P2365任务安排为例,展示了如何在有限空间内存下处理任务安排问题。

题目描述

给定两个长度为 n n n 的数组 t , f t, f t,f 和一个正整数 s s s,设 s u m t [ i ] = ∑ j = 1 i t [ j ] sumt[i]=\sum_{j=1}^{i}t[j] sumt[i]=j=1it[j] s u m f [ i ] = ∑ j = 1 i f [ j ] sumf[i]=\sum_{j=1}^{i}f[j] sumf[i]=j=1if[j]

d p [ i ] [ j ] = min ⁡ { d p [ i − 1 ] [ p ] + ( s u m t [ j ] + s × i ) × ( s u m f [ j ] − s u m f [ p ] ) } , 0 ≤ p ≤ j − 1 dp[i][j]=\min\{dp[i - 1][p] + (sumt[j] + s \times i) \times (sumf[j] - sumf[p])\},0\le p\le j-1 dp[i][j]=min{dp[i1][p]+(sumt[j]+s×i)×(sumf[j]sumf[p])},0pj1

请优化上述方程,使状态转移时间复杂度从 O ( n ) O(n) O(n) 降低为 O ( log ⁡ n ) O(\log n) O(logn)

数据范围

1 ≤ n ≤ 5000 1\le n \le 5000 1n5000 0 ≤ s ≤ 50 0 \le s \le 50 0s50 1 ≤ t i , f i ≤ 100 1\le t_i,f_i \le 100 1ti,fi100

推导过程

d p [ i ] [ j ] = min ⁡ { d p [ i − 1 ] [ p ] + ( s u m t [ j ] + s × i ) × ( s u m f [ j ] − s u m f [ p ] ) } , 0 ≤ p ≤ j − 1 dp[i][j]=\min\{dp[i - 1][p] + (sumt[j] + s \times i) \times (sumf[j] - sumf[p])\},0\le p\le j-1 dp[i][j]=min{dp[i1][p]+(sumt[j]+s×i)×(sumf[j]sumf[p])},0pj1

去掉 min ⁡ \min min,并化简公式,

d p [ i − 1 ] [ p ] = ( s u m t [ j ] + s × i ) × s u m f [ p ] + d p [ i ] [ j ] − ( s u m t [ j ] + s × i ) × s u m f [ j ] dp[i - 1][p] = (sumt[j] + s \times i) \times sumf[p] + dp[i][j] - (sumt[j] + s \times i) \times sumf[j] dp[i1][p]=(sumt[j]+s×i)×sumf[p]+dp[i][j](sumt[j]+s×i)×sumf[j]

y ( p ) = d p [ i − 1 ] [ p ] y(p) = dp[i-1][p] y(p)=dp[i1][p] k ( i , j ) = s u m t [ j ] + s × i k(i,j) = sumt[j] + s \times i k(i,j)=sumt[j]+s×i x ( p ) = s u m f [ p ] x(p) = sumf[p] x(p)=sumf[p] b ( i , j ) = d p [ i ] [ j ] − ( s u m t [ j ] + s × i ) × s u m f [ j ] b(i,j) = dp[i][j] - (sumt[j] + s \times i) \times sumf[j] b(i,j)=dp[i][j](sumt[j]+s×i)×sumf[j],则上式可以写成,

y ( p ) = k ( i , j ) x ( p ) + b ( i , j ) y(p) = k(i,j)x(p) + b(i,j) y(p)=k(i,j)x(p)+b(i,j)

在这条式子里, y ( p ) y(p) y(p) k ( i , j ) k(i,j) k(i,j) x ( p ) x(p) x(p) 都是已知的,只有 b ( i , j ) b(i,j) b(i,j) 是未知的,因为 b ( i , j ) b(i,j) b(i,j) 包含要求的 d p [ i ] [ j ] dp[i][j] dp[i][j]

我们的目标是使 d p [ i ] [ j ] dp[i][j] dp[i][j] 最小,也就是使截距 b ( i , j ) b(i,j) b(i,j) 最小,如果把 ( x ( p ) , y ( p ) ) (x(p), y(p)) (x(p),y(p)) 看作平面上的点,只需要用一条斜率为 k ( i , j ) k(i,j) k(i,j) 的直线从下往上平移,那么直线第一个触碰到的点 ( x ( q ) , y ( q ) ) (x(q), y(q)) (x(q),y(q)) 就是我们需要的答案,此时 b ( i , j ) = y ( q ) − k ( i , j ) x ( q ) b(i,j)=y(q)-k(i,j)x(q) b(i,j)=y(q)k(i,j)x(q) d p [ i ] [ j ] = b ( i , j ) + ( s u m t [ j ] + s × i ) × s u m f [ j ] dp[i][j]=b(i,j)+(sumt[j] + s \times i) \times sumf[j] dp[i][j]=b(i,j)+(sumt[j]+s×i)×sumf[j]

从上图可以发现,只有在下凸壳边缘的点才有可能被选中,点 C C C 在下凸壳内不可能被选中,所以我们需要维护一个下凸壳,支持在最右边插入一个点和查询给定斜率的最小截距。

插入一个点

由于 x ( p ) x(p) x(p) 是单调递增的,所以我们总是在下凸壳的最右边插入一个点。

设将要插入的点为 A A A,下凸壳最右边的点为 B B B,下凸壳次右边的点为 O O O

如果 O A OA OA O B OB OB 的顺时针方向或 O A OA OA O B OB OB 重合,此时点 B B B 在下凸壳内,需要删掉点 B B B,继续和下凸壳新的最右边两个点比较。

如果 O A OA OA O B OB OB 的逆时针方向,那么直接把点 A A A 插入到下凸壳末尾。

查询给定斜率的最小截距

下凸壳相邻点构成线段的斜率是单调递增的,我们把斜率从左往右存下来,然后使用二分查找,找到第一个比 k ( i , j ) k(i,j) k(i,j) 大的斜率,这个斜率位置上的点就是我们要找的。

总结

斜率优化需要先把DP方程化简成 y ( p ) = k ( i , j ) x ( p ) + b ( i , j ) y(p) = k(i,j)x(p) + b(i,j) y(p)=k(i,j)x(p)+b(i,j) 的形式,然后使用上凸壳或下凸壳维护 ( x ( p ) , y ( p ) ) (x(p),y(p)) (x(p),y(p)),使得能够在 O ( log ⁡ n ) O(\log n) O(logn) 的时间复杂度内算出 d p [ i ] [ j ] dp[i][j] dp[i][j]

例题

洛谷 P2365 任务安排

这题卡内存,需要使用滚动数组。

例题代码

#include <bits/stdc++.h>
using namespace std;
#define int long long
#define uint unsigned int
#define pii pair<int, int>
template <typename T> using vector2 = vector<vector<T>>;
template <typename T> using MaxHeap = priority_queue<T>;
template <typename T> using MinHeap = priority_queue<T, vector<T>, greater<T>>;
#define all(x) (x).begin(), (x).end()
// #define endl '\n'
// #define endl " line in : " << __LINE__ << endl

// 向量点积和叉积
struct Point { int x, y; };
#define Vector Point
Vector operator-(Point a, Point b) {
  return { a.x - b.x, a.y - b.y };
}
int cross(Vector a, Vector b) {
  return a.x * b.y - b.x * a.y;
}
// 返回值>0,ob在oa的逆时针方向
// 返回值<0,ob在oa的顺时针方向
// 返回值=0,oa与ob重合
// 逆正,顺负,重零
int vcmp(Point o, Point a, Point b) {
  return cross(a - o, b - o);
}
struct DownConvexHull {
  vector<Point> pt;
  vector<double> ks;
  void insert(Point p) {
    // p.x 应该单调递增
    if (pt.size())
      assert(p.x > pt.back().x);
    while (pt.size() >= 2) {
      int n = pt.size();
      Point p0 = pt[n - 2], p1 = pt[n - 1], p2 = p;
      if (vcmp(p0, p1, p2) <= 0)
        pt.pop_back(), ks.pop_back();
      else
        break;
    }
    if (pt.size())
      ks.push_back(1.0 * (pt.back().y - p.y) / (pt.back().x - p.x));
    pt.push_back(p);
  }
  int query(int k) {
    assert(pt.size() >= 1);
    if (pt.size() == 1) {
      // y = kx + b, b = y - kx;
      return pt[0].y - k * pt[0].x;
    }
    int i = lower_bound(all(ks), k) - ks.begin();
    return pt[i].y - k * pt[i].x;
  }
  void clear() {
    pt.clear(), ks.clear();
  }
};

const int N = 5005, INF = 1e16, P = 998244353;
int n, s, t[N], f[N], sumt[N], sumf[N], dp[2][N], *f0 = dp[0], *f1 = dp[1];
void test() {
  cin >> n >> s;
  for (int i = 1; i <= n; i++)
    cin >> t[i] >> f[i];
  for (int i = 1; i <= n; i++) {
    sumt[i] = sumt[i - 1] + t[i];
    sumf[i] = sumf[i - 1] + f[i];
  }
  int ans = INF;
  fill(f0, f0 + N, INF);
  f0[0] = 0;
  DownConvexHull dch;
  for (int i = 1; i <= n; i++) {
    fill(f1, f1 + N, INF);
    dch.clear();
    for (int j = i; j <= n; j++) {
      // y = dp[i-1][k]
      // x = sumf[k]
      if (f0[j - 1] != INF)
        dch.insert({ sumf[j - 1], f0[j - 1] });
      // b = dp[i][j] - (sumt[j] + s * i) * sumf[j]
      // dp[i][j] = (sumt[j] + s * i) * sumf[j] + b
      // k = (sumt[j] + s * i)
      f1[j] = (sumt[j] + s * i) * sumf[j] + dch.query(sumt[j] + s * i);
    }
    swap(f0, f1);
    ans = min(ans, f0[n]);
  }
  cout << ans << endl;
}
signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0);
  // int T; cin >> T; while (T--)
    test();
  return 0;
}

最后,送给大家一只可爱的绫华。

在这里插入图片描述

<think>好的,用户想了解斜率优化动态规划的概念及实现方法。首先,我需要回忆一下动态规划的基础知识,特别是优化方法。动态规划通常有递推和记忆化搜索两种实现方式,而斜率优化属于一种高级的优化技巧,用于降低时间复杂度。 用户提到的参考引用中提到,递推的效率比记忆化搜索高,并且很多优化技巧如滚动数组、单调队列、斜率优化都建立在递推上。所以,斜率优化应该属于递推方法的一种扩展。 接下来,我需要明确斜率优化的核心思想。根据已有的知识,斜率优化通常用于处理状态转移方程中的决策变量,通过分析转移方程的数学形式,将其转化为斜率问题,从而利用单调队列或凸包等数据结构来优化决策过程。 例如,当状态转移方程形如$dp[i] = \min\{dp[j] + f(i,j)\}$,其中$f(i,j)$可以分解为与i和j相关的项,可能可以将其转换为斜率的形式。比如,若$f(i,j)=a[i]*b[j]+c[i]+d[j]$,可能将式子整理为关于$b[j]$和$dp[j]+d[j]$的线性表达式,进而比较不同j值的斜率,选择最优的决策点。 需要解释为什么这种方法有效,可能涉及到维护一个下凸包或上凸包,确保每次决策时选取的j是最优的,从而将时间复杂度从O(n^2)降低到O(n)或O(n log n)。 然后,结合用户提供的引用内容,特别是引用[2]提到的QP优化DP结合,可能斜率优化在路径规划中的应用,但用户的问题更关注动态规划中的斜率优化,所以需要专注于算法实现的步骤。 需要给出具体的例子,比如任务安排问题,或者最大子序列问题,来展示斜率优化的应用步骤。例如,经典的任务安排问题,其中状态转移方程可以斜率优化,通过维护一个单调队列来快速找到最优的前驱状态。 在实现步骤方面,应该分步骤解释:问题分析、状态转移方程整理为斜率形式、数据结构的选择(如单调队列)、维护凸包的过程、以及最终的时间复杂度分析。 同时,要注意数学表达式的正确格式,使用$$...$$和$...$,并确保LaTeX语法正确。例如,将状态转移方程写成$$dp[i] = \min_{j<i}\{ dp[j] + (S + T_i)(C_i - C_j) \}$$,然后展开分析。 另外,用户提供的引用中有提到变积分PID和QP优化,虽然这些可能与斜率优化无直接关联,但需要确保不混淆概念,专注于动态规划中的斜率优化部分。 最后,生成相关问题时要围绕斜率优化的应用场景、与其他优化方法的比较、具体实现中的挑战等,帮助用户深入理解。</think>### 斜率优化动态规划详解 #### 概念解析 斜率优化动态规划(Slope Optimization DP)是一种通过**分析状态转移方程的斜率特性**来优化决策过程的方法,可将部分动态规划问题的时间复杂度从$O(n^2)$降至$O(n)$或$O(n \log n)$[^1]。其核心是将状态转移方程转换为**线性关系式**,通过维护**决策点集的凸包**快速筛选最优决策。 #### 算法原理 1. **状态转移方程重构** 设状态转移方程为: $$dp[i] = \min_{j<i}\{ dp[j] + f(i,j) \}$$ 若$f(i,j)$可分解为$A(i) \cdot B(j) + C(i) + D(j)$形式,则方程可改写为: $$dp[i] = C(i) + \min_{j<i}\{ A(i) \cdot B(j) + (dp[j] + D(j)) \}$$ 此时决策变量$j$的优劣可转化为**点$(B(j), dp[j]+D(j))$的斜率比较**。 2. **凸包维护** 通过维护决策点集的**下凸包**(当$A(i)$单调递增)或**上凸包**(当$A(i)$单调递减),使用单调队列/栈快速剔除无效决策点。 #### 实现步骤(以经典任务安排问题为例) **问题描述** 处理$n$个任务,每个任务耗时$t_i$,费用系数$c_i$。目标最小化总费用: $$总费用 = \sum_{i=1}^n (\text{完成时间} \times c_i)$$ **状态转移方程** $$dp[i] = \min_{j<i}\left\{ dp[j] + (S + T_i)(C_i - C_j) \right\}$$ 其中$T_i = \sum_{k=1}^i t_k$, $C_i = \sum_{k=1}^i c_k$,$S$为启动延迟。 **斜率优化过程** 1. **方程变形** 将方程展开为: $$dp[i] = S \cdot C_i + T_i \cdot C_i + \min_{j<i}\{ dp[j] - S \cdot C_j - T_i \cdot C_j \}$$ 令: $$ \begin{cases} k = T_i \\ x_j = C_j \\ y_j = dp[j] - S \cdot C_j \\ \end{cases} $$ 则状态转移简化为: $$dp[i] = (S + T_i)C_i + \min_{j<i}\{ y_j - k x_j \}$$ 2. **决策点筛选** 最优决策点需满足: $$\frac{y_j - y_k}{x_j - x_k} \leq k_i \quad (\text{下凸包条件})$$ 使用单调队列维护凸包,队首即为当前最优决策。 **Python实现示例** ```python def slope_optimization_dp(n, S, t, c): T = [0]*(n+1) # 前缀时间和 C = [0]*(n+1) # 前缀费用和 dp = [0]*(n+1) q = deque([0]) # 单调队列存储决策点索引 for i in range(1, n+1): T[i] = T[i-1] + t[i-1] C[i] = C[i-1] + c[i-1] # 剔除队首非最优决策 while len(q)>=2 and (dp[q[1]] - dp[q[0]]) <= (S + T[i])*(C[q[1]] - C[q[0]]): q.popleft() j = q[0] dp[i] = dp[j] + (S + T[i] - T[j])*(C[i] - C[j]) # 维护下凸包 while len(q)>=2 and (dp[i]-dp[q[-1]])*(C[q[-1]]-C[q[-2]]) <= (dp[q[-1]]-dp[q[-2]])*(C[i]-C[q[-1]]): q.pop() q.append(i) return dp[n] ``` #### 关键优化点 - **时间复杂度**:从$O(n^2)$优化至$O(n)$ - **数据结构**:单调队列实现$O(1)$时间复杂度的决策筛选 - **适用条件**:状态转移方程需满足**决策单调性**且参数满足**斜率单调性** #### 典型应用场景 1. 任务调度问题 2. 最优库存控制 3. 资源分配优化 4. 路径规划中的轨迹平滑(如自动驾驶中的QP优化前处理[^2])
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值