
数据结构与算法教程之动态规划(1)——动态规划入门
动态规划
在学习之前
动态规划是什么
动态规划(dynamic programming)是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法。20世纪50年代初美国数学家R.E.Bellman等人在研究多阶段决策过程(multistep decision process)的优化问题时,提出了著名的最优化原理(principle of optimality),把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解,创立了解决这类过程优化问题的新方法——动态规划。1957年出版了他的名著《Dynamic Programming》,这是该领域的第一本著作。
这是百度百科的定义,当然听上去很牛啤。
但我们是学算法的,不是学算法历史的,所以这些并不重要。
动态规划,简称动规(dp),一般来说,是指设计状态以及转移,并且使得初始条件和结果的状态可表示,利用转移从初始条件推得结果的一种算法。(以上都是我瞎扯,忽略即可)
简单来说,动态规划最重要的是状态和转移。
动态规划的分类
根据百度百科, 一般可分为线性动规,区域动规,树形动规,背包动规四类。
当然在每个大类中肯定还能有各种小类,比如背包 dp 就有九讲。(雾)
实际上,广义的动态规划几乎涵盖了大部分的算法,许多算法都是一种特殊的动态规划。
一般动态规划
虽然看起来是叫一般动态规划,但很多时候,最难的题也就是一般动态规划,所以千万不要觉得这是最简单的。
状态设计与转移
上面说过,动态规划最重要的是状态和转移,所以我们先来看看怎么设计。
一般状态设计是有要求的:
- 设计的状态可以覆盖所有可能性;
- 初始和结束都可以表示为有限个状态的运算;
- 设计的状态有确定有限的转移关系;
- 要尽量减少状态的复杂度与转移的复杂度;
- 希望状态可以有一个合理的便利顺序(暂时想到这些)
听上去还是跟没讲一样,所以我们直接看题。
第一题:最小路径和
给定一个包含非负整数的 m×n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
保证 n,m≤1000。
样例输入:
3 3
1 3 1
1 5 1
4 2 1
样例输出:
7
题解
这一题的关键是到右下角路径总和最小。
我们可以将状态拓宽一下:dp[i][j] 表示走到 (i,j) 时,最小的路径数字和。
因为 (i,j) 只能从 (i,j−1) 和 (i−1,j) 走过来(不存在自动扔掉),所以转移也就是 dp[i][j−1]→dp[i][j],dp[i−1][j]→dp[i][j]。
具体来说我们可以得到转移方程 dp[i][j]=min(dp[i][j−1],dp[i−1][j])+a[i][j],a[i][j] 表示 (i,j) 上的数字。
初值只需要 dp[1][1]=a[1][1]。
而状态的顺序只要保证 dp[i−1][j] 和 dp[i][j−1] 在 dp[i][j] 前被转移到。
故我们只需要 i=1∼n,j=1∼m 即可。
程序主体部分大概会长这样:
分析
在这道题的解题过程中,我们会发现一个大致的思路:
- 首先构想一种可以用若干数表示的状态,符合上面的要求;
- 找到状态之间关系,并尝试写出其转移方程;
- 找到状态的初值和边界值;
- 找到状态的合适枚举顺序;
- 完成程序编写。
第二题:猴子跳台阶
一个顽猴在一座有 n 级台阶的小山上爬山跳跃,猴子上山一步可跳 x 级或跳 y 级,试求猴子上山到 n 级台阶有多少种不同的爬法?
猴子从山脚开始跳,可认为是第 0 阶。
输入顺序为 n,x,y,保证 x≤y≤n≤100。
样例输入:
3 1 2
样例输出:
3
样例解释:
3=1+1+1=1+2=2+1。
题解
同样道理,我们可以设计状态 dp[i] 表示到达第 i 级台阶有多少种不同方法。
那么很容易得到转移方程 dp[i]=dp[i−x]+dp[i−y]。
这里的顺序也只需要 i=1∼n,初值也就是 dp[0]=1。
但是这样写出来的是错误的程序。
因为有 x=y 的情况。
当然这种情况只需要特殊判断即可。
分析
这道题告诉了我们,我们要多考虑极端情况,对数据做到全覆盖。
绝对不能因为大多数情况下没错就认为是对的,一定要尽力排查各种特殊情况。
算法复杂度分析
在大多数动态规划的题目中,我们不仅需要正确的状态表示与转移,还需要尽量优的表示法。
那么如何判断一种状态表示的优劣呢?我们需要算法复杂度。
算法复杂度一般包括时间复杂度和空间复杂度。
时间复杂度可以认为是程序中进行基础运算次数的估算,而空间复杂度则是程序使用变量的占用内存大小估算。
因为很多时候不能对一个算法做到精确计算(考虑到实现的不同),我们这里的估算一般是用 O 符号表示的数量级估算。
以上面两题为例:
第一题:时间复杂度 O(nm),空间复杂度 O(nm)。
第二题:时间复杂度 O(n),空间复杂度 O(n)。
注意第一题的 n,m 大小同阶,故 O(nm) 也可以表示为 O(n2)。
空间上,我们可以根据大致的常数与复杂度的乘积估算占用内存。
时间上,在 1s 内,一般认为以下数量级以及数据范围的对应较合适(需要根据实际代码常数进行调整):
- O(n4)→n≤100
- O(n3)→n≤400
- O(n2)→n≤5000
- O(n√n)→n≤100000
- O(nlog2n)→n≤200000
- O(nlogn)→n≤1000000
- O(n)→n≤20000000
- O(2n)→n≤25
总体来说大概就是,常数乘上复杂度不超过 108 为佳。
相关习题
这里的题目都比较简单,甚至可以做到 O(1) 的空间复杂度,有利于一开始优化算法的训练。
No Comments