[笔记]动态规划优化(斜率优化,决策单调性优化)

news/2024/11/28 21:27:34/文章来源:https://www.cnblogs.com/Link-Cut-Y/p/18575224

本文主要记录某些动态规划思路及动态规划优化。

首先先把以前写过的斜率优化祭出来。

斜率优化

\(\text{P5017 [NOIP2018 普及组] 摆渡车}\)

经典例题。

\(f_i\) 表示最后班车在 \(i\) 时刻发车,所有人等待时间和的最小值。(这里的所有人是指到达时刻小于等于 \(i\) 的所有人)。

容易列出转移方程:

\[f_i = \min\{f_j + \operatorname{cost(j + 1, i)}\}(j \le i - m) \]

其中 \(\operatorname{cost(i, j)}\) 到达时间在 \([i, j]\) 范围内的所有人等待的时间和。也就是 \(\sum \limits_{k} (j - t_k)\),其中 \(t_k \in [i, j]\)

然后考虑如何 \(O(1)\) 计算 \(\operatorname{cost(i, j)}\)。首先拆式子,变成 \(\operatorname{cost(i, j)} = \sum \limits_{k} j - \sum \limits_{k} t_k\)。设在范围内的 \(k\) 共有 \(c(i, j)\) 个,在范围内的 \(t_k\) 的和为 \(s(i, j)\),那么就可以 \(O(1)\) 计算。而 \(c, s\) 可以通过桶上前缀和得到。

具体的,假设 \(s_i\) 为到达时间小于等于 \(i\) 的人到达时间的和,\(c_i\) 表示到达时间小于等于 \(i\) 的人的个数,那么 \(\operatorname{cost(i, j)} = j \times (c_j - c_{i - 1}) - (s_j - s_{i - 1})\)

那么这样整个算法复杂度就是 \(\Theta(n ^ 2)\) 的。只能通过 \(50 \%\) 的数据。

接下来引入斜率优化。假设有 \(j_1, j_2\) 两个决策点,且 \(j_2 > j_1\)\(j_2\) 优于 \(j_1\)。那么有

\[f_{j_1} + \operatorname{cost(j_1 + 1, i)} \le f_{j_2} + \operatorname{cost(j_2 + 1, i)} \]

接下来把 \(\operatorname{cost}\) 展开,得:

\[f_{j_1} + i \times (c_i - c_{j_1}) - (s_i - s_{j_1}) \le f_{j_2} + i \times (c_i - c_{j_2}) - (s_i - s_{j_2}) \]

展开,得:

\[f_{j_1} + i \times c_i - i \times c_{j_1} - s_i + s_{j_1} \le f_{j_2} + i \times c_i - i \times c_{j_2} - s_i + s_{j_2} \]

两边同时消掉 \(i \times c_i, - s_i\),得:

\[f_{j_1} - i \times c_{j_1} + s_{j_1} \le f_{j_2} - i \times c_{j_2} + s_{j_2} \]

接下来,将含有 \(i\) 的项移到左边,其余的移到右边:

\[i \times (c_{j_2} - c_{j_1}) + s_{j_1} \le (f_{j_2} + s_{j_2}) - (f_{j_1} + s_{j_1}) \]

假设 \(c_{j_2} - c_{j_1} \ne 0\),那么可以将 \(c_{j_2} - c_{j_1}\) 除到右边:

\[i \le \dfrac{(f_{j_2} + s_{j_2}) - (f_{j_1} + s_{j_1})}{(c_{j_2} - c_{j_1})} \]

\(\mathcal{f(x) = (f_{x} + s_x)}\),那么 \(i \le \dfrac{f(j_2) - f(j_1)}{c_{j_2} - c_{j_1}}\)

假设横坐标为 \(c_{x}\),那么不等式右边可以斜率形式。设两个决策点 \(j_1, j_2\) 之间的斜率为 \(k = \dfrac{f(j_2) - f(j_1)}{c_{j_2} - c_{j_1}}\),而待转移点为 \(i\),那么有:

