本文一共分三部分:算法
动态规划(dynamic programming
,简称DP
), 是求解决策过程最优化的数学方法,经过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划经常适用于有重叠子问题和最优子结构性质的问题。动态规划是一种利用空间换时间来求解最优解的的方法,通常在编程中的时间复杂度会少于常规解法(如暴力解法,回溯算法)。编程
动态规划只能应用于有最优子结构的问题。最优子结构的意思是局部最优解能解决全局最优解。数组
下面看几个例子>bash
假设你正在爬楼梯。须要 n 阶你才能到达楼顶。 每次你能够爬 1 或 2 个台阶。你有多少种不一样的方法能够爬到楼顶呢?app
注意:给定 n 是一个正整数。函数
解法一:暴力解法优化
在暴力解法中,咱们将会把全部可能爬的楼梯阶数进行组合,也就是1和2. 而在每一步中咱们都会递归调用原函数模拟爬1阶和2阶的情形,并返回两个函数的返回值之和。ui
// 其中i定义了当前阶数,n定义的是目标阶数
climbStairs(i, n) = climbStairs(i+1, n) + climbStairs(i+2, n)
复制代码
// Swift实现算法
func climbStairs(_ n: Int) -> Int {
return climbStairs(0, n: n)
}
func climbStairs(_ i: Int, n: Int) -> Int {
if i > n {
return 0
}
if i == n {
return 1
}
return climbStairs(i + 1, n: n) + climbStairs(i + 2, n: n)
}
复制代码
复杂度分析:spa
时间复杂度:O(2^n)。树形递归的大小为2^ncode
n = 5时,递归树是这样的
解法二:记忆化递归
使用暴力法求解,每一步计算结果都出现了冗余。另外一种思路是,咱们能够把每一步的结果存储在memo数组之中,每当函数再次调用,咱们就直接从memo数组返回结果。 在memo数组的帮助下,咱们获得一个修复的递归树,其大小减少到n。
func climbStairs(_ n: Int) -> Int {
var memo = Array(repeating: 0, count: n)
return climbStairs(0, n: n, memo: &memo)
}
func climbStairs(_ i: Int, n: Int, memo: inout Array<Int>) -> Int {
if i > n {
return 0
}
if i == n {
return 1
}
if memo[i] > 0 {
return memo[i]
}
memo[i] = climbStairs(i + 1, n: n, memo: &memo) + climbStairs(i + 2, n: n, memo: &memo);
return memo[i]
}
复制代码
复杂度分析:
解法三:动态规划
不难发现,这问题能够被分解成一些包含最优子结构的子问题,即它的最优解能够从其子问题的最优解来有效的构建,咱们可使用动态规划来解决这一问题。
第i阶能够由如下两种方法获得:
(i-1)
阶向后爬一阶。(i-2)
阶向后爬二阶。 因此,到达第i阶的方法总数就是(i-1)
阶和(i-2)
阶方法数之和。令dp[i]表示能到达第i阶的方法总数: dp[i] = dp[i-1] + dp[i-2]
func climbStairs(_ n: Int) -> Int {
if n == 1 {
return 1
}
var dp = Array(repeating: 0, count: n+1)
dp[1] = 1
dp[2] = 2
for i in 3...n {
dp[i] = dp[i-1] + dp[i-2]
}
return dp[n]
}
复制代码
经过这个例子,咱们能够总结如下,动态规划的思考过程: 动态规划的思考过程能够总结为:大事化小,小事化了。
大事化小
一个较大的问题,经过找到与子问题的重叠,把复杂的问题划分为多个小问题,也成为状态转移。
小事化了
小问题的解决一般是经过初始化,直接计算结果获得;
具体的步骤:
若是咱们有面值为1元、3元和5元的硬币若干枚,如何用最少的硬币凑够11元?
if i < 3 dp[i] = dp[i-1] + 1
else
if i >= 5 dp[i] = min{dp[i-1]+1, dp[i-3]+1, dp[1-5]+1}
else dp[i] = min{dp[i-1]+1, dp[i-3]+1}
复制代码
i = 0
时,0个硬币便可 当i = 1, i = 3, i = 5
时,只须要1个硬币便是最优解代码以下:
func countMoney(_ n: Int) -> Int {
if n == 0 {
return 0
}
var dp = Array(repeating: 0, count: n+1)
dp[1] = 1
dp[3] = 1
dp[5] = 1
for i in 1...n {
if i < 3 {
dp[i] = dp[i-1] + 1
} else
if i >= 5 {
dp[i] = min(dp[i-1] + 1, dp[i-3]+1, dp[i-5]+1)
} else
{
dp[i] = min(dp[i-1] + 1, dp[i-3] + 1)
}
}
return dp[n]
}
复制代码
接下来,咱们再看一到题
话说有一哥们去森林里玩发现了一堆宝石,他数了数,一共有n个。 但他身上能装宝石的就只有一个背包,背包的容量为C。这哥们把n个宝石排成一排并编上号: 0,1,2,…,n-1。第i个宝石对应的体积和价值分别为V[i]
(V
表明volume
)和W[i]
(W
表明worth
)。排好后这哥们开始思考: 背包总共也就只能装下体积为C的东西,那我要装下哪些宝石才能让我得到最大的利益呢?
按照上面的步骤对问题进行分析: 咱们定义一个函数F(i, j)
,表示可以装的宝石的最大价值,i
表示有的宝石的,是一个数组(0, 1, 2,..., i)
,j
表示背包的容量,假设,咱们这里一共有6和宝石,体积分别为 V = [2, 2, 6, 5, 4, 3]
,对应的价值分别是W = [6, 3, 5, 4, 6, 2]
。
当背包容量C = 1
时,则F(0, 1) = 0; F(1, 1) = 0; ... ; F(5, 1) = 0
;
当背包容量C = 2
时,则F(0, 2) = 6; F(1, 2) = 6; ...; F(5, 2) = 6
;
当背包容量C = 3
时,则F(0, 3) = 6; F(1, 3) = 6; ...; F(5, 3) = 6
;
由此,咱们能够得出结论,背包能装的宝石的最大价值和宝石数量及背包容量有关。
咱们目的是求怎么样把n个宝石,最大价值的装到背包里。
咱们作个假设: 先把下标从1开始计算。n个宝石的下标分别是1,2,3,...,n
1,2,3,...,n-1
,就能求出咱们所需的最大价值,用上面的函数表达式表示为:F(n-1, C)
;n
个宝石是咱们想要的宝石,那么背包要把第n个宝石的空间减出来,剩余空间来装其余宝石,那么能装的最大价值的宝石函数表达式为:F(n-1, C-V[n])
; 则空间为C的背包能装的最大价值是F(n-1, C-V[n])+W[n]
这里只有这两种状况,因此,咱们把这两种状况综合一下,最大值就是咱们要求解的函数的最终值。 F(n, C) = MAX(F(n-1, C), F(n-1, C-V[n])+W[n])
那么,就是说n
个宝石的最大价值和n-1
个宝石的最大价值有关。
按照咱们上面所说的步骤来求解:
F(n, C)
F(n, C) = MAX(F(n-1, C), F(n-1, C-V[n])+W[n])
F(0, 0) = 0
;代码实现以下:
struct Diamond {
var id: String
var volume: Int
var value: Int
var isSelected = false
init(_ volume: Int, value: Int, id: String) {
self.volume = volume
self.value = value
self.id = id
}
}
class Knapsack {
var number: Int // 物品数量
var C: Int // 背包最大致积或最大重量
var diamonds = Array<Diamond>()
var V = Array<Array<Int>>()
init(number: Int, C: Int) {
self.C = C
self.number = number
// 初始化一个二维数组
self.V = initializeArray()
// 初始化宝石对象
setDiamonds()
// 打印现有的宝石
printDiamonds()
}
// 初始宝石数组
func setDiamonds() {
for i in 0...number {
if i == 0 {
diamonds.append(Diamond(0, value: 0, id: "0"))
} else {
diamonds.append(Diamond(i + 1, value: i + 2, id: String(i)))
}
}
}
// 打印全部宝石
func printDiamonds() {
for diamond in diamonds {
print("id:\(diamond.id), value:\(diamond.value), volume:\(diamond.volume)")
}
}
// 打印选中/未选中宝石
func printDiamonds(_ selected: Bool) {
if selected {
print("被选中的宝石:")
} else {
print("未被选中的宝石:")
}
for diamond in diamonds {
if diamond.isSelected == selected && diamond.volume != 0 {
print("id:\(diamond.id), value:\(diamond.value), volume:\(diamond.volume)")
}
}
}
// 初始化dp数组
func initializeArray() -> [[Int]] {
var myArr = Array<Array<Int>>()
for _ in 0...number {
myArr.append(Array(repeating: 0, count: C+1))
}
return myArr
}
func findOptimalSolution() -> Int {
// i = 0, j = 0为边界条件,初始化的时候,初始化为0
// 填充二维数组
for i in 1...number {
for j in 1...C {
if j < diamonds[i].volume {
// 当剩余的空间不够装这个宝石的时候,当前数组元素值与上个元素值相同
V[i][j] = V[i-1][j]
} else {
// 当剩余空间够装的下该宝石的时候,则动态规划该宝石是否要选中该宝石
V[i][j] = max(V[i-1][j], V[i-1][j-diamonds[i].volume] + diamonds[i].value)
}
}
}
// 二维数组最后一个元素就是最大价值
return V[number][C]
}
// 查找哪些宝石被选中
func findSelectedDiamonds(i: Int, j: Int) {
if i > 0 {
if V[i][j] == V[i-1][j] {
diamonds[i].isSelected = false
findSelectedDiamonds(i: i-1, j: j)
} else {
if j - diamonds[i].volume >= 0 && V[i][j] == V[i-1][j-diamonds[i].volume] + diamonds[i].value {
diamonds[i].isSelected = true
findSelectedDiamonds(i: i - 1, j: j - diamonds[i].volume)
}
}
}
}
}
复制代码
用表格表示以下:
背包体积:1 | 背包体积:2 | 背包体积:3 | 背包体积:4 | 背包体积:5 | 背包体积:6 | 背包体积:7 | |
---|---|---|---|---|---|---|---|
宝石1(volume = 2, worth = 6) |
0 | 6 | 6 | 6 | 6 | 6 | 6 |
宝石2(volume = 2, worth = 3) |
0 | 6 | 6 | 9 | 9 | 9 | 9 |
宝石3(volume = 6, worth = 5) |
0 | 6 | 6 | 9 | 9 | 9 | 9 |
宝石4(volume = 5, worth = 4) |
0 | 6 | 6 | 9 | 9 | 9 | 10 |
宝石5(volume = 4, worth = 6) |
0 | 6 | 6 | 9 | 9 | 12 | 12 |
一个序列有N
个数:A[1],A[2],…,A[N]
,求出最长非降子序列的长度。
分析这个问题: 咱们定义一个数组dp
,dp[i]
表示0...i
之间序列的最大上升子序列,其中i<N
。 拿个简单的输入举个例子:假设输入是:[10, 9, 2, 5, 3, 7, 101]
dp[0] = 1, ([10])
dp[1] = 1, ([10, 9])
dp[2] = 2, ([10, 9, 2])
dp[3] = 2, ([10, 9, 2, 5])
dp[4] = 2, ([10, 9, 2, 5, 3])
dp[5] = 3, ([10, 9, 2, 5, 3, 7])
dp[6] = 4, ([10, 9, 2, 5, 3, 7, 101])
复制代码
想要求dp(i)
,就把i前面的各个子序列中,最后一个数不大于A[i]
的序列长度加1,而后取出最大的长度即为dp(i)
。
按照上面的步骤求解:
dp[i]
dp[i] = max(dp[j])+1, ∀0≤j<i
dp[0] = 1
代码以下:
/* * 这种方法依赖于这样一个事实,即给定数组中索引i以前的最长递增子序列与数组中稍后出现的元素无关。所以,若是咱们知道LIS到i索引的长度,咱们就能够根据索引j为0≤j≤(i+1)的元素包含(i+1)元素,从而计算出LIS可能的长度。 * 咱们使用一个dp数组来存储所需的数据。dp[i]表示仅考虑到i索引的数组元素,且必须包含i元素的状况下,可能的最长递增子序列的长度。为了找出dp[i],咱们须要尝试在每一个可能的递增子序列中追加当前元素(nums[i])到(i-1)索引(包括(i-1)索引),这样经过添加当前元素造成的新序列也是递增子序列。所以,咱们能够很容易地肯定dp[i]使用: * dp[i] = max(dp[j])+1, ∀0≤j<i * 最终,全部dp[i]的最大值来肯定最终的结果。 * LIS.length = max(dp[i]),∀0≤j<i * 时间复杂度:两层循环 O(n^2) * 空间复杂度:O(n) */
func lengthOfLIS(_ nums: [Int]) -> Int {
if nums.count == 0 {
return 0
}
var dp = Array(repeating: 0, count: nums.count)
dp[0] = 1
var maxAns = 1
for i in 1..<dp.count {
var maxVal = 0
for j in 0..<i {
if nums[i] > nums[j] {
maxVal = max(maxVal, dp[j])
}
}
dp[i] = maxVal + 1
maxAns = max(maxAns, dp[i])
}
return maxAns
}
复制代码
平面上有N*M
个格子,每一个格子中放着必定数量的苹果。你从左上角的格子开始,每一步只能向下走或是向右走,每次走到一个格子上就把格子里的苹果收集起来, 这样下去,你最多能收集到多少个苹果。
i
表示行,j
表示列 按照上面的步骤求解:
dp[i][j]
则和dp[i-1][j]
和dp[i][j-1]
有关,dp[i][j]
dp[i][j] = A[i][j] + max(dp[i-1][j], if i > 0; dp[i][j-1], if j > 0)
dp[0][0] = A[0][0]
伪代码实现:
int[][] dp
for i = 0; i < N - 1; i++
for j = 0; j < M - 1; j++
if i == 0 dp[i][j] = dp[i][j-1] + A[i][j]
if j == 0 dp[i][j] = dp[i-1][j] + A[i][j]
else dp[i][j] = max(dp[i-1][j], dp[i][j-1])
return dp[N-1][M-1]
复制代码
参考连接: