动态规划(一)
dynamic programming:此处的programming表示用的是表格法,非编程的意思 。
类似于分治,通过组合子问题的解来解决原问题。分治将问题划分互不相交的子问题,递归求解子问题,并且将子问题的解结合起来,求得原解。而动态规划要求子子问题重叠。动态规划是递归的优化。当采用递归重复调用相同子问题时,就可以使用动态规划来解决。
问题特征:
- overlapping subproblems(子问题重叠的情况):即不同子问题具有公共的子子问题,动态规划对每个子子问题只求解一次并存储,避免重复计算。
- optimal substructure(子问题的解可以用于求最终解):例如,最短路径Shortest Path问题,如果x在u->v的SP上,则SP[u->v]=SP[u->x]+SP[x->v],应用:Floyd,Bellman-Ford
思想:
- 将问题化为子问题
- 找到子问题的最优解
- 存储子问题的结果(表或者备忘录)
- 使用子问题的结果,以防止子问题被多次计算
- 最后计算问题的最终解。
步骤:
- 确定状态:根据最后一步的情况,化成子问题
- 转移方程:求最小最大计数等
- 初始条件和边界情况
- 确定计算顺序
解决模式:
Memoization(备忘录):Top Down : 递归+备忘录,自顶向下,记录计算过的值,触底返回
Tabulation(表):Bottom Up:循环+表,自底向上,从dp[0]一直填充到dp[n],按顺序填充表,并直接从已知状态计算未知,因此称为制表法。
**例子:**比如求解斐波拉契数列,采用动态规划可以把时间复杂度从指数级别降到多项式级别。采用动态规划,时间复杂度为O(n). Memoization图解、Tabulation图解
// 采用递归,时间复杂度T(n)=2*T(n-1)+1,O(2^n),指数级
int fib_recur(int n){
if(n<=1) return n;
return fib(n-2)+fib(n-1);
}
// Memoization:递归+备忘录
vector<int> memo(n+1,0);
int fib_Memo(int n){
if(memo[n]==0){
if(n<=1) memo[n]=n;
else memo[n]=fib_Memo(n-1)+fib(n-1);
}
return memo[n];
}
// Tabulation : 循环+表
int fib_Tabul(int n){
vector<int> F(n+1,0);
int F[0]=0,F[1]=1;
for(int i=2;i<=n;i++){
F[i]=F[i-2]+F[i-1];
}
return F[n];
}
最长增长子序列问题
Longest Increasing Subsequence:找到序列中的最长增长序列的长度
例如:Given sequence A={10,22,9,33,21,50,41,60}
可以得到Increasing Subsequence={10},{9,33,41},{33,41,60},{33,50,60},{41},等
而 LIS = {10,22,33,50,60},{10,22,33,41,60},算法返回5
思路:
-
创建一个数组L[],用于存放每个元素终止的LIS长度。max(L[])为结果
-
假设最后一步将Ak放入LIS,则
若存在 A [ k ] > A [ i ] A[k]>A[i] A[k]>A[i],则 L [ k ] = m a x ( L [ i ] ) + 1 L[k]=max(L[i])+1 L[k]=max(L[i])+1
若对于所有的 i < k i<k i<k,有 A [ k ] < = A [ i ] A[k]<=A[i] A[k]<=A[i], L [ k ] = 1 L[k]=1 L[k]=1
最长公共子串问题
Longest Common Subsequence: 给定两个序列,找到二者的最长公共子序列。
例如:A=“abcdefg”,B=“abxdfg”,则LCS=“abdfg”
例如求 A[0…m]=“AGGTAB”,B[0…n]=“GXTXAYB”
思路:
- 创建一个数组L用于存放LCS
- 转移方程:最后一个元素两种可能,相同或者不同
- 若 A [ m − 1 ] = = B [ n − 1 ] A[m-1]==B[n-1] A[m−1]==B[n−1], L ( A [ 0... m − 1 ] , B [ 0... n − 1 ] ) = 1 + L ( A [ 0.. m − 2 ] , Y [ 0.. n − 2 ] ) L(A[0...m-1],B[0...n-1])=1 + L(A [0..m-2],Y [0..n-2]) L(A[0...m−1],B[0...n−1])=1+L(A[0..m−2],Y[0..n−2])
- 否则 L ( A [ 0... m − 1 ] , B [ 0... n − 1 ] ) = m a x ( L ( A [ 0.. m − 2 ] , Y [ 0.. n − 1 ] ) , L ( A [ 0.. m − 2 ] , Y [ 0.. n − 1 ] ) ) L(A[0...m-1],B[0...n-1])=max(L(A [0..m-2],Y [0..n-1]),L(A [0..m-2],Y [0..n-1])) L(A[0...m−1],B[0...n−1])=max(L(A[0..m−2],Y[0..n−1]),L(A[0..m−2],Y[0..n−1]))
- 边界条件,若下标<0,就返回0
编辑距离问题
编辑距离:给定两个字符串A[m],B[n],有插入、删除、更改三种操作。计算将A转换为B所需要的最小操作次数。
例如:A =“ geek”,B=“ gesek”,1次,插入s即可
A=“ cat”,B =“ cut”,1次,更改a为u即可
A=“ sunday”,B =“ saturday”,3次,将un转为atur需将n换入r,插入t,插入a
递归思路:
考虑最后两个元素A[m-1],B[n-1]是否相同
- 若相同,则忽略最后一个字符,问题缩小一个长度
- 否则,计算三种此操作的代价,并选取最小+1
- 插入:计算A[m],B[n-1]
- 删除:计算A[m-1],B[n]
- 修改:计算A[m-1],B[n-1]
- 边界:m=0,返回n;若n=0,返回m
优化时间复杂度:建个矩阵(m+1)*(n+1)来记录操作代价,防止递归重复计算
优化空间复杂度:实际每次计算只需要用记录矩阵的一行,只需创一个2*(m+1)的数组即可,+1是为了考虑空字符串。
最低代价路径
Min Cost Path:给定代价矩阵cost[] []且均为正整数,矩阵中的位置(m,n)。要求从(0,0)到(m,n)的最小成本路径的成本。可以往右走,也可以往下走,也可以往下的对角线走。
思路:minCost(m, n) = min (minCost(m-1, n-1), minCost(m-1, n), minCost(m, n-1)) + cost[m] [n]。创建一个二维数据,记录结果即可。
硬币问题
第一种硬币问题说明:
coins面额 S = { S1, S2, … , Sm} ,数量无限,取总面值为W,共有几种方式?
例:coins面额 S={1,2,3},数量无限,取总面额 W=5,则(1,1,1,1,1),(1,1,1,2),(1,1,3),(1,2,2),(2,3),共5种
例:coins面额={2,3,5,10},数量无限,取总面额w=15有几种可能?
思路:
分成两种情况:
- 不包含第Sm个硬币
- 至少包含1个Sm的硬币
则count(S[],m,n) = count(S[],m-1,W)+count(S[],m,W-Sm)
创建(W+1)*m的零矩阵,用于存储面额
第二种硬币问题:coins面额={2,5,7},取总面额为27,找出可凑得总面额的硬币数量最小情况。
思路:
假设最后一个硬币面额是A,则 f ( 27 ) = f ( 27 − A ) + 1 f(27)=f(27-A)+1 f(27)=f(27−A)+1,但是最后一个硬币的面额有三种可能,A={2,5,7},所以 f ( 27 ) = m i n { f ( 27 − 2 ) + 1 , f ( 27 − 5 ) + 1 , f ( 27 − 7 ) + 1 } f(27)=min\{ f(27-2)+1,f(27-5)+1,f(27-7)+1 \} f(27)=min{f(27−2)+1,f(27−5)+1,f(27−7)+1},因此写作一般情况则为
f ( X ) = m i n { f ( X − 2 ) + 1 , f ( X − 5 ) + 1 , f ( X − 7 ) + 1 } f(X)=min\{f(X-2)+1,f(X-5)+1,f(X-7)+1\} f(X)=min{f(X−2)+1,f(X−5)+1,f(X−7)+1}
然后要处理初始条件和边界情况:
- 边界情况:若X-2,X-5,X-7<0,则数组越界,直接返回无穷。例f[1]=无穷
- 初始值:f[0]=0
机器人走路问题
m*n个格子,机器人只能往下或者往右走,则从左上角走到右下角有几种走法?
思路:
-
确定状态:
根据最后一步的情况,机器人只可能从A(m-2,n-1)或者B(m-1,n-2)两个位置走到终点。假设有X种方式走到A(m-2,n-1),Y种方式走到B(m-1,n-2),则共有X+Y种走法。
-
化成子问题,
机器人有几种方式从左上角走到A,有几种方式走到B。这么一来格子的大小就缩小了一行,或者一列。子问题重叠,可用DP解。
-
转移方程:
$ f(m,n)=f(m-1,n)+f(m,n-1)$
-
初始条件和边界情况
初始条件:f(0,0)=1
边界情况:若m=0, f(m,n)=1;第一行只能从右边走过来
若n=0, f(m,n)=1;第一列只能从上边走下来
青蛙跳问题
有n块石头,分别在x轴的0,1,…,n-1位置。一青蛙要从0跳到n-1,在i上最多可以往右跳ai。问青蛙能否跳到n-1。例子a=[2,3,1,1,4] 能,a=[3,2,1,0,4] 不能。
-
确定状态
根据最后一跳的情况 k < n − 1 & & k + a [ k ] > = n − 1 k<n-1 \&\& k+a[k]>=n-1 k<n−1&&k+a[k]>=n−1
-
化成子问题
青蛙能否跳到石头k,子问题重叠,可dp
-
转移方程
找到所有能满足条件的k,只要有一个能跳到
-
初始条件和边界情况
初始条件:f[0]=true
边界情况:可以不考虑
动态规划题的题型判断
- 计数:有多少种方式可以实现结果
- 求最大最小值:
- 求存在性
参考资料
- https://zhuanlan.zhihu.com/p/78220312
- https://www.youtube.com/watch?v=lVR2u9lsxl8&list=PLdo5W4Nhv31aBrJE1WS4MR9LRfbmZrAQu
- https://www.bilibili.com/video/av45990457?from=search&seid=6213316788932313952
- 看蓝色链接点开即可。