数据结构与算法、讲解、动态规划一脸懵?看完之后轻松掌握!

来源 | 昊天码字

责编 | Carol

封图 | CSDN 付费下载于视觉中国

碰到动态规划问题摸不着头脑?总结不出动态规划的类型?有多少人曾经历过这种迷茫与无助?看完本文,让你一脚迈进动态规划的大门。

我们在用递归求解问题的过程中,可能会存在将递归的某一环节计算多次的现象,因为之前的递归结果无法有效存储,下次碰到一样的值还需要重新计算一次,下面引出一个例子:

相信大家都知道斐波那契数列,那我们由斐波那契数列递归求解过程中带来的问题来引出动态规划问题。

斐波那契数列

斐波那契数列的规律为:当n > 2时,fib(n)=fib(n-1)+fib(n-2),当n1或2时,fib(n)=1。假如我们让电脑代入这个递归式来求fib(5),则求解过程如下图:

求fib(5)

我们可以看到,如果我们按照如上的递归式来求一个斐波那契数,那么当n很大时,这棵树会非常庞大,且从中我们也可看出,电脑会非常傻的把fib(3)算了两遍,这种问题叫做重叠子问题,所以这个算法的时间复杂度会达到O(2^n),但实际上我们没有必要把fib(3)算两遍,我们在算第一遍时把它保存起来,在下次需要时直接调用就可以了。

因此我们在计算fib(5)时,完全可以从fib(1)开始计算,从1一直算到5,并不断保存,在必要时调用之前保存的值,而不需要重新运算,这样算法的时间复杂度瞬间就降到O(n)了。

所以我们在做动态规划时,首先要用到的就是这个思想,可以看出,动态规划的特点之一就是开辟内存保存数值,那么我们可以根据开辟内存的方式分为一维动态规划问题和二维动态规划问题,下面我们来具体介绍。

第一类问题(一维)

1.求薪水问题

我们来看下面的图,从上到下依次为8种工作,横轴代表工作所占的时间段,红色字体代表每份工作的薪水,两份同一时间的工作不能同时做,我们求的问题是一个员工怎样在0到11的时间段内获取最大的薪水。

我们首先思考这道题的本质,就是选不重合的工作,求能最多得到的薪水,我们先建立一个数组workvalue来保存每份工作的薪水。

我们先按照开辟内存的思想来做这道题,之后我会说明为什么要开辟内存。由上文的动态规划规律可知,我们可以依次从只做前一个工作能得到的最大薪水、只做前二个工作能得到的最大薪水、只做前三个工作能得到的最大薪水来思考,并建立一个value数组来保存它们,假如我们想求前6个工作中选哪些工作才能赚的最多,那么我们可以有两个选择:做或者不做这份工作,第一种,假如我们选择第6份工作,我们先找到最近的一个与第6份工作时间不重合的工作(第2份工作),那最大薪水为做第6份工作得到的钱加上做前2个工作能得到的最大的薪水(本题就是第6份工作的薪水3+value[2]);第二种,我们可以选择不做第6份工作,那么最大的薪水就是做前5个工作得到的最大的薪水。因此:

value[6]=max(3+value[2],value[5]);

我们可以看出,如果我们不开辟数组,用递归的思想接着求value[2],那么不可避免地计算了重复子问题,因此我们选择开辟数组,从前往后用动态规划的思想做。此外,我们还应设立pre数组来存每个工作的前一个与之不重合的工作,例如pre[6]=2。因此我们可以写出如下代码来解决上述问题:

#include <iostream>
using namespace std;
int value[10];
int workvalue[8] = {5, 1, 8, 4, 6, 3, 2, 4};
int pre[8] = {0, 0, 0, 1, 0, 2, 3, 5};
int main() {value[0] = 0;value[1] = 5;value[2] = 5;for (int i = 3; i <= 8; i++) {value[i] = max(value[i - 1], value[pre[i - 1]] + workvalue[i - 1]);}cout << value[8];return 0;
}

