算法学习笔记:动态规划详解(示例:Go)

什么是动态规划?

动态规划(Dynamic Programming, DP)是一种解决复杂问题的方法,通过将问题分解为更简单的子问题来解决。动态规划通常用于优化问题,特别是当一个问题可以被分解为多个重叠的子问题时。其核心思想是通过保存子问题的结果(记忆化)来避免重复计算,从而提高效率。

动态规划的基本步骤

  1. 定义子问题:将原问题分解为若干子问题。
  2. 猜测(猜解法):根据子问题的解,猜测原问题的解。
  3. 递归和记忆化:通过递归求解子问题,并使用记忆化技术保存子问题的解。
  4. 递推关系:根据子问题的解,找到原问题解的递推关系。
  5. 解决原问题:通过递推关系,逐步求解原问题。

经典例子:斐波那契数列

我们先从一个简单的例子开始:斐波那契数列。

斐波那契数列的定义

斐波那契数列是一个经典的递归问题,定义如下:

F ( n ) = F ( n − 1 ) + F ( n − 2 ) F(n) = F(n-1) + F(n-2) F(n)=F(n1)+F(n2)
F ( 0 ) = 0 , F ( 1 ) = 1 F(0) = 0, F(1) = 1 F(0)=0,F(1)=1

动态规划解法

我们可以使用动态规划来避免重复计算。

package main

import "fmt"

// 计算第n个斐波那契数
func fibonacci(n int) int {
    if n <= 1 {
        return n
    }
    // 创建一个数组来存储斐波那契数
    dp := make([]int, n+1)
    // 初始化base cases
    dp[0] = 0
    dp[1] = 1

    // 自底向上计算斐波那契数
    for i := 2; i <= n; i++ {
        dp[i] = dp[i-1] + dp[i-2]
    }
    // 返回结果
    return dp[n]
}

func main() {
    n := 10
    fmt.Printf("第%d个斐波那契数是: %d\n", n, fibonacci(n))
}
解释
  1. 初始化

    dp := make([]int, n+1)
    dp[0] = 0
    dp[1] = 1
    

    创建一个数组dp来存储斐波那契数,从F(0)F(n)

  2. 填表

    for i := 2; i <= n; i++ {
        dp[i] = dp[i-1] + dp[i-2]
    }
    

    使用循环自底向上计算每个斐波那契数,避免了重复计算。

  3. 返回结果

    return dp[n]
    

    最后返回F(n)

背包问题

我们再来看一个稍微复杂的例子:01背包问题。

01背包问题的描述

01背包问题是一个经典的动态规划问题,描述如下:

  • n个物品,每个物品都有一个重量weights[i]和一个价值values[i]
  • 给定一个容量为W的背包,选择物品放入背包使得总重量不超过W,并且背包中的总价值最大。

动态规划的解法

动态规划的核心思想是将问题分解为子问题,并通过保存子问题的结果来避免重复计算。
具体来说,我们通过构建一个二维数组dp来记录子问题的解,其中dp[i][w]表示前i个物品中,总重量不超过w的最大价值。

转移方程

根据当前物品是否放入背包,可以分两种情况:

  1. 不放入第i个物品:那么最大价值等于前i-1个物品在总重量w时的最大价值,即dp[i][w] = dp[i-1][w]
  2. 放入第i个物品:那么最大价值等于前i-1个物品在总重量w - weights[i-1]时的最大价值加上第i个物品的价值,即dp[i][w] = dp[i-1][w-weights[i-1]] + values[i-1]

因此,转移方程为:
d p [ i ] [ w ] = max ⁡ ( d p [ i − 1 ] [ w ] , d p [ i − 1 ] [ w − w e i g h t s [ i − 1 ] ] + v a l u e s [ i − 1 ] ) dp[i][w] = \max(dp[i-1][w], dp[i-1][w-weights[i-1]] + values[i-1]) dp[i][w]=max(dp[i1][w],dp[i1][wweights[i1]]+values[i1])

代码详细解释

package main

import "fmt"

// 01背包问题
func knapsack(weights, values []int, W int) int {
    n := len(weights)
    // 创建二维数组dp,初始化为0
    dp := make([][]int, n+1)
    for i := range dp {
        dp[i] = make([]int, W+1)
    }

    // 填表,i从1到n表示物品,w从0到W表示当前背包容量
    for i := 1; i <= n; i++ {
        for w := 0; w <= W; w++ {
            if weights[i-1] <= w {
                // 取不放入和放入物品的最大值
                dp[i][w] = max(dp[i-1][w], dp[i-1][w-weights[i-1]] + values[i-1])
            } else {
                // 当前物品不能放入背包,继承前一状态
                dp[i][w] = dp[i-1][w]
            }
        }
    }
    // 返回最大价值
    return dp[n][W]
}

func max(a, b int) int {
    if a > b {
        return a
    }
    return b
}

func main() {
    weights := []int{2, 1, 3, 2}
    values := []int{3, 2, 4, 2}
    W := 5
    fmt.Printf("最大价值是: %d\n", knapsack(weights, values, W))
}
步骤解释
  1. 初始化二维数组dp

    dp := make([][]int, n+1)
    for i := range dp {
        dp[i] = make([]int, W+1)
    }
    

    创建一个大小为(n+1) x (W+1)的二维数组dp,初始化为0。dp[i][w]表示前i个物品中,总重量不超过w的最大价值。

  2. 填表过程

    for i := 1; i <= n; i++ {
        for w := 0; w <= W; w++ {
            if weights[i-1] <= w {
                dp[i][w] = max(dp[i-1][w], dp[i-1][w-weights[i-1]] + values[i-1])
            } else {
                dp[i][w] = dp[i-1][w]
            }
        }
    }
    

    外层循环遍历每个物品i,内层循环遍历每个可能的背包容量w

    • 如果当前物品的重量小于等于当前容量w,则考虑两种情况:不放入当前物品或放入当前物品,取两者的最大值。
    • 如果当前物品的重量大于当前容量w,则不能放入当前物品,直接继承前一个状态的最大值。
  3. 返回结果

    return dp[n][W]
    

    返回dp[n][W],即考虑所有物品,在背包容量为W时的最大价值。

通俗解释

假设我们有一个背包,容量是5,现在有四个物品,每个物品都有一个重量和一个价值。我们要做的就是决定哪些物品放入背包,使得背包中的总价值最大,同时总重量不能超过5

我们通过动态规划的方法,逐个考虑每个物品和每种可能的背包容量,记录下在每种情况下的最大价值。最后,我们就可以得到在背包容量为5时,能够取得的最大价值。

这个过程类似于在一个表格中填数字,通过比较每一步可能的结果,逐步找到最优解。

总结

动态规划通过将问题分解为子问题,并保存子问题的结果来提高效率。通过定义状态和转移方程,我们可以逐步求解复杂问题。

  • 18
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

风不归Alkaid

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值