数据结构与算法设计分析——动态规划

news2024/12/24 0:05:39

目录

  • 一、动态规划的定义
  • 二、动态规划的基本要素和主要步骤
    • (一)最优子结构
    • (二)重叠子问题
  • 三、贪心法、分治法和动态规划的对比
    • (一)贪心法
    • (二)分治法
    • (三)动态规划
  • 四、动态规划的递归和迭代法求解
    • (一)由顶向下的递归法
    • (二)由底向上的迭代法
  • 五、动态规划的应用
    • (一)斐波那契数列
    • (二)汉诺塔
    • (三)最优二叉查找树
    • (四)矩阵连乘
    • (五)0-1背包

一、动态规划的定义

动态规划的基本思想是将问题分成若干个子问题,先求解子问题,然后从子问题的解进而得到原问题的解。

二、动态规划的基本要素和主要步骤

动态规划算法的两个基本要素是最优子结构和重叠子问题,其主要步骤如下:
①问题需要具有最优子结构性质;
②构造最优值的递归表达式;
③最优值的算法描述;
④构造最优解。

(一)最优子结构

问题可分为若干个子问题,最优子结构指的是问题的最优解可以由其子问题的最优解求解出来,它的也是依据将复杂问题分解成简单子问题的方法。总的来说,某一问题可用动态规划算法求解的显著特征是该问题具有最优子结构性质。

(二)重叠子问题

当划分的子问题中有些子问题重复出现时,这些问题是会被重复计算和求解的,从而会导致算法效率低且造成空间开销,而动态规划的优势在求解划分的重叠子问题的时候,将第一次求解的解通过数组或表存储起来,从而可以避免重复计算后面相同的子问题。

三、贪心法、分治法和动态规划的对比

(一)贪心法

每一步都选择当前最优解,而不考虑该决策对整体的影响。贪心算法通常适用于简单、容易分解的问题,即具有贪心选择性质最优子结构两个重要的性质的问题求解。贪心法总是做出最好的选择,可以快速地得到近似上的最优解的情况(局部最优选择),时间复杂度较低,但其缺点是不能保证得到全局上的最优解。

(二)分治法

可分为分解、治理两大步骤,其通常适用于优化问题,采用递归的思想,每次将问题分成两个或更多的小问题,由于各个子问题是相互独立的,所以通过递归最终合并可以很容易得到原问题的解,但若各个子问题不是相互独立的时,则会造成重复,从而会有很高的时间复杂度。

(三)动态规划

与分治法不同的是,动态规划通常解决的是重叠子问题性质最优子结构性质的问题,其中解决子问题只需一次,解决后会将其解保存并重复使用,避免重复计算。动态规划通常采用自底向上的方式,通过先解决子问题,再解决大问题的方式进行求解。动态规划适合用于优化问题,并且能够保证得到全局最优解。但对比贪心法、分治法算法,由于需要存储各种状态,所以其需要的空间更大。

三种算法的对比如下表:

名称贪心法分治法动态规划
适用性一般问题优化问题优化问题
求解线性求解递归求解递归和迭代求解
求解顺序先选择后解决子问题先选择后解决子问题先解决子问题后选择
特征由顶向下由顶向下由顶向下、由底向上
最优子结构满足不满足满足
子问题规模仅一个子问题所有子问题所有子问题
子问题独立性仅一个子问题每个子问题独立每个子问题重叠不独立
子问题最优解部分最优解全部最优解部分最优解

四、动态规划的递归和迭代法求解

(一)由顶向下的递归法

由顶向下的递归法也被称为带记忆的由顶向下法,可概括为递归+可记忆,是一种自上而下的分治思想,一开始将问题分成子问题,通过递归先解决子问题,这里的可记忆指的是保存每个子问题的解,这些解被保存到一个数组或表格中,其目的是为了避免重复计算,节省时间。该方法通常由递归函数实现,同时,结合记忆化可以消除重复计算,从而大幅度提升计算效率,缩短时间。

(二)由底向上的迭代法

由底向上的迭代方法可概括为迭代+动态规划,是一种自下而上的构建思想。通过将问题分成相互独立、可简单直接求解的子问题,并将子问题的解按由小到大的顺序保存下来,逐步构建出问题的最优解,即当求解某个子问题时,其所依赖的更小的子问题已经是求解了的,从而每个子问题只需求解一次即可。该方法通常由循环语句实现,可以避免采用递归函数时所带来的额外开销。