求出结果等于13,即最多的薪水。那么这道问题解决,鉴于如果此题换个衣服大家可能会不认识的现象,我们接下来再搞一道同类型不同描述的问题。

2.求最大数

我们可以在如下arr数组中的数字中可以任意选数字,但是不能选相邻的数字,输出我们选的数字中和最大的值。

arr数组

我们可以一眼看出,此题同样是个选择与否的问题,不能选相邻的数就如求薪水问题中的不能同时做两份工作,我们先来试试递归,用递归从后往前来做,我们设opt(n)是前n个数中选择的数相加最大的值,那么当我们考虑opt(n)时,我们有两种选择:选或者不选数组n位置所在的数,如果选择n,那它的值就为arr[n]+opt[n-2],如果不选择n,那它的值就为opt[n-1]。而opt(n)就是两种选择中的最大值。假如我们求opt(6),那么就如下图所示:

递归思路

那递归出口就是opt(1)=arr[1],opt(2)=max(arr[2],opt(1)),因此我们可以得出如下代码(在此代码中由于数组从0开始,因此在相应的地方-1):

#include <iostream>
using namespace std;
int arr[7] = {1, 2, 4, 1, 7, 8, 3};int rec(int arr[], int i) {if (i == 0) return arr[0];else if (i == 1) return max(arr[0], arr[1]);else {int A = rec(arr, i - 2) + arr[i];int B = rec(arr, i - 1);return max(A, B);}
}
int main() {cout << rec(arr, 6);
}

毫无疑问我们也计算了重叠子问题,由上面的动态规划可知,我们可以通过创建数组来保存相应的值,以此减少不必要的计算,因此,动态规划的代码如下:

#include <iostream>
using namespace std;
int arr[7] = {1, 2, 4, 1, 7, 8, 3};
int opt[7];int rec(int arr[], int j, int opt[]) {opt[0] = arr[0];opt[1] = max(arr[0], arr[1]);for (int i = 2; i < 7; i++) {int A = opt[i - 2] + arr[i];int B = opt[i - 1];opt[i] = max(A, B);}return opt[j];
}
int main() {cout << rec(arr, 6, opt);
}

好,我们来思考,开辟一维内存的原因是其中只能找到一个变量,如前n个工作的最大薪水、前n个数的最大值,那当我们存在多个变量时又该如何求解呢?所以我们引出二维动态规划。

第二类问题(二维)

1.选取数字

给定一个数,我们是否可以在如下数组中找到任意个数,使选中的所有数值之和等于给定的数,如果可以找到,则返回true,如果不可以找到,则返回false

arr数组

首先我们先用递归的思想来看这道题,假如给定一个数9,我们怎样写出一个算法来让计算机判断数组中是否有任意个数之和为9呢?还是老套路,我们创建一个递归函数,传给它数组、数组的最后一个下标(此题为5)、目标值。我们从数组中第六个元素开始看,我们有两种选择:第一种,我们选择这个元素,如果选择的话,那么我们接下来调用递归函数,它的实参就是数组、此下标数减1、目标数减此下标的值(因为我们选了这个下标上的值,所以应该把值减去);第二种,我们不选择这个数,那么我们接下来调用递归函数,它的实参就是数组、此下标数减1、目标数。如果这两种选择有一种能够成功找出结果,那么就返回true,如果两种选择都不满足,那么则返回false即可。

接下来我们要思考递归的中止条件:

1.当目标值被减为0时,直接返回true

2.当下标数为0时,如果下标0对应的数组值不等于此时的目标值,则返回false

3.当选择此下标值时,如果此下标对应的数组值大于目标值,则不能做此选择

好,既然确定了终止条件,那么我们来看代码:

#include <iostream>
using namespace std;int arr[6] = {3, 34, 4, 12, 5, 2};int rec_subset(int arr[], int i, int s) {if (s == 0)return true;else if (i == 0)return arr[0] == s;else if (arr[i] > s)return rec_subset(arr, i - 1, s);else {bool A = rec_subset(arr, i - 1, s - arr[i]);bool B = rec_subset(arr, i - 1, s);return A or B;}
}
int main() {cout << rec_subset(arr, 5, 13);
}

由于此递归重复了大量重复子问题,接下来我们要思考的是,怎样用动态规划的方式从前往后做这道题?

首先我们判断此动态规划为二维问题,因为有两个东西可以作为二维数组的两个下标:目标值、arr数组中的元素的下标。因为我们从刚才的递归可以看出目标值从后往前是不断递减的,因此我们就可以从0开始往后递增,arr数组中的元素的个数也可以依次增加,所以作为二维数组的另一个下标。因为我们返回的是布尔值,所以我们需要建立二维布尔数组如下:

初始的二维数组

这就是我们要保存每个数据的二维数组,第一行代表的是目标值从1到9,列代表的是下标数,每个格子代表用当前的元素可否能找出数据使其和等于目标值,那怎么样填此二维数组呢?

现在我们来思考一下我们上面所想到的终止条件:

1.当目标值被减为0时,直接返回true,所以第一列都为true

2.当下标数为0时,如果下标0对应的数组值不等于此时的目标值,则返回false,因此第一行除了3等于第0个元素对应的数组数据为true外,其余都为false

因此我们可以把此表改成如下:

改过后的二维数组

我们用T代表true,用F代表false,其中第0行第0列是T或者F都可以。

接下来我们就可以给二维数组里面的其他元素赋值了,比如第ij列,我们先判断i下标对应的数组元素值是否大于j(目标元素)。

如果不大于,那么就可以分为两种情况:第一种,其值等于行为i-1,列为j-arr[i]的值(代表选了此下标对应的数);第二种行为i-1,列为j(代表没有选此下标对应的数)。当两种情况有一种以上为T时,第ij列的值就为T,否则就为F

如果大于,意为我们不能选择此时对应的元素值,因为如果选择的话,目标值就变为负数了,所以我们仅有一种选择,那么第ij列的值就等于i-1j列的值。

以此类推,填满此二维数组,最终的值为二维数组最右下角的值。我们来看代码:

#include <iostream>
using namespace std;int arr[6] = {3, 34, 4, 12, 5, 2};bool dpsubset(int arr[], int s) {bool subset[6][s + 1];for (auto j : subset) {j[0] = true;}for (auto &j : subset[0]) {j = false;}subset[0][arr[0]] = true;for (int h = 1; h < 6; h++) {for (int k = 1; k < s + 1; k++) {if (arr[h] > k)subset[h][k] = subset[h - 1][k];else {bool A = subset[h - 1][k];bool B = subset[h - 1][k - arr[h]];subset[h][k] = A or B;}}}return subset[5][s];
}int main() {cout << dpsubset(arr, 13);
}

由此问题引出另外一个问题,我们怎样求得我们选择的是哪些数呢?别急,看了下面的题目,我们就会解决了。

2.背包回溯

现在有四个物品,背包的总容量为8,背包最多能装入价值为多少的物品?都装了哪些物品?

问题描述

毫无疑问,这是二维动态规划,怎么判断的呢?因为它有两个东西可以作为二维数组的两个下标,一个是物品编号,一个是背包容量,所以我们创建二维数组如下:

二维数组

其中第一行代表背包容量,第一列代表物品个数,每个格子为在当前背包容量的情况下考虑当前的物品所能装下的最大价值是多少。

现在我们来填格子,首先第二行全部为0,因为此时没有物品;其次第二列为0,因为此时背包容量为0。现在我们来填其余表格,在填之前,我们先考虑当前格子对应的物品是否能装入背包。

