文章来自 OI wiki ,转载仅作学习使用

动态规划应用于子问题重叠的情况:

  1. 要去刻画最优解的结构特征;
  2. 尝试递归地定义最优解的值(就是我们常说的考虑从 \(i - 1\) 转移到 \(i\));
  3. 计算最优解;
  4. 利用计算出的信息构造一个最优解。

钢条切割

给定一段钢条,和不同长度的价格,问如何切割使得总价格最大。

为了求解规模为 \(n\) 的原问题,我们先求解形式完全一样,但规模更小的子问题。

即当完成首次切割后,我们将两段钢条看成两个独立的钢条切割问题实例。

我们通过组合相关子问题的最优解,并在所有可能的两段切割方案中选取组合收益最大者,构成原问题的最优解。

最优子结构:问题的最优解由相关子问题的最优解组合而成,而这些子问题可以独立求解。

动态规划的两种实现方法:

  • 带备忘的自顶向下法(记忆化搜索);
  • 自底向上法(将子问题按规模排序,类似于递推)。

算导用子问题图上按照逆拓扑序求解问题,引出记忆化搜索。

重构解(输出方案):转移的时候记录最优子结构的位置。

矩阵链乘法

给出 \(n\) 个矩阵的序列,希望计算他们的乘积,问最少需要多少次乘法运算?

(认为 \(p \times q\) 的矩阵与 \(q\times r\) 的矩阵相乘代价是 \(p\times q\times r\)。)

完全括号化方案是指要给出谁先和谁乘。

动态规划原理

两个要素:

最优子结构

具有最优子结构也可能是适合用贪心的方法求解。

注意要确保我们考察了最优解中用到的所有子问题。

  1. 证明问题最优解的第一个组成部分是做出一个选择;
  2. 对于一个给定问题,在其可能的第一步选择中,你界定已经知道哪种选择才会得到最优解。你现在并不关心这种选择具体是如何得到的,只是假定已经知道了这种选择;
  3. 给定可获得的最优解的选择后,确定这次选择会产生哪些子问题,以及如何最好地刻画子问题空间;
  4. 证明作为构成原问题最优解的组成部分,每个子问题的解就是它本身的最优解。方法是反证法,考虑加入某个子问题的解不是其自身的最优解,那么就可以从原问题的解中用该子问题的最优解替换掉当前的非最优解,从而得到原问题的一个更优的解,从而与原问题最优解的假设矛盾。

要保持子问题空间尽量简单,只在必要时扩展。

最优子结构的不同体现在两个方面:

  1. 原问题的最优解中涉及多少个子问题;
  2. 确定最优解使用哪些子问题时,需要考察多少种选择。

子问题图中每个定点对应一个子问题,而需要考察的选择对应关联至子问题顶点的边。

经典问题:

  • 无权最短路径: 具有最优子结构性质。
  • 无权最长(简单)路径: 此问题不具有,是 NPC 的。区别在于,要保证子问题无关,即同一个原问题的一个子问题的解不影响另一个子问题的解。相关:求解一个子问题时用到了某些资源,导致这些资源在求解其他子问题时不可用。

子问题重叠

子问题空间要足够小,即问题的递归算法会反复地求解相同的子问题,而不是一直生成新的子问题。

重构最优解

存表记录最优分割的位置,就不用重新按照代价来重构。

最长公共子序列

子序列允许不连续。

每个 \(c[i][j]\) 只依赖于 \(c[i - 1][j]\)、\(c[i][j - 1]\) 和 \(c[i - 1][j - 1]\)。

记录最优方案的时候可以不需要额外建表(优化空间),因为重新选择一遍(转移过程)也是 \(O(1)\) 的。

最优二叉搜索树

给二叉搜索树的每个节点定义一个权值,问如何安排使得权值和深度的乘积最小。

考虑当一棵子树成为了一个节点的子树时,答案(期望搜索代价)有何变化?

由于每个节点的深度都增加了 1,这棵子树的期望搜索代价的增加值应为所有概率之和。

tD/eD 动态规划:

状态空间是 \(O(n^t)\) 的,每一项依赖其他 \(O(n^e)\) 项。

最长连续不下降子序列

我们的目标是求出给定序列的一个最长的连续子序列,满足这个序列中的后一个元素 不小于 前一个元素。

