动态规划
Dynamic Programming
1.递推求解
由于递归问题的解可能会重复计算多次,产生不必要的计算,所以可以将计算过的子问题的解保存起来,就是递推。
典型的问题包括斐波那契数列,N阶上楼梯问题
N阶上楼梯问题
每次上楼梯可以上1格或者两格,所以上到第n格总共的方式数目 d p [ n ] = d p [ n − 1 ] + d p [ n − 2 ] dp[n]=dp[n-1]+dp[n-2] dp[n]=dp[n−1]+dp[n−2]
2.最大连续子序列和
给定一个序列{ A 1 A_{1} A1, A 2 A_{2} A2, A 3 A_{3} A3, A 4 A_{4} A4… A n A_{n} An},找到一个连续的子序列(下标连续),使得子序列的和最大。
令dp[i]
为以a[i]
作为结尾的连续序列最大和。
则dp[i]=max(a[i],dp[i-1]+a[i])
拓展:最大连续子矩阵和(二维情况)
解决办法:假设最大矩阵在第 i 行到第 j 行,则会出现两种情况:
i == j
问题转化为一维的问题,求第 i 行的最大连续子序列和即可。i != j
将第i行到第j行的元素加起来,得到一个一维数组,求最大连续子序列和就是最大子矩阵的和。(每行元素的累加结果可以通过构造一个辅助数组得到)
3.最长递增子序列
给定一个序列{ A 1 A_{1} A1, A 2 A_{2} A2, A 3 A_{3} A3, A 4 A_{4} A4… A n A_{n} An},找到一个最长的子序列(下标可以不连续),对子序列中的任意下标 x < y x<y x<y有 A x A_{x} Ax< A y A_{y} Ay.
令dp[i]
表示以a[i]
为末尾的最长递增子序列的长度。
则dp[1]=1;
dp[i]=max(1,dp[j]+1) j<i && a[j]<a[i]
由于下标可以不连续,所以要找到前面那个最长递增子序列长度。所以在循环遍历第i个位置的前面的子序列时,要让dp[i]=max(dp[i],dp[j]+1)
,而不是dp[i]=max(1,dp[j]+1)
.后面那个表达式其实只判断了前一个位置的子序列长度。
拓展:最大上升子序列和(下标可以不连续)
给定一个序列,找到一个上升子序列,使得子序列和最大。求这个和。
dp[i]
表示以a[i]
为结尾的最大上升子序列和。
则dp[1]=a[i];
dp[i]=max(a[i],dp[j]+a[i]) j<i && a[j]<a[i]
.如果a[i]
前面的元素都比它大,则dp[i]=a[i]
.
4.最长公共子序列
给定两个字符串 S 1 S_{1} S1和 S 2 S_{2} S2,求一个最长公共子串,他同时为 S 1 和 S_{1}和 S1和 S 2 S_{2} S2的子串,且要求它长度最长,并确定这个长度。
设置二维数组dp[][]
,dp[i][j]
表示以
S
1
[
i
]
S_{1}[i]
S1[i]为末尾和
S
2
[
j
]
S_{2}[j]
S2[j]为末尾的最长公共子序列的长度。最长公共子序列的长度是dp[n][m]
的值。
根据 S 1 [ i ] S_{1}[i] S1[i]和 S 2 [ j ] S_{2}[j] S2[j]的关系,分为两种情况:
-
S
1
[
i
]
=
=
S
2
[
j
]
S_{1}[i]==S_{2}[j]
S1[i]==S2[j] ,
dp[i][j]=dp[i-1][j-1]+1
-
S
1
[
i
]
!
=
S
2
[
j
]
S_{1}[i]!=S_{2}[j]
S1[i]!=S2[j],
dp[i][j]=max(dp[i-1][j],dp[i][j-1])
边界情况:
dp[i][0]=0
dp[0][j]=0
5.背包问题
1. 0-1背包
有
n
件物品,每个物品价值为w[i]
,价值为v[i]
,现在有容量为m
的背包,选择物品使得装入背包价值最大。求最大价值。
设置一个二维数组dp[i][j]
,表示将前i
个物品装入容量为j
的背包时,能获得的最大价值。dp[n][m]
为原问题的解。
对于第i个物品,只有装与不装两种状态:
- 装入背包,
dp[i][j]=dp[i-1][j-w[i]]+v[i]
- 不装入背包,
dp[i][j]=dp[i-1][j]
dp[i][j]=max(dp[i-1][j-w[i]]+v[i],dp[i-1][j])
注意:
- 转移时要注意判断
j-w[i]
的值是不是负数,如果是负数不能转移。 - 边界情况
dp[0][j]=0 dp[i][0]=0
转移方程优化:
由于上面的状态转移方程中,dp[i][j]
的值仅与dp[i-1][j-w[i]]
和dp[i-1][j]
的值有关,也就是说只和二维数组的上一行有关,所以状态转移方程可以优化成一维数组:
dp[j]=max(dp[j-w[i]]+v[i],dp[j])
这个方程只是在存储上进行了优化,实际上算法的时间复杂度并没有降低,我们还是要写两重循环进行遍历。这个方程可以理解为对于不同的物品加入背包,判断当前容量为j时这个物品的加入是否能使得当前dp[j]
的值增加,即价值增大,如果增大就更新,否则不更新。
遍历顺序:
for i = 0-n:
for j = m-0:
if j>=w[i] dp[j]=max(dp[j],dp[j-w[i]])
print(dp[m])
2.完全背包
将01背包进行扩展,每种物品可以拿多个,就是完全背包问题。
和01背包一样,设置二维数组dp[i][j]
,表示将前i
个物品装入容量为j
的背包时,能获得的最大价值。两种状态,取或者不取第i件物品:
- 不取第
i
件物品,dp[i][j]=dp[i-1][j]
- 取第
i
件物品,由于可以取多次,所以状态方程转移到dp[i][j-w[i]]
,而不是dp[i-1][j-w[i]]
。即dp[i][j]=dp[i][j-w[i]]+v[i]
转移方程优化:
dp[j]=max(dp[j],dp[j-w[i]]+v[i])
可以发现状态转移方程和01背包一模一样,为了保证转移正确,要保证每次更新确定状态dp[j]
时,dp[j-w[i]]
已经完成了本次更新修改。所以要正序遍历j,而01背包是逆序遍历j。
遍历顺序:
for i = 0-n:
for j = 0-m:
if j>=w[i] dp[j]=max(dp[j],dp[j-w[i]])
print(dp[m])
3.多重背包
多重背包问题介于01背包和完全背包之间,每件物品最多取k件,求装入背包物品最大价值。
我们可以把多重背包问题转换为01背包问题,将每种物品都视为k中价值和重量都不同的物品,时间复杂度为 O ( m ∑ k i ) O(m\sum k_{i}) O(m∑ki).降低每种物品的数量可以降低其复杂度。可以采用一种更有技巧的拆分:
将原数量为k的物品分为若干组,每组包含 2 0 , 2 1 . . . 2^{0},2^{1}... 20,21...,类似于二进制的拆分,将物品数量降低,同时通过原物品与新物品的不同组合,可以得到0-k之间的任意件物品的价值总和,所以对这些新物品做01背包,可以得到多重背包的解。优化后时间复杂度为 O ( m ∑ l o g 2 ( k i ) O(m\sum log_{2}(k_{i}) O(m∑log2(ki).
6.其它问题
动态规划问题灵活多变,这里只是介绍了几种经典且常见的动态规划问题,更多问题要结合实际定义状态数组,写出状态转移方程。