如果能装下当前物品,那么分两种情况:第一种为装入背包,获得此物品的价值,然后减去它所占的空间,找对应剩余空间容量、物品数减一对应的最大价值(就是找之前的格子),然后两个价值相加;第二种为不装,那么当前最大价值等于对应空间容量不变、物品数量减一的最大价值(就是此格子头顶的格子)。最后此格子的值等于两种情况中最大的值。

如果不能装入背包,那么当前最大价值等于对应空间容量不变、物品数量减一的最大价值(就是此格子头顶的格子),最后此格子的值等于此格子头顶上的格子的值。

在我们填满二维数组后,来考虑一下新的问题:回溯,如何找到背包放的物品编号?

我们先从二维数组最右下角的格子找,如果它的值等于它头顶上的格子,则证明它没装此时对应的物品,否则就是装了此物品,这个格子的值等于这个格子的两个下标分别减一和此时放入背包的物品的所占空间的格子对应的值加上这个格子对应的物品价值,开辟一个数组,这样回溯,如果找到放入的物品,则在此数组对应的位置上设为1,没放入此物品则在数组对应的位置上设为0,最后输出数组即可,大家看代码:

/*
i物品编号 1   2   3   4
w体积    2   3   4   5
v价值    3   4   5   6
*/
#include <iostream>
using namespace std;
int weight[5] = {0, 2, 3, 4, 5};
int value[5] = {0, 3, 4, 5, 6};
int dp[5][9];
int object[5];void Dynamic() {for (int i = 1; i < 5; i++) {for (int j = 1; j < 9; j++) {if (weight[i] > j)dp[i][j] = dp[i - 1][j];elsedp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);}}
}void Find(int i, int j) {if (i == 0) {for (int i = 0; i < 5; i++)cout << object[i] << " ";return;}if (dp[i][j] == dp[i - 1][j]) {object[i] = 0;Find(i - 1, j);} else if (dp[i][j] == dp[i - 1][j - weight[i]] + value[i]) {object[i] = 1;Find(i - 1, j - weight[i]);}
}
int main() {Dynamic();cout << dp[4][8];Find(4, 8);
}

延伸

如果你看到这里,并且认真看完上述文字后,恭喜你初步掌握了动态规划的常见问题,最好把上述代码反复敲几遍,第一遍照着敲,第二表凭借理解单独敲。马化腾曾说过一句话,给我感触很大,“用最笨的方式去领悟编程,用抄代码的方式来培养感觉”。

此外大家就可以不断在各个平台上刷题来提升自己的感觉,一起加油!

 

推荐阅读

  • 手把手教你配置VS Code 远程开发工具,工作效率提升N倍

  • 用大白话彻底搞懂 HBase RowKey 详细设计

  • 后端程序员必备:书写高质量SQL的30条建议

  • Go 远超 Python,机器学习人才极度稀缺,全球 16,655 位程序员告诉你这些真相!

  • 任正非谈“狼文化”:华为没有 996,更没有 007

  • 区块链必读“上链”哲学:“胖链下”与“瘦链上”

  • 在商业中,如何与人工智能建立共生关系?

真香,朕在看了!

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

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

相关文章

搜索场景下的智能推荐演变之路

摘要&#xff1a;传统的推荐手段主要还是深度挖掘用户行为和内容本身相似性的价值&#xff0c;包括但不限于协同过滤&#xff0c;内容表征向量召回&#xff0c;以及各式各样的点击率预估模型&#xff0c;然后这样的推荐行为缺乏内在的逻辑性和可解释性&#xff0c;有一种知其然…

调查了 17,000 多位程序员,当前的云原生开发现状究竟如何?

整理 | 弯月&#xff0c;责编 | 郭芮头图 | CSDN 下载自东方IC出品 | CSDN&#xff08;ID&#xff1a;CSDNnews&#xff09;容器的标准化使用改变了软件的开发方式&#xff0c;我们迎来了开发运维的时代&#xff0c;基于云原生的开发能够帮助我们构建更灵活、更强大的应用程序。…