因为是连续的,所以只要与上一个元素进行比较即可。

int a[MAXN];
int dp() {
int now = 1, ans = 1;
for (int i = 2; i <= n; i++) {
if (a[i] >= a[i - 1])
now++;
else
now = 1;
ans = max(now, ans);
}
return ans;
}

最长不下降子序列

与最长连续不下降子序列不同的是,不需要这个子序列是连续的了。

求最长子序列的方法有两种。

最简单的第一种

\(O\left(n^2\right)\) 的算法。每一次从头扫描找出最佳答案。

int a[MAXN], d[MAXN];
int dp() {
d[1] = 1;
int ans = 1;
for (int i = 2; i <= n; i++) {
for (int j = 1; j < i; j++)
if (a[j] <= a[i]) {
d[i] = max(d[i], d[j] + 1);
ans = max(ans, d[i]);
}
}
return ans;
}

稍复杂的第二种

\(O\left(n \log n\right)\) 的算法,参考了这篇文章 https://www.cnblogs.com/itlqs/p/5743114.html

首先,定义 \(a_1 \dots a_n\) 为原始序列,\(d\) 为当前的不下降子序列,\(len\) 为子序列的长度,那么 \(d_{len}\) 就是长度为 \(len\) 的不下降子序列末尾元素。

初始化:\(d_1=a_1,len=1\)。

现在我们已知最长的不下降子序列长度为 1,那么我们让 \(i\) 从 2 到 \(n\) 循环,依次求出前 \(i\) 个元素的最长不下降子序列的长度,循环的时候我们只需要维护好 \(d\) 这个数组还有 \(len\) 就可以了。关键在于如何维护。

考虑进来一个元素 \(a_i\):

  1. 元素大于 \(d_{len}\),直接 \(d_{++len}=a_i\) 即可,这个比较好理解。
  2. 元素等于 \(d_{len}\),因为前面的元素都小于它,所以这个元素可以直接抛弃。
  3. 元素小于 \(d_{len}\),找到 第一个 大于它的元素,插入进去,其他小于它的元素不要。

那么代码如下:

for (int i = 0; i < n; ++i) scanf("%d", a + i);
memset(dp, 0x1f, sizeof dp);
mx = dp[0];
for (int i = 0; i < n; ++i) {
*std::upper_bound(dp, dp + n, a[i]) = a[i];
}
ans = 0;
while (dp[ans] != mx) ++ans;

经典问题(来自习题)

DAG 中的最长简单路径

\(dp[i] = \max(dp[j] + 1), ((j, i) \in E)\)

最长回文子序列

\[dp[i][i + len] =
\begin{cases}
dp[i + 1][i + len - 1] + 2, & \text{if}\ s[i] = s[i + len] \\[2ex]
\max(dp[i + 1][i + len], dp[i][i + len - 1]), & \text{else}
\end{cases}
\]

边界:\(dp[i][i] = 1\)。

注意:\(dp[i][j]\) 表示的是闭区间。

也可以转化为 LCS 问题,只需要把 \(a\) 串反转当做 \(b\),对 \(a\) 和 \(b\) 求 LCS 即可。

证明在 这里

注意区分子串(要求连续)的问题。

最长回文子串

\(O(n^2)\):\(dp[i] = \max(dp[j] + 1), s(j + 1 \cdots i)\) 是回文

\(O(n)\):Manacher

\(p[i]\) 表示从 \(i\) 向两侧延伸(当然要保证两侧对应位置相等)的最大长度。

为了处理方便,我们把原串每两个字符之间加一个(不包含在原串中的)#,开头加一个 $

这样得到的回文串长度就保证是奇数了

考虑如果按顺序得到了 \(p[1 \cdots i - 1]\),如何计算 \(p[i]\) 的值?

如果之前有一个位置比如说是 \(id\),有 \(p[id] + id > i\) 那么 \(i\) 这个位置是被覆盖了的,根据 \(id\) 处的对称性,我们找 \(p[id \times 2 - i]\) 延伸的部分被 \(p[id]\) 延伸的部分所覆盖的那段,显然这段对称回去之后是可以从 \(i\) 处延伸出去的长度。

如果找不到呢?就先让 \(p[i] = 1\) 吧。