以上两种方法具有相同的渐进运行时间,仅有的差异是在某些特殊情况下,由顶向下方法并未真正递归地考察所有可能的子问题。由于没有频繁的递归函数调用的开销,由底向上方法的时间复杂性函数通常具有更小的系数。

五、动态规划的应用

(一)斐波那契数列

由斐波那契数列(Fibonacci),可得
递归关系式:F(n) = F(n-1) + F(n-2) ,
其中F(0)=0,F(1) = F(2) = 1。

f(n)的求解可以类比一棵二叉树,以F(5)为例,根据递归关系式可画出二叉树,如下图:
在这里插入图片描述
1、若采用不带记忆的由顶向下的递归法时,其中有重复的子问题,会造成重复计算,从而加大开销,且当计算的n值越来越大时,空间开销会更大。
所以,采用带记忆的由顶向下的递归法,通过建立一个一维数组来保存每个子问题的解,当计算时,只需从数组中取出相应的值即可,从而可以避免重复计算【避免子问题重叠】。这种方法只需求需要的相应值即可,该树中,有重复的子问题如下:
在这里插入图片描述
创建一个数组,首先将F(0)、F(1)和F(2)的解存在数组中,如下:

F(0)F(1)F(2)
数组011

求解F(3)时,根据递归关系式,F(n) = F(n-1) + F(n-2) ,即F(3) = F(2) + F(1) =1+1=2,直接取数组中F(2)和F(1)的值代入计算即可,然后将F(3)存放在数组中,如下:

F(0)F(1)F(2)F(3)
数组0112

求解F(4)时,根据递归关系式,F(n) = F(n-1) + F(n-2) ,即F(4) = F(3) + F(2) =2+1=3,直接取数组中F(3)和F(2)的值代入计算即可,然后将F(4)存放在数组中,如下:

F(0)F(1)F(2)F(3)F(4)
数组01123

……依次最终求得F(5)=F(4) + F(3)=3+2=5:

F(0)F(1)F(2)F(3)F(4)F(5)
数组011235

2、若采用由底向上的迭代方法,自下而上的构建,通常由循环语句实现,可以避免采用递归函数时所带来的额外开销,如下代码,通过for()循环实现:

#include <stdio.h>
int main()
{
    int i, n;
    long long int f1 = 1, f2 = 1, f;		//初始值f1=f2=1
    printf("请输入要输出的斐波那契数列项数:");
    scanf("%d", &n);
    printf("斐波那契数列前%d项为:\n", n);
    printf("%lld %lld ", f1, f2);
    for (i = 3; i <= n; i++){
        f = f1 + f2;
        printf("%lld ", f);
        f1 = f2;
        f2 = f;
    }
    printf("\n");
    return 0;
}

(二)汉诺塔

首先,这里简单地以一个三层的汉诺塔,熟悉一下汉诺塔的游戏规则:一共有三根柱子,第一根柱子上有三个从上到下由小到大的圆盘,规定每次在三根柱子之间一次只能移动一个圆盘,且小圆盘上不能放大圆盘,试将第一根的三个圆盘移动到第三根柱子上。

点击链接可以试试,怎么让移动的次数最少?
汉诺塔可视化小游戏 Tower of Hanoi

最终的目的是完成的步数越少越好,我们可以很容易地得到三层的汉诺塔的最少移动步数为7次,移动过程中三个柱子共有8种不同的状态,如下:
请添加图片描述
同样的,四层的汉诺塔的最少移动步数为15次,而移动过程中三个柱子共有16种不同的状态:
在这里插入图片描述
五层的汉诺塔的最少移动步数为31次,而移动过程中三个柱子共有32种不同的状态:
在这里插入图片描述
……
通过数学归纳法,可得,当汉诺塔的层数为n时,最少的移动次数为 2n-1次,移动过程中三个柱子共有2n 种不同的状态,其时间复杂度为O(2n) 。

  • 汉诺塔问题的动态规划优化问题是通过带记忆的由顶向下法求解,即递归+可记忆,先解决小的问题,然后将问题的规模从小到大逐步扩大,最终得到问题的答案,且过程中避免了重复计算。【避免子问题重叠

在这里插入图片描述
若以f[ n ]表示n个圆盘从TOWER 1移动到TOWER 3的最少步数,则f[1] = 1,即一个圆盘移动到TOWER 3的步数为1,而当n>1时,分析可知:

为了符合规则,需要先将一部分移动到TOWER 2上面,即有n-1个圆盘从TOWER 1经TOWER 3移动到TOWER 2上面,然后再将最大的圆盘移动到TOWER 3上面,由于TOWER 2已经是有序的,所以,需要将这n-1个圆盘从TOWER 2移动到TOWER 1,最终再移动到TOWER 3上。

可得,n个圆盘从TOWER 1移动到TOWER 3的最少步数为f[ n ]=f (n -1) + 1 + f (n - 1)=2f (n - 1)+1= 2n-1+1,即T(n)= 2n-1+1,所以时间复杂度为O(2n) 。

也可以从圆盘的数量来计算,按照规则,一个圆盘从TOWER 1移动到TOWER 3需要1步,两个圆盘从TOWER 1移动到TOWER 3需要3步(小的圆盘移动到中转点,再将大的圆盘移动到终点,最后将小的圆盘移动到终点),三个圆盘从TOWER 1移动到TOWER 3需要7步,……,n个圆盘从TOWER 1移动到TOWER 3需要3步 2n-1步。

(三)最优二叉查找树

1、最优二叉查找树的定义
在n个不同关键字组成的有序序列中,每个关键字被查找的概率为pi,通过关键字构造一棵的二叉查找树,它具有最小平均比较次数,即为最优二叉查找树(OBST),且左右子树也是最优二叉查找树,但最优二叉查找树不一定是高度最小的二叉查找树。
2、二叉查找树平均比较次数的计算
设有n=6个关键字的集合,各个实结点的查找概率分别为5:5%、2:30%、9:10%、0:3%、4:14%、6:25%,假设虚结点的查找概率分别为:e0:2%、e1:10%、e2:5%、e3:5%、e4:11%、e5:15%、e6:10%,计算二叉查找树的平均比较次数:
在这里插入图片描述
实结点:1×0.05+2×(0.3+0.1)+3×(0.03+0.14+0.25)=2.11;
虚结点:2×0.02+3×(0.1+0.05+0.05+0.11+0.15+0.1)=1.72,
即二叉查找树的平均比较次数为2.11+1.72=3.83。
3、最优子结构
最优二叉查找树中采用了动态规划的思想,分析其最优子结构:若一个二叉查找树是最优二叉查找树,可将其分为根结点、左子树和右子树,所以其左、右子树也是最优二叉查找树。
4、构建最优二叉查找树的分析
构建一个含n个关键字的最优二叉查找树的时间复杂度为O(n3),由于通过使用二维数组,避免重复计算子树的最小权值和【避免子问题重叠】,从而提高了算法的效率,其空间复杂度为O(n2)。

(四)矩阵连乘

问题描述:在《线性代数》里面,学过矩阵的乘法,若干个矩阵相乘时,由于满足结合律,即(AB)C = A (BC),可以通过加括号可以改变乘积的顺序,而结果不改变。若从相乘的计算量上来看,怎么让计算所需要的代价最少,即怎么通过加括号(改变乘积顺序),来使计算量最小,这是通过动态规划来优化问题的所在。

  • 可将问题划分成两个子问题,即两个部分的矩阵相乘,分别对两个子问题进行递归求解,通过定义一个二维数组C[i][j]来表示第i个矩阵到第j个矩阵相乘的最小代价,以分界点k分割问题,对于两个子问题可分别表示为C[i][k]和C[k+1][j],然后通过相同的方法继续进行递归求解,由于第i个矩阵的行数在p[i-1],其列数在p[i],所以递归式为C[i][j]=C[i][k]+C[k+1][j]+p[i-1]×p[k]×p[j],该算法的时间复杂度取决于对所有矩阵求优解,即递归式上花费的时间,时间复杂度为O(n3)。

(五)0-1背包

问题描述:有n件物品,对某一物品i,其价值为V,重量为W,怎么选择将物品放入背包中,使得放入背包的物品的总价值最大,而动态规划就是来优化这个问题。

  • 通过一个数组C[i][j]表示i个物品放入背包,此时背包容量为j所能得到的最大价值,由于当每个物品放入背包时,都要两种情况,能放进背包的要求是其所占重量要小于或等于当前背包剩余容量,即此时总价值为C[i-1][j-wi]+vi;不能放进背包的情况时,此时总价值为C[i-1][j],然后通过这两种状态取最大值,即C[i][j]=Max{C[i-1][j],C[i-1][j-wi]+vi}。由于得到的是背包的最大价值,设i=n、j=W,再通过一开始的最优解C[n][W]的值反推,确定放入背包的相应物品,即实现放入背包物品价值最大化,该算法的时间复杂度取决于物品个数n的一个for()循环语句和物品的重量W的一个for()循环语句,故其时间复杂度为O(nW)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1119572.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

flink中使用GenericWriteAheadSink的优缺点

背景 GenericWriteAheadSink是flink中提供的实现几乎精确一次输出的数据汇抽象类&#xff0c;本文就来看一下使用GenericWriteAheadSink的优缺点 GenericWriteAheadSink的优缺点 先看一下GenericWriteAheadSink的原理图 优点&#xff1a; 几乎可以精确一次的输出&#xf…

[人工智能-综述-15]:第九届全球软件大会(南京)有感 -4-大语言模型全流程、全方面提升软件生产效能

目录 一、软件生产通用模型 1.1 企业软件生产模型 1.2 软件项目管理 VS 软件工程 1.3 企业管理与部门管理 二、第一步&#xff1a;企业数字化&#xff1a;企业信息系统 三、第二步&#xff1a;软件生产自动化&#xff1a;DevOps 四、第四步&#xff1a;软件生产智能化&a…

LeetCode讲解篇之77. 组合

文章目录 题目描述题解思路题解代码 题目描述 题解思路 遍历nums&#xff0c;让当前数字添加到结果前缀中&#xff0c;递归调用&#xff0c;直到前缀的长度为k&#xff0c;然后将前缀添加到结果集 题解代码 func combine(n int, k int) [][]int {var nums make([]int, n)fo…

lv8 嵌入式开发-网络编程开发 20 域名解析与http服务实现原理

目录 1 域名解析 2 如何实现万维网服务器&#xff1f; 2.1 HTTP 的操作过程 2.2 万维网服务器实现 1 域名解析 域名解析gethostbyname函数 主机结构在 <netdb.h> 中定义如下&#xff1a; struct hostent {char *h_name; /* 官方域名 */char **h_alias…

MIT 6.824 -- Cache Consistency -- 11

MIT 6.824 -- Cache Consistency -- 11 引言严峻挑战锁服务缓存一致性问题案例演示优化 原子性问题故障恢复问题log内容故障恢复 小结 课程b站视频地址: MIT 6.824 Distributed Systems Spring 2020 分布式系统 推荐伴读读物: 极客时间 – 大数据经典论文解读DDIA – 数据密集…

网工记背命令(7)----静态路由(负载分担,主备备份)

1.静态路由负载分担 如图所示&#xff0c;属于不同网段的主机通过几台 Switch 相连&#xff0c;要求不配置动态路由协议&#xff0c;使不同网 段的任意两台主机之间能够互通&#xff0c;从拓扑图中可以看出&#xff0c;从 PCA 到 PCC 有两条路径可以过去&#xff0c;分别是 PC…

第三章 内存管理 十三、页面置换算法(最佳置换算法、先进先出置换算法、最近最久未使用置换算法、时钟置换算法、改进型的时钟置换算法)

目录 一、定义 二、分类 1、最佳置换算法 / 最远置换算法&#xff08;OPT&#xff0c;Optimal): 1.1、定义&#xff1a; 1.2、例子&#xff1a; 2、先进先出置换算法(FIFO&#xff09;: 2.1、定义&#xff1a; 2.2、实现方法&#xff1a; 2.3、例子&#xff1a; 3、最…

【Pillow库的内涵】01/3 进行基本图像操作

一、说明 Pillow 具有被 Python 社区广泛使用的优势&#xff0c;并且它不像其他一些图像处理库那样具有陡峭的学习曲线。应用PIL库的Image对象&#xff0c;益处很多&#xff0c;首先它可以处理网上URL文件&#xff0c;其次&#xff0c;图片可以方面转化成int32、64或float类型&…

蓝桥杯 (猜生日、棋盘放麦子、MP3储存 C++)

思路&#xff1a; 1、用循环。 2、满足条件&#xff0c;能整除2012、3、12且month等于6、day<30 #include<iostream> using namespace std; int main() {for (int i 19000101; i < 20120312; i){int month i / 100 % 100;int day i % 100;if (i % 2012 0 &…

