C++数据结构算法(五)动态规划

简介: C++数据结构算法(五)动态规划

最优化问题


生活中我们常常遇到这样一些问题:


举例——接水问题


有nn个人,每个人接水时间为t_iti,现在只有一个水龙头,请问如何安排nn个人的顺序,使得每个人的平均等待时间最少?


举例——旅行商问题


给定nn个城市,两两城市之间都有公路连接,并且连接ii城市和jj城市之间的公路距离为w_{i, j}wi,j。现有一个旅行商,希望从一个点出发,经过所有城市,再回到起始点。并且,旅行商只愿意经过每个城市一次。请问整个过程的最短距离是多少?


举例——背包问题


给定nn个物品,每个物体有个体积v_ivi和一个价值p_ipi。现有一个容量为VV的背包,请问如何选择物品装入背包,使得获得的总价值最大?


看到上面的例子,我们发现这些问题都是在最大化(或者最小化)某个指标:最小化平均等待时间、最小化总旅行路程、最大化背包里的物品个数。这种类型的问题我们一般称为最优化问题。


最优化问题(optimization problem)是在一些约束下,通过进行一些决策,使得最终获益最大(或损失最小)的一类问题。


可以感受到,最优化问题和现实中的生产和生活场景联系非常紧密。所以,解决最优化问题,是一个非常重要的课题。


image.png


例题带入:



image.png

image.png


代码实现


动态规划的代码实现相对简单,基本上是使用循环来计算递推序列的过程。下面直接给出代码:

#include <bits/stdc++.h>
#define N 1005
#define M 110
using namespace std;
int n;
int a[N][N], f[N][N];
int main() {
    // 输入
  cin >> n;
  for (int i = 1; i <= n; ++i) 
  for (int j = 1; j <= i; ++j)
    cin >> a[i][j];
    // 动态规划过程
  for (int i = 1; i <= n; ++i)
  for (int j = 1; j <= i; ++j)
    f[i][j] = max(f[i - 1][j - 1], f[i - 1][j]) + a[i][j];
      // 此处没有讨论 j == 1 和 i == j 的情况
      // 是因为当 j == 1 时,f[i - 1][j] == 0
      // 是因为在数字金字塔所有数字都是正数的情况下
      // max函数一定不会选择用f[i - 1][j]来转移
      // i == j 的情况同理
    // 输出
  int ans = 0;
  for (int i = 1; i <= n; ++i) ans = max(ans, f[n][i]); // 求第n行的最大值
  cout << ans << endl;
  return 0;
}


空间复杂度


该问题的空间复杂度是O(n^2)O(n2)。


时间复杂度


动态规划因为大部分都是由一些for循环组成,所以复杂度分析相对简单。在本例中,因为有两层for循环,并且都是nn左右的数量级,所以整个算法的复杂度为O(n^2)O(n2)。


动态规划分析流程和条件


现在让我们一起总结一下动态规划分析流程和条件:


首先是动态规划分析流程:


在数字金字塔的分析中我们发现,用动态规划解决问题的过程,就是一个把原问题的过程变成一个阶段性决策的过程。


比如在数字金字塔问题中,路径每往下延伸一行,我们就进行到下一个阶段,或者步骤。而在每一个步骤里,我们需要决策到底是从左上过来,还是从右上过来。在运用动态规划方法分析问题的过程中,下面四个要素是要明确的:


状态。状态用于描述每一个步骤的参数以及结果。在数字金字塔的例子中,每个f[i][j]表示的就是一个状态。其中数组下标是当前路径的结尾,而值是以i行j列元素为结尾的所有路径中的最大值。

转移方程。转移方程用于描述不同状态之间的关系。在上面的例子中,f[i][j] = max(f[i - 1][j - 1], f[i - 1][j]) + a[i][j]就是一条转移方程。它描述了结尾为下一行的第j个结点的路径,和以上一行第j-1个结点和第j个结点路径之间的关系。

初始状态。初始状态描述的是整个转移方程推导的开始,是不需要经由别的状态就知道结果的状态。上面的例子中,f[1][1]=a[i][j]就是初始状态。我们以这个状态为起点,最终推导出整个三角形上每一个位置的答案。

转移方向。转移方向描述的是推导出不同状态的解的先后关系。我们之所以要明确转移方向,是因为我们不希望"已知B状态只能由A状态推到过来。但是当我们想推导B时,发现A状态的结果我们还不知道”类似的事情发生。比如由转移方程中f[i][j] = max(f[i - 1][j - 1], f[i - 1][j]) + a[i][j],我们发现,如果想推导f[i][j],必须先推导f[i - 1][j - 1]和f[i - 1][j]。所以,按照i从小到大,j从小到大的顺序推导是一种可行的推导方向。

所以,为了用动态规划解决问题,我们就需要明确上面四个方面,其中最重要的就是设计状态和转移方程。


动态规划分析流程和条件


动态规划条件


那么,是不是所有最优化类问题都能用动态规划来解决呢?


不是。


那么,使用动态规划需要满足什么条件?


在这里指出,用动态规划求解要求我们设计出状态和转移方程,使得它们满足下面三个条件:


最优子结构:原问题的最优解,必然是通过子问题的最优解得到的。比如上面的例子中,我们提过,如果所有以77为结尾的路径里面,有一条的数字和最大。那么,在所有经由77到达22的路径里,我们一定选择到达77的和最大的一条。所以,这样的问题具有最优子结构的性质。


无后效性:前面状态的决策不会限制到后面的决策。比如说数字金字塔问题里,无论以任何方式走到77,我们都可以在后面接一段从77走到22,变成一条到达22的路径。所以,数字金字塔没有后效性。但是,在旅行商问题里,如果我们从11号城市开始,走到33号城市,那么途中经没经过22号,将会影响到33号城市后面的路径。这个场景就是有后效性的例子。


重复子问题:一个子问题可以被重复利用到多个父亲状态中。我们发现在下面这张图中,f[3][2]既可以用来更新f[4][2],又可以用来更新f[4][3]。那么,因为我们把它存在数组里,所以只需要计算一次f[3][2],就可以使用很多次。也就是说,f[4][2]和f[4][3]有个共同的子问题f[3][2]。


image.png


动态规划算法的关键在于解决冗余,这是动态规划算法的根本目的。


动态规划实质上是一种以空间换时间的技术,它在实现的过程中,不得不存储产生过程中的各种状态,所以它的空间复杂度要大于其他的算法。


选择动态规划算法是因为动态规划算法在空间上可以承受,而搜索算法在时间上却无法承受,所以我们舍空间而取时间。


总结


动态规划是一种解决某种最优化问题的方法。

最优化问题的目的就是在一些场景限制下,通过不同的决策,达到最大收益或者最小损失。


使用动态规划的条件是:最优子结构、无后效性和重复子问题。


最优子结构保证了我们能够通过选取子问题的最优解最终拼成原问题的解;


无后效性保证了整个过程的推导是同一个方向的,不会出现环的情况;


重复子问题一定程度上保证了总状态个数不会与每个状态的选择数呈指数增长。


使用动态规划解决问题,需要明确状态设计、转移方程、初始状态和转移方向四个方面。这样,我们就可以用类似根据递推式计算数列第nn项的方法得到最终结果。


image.png


数字金字塔问题:给定一个nn层的金字塔,求一条从最高点到底层任意点的路径使得路径经过的数字之和最大。注:每一步可以走到左下方的点也可以到达右下方的点。


完整代码:

#include <bits/stdc++.h>
#define N 1005
#define M 110
using namespace std;
int n;
int a[N][N], f[N][N];
int main() {
    // 输入
  cin >> n;
  for (int i = 1; i <= n; ++i) 
  for (int j = 1; j <= i; ++j)
    cin >> a[i][j];
    // 动态规划过程
  for (int i = 1; i <= n; ++i)
  for (int j = 1; j <= i; ++j)
    f[i][j] = max(f[i - 1][j - 1], f[i - 1][j]) + a[i][j];
      // 此处没有讨论 j == 1 和 i == j 的情况
      // 是因为当 j == 1 时,f[i - 1][j] == 0
      // 是因为在数字金字塔所有数字都是正数的情况下
      // max函数一定不会选择用f[i - 1][j]来转移
      // i == j 的情况同理
    // 输出
  int ans = 0;
  for (int i = 1; i <= n; ++i) ans = max(ans, f[n][i]); // 求第n行的最大值
  cout << ans << endl;
  return 0;
}

复杂度分析


空间复杂度: 数字金字塔的空间复杂度是O(n^2)O(n2)。


时间复杂度:动态规划因为大部分都是由一些for循环组成,所以复杂度分析相对简单。在本例中,因为有两层for循环,并且都是nn左右的数量级,所以整个算法的复杂度为O(n^2)O(n2)。


动态规划算法的关键在于解决冗余,舍空间而取时间。

相关文章
|
6天前
|
算法 C++ 容器
C++标准库中copy算法的使用
C++标准库中copy算法的使用
|
6天前
|
安全 编译器 C语言
【C++数据结构】string的模拟实现
【C++数据结构】string的模拟实现
|
1月前
|
算法 开发者 Python
惊呆了!Python算法设计与分析,分治法、贪心、动态规划...这些你都会了吗?不会?那还不快来学!
【7月更文挑战第10天】探索编程巅峰,算法至关重要。Python以其易读性成为学习算法的首选。分治法,如归并排序,将大问题拆解;贪心算法,如找零问题,每步求局部最优;动态规划,如斐波那契数列,利用子问题解。通过示例代码,理解并掌握这些算法,提升编程技能,面对挑战更加从容。动手实践,体验算法的神奇力量吧!
56 8
|
2天前
|
机器学习/深度学习 人工智能 算法
【人工智能】线性回归模型:数据结构、算法详解与人工智能应用,附代码实现
线性回归是一种预测性建模技术,它研究的是因变量(目标)和自变量(特征)之间的关系。这种关系可以表示为一个线性方程,其中因变量是自变量的线性组合。
9 2
|
5天前
|
机器学习/深度学习 算法 Java
算法设计(动态规划应用实验报告)实现基于贪婪技术思想的Prim算法、Dijkstra算法
这篇文章介绍了基于贪婪技术思想的Prim算法和Dijkstra算法,包括它们的伪代码描述、Java源代码实现、时间效率分析,并展示了算法的测试用例结果,使读者对贪婪技术及其应用有了更深入的理解。
算法设计(动态规划应用实验报告)实现基于贪婪技术思想的Prim算法、Dijkstra算法
|
5天前
|
算法 Java 测试技术
算法设计(动态规划实验报告) 基于动态规划的背包问题、Warshall算法和Floyd算法
这篇文章介绍了基于动态规划法的三种算法:解决背包问题的递归和自底向上实现、Warshall算法和Floyd算法,并提供了它们的伪代码、Java源代码实现以及时间效率分析。
算法设计(动态规划实验报告) 基于动态规划的背包问题、Warshall算法和Floyd算法
|
23天前
|
存储 算法 索引
算法与数据结构
算法与数据结构
26 8
|
26天前
|
搜索推荐 算法
【数据结构】排序算法——Lesson2
【7月更文挑战第24天】
14 3
|
3天前
|
存储 缓存 算法
深入解析B树:数据结构、存储结构与算法优势
深入解析B树:数据结构、存储结构与算法优势
|
26天前
|
存储 数据格式 运维
开发与运维C++问题之更改数据模型为通用数据结构如何解决
开发与运维C++问题之更改数据模型为通用数据结构如何解决
15 1

热门文章

最新文章