之后再暴力延伸一下。

可以证明是 \(O(n)\) 的。

至于如何找是否有这么一个 \(id\) 呢?递推的时候存一个 \(max\) 就好了。

代码:

#include <bits/stdc++.h>
using namespace std;
#define f(x, y, z) for (int x = (y), __ = (z); x < __; ++x)
#define g(x, y, z) for (int x = (y), __ = (z); x <= __; ++x)
#define fd(x, y, z) for (int x = (y), __ = (z); x > __; --x)
#define gd(x, y, z) for (int x = (y), __ = (z); x >= __; --x)
const int MAXN = 120033;
char buf[MAXN];
char s[MAXN * 2];
int p[MAXN * 2];
int n, ans, T;
void init() {
int mx = 0, id;
f(i, 1, n) {
if (mx > i) p[i] = std::min(p[2 * id - i], p[id] + id - i);
else
p[i] = 1;
while (s[i + p[i]] == s[i - p[i]]) ++p[i];
if (p[i] + i > mx) {
mx = p[i] + i;
id = i;
}
}
}
int main() {
while (~scanf("%s", buf)) {
n = strlen(buf);
s[0] = '$';
s[1] = '#';
f(i, 0, n) {
s[i * 2 + 2] = buf[i];
s[i * 2 + 3] = '#';
}
n = n * 2 + 2;
s[n] = '\0';
init();
ans = 0; f(i, 0, n) if (ans < p[i]) ans = p[i];
printf("%d\n", ans - 1);
}
return 0;
}

双调欧几里得旅行商问题

好像出成了某一年程设期末。

upd:其实是 程设期末推荐练习 里面的。

书上的提示是:从左到右扫描,对巡游路线的两个部分分别维护可能的最优解。

说的就是把回路给拆开吧。

思路一

\(dp[i][j]\) 表示 \(1 \cdots i\) 和 \(1 \cdots j\) 两条路径。

我们可以人为要求 \(1 \cdots i\) 是更快的那一条路径。

这样考虑第 \(i\) 个点分给谁。

如果是分给快的那条:

\(dp[i][j] = \min(dp[i - 1][j] + dis[i - 1][i]),\ j = 1 \cdots i\)

如果是慢的,原来是慢的那条就变成了快的,所以另一条是到 \(i - 1\) 那个点:

\(dp[i][j] = \min(dp[i - 1][j] + dis[j][i]),\ j = 1 \cdots i\)

答案是 \(\min(dp[n][i] + dis[n][i])\)。

(从一开始编号,终点是 \(n\))

代码:https://github.com/Ir1d/Fantasy/blob/master/openjudge/cssx/2018rec/11.cpp

思路二

把 \(dp[i][j]\) 定义反过来,不是 \(1 \cdots i\) 和 \(1 \cdots j\)。

改成是 \(i..n\) 和 \(j \cdots n\),不要求哪个更快。

这样的转移更好写:

我们记 \(k = \max(i, j) + 1\)

\(k\) 这个点肯定在两条路中的一个上,\(dp[i][j]\) 取两种情况的最小值即可。

\(dp[i][j] = \min(dp[i][k] + dis[k][j], dp[k][j] + dis[i][k])\)

边界是:\(dp[i][n] = dp[n][i] = dis[n][i]\)。

答案是 \(dp[1][1]\)。

整齐打印

希望最小化所有行的额外空格数的立方之和。

注意到实际问题要求单词不能打乱顺序,所以就好做了起来。不要把题目看复杂。

\(dp[i] = \min(dp[j] + cost[j][i])\)

不知道这样可不可做:有 \(n\) 个单词,可以不按顺序打印,问怎么安排,使得把他们打印成 \(m\) 行之后,每行的空格之和最小。

编辑距离

变换操作有 \(6\) 种,复制、替换、删除、插入、旋转、终止(结束转换过程)。

最优对齐问题

把空格符插入到字符串里,使得相似度最大。

定义了按字符比较的相似度。

然后发现最优对齐问题可以转换为编辑距离问题。

相当于仅有三个操作的带权编辑距离。

copy    :  1
replace : -1
insert : -2

公司聚会计划

没有上司的舞会。

\(dp[x][0]\) 是没去,\(dp[x][1]\) 是去了。

