常见dp模型的状态定义和计算方法

dp问题核心就是从具体问题中抽象出一个可迭代计算的状态,接下来我将记录一些常见dp模型的状态。

对于dp问题,代码并不重要,重要的是解决问题的思路,所以在本文中我重点介绍这些常见dp模型的解决思路。

01背包

问题描述

有 N件物品和一个容量是 V 的背包。每件物品只能使用一次。第 i 件物品的体积是 vi,价值是 wi。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。

思路:是一个二维dp,dp[i][j]的含义是只取前i个物品,且最大容量为j时的最大价值。

计算公式:我们要求dp[i][j]时有两种情况,两种情况取最大值即可。

  • 取第i个物品,那么dp[i][j] = dp[i - 1][j - vi] + wi,注意这种情况下需要满足j >= vi
  • 不取第i个物品,那么dp[i][j] = dp[i - 1][j]

完全背包问题

问题描述:和01背包基本相同,但每件物品可以使用无限次。

有 N种物品和一个容量是 V的背包,每种物品都有无限件可用。第 i种物品的体积是 vi,价值是 wi。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。

思路:和01背包基本相同,是一个二维dp,dp[i][j]的含义是只取前i个物品,且最大容量为j时的最大价值。

计算公式:我们要求dp[i][j]时有两种情况,两种情况取最大值即可。

  1. 取第i个物品,要区分具体取几个第i个物品
    • 取一个dp[i][j] = max(dp[i][j], dp[i - 1][j - vi] + wi)
    • 取两个dp[i][j] = max(dp[i][j], dp[i - 1][j - 2 * vi] + 2 * wi)
    • 取三个dp[i][j] = max(dp[i][j], dp[i - 1][j - 3 * vi] + 3 * wi)
    • …一直循环下去,直至第i个物品的总体积大于j
  1. 不取第i个物品,那么dp[i][j] = dp[i - 1][j]

上面是完全背包的朴素解法,需要跑三层循环,复杂度是O(n * v ^ 2),可以优化到O(n * v),但优化方法比较难讲,在这里不进一步说明。

多重背包问题

问题描述:和完全背包基本相同,但每个物品的取出次数是有限制的。

有 N 种物品和一个容量是 V 的背包。第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。输出最大价值。

思路:和完全背包基本相同,是一个二维dp,dp[i][j]的含义是只取前i个物品,且最大容量为j时的最大价值。

计算公式:我们要求dp[i][j]时有两种情况,两种情况取最大值即可。

  1. 取第i个物品,要区分具体取几个第i个物品
    • 取一个dp[i][j] = max(dp[i][j], dp[i - 1][j - vi] + wi)
    • 取两个dp[i][j] = max(dp[i][j], dp[i - 1][j - 2 * vi] + 2 * wi)
    • 取三个dp[i][j] = max(dp[i][j], dp[i - 1][j - 3 * vi] + 3 * wi)
    • …一直循环下去,直至第i个物品的总体积大于j 或者物品个数大于si
  1. 不取第i个物品,那么dp[i][j] = dp[i - 1][j]

上面是多重背包的朴素解法,需要跑三层循环,复杂度是min(O(n * si * v), O(n * v * v)),也可以进一步优化,但优化方法比较难讲,在这里不进一步说明。

分组背包问题

问题描述:和前面的背包问题差别有点大。

有 N 组物品和一个容量是 V 的背包。每组物品有若干个,同一组内的物品最多只能选一个。每件物品的体积是 vij,价值是 wij,其中 i 是组号,j 是组内编号。求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。输出最大价值。

思路:和前面的背包问题基本相同,是一个二维dp,dp[i][j]的含义是只取前i个分组,且最大容量为j时的最大价值。

计算公式:我们要求dp[i][j]时有两种情况,两种情况取最大值即可。

  1. 取第i组内的某个物品,要区分具体取哪个物品。
    • 取i组中的第1个,dp[i][j] = max(dp[i][j], dp[i - 1][j - vi1] + wi1)
    • 取i组中的第2个,dp[i][j] = max(dp[i][j], dp[i - 1][j - vi2] + wi2)
    • 取i组中的第3个,dp[i][j] = max(dp[i][j], dp[i - 1][j - vi3] + wi3)
    • …一直循环下去,直至遍历玩第i组的所有物品。在迭代过程中要注意判断该物品的体积是否小于等于j,只有小于等于j才可以取出该物品。
  1. 不取第i组中的物品,那么dp[i][j] = dp[i - 1][j]

最长上升子序列

问题描述:给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。

思路:是一个一维线性dp,表示状态的数组为dp[N],dp[i]的含义是以第i个数为结尾的最长上升子序列的长度。

计算公式:for j in range [1, i - 1]: 如果j号元素小于i号元素,那么dp[i] = max(dp[i], dp[j] + 1)

朴素做法是O(n ^ 2)的,可以优化到O(n)

最长公共子序列

问题描述:给定两个字符串A、B,长度分别为n、m,求最长公共子序列长度。

