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


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

相关文章
|
3天前
|
算法 Go 数据库
数据结构/C++:位图 & 布隆过滤器
数据结构/C++:位图 & 布隆过滤器
10 0
|
3天前
|
存储 算法 C++
数据结构/C++:哈希表
数据结构/C++:哈希表
8 2
|
3天前
|
存储 算法 Java
数据结构/C++:红黑树
数据结构/C++:红黑树
11 3
|
3天前
|
存储 算法 C++
数据结构/C++:AVL树
数据结构/C++:AVL树
7 2
|
3天前
|
存储 C语言 C++
数据结构/C++:二叉搜索树
数据结构/C++:二叉搜索树
10 1
|
3天前
|
机器学习/深度学习 存储 算法
数据结构与算法 动态规划(启发式搜索、遗传算法、强化学习待完善)
数据结构与算法 动态规划(启发式搜索、遗传算法、强化学习待完善)
9 1
|
5天前
|
存储 数据库 C++
高效处理大规模数据集的概率型数据结构—— 布隆过滤器 [C++入门]
高效处理大规模数据集的概率型数据结构—— 布隆过滤器 [C++入门]
13 0
|
6天前
|
搜索推荐 C语言
【C语言/数据结构】排序(归并排序|计数排序|排序算法复杂度)
【C语言/数据结构】排序(归并排序|计数排序|排序算法复杂度)
11 0
|
6天前
|
存储 算法
Leetcode 30天高效刷数据结构和算法 Day1 两数之和 —— 无序数组
给定一个无序整数数组和目标值,找出数组中和为目标值的两个数的下标。要求不重复且可按任意顺序返回。示例:输入nums = [2,7,11,15], target = 9,输出[0,1]。暴力解法时间复杂度O(n²),优化解法利用哈希表实现,时间复杂度O(n)。
18 0
|
2天前
|
算法 数据安全/隐私保护 计算机视觉
基于二维CS-SCHT变换和LABS方法的水印嵌入和提取算法matlab仿真
该内容包括一个算法的运行展示和详细步骤,使用了MATLAB2022a。算法涉及水印嵌入和提取,利用LAB色彩空间可能用于隐藏水印。水印通过二维CS-SCHT变换、低频系数处理和特定解码策略来提取。代码段展示了水印置乱、图像处理(如噪声、旋转、剪切等攻击)以及水印的逆置乱和提取过程。最后,计算并保存了比特率,用于评估水印的稳健性。