阿里研究员:测试稳定性三板斧,我怎么用?

阿里妹导读&#xff1a;如何治理测试稳定性问题&#xff1f;很多人会说&#xff1a;环境、流程管控、监控、工具化、加机器、专人负责、等等。这些都是对的。不过这些都是解决方案层面的&#xff0c;而不是方法论和理论体系层面的。今天&#xff0c;阿里研究员郑子颖来说说测试…

阿里架构总监一次讲透中台架构,13页PPT精华详解,建议收藏!

本文整理了阿里几位技术专家&#xff0c;如架构总监 谢纯良&#xff0c;中间件技术专家 玄难等几位大牛&#xff0c;关于中台架构的几次分享内容&#xff0c;将业务中台形态、中台全局架构、业务中台化、中台架构图、中台建设方法论、中台组织架构、企业中台建设实施步骤等总共…

Redis 6.0 的客户端缓存是怎么肥事?一文带你了解!

来源 | 程序员历小冰责编 | Carol封图 | CSDN 付费下载于视觉中国近日 Redis 6.0.0 GA 版本发布&#xff0c;这是 Redis 历史上最大的一次版本更新&#xff0c;包括了客户端缓存 (Client side caching)、ACL、Threaded I/O 和 Redis Cluster Proxy 等诸多更新。我们今天就依次聊…

AI时代,你的职业会是?99%的人都无法直面!

在我10岁的时候&#xff0c;算命先生曾对说我30岁时我会每天与八阿哥玩在一起。 当时懵懂的我一脸茫然&#xff0c;想着谁是我的八阿哥&#xff0c;却在30岁的这一年意识到自己确实日以继夜的与八阿哥在一起。 曾经&#xff0c;我们也担心自己未来的工作岗位是否会被人工智能给…

Java 12 新特性概述

Java 12 已如期于 3 月 19 日正式发布&#xff0c;此次更新是 Java 11 这一长期支持版本发布之后的一次常规更新&#xff0c;截至目前&#xff0c;Java 半年为发布周期&#xff0c;并且不会跳票承诺的发布模式&#xff0c;已经成功运行一年多了。通过这样的方式&#xff0c;Jav…

5G +边缘计算,优酷如何做云渲染?

作者| 阿里文娱高级技术专家 伊耆责编 | 屠敏头图 | CSDN 下载自东方 IC出品 | CSDN&#xff08;ID&#xff1a;CSDNnews&#xff09;当5G来了&#xff0c;视频还是平面的影像吗&#xff0c;只能静静观看吗&#xff1f;一定不是&#xff01;现在&#xff0c;你可以像玩游戏一样…

不做会死!互联网时代的企业必定都要实现中台

AI 前线导读&#xff1a; 自 2018 年底以来&#xff0c;伴随着阿里、腾讯、百度、京东等一众互联网巨头的大规模组织架构调整&#xff0c;“中台”的热度陡然攀升。一时间&#xff0c;各大互联网公司纷纷开始跟随建设中台。中台的概念是被阿里带火的&#xff0c;2015 年&#x…

包机制。。

包机制 为了更好的组织类&#xff0c;java提供了包机制&#xff0c;用于区别类的命名空间//本质就是文件夹 包语法格式 package pkj[.pkg[.pkg3...]];一般利用公司域名倒置作为包名&#xff1a;com.boss.xxx 导入包语法 import package1[.package2...].(classname|*);尽量不要…

ETL异构数据源Datax_使用querySql_08

使用说明 当用户配置了这一项之后&#xff0c;DataX系统就会忽略table&#xff0c;column 这些配置型&#xff0c;直接使用这个配置项的内容对数据进行筛选&#xff0c;例 如需要进行多表join后同步数据&#xff0c;使用select a,b from table_a join table_b on table_a.id t…

