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


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

相关文章
|
12天前
|
存储 C语言 C++
【C++数据结构——栈与队列】顺序栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现顺序栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 1.初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储
128 75
|
12天前
|
存储 C++
【C++数据结构——树】哈夫曼树(头歌实践教学平台习题) 【合集】
【数据结构——树】哈夫曼树(头歌实践教学平台习题)【合集】目录 任务描述 相关知识 测试说明 我的通关代码: 测试结果:任务描述 本关任务:编写一个程序构建哈夫曼树和生成哈夫曼编码。 相关知识 为了完成本关任务,你需要掌握: 1.如何构建哈夫曼树, 2.如何生成哈夫曼编码。 测试说明 平台会对你编写的代码进行测试: 测试输入: 1192677541518462450242195190181174157138124123 (用户分别输入所列单词的频度) 预
49 14
【C++数据结构——树】哈夫曼树(头歌实践教学平台习题) 【合集】
|
12天前
|
存储 C++ 索引
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
【数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】初始化队列、销毁队列、判断队列是否为空、进队列、出队列等。本关任务:编写一个程序实现环形队列的基本运算。(6)出队列序列:yzopq2*(5)依次进队列元素:opq2*(6)出队列序列:bcdef。(2)依次进队列元素:abc。(5)依次进队列元素:def。(2)依次进队列元素:xyz。开始你的任务吧,祝你成功!(4)出队一个元素a。(4)出队一个元素x。
34 13
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
|
8天前
|
负载均衡 算法 安全
探秘:基于 C++ 的局域网电脑控制软件自适应指令分发算法
在现代企业信息化架构中,局域网电脑控制软件如同“指挥官”,通过自适应指令分发算法动态调整指令发送节奏与数据量,确保不同性能的终端设备高效运行。基于C++语言,利用套接字实现稳定连接和线程同步管理,结合实时状态反馈,优化指令分发策略,提升整体管控效率,保障网络稳定,助力数字化办公。
44 19
|
2天前
|
算法 Java C++
【潜意识Java】蓝桥杯算法有关的动态规划求解背包问题
本文介绍了经典的0/1背包问题及其动态规划解法。
26 5
|
12天前
|
存储 人工智能 算法
【C++数据结构——图】最短路径(头歌教学实验平台习题) 【合集】
任务描述 本关任务:编写一个程序,利用Dijkstra算法,实现带权有向图的最短路径。 相关知识 为了完成本关任务,你需要掌握:Dijkst本关任务:编写一个程序,利用Dijkstra算法,实现带权有向图的最短路径。为了完成本关任务,你需要掌握:Dijkstra算法。带权有向图:该图对应的二维数组如下所示:Dijkstra算法:Dijkstra算法是指给定一个带权有向图G与源点v,求从v到G中其他顶点的最短路径。Dijkstra算法的具体步骤如下:(1)初始时,S只包含源点,即S={v},v的距离为0。
45 15
|
12天前
|
Java C++
【C++数据结构——树】二叉树的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现二叉树的基本运算。​ 相关知识 创建二叉树 销毁二叉树 查找结点 求二叉树的高度 输出二叉树 //二叉树节点结构体定义 structTreeNode{ intval; TreeNode*left; TreeNode*right; TreeNode(intx):val(x),left(NULL),right(NULL){} }; 创建二叉树 //创建二叉树函数(简单示例,手动构建) TreeNode*create
36 12
|
12天前
|
C++
【C++数据结构——树】二叉树的性质(头歌实践教学平台习题)【合集】
本文档介绍了如何根据二叉树的括号表示串创建二叉树,并计算其结点个数、叶子结点个数、某结点的层次和二叉树的宽度。主要内容包括: 1. **定义二叉树节点结构体**:定义了包含节点值、左子节点指针和右子节点指针的结构体。 2. **实现构建二叉树的函数**:通过解析括号表示串,递归地构建二叉树的各个节点及其子树。 3. **使用示例**:展示了如何调用 `buildTree` 函数构建二叉树并进行简单验证。 4. **计算二叉树属性**: - 计算二叉树节点个数。 - 计算二叉树叶子节点个数。 - 计算某节点的层次。 - 计算二叉树的宽度。 最后,提供了测试说明及通关代
37 10
|
12天前
|
算法 C++
【C++数据结构——图】最小生成树(头歌实践教学平台习题) 【合集】
【数据结构——图】最小生成树(头歌实践教学平台习题)目录 任务描述 相关知识 测试说明 我的通关代码: 测试结果:【合集】任务描述 本关任务:编写一个程序求图的最小生成树。相关知识 为了完成本关任务,你需要掌握:1.建立邻接矩阵,2.Prim算法。建立邻接矩阵 上述带权无向图对应的二维数组,根据它建立邻接矩阵,如图1建立下列邻接矩阵。注意:INF表示无穷大,表示整数:32767 intA[MAXV][MAXV];Prim算法 普里姆(Prim)算法是一种构造性算法,从候选边中挑
36 10
|
12天前
|
存储 算法 C++
【C++数据结构——图】图的邻接矩阵和邻接表的存储(头歌实践教学平台习题)【合集】
本任务要求编写程序实现图的邻接矩阵和邻接表的存储。需掌握带权有向图、图的邻接矩阵及邻接表的概念。邻接矩阵用于表示顶点间的连接关系,邻接表则通过链表结构存储图信息。测试输入为图的顶点数、边数及邻接矩阵,预期输出为Prim算法求解结果。通关代码提供了完整的C++实现,包括输入、构建和打印邻接矩阵与邻接表的功能。
37 10