思路:是一个二维线性dp,表示状态的二维数组是dp[n][m],dp[i][j]的含义是A的前i个字符和B的前j个字符的最长公共子序列长度。

计算公式:如果A的第i个字符等于B的第j个字符,那么dp[i][j] = max(dp[i - 1][j],dp[i][j - 1], dp[i - 1][j - 1] + 1);否则dp[i][j] = max(dp[i - 1][j],dp[i][j - 1])

这个计算公式的推算看似简单,但其实不简单。因为dp[i - 1][j]和dp[i][j - 1]两者所代表的情况是有交叉部分的,注意这里dp[i][j]的含义在是A的前i个字符里选子序列,而不一定包含第i个字符。虽然有交叉重叠,但两者的并集却能够覆盖所有的情况,所以公式的计算结果是正确的。

最短编辑距离

问题描述

给定两个字符串 A 和 B,现在要将 A 经过若干操作变为 B,可进行的操作有:

  • 删除–将字符串 A 中的某个字符删除。
  • 插入–在字符串 A 的某个位置插入某个字符。
  • 替换–将字符串 A 中的某个字符替换为另一个字符。

现在请你求出,将 A 变为 B 至少需要进行多少次操作。

思路:这是一个二维dp,dp[i][j]的含义是把A的前i个字符变为B的前j个字符需要的最小编辑距离。计算dp[i][j]时,这里主要分以下情况:

  1. A[i] == B[j],那么可以不操作A的最后一位字符,可以只操作A前面i - 1位字符使其变为B的前j - 1位字符即可,也就是dp[i][j] = dp[i - 1][j - 1]。要注意,这里求出的dp[i][j]不一定是最小的dp[i][j],因为只是可以不操作最后一个字符,也可以操作最后一个字符,所以要继续计算下面的情况。
  2. A[i] != B[j],这时候有三种操作方法
    ①在A[i]末尾添加一个等于B[j]的字符,那么只需操作A的前i个字符使其变为B的前j - 1位字符即可,也就是dp[i][j] = dp[i][j - 1] + 1
    ②把A[i]末尾的字符替换为B[j],那么只需操作A的前i - 1个字符使其变为B的前j - 1位字符即可,也就是dp[i][j] = dp[i - 1][j - 1] + 1
    ②把A[i]末尾的字符删掉,那么只需操作A的前i - 1个字符使其变为B的前j位字符即可,也就是dp[i][j] = dp[i - 1][j] + 1

递推公式为:

if(a[i] == b[j]){
    
    
  dp[i][j] = min(dp[i - 1][j - 1], dp[i][j]);
}
dp[i][j] = min(dp[i][j - 1] + 1, dp[i][j]);
dp[i][j] = min(dp[i - 1][j - 1] + 1, dp[i][j]);
dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j]);

注意最短编辑距离的dp和字符串下标最好从1开始,要对二维数组dp的第零行和第零列进行初始化,初始化规则是dp[i][0] = i,即把A的前i个字符串变成空字符串需要i次操作,同理dp[0][j] = j

区间dp

先记录一下简单区间dp的解法。

问题描述:石子合并

设有 N 堆石子排成一排,其编号为 1,2,3,…,N。每堆石子有一定的质量,可以用一个整数来描述,现在要将这 N 堆石子合并成为一堆。

每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。

例如有 4 堆石子分别为 1 3 5 2, 我们可以先合并 1、2 堆,代价为 4,得到 4 5 2, 又合并 1、2 堆,代价为 9,得到 9 2 ,再合并得到 11,总代价为 4+9+11=24;

如果第二步是先合并 2、3 堆,则代价为 7,得到 4 7,最后一次合并代价为 11,总代价为 4+7+11=22。

找出一种合理的方法,使总的代价最小,输出最小代价。

思路:这是一个二维dp,dp[i][j]的含义是合并第i堆到第j堆之间所有石子所消耗的最小代价,包括第i堆和第j堆。

计算公式

  • 当i == j时,dp[i][j]显然等于零。
  • 当i < j时,对于i <= k < j有如下计算方法:
    1. dp[i][j] = max( dp[i][k] + dp[k + 1][j] + sum(i,j), dp[i][j] ),其中sum(i, j)代表的是i堆到j堆所有石子质量总和。
    1. 遍历所有k值,反复执行上述计算公式,得出dp[i][j]

迭代方式

对于区间dp,我们并不能按照从头到尾的顺序遍历二维dp数组。因为按照上述的计算公式,dp[i][j]的值依赖于dp[k][m],其中i <= k <=m <= j,也就是说,要求出合并i到j的最小代价,我们必须先求出合并i到j所有子区间的最小代价。

所以我们按照区间长度从小到大的顺序迭代二维dp数组,先迭代i = j的所有情况,然后迭代i + 1 = j的所有情况,然后i + 2 = j,i + 3 = j,…,i + N - 1 = j

猜你喜欢

转载自blog.csdn.net/m0_52744273/article/details/129491243