我被“非结构化数据包围了”,请求支援!

阿里妹导读&#xff1a;非结构化数据的内容占据了当前数据海洋的80%。换句话来说&#xff0c;就是我们都被“非结构化数据”包围了。由于非结构化数据的信息量和信息的重要程度很难被界定&#xff0c;因此对非结构化数据的使用成为了难点。如果说结构化数据用详实的方式记录了企…

82年 AI程序员征婚启示火了!年薪百万,女生神回复

最近在某社区&#xff0c;一则程序员征婚启示火了&#xff01;很多女生在评论区表示“全中”&#xff0c;想交流看看。然后评论区就炸了&#xff0c;有人恶意说yp&#xff0c;有人说看中了楼主的钱。笔者一翻&#xff0c;发现楼主果然无意中透露了百万年薪收入&#xff0c;虽然…

AWS 专家教你使用 Spring Boot 和 DJL ,轻松搭建企业级机器学习微服务!

作者 | Qing Lan&#xff0c;Mikhail Shapirov责编 | Carol封图 | CSDN 下载自视觉中国出品 | CSDN云计算&#xff08;ID&#xff1a;CSDNcloud&#xff09;许多AWS云服务的用户&#xff0c;无论是初创企业还是大公司&#xff0c;都在逐步将机器学习 (ML) 和深度学习 (DL) 任务…

【从入门到放弃-ZooKeeper】ZooKeeper入门

前言 ZooKeeper是一个分布式服务协调框架&#xff0c;可以用来维护分布式配置信息、服务注册中心、实现分布式锁等。在Hbase、Hadoop、kafka等项目中都有广泛的应用。随着分布式、微服务的普及&#xff0c;ZooKeeper已经成为我们日常开发工作中无法绕过的一环&#xff0c;本文…

ln: failed to create symbolic link ‘/usr/bin/mysql’: File exists

问题描述&#xff1a; ln -s /usr/local/mysql/bin/mysql /usr/bin 在centos7进行软链接设置的时候&#xff0c;出现了这么问题&#xff1a;问题就是说这个文件已存在&#xff0c; 解决方法&#xff1a;覆盖之前的 ln -sf /usr/local/mysql/bin/mysql /usr/bin

读透《阿里巴巴数据中台实践》,其到底有什么高明之处?

最近阿里巴巴分享了《阿里巴巴数据中台实践》这个PPT&#xff08;自行搜索原始文章&#xff09;&#xff0c;对于数据中台的始作俑者&#xff0c;还是要怀着巨大的敬意去学习的&#xff0c;因此仔细的研读了&#xff0c;希望能发现一些不一样的东西。 读这些专业的PPT&#xf…

如果你也想做实时数仓…

数据仓库也是公司数据发展到一定规模后必然会提供的一种基础服务&#xff0c;数据仓库的建设也是“数据智能”中必不可少的一环。本文将从数据仓库的简介、经历了怎样的发展、如何建设、架构演变、应用案例以及实时数仓与离线数仓的对比六个方面全面分享关于数仓的详细内容。 …

华为云战略投入政企市场,发布华为云Stack

2020年5月15日&#xff0c;华为云发布政企战略&#xff0c;并宣布华为云Stack系列新品正式上市。华为云Stack是位于政企客户本地数据中心的云基础设施&#xff0c;能为政企客户提供在云上和本地部署体验一致的云服务。随着政企智能升级进入深水区&#xff0c;华为云将战略投入政…

如何在 Apache Flink 中使用 Python API?

本文根据 Apache Flink 系列直播课程整理而成&#xff0c;由 Apache Flink PMC&#xff0c;阿里巴巴高级技术专家 孙金城 分享。重点为大家介绍 Flink Python API 的现状及未来规划&#xff0c;主要内容包括&#xff1a;Apache Flink Python API 的前世今生和未来发展&#xff…