一、动态规划求解步骤及使用条件
- 将原问题分解为子问题
- 需要将原问题分解为若干个子问题,子问题与原问题形式相同或类似。但规模变小。子问题都解决,原问题解决。
- 子问题一旦求出就会被保存,所以每个子问题只需要解一次。
- 确定状态
- 将子问题相关的各个变量的一组取值,称之为一个“状态”。一个状态对应于一个或多个字问题,某个状态下的值就是这个状态所对应的子问题的解。
所有状态的集合构成问题的“状态空间”。状态空间的大小与用动态规划解决问题的时间复杂度直接相关。
- 确定初始状态的值
最小子问题的解,无需计算就可得出,通常是递归的边界条件。 - 确定状态转移方程
如何从一个或多个已知的“状态”的“值”求出另一个状态的值(“人人为我”递推型)。状态转移可以用递推公式表示,也称为状态转移方程。
二、 动态规划适用条件
- 问题具有最优子结构性质。如果问题的最优解所包含的子问题也是最优的,称问题具有最优子结构性质。
- 无后效性。当前若干个状态值一旦确定,则后过程的演变只和这若干个状态值有关,和之前如何演变到该状态无关。
三、常见子问题分解形式和状态转移形式
- 将原问题根据问题规模N进行子分解
典型问题:打家劫舍、 删除并获得点数、爬楼梯、斐波那契问题
打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷 窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。
子问题:(思路)问题分解将规模N将低,f(n)表示n个房间可获得的最大值。第n个房间有两种情况拿或不拿,对应f(n-1)和f(n-2)两个子问题,取两者最大值。
f(n) = max{ f(n-1) , f(n-2) + money[n] }//状态转移方程
f(0) = 0,f(1) = money[1];//初始状态
//至此我们根据f(0),f(1)+状态转移方程逐步向后推算就可解出f(n)。
- 根据原问题位置或问题解构成形式分解子问题
典型问题:不同路径、最长回文子串
最长回文子串
给你一个字符串 s,找到 s 中最长的回文子串。
如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。
示例 1:
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
示例 2:
输入:s = "abababa"
输出:"abababa"
子问题:这时如果我们参考降低字符串长度N规模来分解子问题就会暴露问题。f(n)与f(k),k<n并无关系f(n)为子回文串则f(k)明显不是。观察abababa,babab,aba,b这些字串,变化发生在两侧这时需要两个变量来记录。用i和j来模拟串长两端变化,f(i,j)表示求字符串i,j之间的回文串,则
dp(i,j) = dp( i+1 , j-1 ) && s[i] == s[j] //状态转移方程
j-1 - (i+1) + 1 < 2,得 j - i < 3 //初始条件,表示子串是单字符一定是回文串
求解过程类似填写一张二维表格,需要注意的是与寻常遍历i,j不同因为dp[i][j]根据dp[i+1][j-1]推出,即表格左下方元素,如果我们先填第一行、第二行,则在dp[0][3]时dp[1][2]并未计算。故正确的填写顺序为沿对角线从上到下填写。对应的是以2为长度的回文子串、3的回文子串。
Tips:输出结果:我们需要在遍历过程中就记录最长子串长度和起始位置。避免填写表格后再遍历找最长字串,类似在求解过程附带打擂台。
本文旨在提供解题的思路故不放程序源代码,题目皆出自leetcode感兴趣的朋友可以去leetcode查找。