\(dp[u][0] = \max(dp[v][0], dp[v][1]), v \in son(u)\)

\(dp[u][1] = w[u] + dp[v][0], v \in son(u)\)

译码算法

Viterbi algorithm 之前写词性标注的时候有用到,好像用在输入法里面也是类似的。

本题中用来实现语音识别,其实就是找一条对应的概率最大的路径。

ref:https://segmentfault.com/a/1190000008720143

基于接缝裁剪的图像压缩

玩过 opencv 的应该有印象,seam carving 就是在做 dp。

题中要求每一行删除一个像,每个像素都有代价,要求总代价最小。

限制:要求相邻两行中删除的像素必须位于同一列或相邻列。

\(dp[i][j] = \min(dp[i - 1][j], dp[i - 1][j - 1], dp[i - 1][j + 1]) + cost[i][j]\)

边界:\(dp[1][i] = cost[1][i]\)。

字符串拆分

相当于问怎么按顺序拼起来使得总代价最小。

等价于之前那个最优二叉搜索树。

\(dp[i][j] = \min(dp[i][k] + dp[k][j]) + l[j] - l[i] + 1,\ k = i + 1 \cdots j - 1\)

注意 \(l[i]\) 表示的是第 i 个切分点的位置。

边界:\(dp[i][i] = 0\)。

就按照区间 dp 的姿势来写就好了。

投资策略规划

引理:存在最优投资策略,每年都将所有钱投入到单一投资中。

这是个很有趣的结论,dp 问题中很常见。

https://fogsail.github.io/2017/05/08/20170508/

剩下的就是个二维 dp,想成从 \((1, i)\) 走到 \((n, m)\) 的路径的问题,然后收益和代价就是边权,网格图只能往右下方走。

库存规划

生产多了少了都有额外的成本,问怎么安排生产策略使得额外的成本尽可能地少。

\(cost[i][j]\) 表示剩下 \(i\) 个月,开始的时候有 \(j\) 台库存的最小成本。

https://walkccc.github.io/CLRS/Chap15/Problems/15-11/

签约棒球自由球员

\(v[i][j]\) 是考虑 \(i\) 之后的位置,总费用为 \(x\) 的最大收益。

https://walkccc.github.io/CLRS/Chap15/Problems/15-12/

类似于背包问题。


当选取的状态难以进行递推时(分解出的子问题和原问题形式不一样),考虑将问题状态分类细化,增加维度。

【动态规划】动态规划基础 (OI wiki)的更多相关文章

  1. 动态规划_基础_最长公共子序列_多种方法_递归/dp

    D: 魔法少女资格面试 题目描述 众所周知,魔法少女是一个低危高薪职业.随着近年来报考魔法少女的孩子们越来越多,魔法少女行业已经出现饱和现象!为了缓和魔法少女界的就业压力,魔法少女考核员丁丁妹决定增加 ...

  2. 动态规划 Dynamic Programming 学习笔记

    文章以 CC-BY-SA 方式共享,此说明高于本站内其他说明. 本文尚未完工,但内容足够丰富,故提前发布. 内容包含大量 \(\LaTeX\) 公式,渲染可能需要一些时间,请耐心等待渲染(约 5s). ...

  3. 动态规划——用二进制表示集合的状态压缩DP

    动态规划当中有非常常见的一个分支--状态压缩动态规划,很多人对于状态压缩畏惧如虎,但其实并没有那么难,希望这文章能带你们学到这个经典的应用. 二进制表示状态 在讲解多重背包问题的时候,我们曾经讲过二进 ...

  4. 动态规划之经典数学期望和概率DP

    起因:在一场训练赛上.有这么一题没做出来. 题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=6829 题目大意:有三个人,他们分别有\(X,Y,Z\)块钱 ...

  5. 【算法学习笔记】动态规划与数据结构的结合,在树上做DP

    前置芝士:Here 本文是基于 OI wiki 上的文章加以修改完成,感谢社区的转载支持和其他方面的支持 树形 DP,即在树上进行的 DP.由于树固有的递归性质,树形 DP 一般都是递归进行的. 基础 ...

  6. 剑指Offer——动态规划算法

    剑指Offer--动态规划算法 什么是动态规划? 和分治法一样,动态规划(dynamic programming)是通过组合子问题而解决整个问题的解. 分治法是将问题划分成一些独立的子问题,递归地求解 ...

  7. DAG 上的动态规划(训练指南—大白书)

    有向无环图(DAG,Directed Acyclic Graph)上的动态规划是学习动态规划的基础.很多问题都可以转化为DAG上的最长路.最短路或路径计数问题. 一.矩形嵌套 题目描述:       ...

  8. [置顶] 刘汝佳《训练指南》动态规划::Beginner (25题)解题报告汇总

    本文出自   http://blog.csdn.net/shuangde800 刘汝佳<算法竞赛入门经典-训练指南>的动态规划部分的习题Beginner  打开 这个专题一共有25题,刷完 ...

  9. DP入门(2)——DAG上的动态规划

    有向无环图(DAG,Directed Acyclic Graph)上的动态规划是学习动态规划的基础.很多问题都可以转化为DAG上的最长路.最短路或路径计数问题. 一.DAG模型 [嵌套矩形问题] 问题 ...

  10. 动态规划:DAG-嵌套矩形

    据说DAG是动态规划的基础,想一想还真的是这样的,动态规划的所有状态和转移都可以归约成DAG DAG有两个典型模型,一个是嵌套矩形问题一个是硬币问题,这里仅介绍一个嵌套矩形问题 等二轮复习的时候再补上 ...