\[\left\{\begin{matrix}i < k \qquad j_2 \text{ is better than } j_1\\\\i > k \qquad j_1 \text{ is better than } j_2\\\\i = k \qquad j_2 \text{ is as good as } j_1 \end{matrix}\right. \]

接下来考虑如下情景:

其中 \(k1 > k3\)。那么可以证明,无论 \(i\) 为多少, \(B\) 点都是无用状态。

所以最优转移点之间的斜率一定严格单调递增。也就是我们要维护一个下凸壳。可以搞一个单调队列来做,然后每次转移的时候二分找到最佳转移点即可。这里的最佳转移点 \(j\) 就是指 \(j, next_j\) 之间的斜率为大于等于 \(i\) 的最小值。

所以搞完了。代码如下:

int cost(int x, int y) {return y * (s[y].first - s[x - 1].first) - (s[y].second - s[x - 1].second);
}
int f(int x) {return dp[x] + s[x].second;
}
double delta(int x1, int x2) {if (s[x2].first - s[x1].first == 0) return 1e-9;return (s[x2].first - s[x1].first);
}
double slope(int x1, int x2) {return (double)(f(x2) - f(x1)) / delta(x1, x2);
}
int get(int S) {int l = 1, r = top;while (l < r) {int mid = l + r >> 1;if (slope(stk[mid], stk[mid + 1]) >= S) r = mid;else l = mid + 1;}return stk[l];
}
int main() {scanf("%d%d", &n, &m);for (int i = 1; i <= n; i ++ ) {scanf("%d", &t[i]);Map[t[i]].first ++ ;Map[t[i]].second += t[i];maxt = max(maxt, t[i]);}s[0].first = Map[0].first;s[0].second = Map[0].second;for (int i = 1; i < maxt + m; i ++ ) {s[i].first = s[i - 1].first + Map[i].first;s[i].second = s[i - 1].second + Map[i].second;}for (int i = 0; i < maxt + m; i ++ ) {if (i >= m) {while (top and slope(stk[top - 1], stk[top]) > slope(stk[top], i - m)) top -- ;stk[ ++ top] = i - m;}dp[i] = s[i].first * i - s[i].second;if (i >= m) {int j = get(i);dp[i] = dp[j] + cost(j + 1, i);}}int ans = 2e9;for (int i = maxt; i < maxt + m; i ++ )ans = min(ans, dp[i]);printf("%d\n", ans);return 0;
}

接下来考虑线性做法。由于标准斜率 \(i\) 也是严格单调递增的,那么就可以把单调栈换成单调队列然后线性转移了。代码如下:

int t[N], dp[N];
int n, m, maxt;
PII Map[N], s[N];
int q[N];int cost(int x, int y) {return y * (s[y].first - s[x - 1].first) - (s[y].second - s[x - 1].second);
}
int f(int x) {return dp[x] + s[x].second;
}
double delta(int x1, int x2) {if (s[x2].first - s[x1].first == 0) return 1e-9;return (s[x2].first - s[x1].first);
}
double slope(int x1, int x2) {return (double)(f(x2) - f(x1)) / delta(x1, x2);
}int main() {scanf("%d%d", &n, &m);for (int i = 1; i <= n; i ++ ) {scanf("%d", &t[i]);Map[t[i]].first ++ ;Map[t[i]].second += t[i];maxt = max(maxt, t[i]);}s[0].first = Map[0].first;s[0].second = Map[0].second;for (int i = 1; i < maxt + m; i ++ ) {s[i].first = s[i - 1].first + Map[i].first;s[i].second = s[i - 1].second + Map[i].second;}int hh = 1, tt = 0;for (int i = 0; i < maxt + m; i ++ ) {if (i >= m) {while (hh < tt and slope(q[tt - 1], q[tt]) > slope(q[tt], i - m)) tt -- ;q[ ++ tt] = i - m;}while (hh < tt and slope(q[hh], q[hh + 1]) <= i) hh ++ ;dp[i] = s[i].first * i - s[i].second;if (i >= m) dp[i] = dp[q[hh]] + cost(q[hh] + 1, i);}int ans = 2e9;for (int i = maxt; i < maxt + m; i ++ )ans = min(ans, dp[i]);printf("%d\n", ans);return 0;
}

所以总结出斜率优化的规律:

  1. 可以表示成 \(f_i = \min(\max)\{f_j + \operatorname{cost(j + 1, i)}\}\) 的形式。

  2. 变形后的标准斜率非单调递增,可以在单调栈 / 单调队列里二分。

  3. 变形后的标准斜率单调递增,可以直接写单调队列暴力转移。

以上的递增也可以换成递减。

如果存在 \(x\) 非单调递增的情况,辣么就需要搞平衡树了。

\(\text{1D}\) 决策单调性优化

原本证明很复杂的样子。实际上证明基本靠打表观察决策点。

决策单调性是指某一类 \(\text{DP}\) 方程,当自变量 \(i\) 单调移动时,其决策点也单调移动。

目前已知的决策单调性满足这样的规律(仅考虑 \(\text{1D}\)):

  1. 方程一般是 \(f_i = f_j + \text{cost}(j + 1, i)\) 的形式。

  2. \(\text{cost}(j + 1, i)\) 的关于 \(i\) 的二阶偏导恒大于 / 小于零。

我知道首先要证什么 \(f\) 满足四边形不等式,还要证明什么 \(w\) 满足四边形不等式且局部单调啥的,但是这是规律。学 OI 谁还证明啊/kk

关于 \(i\) 的二阶偏导为正的意义就是指 \(cost\) 函数随 \(j\) 增大增长率越来越快。否则就是增长率越来越慢。这显然和决策单调性挂钩。

举个例子,如果 \(f_i = f_j + (i - j) ^ 2\),这个显然是满足决策单调性的。它关于 \(i\) 的偏导大概是 \(2(i - j)\) 的样子。然后可以发现这个决策点是单调左移的。

接下来引入二分队列。对于某些情况,比如下面的例子:

\[6, 3, 1 \]

\[\color{red}1, 2, 3 \]

黑色的是初始值,红色的是增长率。求最大值。可以发现,决策点事单调右移的。

然后第一秒的时候,可以发现是第一个数是最优决策点。此时三个数分别为 \(\color{blue}7, 5, 4\)

第二秒的时候,计算机本来以为决策点要向右移动了,但实际上这时候三个数分别为 \(\color{blue}8, 7, 7\)。最优决策点位置不动。

第三秒的时候,计算机觉得第二个数改该为最优决策点了吧?结果还不是。这时候的状态是 \(\color{blue}9, 9, 10\),最优决策点变成了 \(3\)

所以这时候 \(3\) 反超了 \(1\) 决策点。所以我们需要告诉计算机什么时候 \(3\) 反超一。对于这个题,显然可以 \(O(1)\) 计算。然后根据反超时间建一个单调栈 / 单调队列即可。但是有时候这个反超时间要二分。这也就是这个东西叫做二分栈 / 二分队列的原因。

接下来是例题。

\(\text{P1912 [NOI2009] 诗人小G}\)

\(f_i\) 代表在第 \(i\) 个短句后面打一个回车,前 \(i\) 个短句的最小不协调度。然后状态转移方程就是:

\[f_i = f_j + \text{cost}(j + 1, i) \]

然后发现这符合决策单调性的规律。我们把 \(\text{cost}(l, r)\) 拆开看看。

\(\text{cost}(l, r) = |(s_r - s_{l - 1} + r - l - L) ^ P|\)

其中 \(s\) 表示短句长度的前缀和。

首先我们看函数 \(f(x) = x ^ P\) 的二阶导,显然就是 \(f(x)^{(2)} = P (P - 1) x^{P - 2}\)。当 \(x > 0, P \ge 2\) 的时候,这个东西显然是正的,也就是说函数增长率越来越快。接下来分析 \(\text{cost}(l, r)\) 函数。可以固定 \(r\) 不变,然后看看它关于 \(l\) 的变化情况。可以发现,当 \(r\) 不变的时候, \(l\) 越大,其增长率越慢(因为 \(-l\) 变小,\(-s_{l - 1}\) 也变小,所以相当于 \(x\) 变小了)。由于要求最小值,所以决策点应该是单调右移的。

然后套上二分栈二分队列即可。

auto calc = [&](int l, int r) -> long double {return dp[l] + qpow((long double)abs(s[r] - s[l] + r - l - 1 - L), P);
};
auto find = [&](int j, int i) -> int { // To cauculate when j becomes better than iint l = j, r = n + 1, sum = 0;while (l <= r) {int mid = l + r >> 1;if (calc(j, mid) >= calc(i, mid)) r = mid - 1;else l = mid + 1;}return l;
};scanf("%d", &T);
while (T -- ) {scanf("%d%d%d", &n, &L, &P);for (int i = 1; i <= n; i ++ ) {scanf("%s", str[i]);s[i] = s[i - 1] + strlen(str[i]);}hh = tt = 1; q[hh] = 0;for (int i = 1; i <= n; i ++ ) {while (hh < tt and t[hh] <= i) hh ++ ;pre[i] = q[hh], dp[i] = calc(q[hh], i);while (hh < tt and t[tt - 1] >= find(q[tt], i)) tt -- ;t[tt] = find(q[tt], i);q[ ++ tt] = i;}
} 

\(\text{2D}\) 决策单调性优化

这种题目通常可以进行分层。比如 \(f_{i, j} = f_{i - 1, k} + \cdots\)。这样可以关于 \(i\) 进行分层,看做从 \(i - 1\) 层转移到第 \(i\) 层。然后观察层之间的转移是否有决策单调性。

通常来说,二维的决策单调性优化使用分治法来解决。具体是这样的:假设 \(\text{solve(l, r, L, R)}\) 表示当前层处理到了 \([l, r]\) 这个区间,决策点在上一层的 \([L, R]\) 这个区间。然后假设 \(mid = \dfrac{l + r}{2}\)。找到转移到 \(f_{i, mid}\) 的最优决策点 \(f_{i - 1, MID}\)。然后根据决策单调性可知,能够转移到 \(f_{i, [l, mid - 1]}\) 的最优决策点一定在区间 \([L, MID]\),而 \(f_{i, [mid + 1, r]}\) 的最优决策点一定在区间 \([MID, R]\) 中(这里假设决策点的递增不是严格的)。递归处理 \(\text{solve(l, mid - 1, L, MID), solve(mid + 1, r, MID, R)}\) 即可。

可以发现复杂度的瓶颈在于寻找 \(mid\) 的最优决策点。如果每次寻找 \(mid\) 决策点的复杂度为 \(\Theta(\lambda)\),那么时间复杂度就为 \(O(\lambda kn \log n)\) 了。这里的 \(k\) 表示层数。

所以分治法适用于求解 \(mid\) 最优转移点时间复杂度为 \(O(1)\) 或均摊 \(O(1)\) 的情况。

\(\text{CF833B The Bakery}\)

\(f_{i, j}\) 表示将前 \(i\) 个数分成 \(j\) 段的最大价值。那么那么转移方程就是 \(f_{i, j} = f_{k - 1, j - 1} + \text{cost}(k, j)\)。显然可以根据分的段数分层。然后考虑如何求出 \(\text{cost}(l, r)\)。考虑使用莫队的思想,开一个桶,然后搞两个指针移动一下。考虑这个复杂度为什么正确。首先在求 \(mid\) 的时候如果采取下面的写法:

for (int i = min(R, mid); i >= L; i -- ) {int ans = f[i - 1][level - 1] + cost(i, mid);if (ans > f[mid][level]) f[mid][level] = ans, MID = i;
}

那么显然指针是单调左移的。
然后接下来 \(\text{solve}(l, MID)\) 的时候,指针仍然会单调左移。接下来 \(\text{solve}(MID + 1, R)\) 的时候,指针又单调右移。所以指针的移动次数是 \(O(n)\) 级别的。也就是做到了均摊 \(O(1)\) 求最优决策点。

void solve(int l, int r, int L, int R, int level) {if (l > r) return;int mid = l + r >> 1, MID = L;for (int i = min(R, mid); i >= L; i -- ) {int ans = f[i - 1][level - 1] + cost(i, mid);if (ans > f[mid][level]) f[mid][level] = ans, MID = i;}solve(l, mid - 1, L, MID, level);solve(mid + 1, r, MID, R, level);
}int main() {scanf("%d%d", &n, &k);for (int i = 1; i <= n; i ++ )scanf("%d", &w[i]);memset(f, -0x3f, sizeof f);f[0][0] = 0;for (int i = 1; i <= k; i ++ )solve(1, n, 1, n, i);printf("%d\n", f[n][k]);return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/843085.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于Java+SpringBoot+Mysql实现的点卡各种卡寄售平台功能设计与实现一

后台功能:寄售管理、提现管理、订单管理、认证管理、公告管理、用户管理等。 该系统总共21张表,代码整洁,每个功能、接口上都有注释说明。 部分功能:前台用户信息实体类Entity、实名认证信息实体类Entity、银行卡类型信息实体类Entity、寄售卡类型信息实体类Entity、寄售卡…

【java编程】BCEL ClassLoader

BCEL 介绍 BCEL的全名应该是Apache Commons BCEL,属于Apache Commons项目下的一个子项目。Apache Commons大家应该不陌生,反序列化最著名的利用链就是出自于其另一个子项目——Apache Commons Collections。 BCEL库提供了一系列用于分析、创建、修改Java Class文件的API。就…

借助电脑探究双变量函数问题侧记

记录一次借助电脑探究双变量函数问题的全过程前情概要 偶尔看到下面的习题,想到以前自己整理的双变量函数问题,尝试练手时发现,寻找思路不是很简单的问题,探索一番,对整个过程作以记录,成篇为一侧记 . 典型案例 【2025届高三数学月考3第12题】对于函数 \(f(x)=e^x-x^2+a\…

Paypal最新版本 paypal-server-sdk 使用案例(前端 Vue3 + 后端Spring Boot )

背景 在项目中对接Paypal支付,一开始在网上查了好久,发现资料少,而且陈旧,甚至我都没弄清楚我应该哪个SDK。 我到 maven 中央仓库中,搜索 com.paypal.sdk,能查出不少结果,据我所知,至少有三个sdk可以从后端访问到Paypal:paypal-core:非常陈旧,2016年就停止更新了,但…

【java编程】URLClassLoader

从上面我们研究【java编程】双亲委派模式时进行Debug了源代码, 可以发现的是, URLClassLoader是ExtClassLoader && AppClassLoader的父类(不是父亲), public class Launcher {static class ExtClassLoader extends URLClassLoader {}static class AppClassLoader exten…

秒杀系统

前言 秒杀大家都不陌生。自2011年首次出现以来,无论是双十一购物还是 12306 抢票,秒杀场景已随处可见。简单来说,秒杀就是在同一时刻大量请求争抢购买同一商品并完成交易的过程。从架构视角来看,秒杀系统本质是一个高性能、高一致、高可用的三高系统。而打造并维护一个超大…

.NET周刊【11月第4期 2024-11-24】

国内文章 C# 入门深度学习:万字长文讲解微积分和梯度下降 https://www.cnblogs.com/whuanle/p/18551532 这篇文章主要介绍了使用 C# 进行深度学习的方法,特别是微积分在此领域的应用。作者简要讲解了极限、导数等基本概念,并展示了如何在 C# 中实现这些数学运算,例如将一个…

高级程序语言第九次作业

这个作业属于哪个课程:https://edu.cnblogs.com/campus/fzu/2024C 这个作业要求在哪里:https://edu.cnblogs.com/campus/fzu/2024C/homework/13311 学号:102400204 姓名:刘嘉奕不太理解10题a,b两个题目的要求有何不同,一个把值返回该结构,不运用指针,而b要把值赋给合适…

DataSophon集成StreamPark2.1.5

为DataSophon制作streampark-2.1.5安装包.md 下载并解压streampark 2.1.5安装包 StreamPark官网下载 wget -O /opt/datasophon/DDP/packages/apache-streampark_2.12-2.1.5-incubating-bin.tar.gz https://www.apache.org/dyn/closer.lua/incubator/streampark/2.1.5/apache-s…

以Deformable_DETR为例说明训练过程

以Deformable_DETR为例说明使用服务器训练过程 下载程序文件 根据论文提供的github地址fundamentalvision/Deformable-DETR: Deformable DETR: Deformable Transformers for End-to-End Object Detection.下载zip到本地 租用服务器 在autodl平台租用服务器,申请账号氪金之后去…

Qt VTK加载openfoam计算结果

Qt VTK加载openfoam计算结果.foam文件。#include <QApplication> #include <QDebug> #include "qvtkopenglwidget.h" #include <vtkSmartPointer.h> #include <vtkGenericDataObjectReader.h> #include <vtkPolyDataMapper.h> #includ…