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)。


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

相关文章
|
2月前
|
算法 数据处理 C语言
C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合
本文深入解析了C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合,旨在帮助读者掌握这一高效的数据处理方法。
49 1
|
2月前
|
机器学习/深度学习 算法 数据挖掘
K-means聚类算法是机器学习中常用的一种聚类方法,通过将数据集划分为K个簇来简化数据结构
K-means聚类算法是机器学习中常用的一种聚类方法,通过将数据集划分为K个簇来简化数据结构。本文介绍了K-means算法的基本原理,包括初始化、数据点分配与簇中心更新等步骤,以及如何在Python中实现该算法,最后讨论了其优缺点及应用场景。
117 4
|
11天前
|
存储 运维 监控
探索局域网电脑监控软件:Python算法与数据结构的巧妙结合
在数字化时代,局域网电脑监控软件成为企业管理和IT运维的重要工具,确保数据安全和网络稳定。本文探讨其背后的关键技术——Python中的算法与数据结构,如字典用于高效存储设备信息,以及数据收集、异常检测和聚合算法提升监控效率。通过Python代码示例,展示了如何实现基本监控功能,帮助读者理解其工作原理并激发技术兴趣。
49 20
|
3天前
|
存储 算法 安全
基于红黑树的局域网上网行为控制C++ 算法解析
在当今网络环境中,局域网上网行为控制对企业和学校至关重要。本文探讨了一种基于红黑树数据结构的高效算法,用于管理用户的上网行为,如IP地址、上网时长、访问网站类别和流量使用情况。通过红黑树的自平衡特性,确保了高效的查找、插入和删除操作。文中提供了C++代码示例,展示了如何实现该算法,并强调其在网络管理中的应用价值。
|
1天前
|
存储 算法 安全
基于哈希表的文件共享平台 C++ 算法实现与分析
在数字化时代,文件共享平台不可或缺。本文探讨哈希表在文件共享中的应用,包括原理、优势及C++实现。哈希表通过键值对快速访问文件元数据(如文件名、大小、位置等),查找时间复杂度为O(1),显著提升查找速度和用户体验。代码示例展示了文件上传和搜索功能,实际应用中需解决哈希冲突、动态扩容和线程安全等问题,以优化性能。
|
8天前
|
算法 安全 C++
用 C++ 算法控制员工上网的软件,关键逻辑是啥?来深度解读下
在企业信息化管理中,控制员工上网的软件成为保障网络秩序与提升办公效率的关键工具。该软件基于C++语言,融合红黑树、令牌桶和滑动窗口等算法,实现网址精准过滤、流量均衡分配及异常连接监测。通过高效的数据结构与算法设计,确保企业网络资源优化配置与安全防护升级,同时尊重员工权益,助力企业数字化发展。
32 4
|
2月前
|
存储 算法 搜索推荐
Python 中数据结构和算法的关系
数据结构是算法的载体,算法是对数据结构的操作和运用。它们共同构成了计算机程序的核心,对于提高程序的质量和性能具有至关重要的作用
|
2月前
|
数据采集 存储 算法
Python 中的数据结构和算法优化策略
Python中的数据结构和算法如何进行优化?
|
2月前
|
算法
数据结构之路由表查找算法(深度优先搜索和宽度优先搜索)
在网络通信中,路由表用于指导数据包的传输路径。本文介绍了两种常用的路由表查找算法——深度优先算法(DFS)和宽度优先算法(BFS)。DFS使用栈实现,适合路径问题;BFS使用队列,保证找到最短路径。两者均能有效查找路由信息,但适用场景不同,需根据具体需求选择。文中还提供了这两种算法的核心代码及测试结果,验证了算法的有效性。
112 23
|
2月前
|
算法
数据结构之蜜蜂算法
蜜蜂算法是一种受蜜蜂觅食行为启发的优化算法,通过模拟蜜蜂的群体智能来解决优化问题。本文介绍了蜜蜂算法的基本原理、数据结构设计、核心代码实现及算法优缺点。算法通过迭代更新蜜蜂位置,逐步优化适应度,最终找到问题的最优解。代码实现了单链表结构,用于管理蜜蜂节点,并通过适应度计算、节点移动等操作实现算法的核心功能。蜜蜂算法具有全局寻优能力强、参数设置简单等优点,但也存在对初始化参数敏感、计算复杂度高等缺点。
62 20