互联网Java工程师面试题·Java 面试篇·第二弹

目录 15、什么是不可变对象&#xff08;immutable object&#xff09;&#xff1f;Java 中怎么创建一个不可变对象&#xff1f; 16、我们能创建一个包含可变对象的不可变对象吗&#xff1f; 17、Java 中应该使用什么数据类型来代表价格&#xff1f; 18、怎么将 byte 转换为 Str…

图论03-【无权无向】-图的深度优先遍历-路径问题/检测环/二分图

文章目录 1. 代码仓库2. 单源路径2.1 思路2.2 主要代码 3. 所有点对路径3.1 思路3.2 主要代码 4. 路径问题的优化-提前结束递归4.1 思路4.2 主要代码 5. 检测环5.1 思路5.2 主要代码 5. 二分图5.1 思路5.2 主要代码5.2.1 遍历每个联通分量5.2.2 递归判断相邻两点的颜色是否一致…

192:最近的系列思考2/犬岛APP 的使用理解

最近的一些契机&#xff0c;导致一些思考&#xff1a; ​ * 与产品经理意志相悖的产品* 与最初的设计定位不匹配的产品社交大牛的APP一上线就引来诸多关注&#xff0c;作为总设计的纯大大非常简明扼要的说明了这个APP的定位&#xff1a;给内涵&#xff08;含&#xff09;有趣的…

图论04-【无权无向】-图的广度优先遍历

文章目录 1. 代码仓库2. 广度优先遍历图解3.主要代码4. 完整代码 1. 代码仓库 https://github.com/Chufeng-Jiang/Graph-Theory 2. 广度优先遍历图解 3.主要代码 原点入队列原点出队列的同时&#xff0c;将与其相邻的顶点全部入队列下一个顶点出队列出队列的同时&#xff0c;将…

布尔盲注知识点

概念&#xff1a;布尔盲注一般适用于页面没有回显字段(不支持联合查询)&#xff0c;且web页面返回True 或者 false&#xff0c;构造SQL语句&#xff0c;利用and&#xff0c;or等关键字来其后的语句 true 、 false使web页面返回true或者false&#xff0c;从而达到注入的目的来获…

Python---练习:while循环案例:猜数字

需求&#xff1a; 计算机从1 ~ 10之间随机生成一个数字&#xff0c;然后提示输入数字&#xff0c;如果我们输入的数字与随机数相等&#xff0c;则提示恭喜你&#xff0c;答对了。如果输入的数字比随机数大&#xff0c;则提示&#xff0c;猜大了。反之&#xff0c;则提示猜小了…

晶振与晶体

文章目录 基础知识无源晶振 & 有源晶振 博文链接 基础知识 无源晶振 & 有源晶振 博文链接 晶振原理解析

在Vue中使用Mock.js虚拟接口数据实例详解

在Vue项目中使用Mock.js可以方便地模拟接口数据&#xff0c;用于前端开发和测试。Mock.js是一个生成随机数据的库&#xff0c;可以帮助我们快速构建虚拟接口数据。在本文中&#xff0c;我将通过一个实例来详细讲解在Vue中使用Mock.js虚拟接口数据的方法。 首先&#xff0c;我们…

H5前端开发——事件处理

H5前端开发——事件处理 在H5前端开发中,事件处理是非常重要的一部分。通过事件处理,可以实现对用户操作的响应和交互。以下是几种常见的事件处理方式: HTML事件处理: 在HTML标签上直接设定事件处理函数,如<button onclick="myFunction()">点击我</bu…

【每周一测】Java阶段三第二周学习

目录 1、以下哪个过程可以创建Class对象? 2、下列switch代码段输出结果是&#xff08;&#xff09; 3、redis的数据类型 4、⭐以下关于Servlet生命周期说法错误的是&#xff08; &#xff09; 5、⭐下面有关SPRING的事务传播特性&#xff0c;说法错误的是&#xff1f; 6…

自然语言处理---huggingface平台使用指南

1 huggingface介绍 Huggingface总部位于纽约&#xff0c;是一家专注于自然语言处理、人工智能和分布式系统的创业公司。他们所提供的聊天机器人技术一直颇受欢迎&#xff0c;但更出名的是他们在NLP开源社区上的贡献。Huggingface一直致力于自然语言处理NLP技术的平民化(democr…