详解算法之动态规划

31 阅读6分钟

一、概念

动态规划是一种以优化原问题为目标,通过将问题分解成子问题并保存子问题的解来解决复杂问题的算法策略。它通常通过填充表格、矩阵或数组来保存子问题的解,以便在需要时直接获取,避免重复计算。动态规划在具有最优子结构性质的问题中表现出色,这意味着问题的最优解可以通过子问题的最优解有效地构建而成。这种算法常用于解决一些最优化问题,如最短路径、最长公共子序列、背包问题等。动态规划的核心思想是通过组合子问题的解,构建出原问题的解,从而实现对问题的高效求解。

通俗来说,动态规划是一种通过将大问题分解成小问题并解决这些小问题来解决复杂问题的算法策略。在解决问题的过程中,动态规划会保存已经解决的子问题的结果,以便避免重复计算。这种方法有助于提高算法的效率,特别是当问题存在重叠子问题(Overlapping Subproblems)和最优子结构(Optimal Substructure)性质时。

1.计算斐波那契数列的第 n 个数

斐波那契数列是一个经典的动态规划问题。斐波那契数列的定义是:F(0) = 0,F(1) = 1,F(n) = F(n-1) + F(n-2)(对于 n > 1)。

// 计算斐波那契数列的第 n 个数
func fibonacci(_ n: Int) -> Int {
    // 创建一个数组来存储计算过的斐波那契数
    var fibArray = Array(repeating: 0, count: n + 1)
    
    // 初始化前两个斐波那契数
    fibArray[0] = 0
    fibArray[1] = 1
    
    // 计算从第三个数开始的斐波那契数列
    for i in 2...n {
        fibArray[i] = fibArray[i - 1] + fibArray[i - 2]
    }
    
    // 返回第 n 个斐波那契数
    return fibArray[n]
}

// 测试
let result = fibonacci(10)
print("第10个斐波那契数是:\(result)")

2.最短路径问题

最短路径问题是在一个加权有向图或加权无向图中,找到两个节点之间的最短路径。路径的长度是路径上所有边的权重之和。这个问题可以通过动态规划算法来解决。

// 定义一个表示无穷大的常量
let INFINITY = Int.max

// 解决最短路径问题,返回起点到终点的最短路径长度
func shortestPath(graph: [[Int]], start: Int, end: Int) -> Int {
    let size = graph.count
    
    // 创建一个二维数组来存储子问题的最优解
    var dp = Array(repeating: Array(repeating: INFINITY, count: size), count: size)
    
    // 初始化起点到自己的路径长度为 0
    for i in 0..<size {
        dp[i][i] = 0
    }
    
    // 填充二维数组,计算子问题的最优解
    for i in 0..<size {
        for j in 0..<size {
            if graph[i][j] != INFINITY {
                dp[i][j] = graph[i][j]
            }
        }
    }
    
    // 动态规划计算最短路径
    for k in 0..<size {
        for i in 0..<size {
            for j in 0..<size {
                if dp[i][k] != INFINITY && dp[k][j] != INFINITY {
                    dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j])
                }
            }
        }
    }
    
    // 返回最终问题的最优解
    return dp[start][end]
}

// 测试
let graph = [    [0, 2, INFINITY, 4],
    [INFINITY, 0, 1, INFINITY],
    [INFINITY, INFINITY, 0, 2],
    [INFINITY, INFINITY, INFINITY, 0]
]

let startNode = 0
let endNode = 3

let shortest = shortestPath(graph: graph, start: startNode, end: endNode)
print("起点到终点的最短路径长度是:\(shortest)")

// 定义一个表示无穷大的常量
let INFINITY = Int.max

