茶艺师学算法打卡19:动态规划

104 阅读3分钟

茶艺师学算法打卡19:动态规划

学习笔记

对于我这茶艺师而已,递归是算法里的“大山”之一,而动态规划就是另外一座“大山”。
我遇到动态规划的题目,不发怵是不可能的。
里面有着众多的变形题型,由于存在着代码优化,直接背这些题目的代码反而还会坠入迷雾里。
真的只有熟练其解题思路与方法才能“以不变应万变”。
幸好,关于动态规划的解题思路与方法,也是有着众多门派,在里头,总有“一款属于你的”。
这里就用一道经典题 ACW2. 01背包问题 ,来展示2种不同思路吧。

题目说明
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。

第 i 件物品的体积是 vi ,价值是 wi 。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。 输出最大价值。

输入格式
第一行两个整数,N,V ,用空格隔开,分别表示物品数量和背包容积。

接下来有 N 行,每行两个整数 vi,wi ,用空格隔开,分别表示第 i 件物品的体积和价值。

输出格式
输出一个整数,表示最大价值。

数据范围
0<N,V10000<N, V≤1000
0<vi,wi10000<vi, wi≤1000

输入样例

4 5
1 2
2 4
3 4
4 5

输出样例:

8

解法1
先写出其暴力做法,关注人力模拟时的做的决策,依此写出状态转移方程,最后就是确定边界与目标,递推实现。
这里很明显,用 F[i,j]F[i, j] 表示“从前 i 个物品中选了总体积为 j 的物品放入背包,其价值的最大值”。
那么在选第 i 个物品时,就有两个决策:

  • 决策1:不放第 i 个物品,那么 F[i,j]=F[i1,j]F[i, j] = F[i - 1, j]
  • 决策2:放第 i 个物品,那么状态则是从“没选第 i 个物品” 转移过来加上这第 i 个物品的价值 WiW_{i},既 F[i,j]=F[i1,jVi]+Wi, F[i, j] = F[i - 1, j - V_{i}] + W_{i}, if if jVi j \geq V_{i}

初值: F[0,0]=0F[0, 0] = 0, 其他位置负无穷

目标:max0jMF[N][j]\max\limits_{0 \leq j \leq M} F[N][j]

示例代码

f := make([][]int, n + 1) 
for i := 0; i <= n; i++ {
    f[i][] := make([]int, V + 1)
    for j := 0; j <= V; j++ {
        f[i][j] = -1<<31-1
    }
}
f[0][0] = 0  
for i := 1; i <= n; i++ {
    for j := 0; j <= V; j++ {
        f[i][j] = f[i - 1][j]
    }
    for j := v[i]; j <= V; j++{
        f[i][j] = max(f[i][j] ,f[i - 1][j - v[i]] + w[i])
    }
}

ans := 0
for j := 0; j <=V; j++ {
    ans = max(ans, f[N][j])
}

解法2
直接看图

经过以上分析,我们得到状态转移方程
f(i,j)=max(f(i1,j),f(i1,jvi)+wi)f(i, j) = max(f(i - 1, j), f(i - 1, j - vi) + wi)

示例代码1

    for i := 1; i <= n; i++ {
        for j := 0; j <= m; j++ {
               f[i][j] = f[i - 1][j]
               if j >= v[i] {
                   f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i])
               }
        }
    }

观察该方程,我们能发现:

  • i 这一维,只用到 i - 1,这里就可以滚动数组,只保留上一层的数据,优化空间使用
  • j 这一维,要么是 j,要么是 j - vi 这个比自己小的数,那么 j 就可以从大到小遍历

示例代码2

    for i := 1; i <= n; i++ {
        for j := m; j >= v[i]; j-- {
                f[j] = max(f[j], f[j - v[i]] + w[i])
        }
    }