随机推荐

  1. preparedStatement.setString()方法

    PreparedStatement作用:PreparedStatement是预编译的,对于批量处理可以大大提高效率. 也叫JDBC存储过程. 1.预编译sql语句,效率高 2.安全,避免sql注入 3 ...

  2. js剪贴板应用clipboardData

    clipboardData 对象 提供了对剪贴板的访问. 三个方法 1.clearData(sDataFormat) 删除剪贴板中指定格式的数据. 2.getData(sDataFormat) 从剪贴 ...

  3. Azure - 机器学习企业级服务概述与介绍

    Azure 机器学习 - 为端到端机器学习生命周期使用企业级 AI 服务. 关注TechLead,分享AI全维度知识.作者拥有10+年互联网服务架构.AI产品研发经验.团队管理经验,同济本复旦硕,复旦 ...

  4. [USACO2007OPENG] Dining G

    题目描述 Cows are such finicky eaters. Each cow has a preference for certain foods and drinks, and she w ...

  5. Cocos内存管理解析 CCRef/retain/release/autorelease

    Cocos内存管理源码(autorelease解析) 背景 这段时间在做项目的时候,需求需要往spine动作的挂点上绑定按钮节点,由于按钮在编辑器中是加在已有节点上的,所以在往spine上添加挂点时, ...

  6. docker 设计及源码分析

    1.dockerd 是一个长期运行的守护进程(docker daemon).负责管理 docker 容器的生命周期.镜像和存储等.实际还是通过grpc 的协议调用 containerd 的 api 接 ...

  7. Linux磁盘专题

    物理磁盘名次和其作用 盘片:disk 盘片上下都有磁头. 磁盘面: 盘片有上下两面,每一面叫磁盘面 磁头:heads 每个磁头负责一个磁盘面,负责读取数据.将数据写入磁道. 磁头都是固定在机械臂上(机 ...

  8. 【内核】基于 LSM 框架的 ELF 校验控制

    欲实现操作系统对正在加载的 ELF 文件的校验控制,需要借助 LSM 框架. LSM 框架介绍 LSM 全称 Linux Security MOdule,是 Linux 的一个安全模块框架.LSM 为 ...

  9. 数字孪生和GIS融合为环境保护领域带来的变化

    在当今日益关注环境保护和可持续发展的时代,数字孪生和GIS的融合为环境保护领域带来了巨大的变化.数字孪生是一种以3D模型为基础的仿真技术,能够对真实世界进行精确的建模和模拟,而GIS则是一种用于收集. ...

  10. 【OpenCV】【Python】关于cv2.findContours()轮廓索引(编号)解析(RETR_TREE)

    在打算自己实现二维码的定位的时候,看到了相关博文的关于cv2.findContours返回的层级信息来定位三个"回"字从而达到定位二维码的目的,但是返回的hierarchy中的层级 ...