// 解决最短路径问题,返回最短路径的长度
func shortestPath(matrix: [[Int]]) -> Int {
    let rows = matrix.count
    let cols = matrix[0].count
    
    // 创建一个二维数组来存储子问题的最优解
    var dp = Array(repeating: Array(repeating: 0, count: cols), count: rows)
    
    // 初始化起点
    dp[0][0] = matrix[0][0]
    
    // 初始化第一行
    for col in 1..<cols {
        dp[0][col] = dp[0][col - 1] + matrix[0][col]
    }
    
    // 初始化第一列
    for row in 1..<rows {
        dp[row][0] = dp[row - 1][0] + matrix[row][0]
    }
    
    // 填充二维数组,计算子问题的最优解
    for row in 1..<rows {
        for col in 1..<cols {
            dp[row][col] = matrix[row][col] + min(dp[row - 1][col], dp[row][col - 1])
        }
    }
    
    // 返回最终问题的最优解
    return dp[rows - 1][cols - 1]
}

// 测试
let matrix = [    [1, 3, 1],
    [1, 5, 1],
    [4, 2, 1]
]

let shortest = shortestPath(matrix: matrix)
print("最短路径的长度是:\(shortest)")

3、最长公共子序列问题

最长公共子序列(Longest Common Subsequence,简称 LCS)是指在两个序列中(可以是字符串、数组等),找到一个最长的共同子序列,该子序列不要求在原序列中是连续的。

示例:

对于序列 "ABCD" 和 "ACDF","ACD" 是它们的最长公共子序列。

// 解决最长公共子序列问题,返回最长公共子序列的长度
func longestCommonSubsequence(_ text1: String, _ text2: String) -> Int {
    let m = text1.count
    let n = text2.count
    
    // 创建一个二维数组来存储子问题的最优解
    var dp = Array(repeating: Array(repeating: 0, count: n + 1), count: m + 1)
    
    // 填充二维数组,计算子问题的最优解
    for i in 1...m {
        for j in 1...n {
        // 获取当前字符的索引
            let index1 = text1.index(text1.startIndex, offsetBy: i - 1)
            let index2 = text2.index(text2.startIndex, offsetBy: j - 1)
            
            // 检查当前字符是否相等
            if text1[index1] == text2[index2] {
            // 如果当前字符相等,说明是一个公共字符,长度加1
                dp[i][j] = dp[i - 1][j - 1] + 1
            } else {
            // 如果当前字符不相等,取左边和上边的最大值
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
            }
        }
    }
    
    // 返回最终问题的最优解
    return dp[m][n]
}

// 测试
let text1 = "ABCBDAB"
let text2 = "BDCAB"

let length = longestCommonSubsequence(text1, text2)
print("最长公共子序列的长度是:\(length)")

4、背包问题:

背包问题是一个优化问题,其目标是在给定的一组物品中,选择一些物品放入一个容量有限的背包中,以使得放入背包中的物品总价值最大。每个物品有自己的重量和价值,背包有一个最大容量。

示例:

假设有四个物品,其重量分别为 [2, 3, 4, 5],对应的价值分别为 [3, 4, 5, 6],背包的容量为 5。目标是选择物品放入背包,使得背包中物品的总价值最大。

// 解决背包问题,返回最大价值
func knapsackProblem(weights: [Int], values: [Int], capacity: Int) -> Int {
    let n = weights.count
    
    // 创建一个二维数组来存储子问题的最优解
    var dp = Array(repeating: Array(repeating: 0, count: capacity + 1), count: n + 1)
    
    // 填充二维数组,计算子问题的最优解
    for i in 1...n {
        for w in 0...capacity {
            if weights[i - 1] <= w {
                dp[i][w] = max(dp[i - 1][w], values[i - 1] + dp[i - 1][w - weights[i - 1]])
            } else {
                dp[i][w] = dp[i - 1][w]
            }
        }
    }
    
    // 返回最终问题的最优解
    return dp[n][capacity]
}

// 测试
let weights = [2, 3, 4, 5]
let values = [3, 4, 5, 6]
let capacity = 5

let maxValue = knapsackProblem(weights: weights, values: values, capacity: capacity)
print("背包问题的最大价值是:\(maxValue)")