算法分析与设计「五」动态规划
文章目录
- 引例:数字三角形
- 一、动态规划基本思想
- 二、动态规划算法基本要素
- 三、经典题目
- 题目一:最长上升子序列
- 题目二:最长公共子序列
- 题目三:最佳加法表达式
- 题目四:0-1 背包问题
再讲解动态规划之前,先引入一个数字三角形的例题,借此来说明分治法、备忘录法和线性规划法的区别,之后再对动态规划进行讲解。
引例:数字三角形
问题描述
有一个非负整数组成的三角形,第一行只有一个数。除了最下面一行外,每个数的左下方和右下方都有一个数。如下图:
![](/assets/blank.gif)
从第一行开始,每次可以往左下或者右下走一格,直至最下面一行,把沿途数字全部相加,如何才能使得相加和最大?
思路分析
先定义变量。D(r,j)
表示 r 行第 j 个数字;MaxSum(r,j)
表示从 D(r,j)
到底边的各条路径中最大数字和,其中 i,j 均从 1 开始取值。限定条件为:从 D(r,j)
出发,每次只能走 D(r+1,j)
和 D(r+1,j+1)
。则原问题的解就变成了求解 MaxSum(1,1)
。
解法一:分治法
此题可以利用分治法来解决,将原问题分解为若干子问题,代码实现如下(伪代码):
int D[MAXN][MAXN]; // 初始矩阵
int n; // 行号int MaxSum(int i, int j) {if (i == n) return D[i][j];int x = MaxSum(i + 1, j);int y = MaxSum(i + 1, j + 1);return max(x, y) + D[i][j];
}
cout << MaxSum(1,1) // 所求结果即为 MaxSum(1,1)
但是,这种分治算法的复杂度为 O(2n)O(2^{n})O(2n) 是指数级的,计算量及其恐怖。而这是为什么呢 ?
因为存在着大量重复计算!!其计算次数如下:
![](/assets/blank.gif)
解法二:备忘录法(自顶而下)
备忘录法:对于每个子问题建立一个记录项,初始化时,存入一个特殊值(如 -1),表示该子问题未求解。求解过程中,对待每个待求子问题,先查看其相应的记录项。若是特殊值,表示该子问题未求解,需要计算其解并存入记录项中;若不是特殊值,表示该待求子问题已计算过,直接取出该解即可。
如果每算出一个 MaxSum(r,j)
就保存起来(建立备忘录),下次用到其值的时候直接取用,则可免去重复计算。那么,因为三角形的数字总数是 n(n+1)/2n(n+1)/2n(n+1)/2,只需要计算 n(n+1)/2n(n+1)/2n(n+1)/2 次,所以只使用 O(n2)O(n^2)O(n2) 时间便可完成计算。计算次数如下:
![](/assets/blank.gif)
那么具体怎么实现呢?给定一个用于记录子问题最优值的数组 maxSum[][]
,初始为 -1,表示还未知该点的最优解。当 maxSum[i][j] != -1
时,说明已知该点最优解,直接返回该值即可,这样就避免了重复计算。
int D[MAXN][MAXN];
int n;
int maxSum[MAXN][MAXN]; // 存放最优值
// 初始化 maxSum[i][j] = -1
int MaxSum(int i, int j) {if (maxSum[i][j] != -1)return maxSum[i][j];if (i == n)maxSum[i][j] = D[i][j];else {int x = MaxSum(i + 1, j);int y = MaxSum(i + 1, j + 1);maxSum[i][j] = max(x, y) + D[i][j];}return maxSum[i][j];
}
解法三:动态规划法(自底而上)
动态规划法:依据其递推式,自底而上进行计算。在计算过程中,保存已解决的子问题答案。每个子问题只需计算一次,后面只需查看已保存的记录即可。
第二种解法中我们利用了递归来解决,此外,也可以利用循环递推来代替递归。从求解最底层最大和开始,依次向上递推。很显然,我们只需两重循环就可以解决这个问题。
int D[MAXN][MAXN];
int n;
int maxSum[MAXN][MAXN];int main() {int i, j;cin >> n;for (i = 1; i <= n; i++)for (j = 1; j <= i; j++)cin >> D[i][j];// 为最后一行赋值for (int i = 1; i <= n; ++i)maxSum[n][i] = D[n][i];// 自底而上for (int i = n - 1; i >= 1; --i)for (int j = 1; j <= i; ++j)maxSum[i][j] = max(maxSum[i + j][j], maxSum[i + 1][j + 1]) + D[i][j];cout << maxSum[1][1] << endl;return 0;
}
通过解法二的备忘录法和解法三的线性规划法来看,可以发现两者有一个共性,那就是都利用了额外的一个表来保存已求解子问题的解,进而避免了重复计算优化了算法。这也是动态规划的基本思想,同时也是动态规划有别于分治法的地方。
此外,备忘录法也采用表来保存子问题的解,因此被认为是线性规划法的变形。
扩展:空间优化(只了解动态规划,该内容可以跳过)
实际上,为了减小空间复杂度,没有必要使用二维数组 maxSum 来存储每个值。
对此,我们可以进行改善,只需要一维数组 maxSum[] 即可解决问题。因为所采取的算法是自底而上递推,所以我们只需将更新的数据覆盖到最底层即可。例如,对倒数第二层的 2 来说,最大和为 7(2+5)。那么就可以将 7 覆盖到倒数第一层 4 的位置上(因为 4 以后不会再被用到)。以此类推,将倒数第二层的结果更新后一维数组显示如下:
更进一步来说,甚至连一维数组 maxSum[] 都可以不要,直接利用存储原始数据的 D 数组第 n 行来代替一维数组 maxSum[] 即可。实现代码如下:
int D[MAXN][MAXN];
int n;
int *maxSum;int main()
{int i, j;cin >> n;for (i = 1; i <= n; i++)for (j = 1; j <= i; j++)cin >> D[i][j];// 将 maxSum 指向第 n 行maxSum = D[n];for (int i = n - 1; i >= 1; --i)for (int j = 1; j <= i; ++j)maxSum[j] = max(maxSum[j], maxSum[j + 1]) + D[i][j];cout << maxSum[1] << endl;return 0;
}
一、动态规划基本思想
通过引例,我们可以发现动态规划算法其实和分治法类似。其基本思想都是将待求解的问题分解成若干子问题,先求解子问题,再结合这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,有些子问题会被重复计算很多次,最终导致耗费时间是指数级的。我们的改进措施是用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。
一般思路
将原问题分解为若干子问题
子问题与原问题相似,规模变小。子问题一旦解决,原问题也随之解决。一旦子问题的解被求出就要保存,因此所有子问题只需求解一次。
确定状态
我们往往将与子问题相关的各个变量的一组取值称为一个 “ 状态 ”。如数字三角形问题中的行号 r 和列号 j,就是一个状态,而状态的值则表示这个子问题的解。我们常用多维数组来存放状态的值,如数字三角形问题中的
maxSum[][]
。确定初始(边界)状态值
以数字三角形为例,初始状态就是底边数字,值就是底边数字值。
确定状态转移方程
如何从一个已知的 “ 状态 ” 去求出另一个未知 “ 状态 ” 呢,就是需要用到递推式,也被称为 “ 状态转移方程 ” 。如数字三角形中,状态转移方程如下:
二、动态规划算法基本要素
从求解数字三角形问题中,我们可以发现动态规划法的有效性,或者是说使用线性规划法的条件有以下两点:最优子结构性质 和 子问题重叠性质 。
- 最优子结构性质
当问题的最优解中包含了子问题的最优解时,称该问题具有最优子结构。利用问题的最优子结构性质,以自底而上的方式递归地从子问题最优解中逐步构造出整个问题的最优解。例如数字三角形中,先求最底层最大和,再利用已求最底层的最大和求解倒数第二层最大和,…,直到第一层。
- 重叠子问题
在用递归算法自顶而下解决问题时,每次产生的子问题并不总是新问题,有些子问题被反复计算。动态规划利用这些子问题重叠的性质,对每个子问题只求解一次,将其保存在一张表中。
三、经典题目
题目一:最长上升子序列
问题描述
一个数的序列ai,当 a1a_1a1 < a2a_2a2 < … < aSa_SaS 的时候,我们称这个序列是上升的。对于给定的一个序列(a1a_1a1, a2a_2a2, …, aNa_NaN),我们可以得到一些上升的子序列(ai1a_{i1}ai1, ai2a_{i2}ai2, …, aika_{ik}aik),这里1 <= i1{i1}i1 < i2{i2}i2 < … < ik{ik}ik<= N。比如,对于序列(1, 7, 3, 5, 9, 4, 8),有它的一些上升子序列,如 (1, 7),(3, 4, 8) 等等。这些子序列中最长的长度是 4,比如子序列 (1, 3, 5, 8)。
你的任务,就是对于给定的序列,求出最长上升子序列的长度。
// 输入
7
1 7 3 5 9 4 8
// 输出
4
思路分析
① 找子问题
首先想到的是求前 n 个元素最长上升子序列的长度,但是很快就会发现,它不满足动态规划的条件,那就是不具有重叠子问题。前 n 个元素的最长上升序列不一定包含在前 n+1 个最长上升序列中。
经过思考,选定子问题为:求以 aka_kak (k=1,2,...,N)(k=1,2,...,N)(k=1,2,...,N) 为终点的最长上升子序列的长度。这 N 个子问题的解中,最大的那个解就是整个问题的解。
② 确定状态
子问题只与位置 k 有关,k 即为状态,而状态 k 所对应值,即为以 aka_kak 为终点的最大上升子序列长度。
③ 找出状态转移方程
假设 maxLen(k) 表示以 aka_kak 做为终点的最长上升子序列的长度,那么可以得到递推式:
maxLen(k) = max { maxLen (i):1 <= i < k && aia_iai < aka_kak && k≠1 } + 1
代码实现
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;const int MAXN = 1010;
int a[MAXN];
int maxLen[MAXN];int main() {int N;cin >> N;for (int i = 1; i <= N; ++i) {cin >> a[i];maxLen[i] = 1;}// 求出以第 i 个数为终点的最长上升子序列长度for (int i = 2; i <= N; ++i)// 查看以第 j 个数为终点的最长上升子序列长度for (int j = 1; j < i; ++j)if (a[i] > a[j])maxLen[i] = max(maxLen[i], maxLen[j] + 1); // 因为 maxLen[i] 可能会比 maxLen[j] 大cout << *max_element(maxLen + 1, maxLen + N + 1);return 0;
}
max_element(begin,end)
是 C++ STL 中方法,begin
序列起始地址(迭代器),end
序列结束地址(迭代器)。用于找序列中的最值,返回第一个最大元素的地址。其速度远快于for循环遍历找最值。此外,还有查找最小值的min_element(begin,end)
方法。
题目二:最长公共子序列
问题描述
给出两个字符串,求出这样的一个最长的公共子序列的长度:子序列中的每个字符都能在两个原串中找到,而且每个字符的先后顺序和原串中的先后顺序一致。
// 输入
abcfbc abfcab
programming contest
abcd mnp
// 输出
4
2
0
思路分析
输入两个串 s1、s2,设 MaxLen(i,j) 表示:s1 的左边 i 个字符形成的子串,与 s2 左边的 j 个字符形成的子串的最长公共子序列的长度( i、j 从 0 开始算 )MaxLen(i,j) 就是本题的 “ 状态 ” 。
假定 len1 = strlen(s1)、len2 = strlen(s2),那么题目就是要求 MaxLen(len1,len2)。
那么,有递推公式:
if ( s1[i-1] == s2[j-1] ) // s1 的最左边字符是s1[0]MaxLen(i,j) = MaxLen(i-1,j-1) + 1;
elseMaxLen(i,j) = Max(MaxLen(i,j-1),MaxLen(i-1,j) );
由于每个数组单元的计算耗费 O(1)O(1)O(1) 时间,因此该算法共耗时 O(mn)O(mn)O(mn)。
为什么在
s1[i-1] != s2[j-1]
时,MaxLen(i,j) = Max(MaxLen(i,j-1),MaxLen(i-1,j) )
呢?这是因为,MaxLen(i,j)
首先不会比MaxLen(i,j-1)
和MaxLen(i-1,j)
小;其次,如果MaxLen(i,j)
比MaxLen(i,j-1)
和MaxLen(i-1,j)
都大的话,那么就推出了s1[i-1] == s2[j-1]
与条件矛盾,所以只能取两者中较大的一方。
代码实现
#include <iostream>
#include <cstring>
using namespace std;char s1[1005];
char s2[1005];
int maxLen[1005][1005];int main() {while (cin >> s1 >> s2) {int len1 = strlen(s1);int len2 = strlen(s2);int i, j;// 设置边界条件for (i = 0; i <= len1; i++)maxLen[i][0] = 0;for (j = 0; j <= len2; j++)maxLen[0][j] = 0;// 递推方程for (i = 1; i <= len1; i++)for (j = 1; j <= len2; j++)if (s1[i - 1] == s2[j - 1])maxLen[i][j] = maxLen[i - 1][j - 1] + 1;elsemaxLen[i][j] = max(maxLen[i][j - 1], maxLen[i - 1][j]);cout << maxLen[len1][len2] << endl;}return 0;
}
题目三:最佳加法表达式
问题描述
有一个由 1…9 组成的数字串。问如果将 m 个加号插入到这个数字串中,在各种可能形成的表达式中,值最小的那个表达式的值是多少?
// 输入
2
123456
1
123456
4
12345
// 输出
102
579
15
思路分析
设 V(m,n) 表示在 n 个数字中插入 m 个加号所能形成的表达式最小值,那么:
其中,Num(i,j) 表示从第 i 个数字到第 j 个数字所组成的数。数字编号从 1 开始算。此操作复杂度为 O(j−i+1)O(j-i+1)O(j−i+1)(可理解为 O(n)O(n)O(n)) ,可以先将此数组预处理后存起来,避免每次都要运算求解此值耗费时间。
此外,共有 m × n 种状态,故总时间复杂度为 O(m,n)O(m,n)O(m,n) 。
代码实现
#include <iostream>
#include <cstring>
#include <algorithm>
#define INF 0x3f3f3f3f
using namespace std;int Val[100][100];
int Num[100][100];
string numList;int main() {int m = 0, n = 0;while (cin >> m) {cin >> numList;n = numList.length();// 预处理 Num[][]for (int i = 1; i <= n; ++i) {Num[i][i] = numList[i - 1] - '0';for (int j = i + 1; j <= n; ++j)Num[i][j] = Num[i][j - 1] * 10 + numList[j - 1] - '0';}for (int i = 0; i <= m; ++i) {for (int j = 1; j <= n; ++j) {if (i == 0)Val[i][j] = Num[0][j];else if (j < i + 1)Val[i][j] = INF;elsefor (int k = i; k < j; ++k)// 最后一个加号位置Val[i][j] = min(Val[i][j], Val[i - 1][k] + Num[k + 1][j]);}}cout << Val[m][n] << endl;}return 0;
}
题目四:0-1 背包问题
问题描述
有 N 件物品和一个容积为 M 的背包。每种物品只有一件,可以选择放或者不放(N <= 3500,M <= 13000)。
第 i 件物品的体积 w[i]
,价值是 d[i]
。求解将哪些物品装入背包可使价值总和最大。
// 输入
4 5 // 物品数量 背包容积
1 2 // 体积 价值
2 4
3 4
4 5
// 输出
8
思路分析
假设我们使用 F[i][j]
表示取前 i 种物品,使得它们总体积不超过 j 的最优取法取得的价值总和。
则求出 F[N][M]
即为本题题解。
先考虑边界条件:
if (w[1] <= j)F[1][j] = d[1];elseF[1][j] = 0;
下面考虑递推式。对于第 i 种物品,我们可以选择取或者不取两种方案,那么就划分出了两个子问题,
我们对其取优即可
F[i][j] = max(F[i-1][j], F[i-1][j-w[i]]+d[i])
代码实现
#include <iostream>
using namespace std;int N, M;
int w[3505], d[3505];
int F[3505][13005];int main() {cin >> N >> M;for (int i = 1; i <= N; ++i) {cin >> w[i] >> d[i];F[0][i] = 0;}F[0][0] = 0;for (int i = 1; i <= N; ++i)for (int j = 1; j <= M; j++) {// 边界条件if (w[1] <= j)F[1][j] = d[1];elseF[1][j] = 0;// 递推公式if (j - w[i] >= 0)F[i][j] = max(F[i - 1][j], F[i - 1][j - w[i]] + d[i]);elseF[i][j] = F[i - 1][j];}cout << F[N][M];return 0;
}
算法分析与设计「五」动态规划相关推荐
- 算法分析与设计「二」递归算法
文章目录 一.递归思想 二.经典例题 例题一:汉诺塔问题 例题二:波兰表达式 例题三:四则运算表达式求值 例题四:爬楼梯 例题五:放苹果 例题六:二十四点 一.递归思想 递归是算法设计中最常用的手段, ...
- 算法分析与设计「三」二分算法
我们都知道,如果你输入了一个 1 到 1000 之内的数,电脑最多猜 10 次就可以猜到正确的答案.而这是为什么呢 ?其实,这就是用到了本文要讲述的二分搜索算法. 一.什么是二分搜索 在计算机科学中, ...
- 算法分析与设计「一」枚举
文章目录 一.枚举算法思想 二.例题分析 一.枚举算法思想 什么是枚举算法? 在进行归纳推理时,如果逐个考察了某类事件的所有可能情况,因而得出一般结论,那么这结论是可靠的,这种归纳方法叫做 枚举法.也 ...
- 计算机网络「五」 运输层
前言:本文为计算机网络系列第五章笔记,陆续会更新余下内容.文章参了:计算机网络微课堂.<王道考研计算机网络考研复习指导>.<计算机网络( 第7版 )>-- 谢希仁 .本文仅供学 ...
- CSS基础「五」定位
本篇文章为 CSS 基础系列笔记第五篇,参考 黑马程序员pink老师前端入门教程 其他CSS基础相关文章: CSS基础「一」基础选择器 / 字体属性 / 文本属性 / 三种样式表 CSS基础「二」复合 ...
- 算法分析与设计实验报告三——动态规划算法
一.实验目的 掌握动态规划方法贪心算法思想 掌握最优子结构原理 了解动态规划一般问题 二.实验内容 编写一个简单的程序,解决0-1背包问题.设N=5,C=10,w={2,2,6,5,4},v={6,3 ...
- 云原生系列「五」我为啥又看上了serviceMesh?
上一篇文章,说到为什么不看好服务网格,核心是没有强大的运维监控以及性能问题,那么lstio的出现让那个我们看到了一线曙光.因为实际生产活动中,各种运营上报.https证书各种对接.各种监控指标采集.流 ...
- Vue「五」—— 动态组件、插槽、自定义指令
Vue 系列笔记第五篇.本文参考:>> 黑马程序员 Vue 全套视频教程 系列文章阅读
- 算法分析与设计第五章作业
1. 请用回溯法的方法分析"最小重量机器设计问题" 2. 你对回溯算法的理解 回溯法按深度优先策略搜索问题的解空间树.首先从根节点出发搜索解空间树,当算法搜索至解空间树的某一节点时 ...
最新文章
- AI市场扩大催生多样化标注需求
- 【Prometheus】存储
- 1117. H2O 生成
- 第一届大数据科学与工程国际会议(2016)精彩荟萃
- jdi屏幕斜纹_荣耀V10屏幕有斜纹问题,有人甚至因此退货,真的这么严重?
- [POI2014]Solar Panels
- vscode 分析c代码_vs code(C语言)配置教程
- html和css的编程规范,Bootstrap CSS编码规范
- 条码扫描枪在仓库管理无线网络AP解决方案
- c#学习笔记之八 函数的代表delegate的用法:c# 求 三角函数 指数函数 积分
- 程序之外:由电影《少年的你》揭露的bug
- d3力导向图增加节点_d3.js力导向图节点如何都显示在边框内
- 谷歌股票“打折”卖,一股换20股
- 记录----在pycharm中用pip安装CV2(从清华这边的镜像)
- Celery-4.1 用户指南: Optimizing
- 解决因为在此系统上禁止运行脚本。(的问题)
- web前端面试题 1
- android 闹钟定时提醒,安卓手机便签怎么设定三天后的闹钟提醒?
- WordPress主题:7b2柒比贰V2.8.0去授权无限制版(页面完整,无404页面)
- php imagegif 动画,使用PHP的ImageMagick API制作动画GIF