动态规划介绍
动态规划算法递归地将原问题划分为多个相互依赖的子问题,直至最小子问题,分解过程中会出现许多重叠子问题。此外动态规划问题还具有另外两大特性:最优子结构(原问题的最优解所包括的子问题的解也是最优的)、无后效性(给定一个确定的状态,它的未来发展只与当前状态有关,而与过去经历的所有状态无关)。
动态规划算法的流程为:定义状态(当前步骤的值),建立 dp 表(储存每步的状态),确定边界条件(初始状态或基础情况,对应于问题规模的最小可能情况,可以直接给出解而不需要进一步分解或递归求解),推导状态转移方程(当前状态如何由过去若干步状态推导得到)。
一维动态规划
70. 爬楼梯
题目:假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
算法:定义 dp[i] 为爬到第 i 阶可能的方法数。显然上第一阶有一种方法,上第二阶有两种方法(一次上 2 阶或分两次每次上 1 阶),因此边界条件为dp[0]=1,dp[1]=2。想要到达第 i 阶,首先需要到达第 i-1 阶再走 1 步或到达第 i-2 阶再走 2 步,因此状态转移方程为 dp[i] = dp[i-2] + dp[i-1]。
- def climbStairs(n):
-
- if n==1 or n==2:
- return n
-
- dp = [0] * n
- dp[0], dp[1] = 1, 2
-
- for i in range(2, n):
- dp[i] = dp[i-2] + dp[i-1]
-
- return dp[-1]
118. 杨辉三角
题目:在「杨辉三角」中,每个数是它左上方和右上方的数的和。给定一个非负整数 numRows,生成「杨辉三角」的前 numRows 行以二维列表形式返回结果。
![]()
算法:定义 dp[i,j] 为第 i 行第 j 个元素的取值。边界条件为dp[i,0] = 1,dp[i,-1] = 1。对 0 < j < len(dp[i])-1,有 dp[i,j] = dp[i-1,j-1] + dp[i-1,j]。
- def generate(numRows):
-
- dp = []
- for i in range(0, numRows):
- dp.append([1]*(i+1))
-
- for i in range(1, numRows):
- for j in range(1,len(dp[i])-1):
- dp[i][j] = dp[i-1][j-1] + dp[i-1][j]
-
- return dp
198. 打家劫舍
题目:你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
算法:定义 dp[i] 为偷到第 i 家时的最大金额。边界条件为偷到第 0 家和第 1 家时的最大利润,分别为dp[0]=nums[0],dp[1]=max(dp[0],nums[1])。第 i 家要么偷(则不能偷第 i-1 家)要么不偷(则能偷第 i-1 家),偷第 i 家表示上一次偷的是第 i-2 家,不偷第 i 家表示上一次偷的是第 i-1 家,我们取这两种可能方案中金额最大者,可得状态转移方程为 dp[i] = max(dp[i-2]+nums[i],dp[i-1])。
注意 len(nums)= 0, 1, 2的情况。
- def rob(nums):
-
- if not nums:
- return 0
- if len(nums)==1:
- return nums[0]
- if len(nums)==2:
- return max(nums[0], nums[1])
-
- dp = [0]*len(nums)
- dp[0] = nums[0]
- dp[1] = max(dp[0], nums[1])
-
- for i in range(2, len(dp)):
- dp[i] = max(dp[i-2]+nums[i], dp[i-1])
-
- return dp[-1]
279. 完全平方数
题目:给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
例: n = 12时返回 3 (12 = 4 + 4 + 4),n = 13时返回 2 (13 = 4 + 9)。
算法:注意任何正整数一定可以分解成若干完全平方数的和,因为 1 也是完全平方数。定义 dp[i] 为整数 i 最少是多少个完全平方数的和。边界条件为dp[0] = 0,dp[1] = 1。本题特殊之处在于其递推关系式不是第项和前面一项或两项的关系,而是和前面不定数量的项的关系:
dp[i] = min{dp[j]+1|j,k∈Z+,j+k^2=i}
后面的条件等价于
dp[i] = min{dp[i-k^2]+1|k=1,2,...,int(sqrt(i))}
正整数 k 的取值范围即等价于1 <= k< sqrt(i)。
- def numSquares(n):
-
- dp = [float('inf')] * (n+1)
- dp[0] = 0
- dp[1] = 1
-
- for i in range(2, len(dp)):
- for j in range(1, int(n**(1/2))+1):
- dp[i] = min(dp[i], dp[i-j**2]+1)
-
- return dp[-1]
322. 零钱兑换
题目:给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
算法:定义 dp[i] 为凑成整数 i 所需的最少的硬币个数。边界条件为 dp[0] = 0,表示凑成金额 0 需要 0 个硬币。递推关系式是 dp[i] 和前面不定数量的项的关系:
dp[i] = min{dp[j]+1|j∈Z+,k∈coins,j+k=i}
后面的条件等价于
dp[i] = min{dp[i-k]+1|k∈coins,k<=i}.
- def coinChange(coins, amount):
-
- dp = [float('inf')] * (amount+1)
- dp[0] = 0
- for i in range(1, len(dp)):
- for k in coins:
- if k <= i:
- dp[i] = min(dp[i],dp[i-k]+1)
-
- if dp[-1]==float('inf'):
- return -1
- else:
- return dp[-1]
139. 单词拆分
题目:给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true。
例:s = 'leetcode',wordDict = ['leet','code']。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
算法:定义dp[i]为 s 的前 i 项 s[0:i] 是否能用字典中出现的一个或多个单词拼接出。边界条件为 dp[0] = True。递推关系是s[0:i]是否能表示成字典中一个词word和某个s[0:j](j<=i)的拼接,即若 dp[i-len(work)] = True且 len(work) <= i且
s[0:i] == s[0:(i-len(work))] + word, word∈wordDict
成立则 dp[i] = True。
- def wordBreak(s, wordDict):
-
- dp = [False] * (len(s)+1)
- dp[0] = True
-
- for i in range(1,len(s)+1):
- for word in wordDict:
- if (len(word)<=i) and (dp[i-len(word)]) and (s[0:i] == (s[0:(i-len(word))] + word)):
- dp[i] = True
- break
-
- return dp[-1]
300. 最长递增子序列
题目:给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
算法:定义 dp[i] 为以 nums[i] 结尾的最长严格递增子序列的长度。边界条件为 dp[i] = 1(单独一个数nums[i]也能看成一个递增子序列)。递推关系是若nums[j] < nums[i]则nums[0:(j+1)](j < i)和nums[i]可以组成一个递增子列:
dp[i] = max{dp[j]+1|j=0,...,i-1.nums[j]
最后返回 max(dp)。
- def lengthOfLIS(nums):
-
- dp = [1] * len(nums)
-
- for i in range(1, len(dp)):
- for j in range(0,i):
- if nums[i] > nums[j]:
- dp[i] = max(dp[i], dp[j] + 1)
-
- return max(dp)
152. 乘积最大子数组
题目:给你一个整数数组 nums ,请你找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。测试用例的答案是一个 32-位 整数。
例:输入 nums=[2,3,-2,4] 输出 6 (2×3)。输入 nums=[-2,0,-1] 输出 0。
算法:定义 dp[i] 为 nums[j] (j<=i) 作为最后一个乘积因子的连续子数组的最大乘积。边界条件为 dp[0] = nums[0]。递推关系需要借助另外两个变量分别表示以 nums[i] 作为最后一个乘积因子的连续子数组的最小乘积 cur_min 和最大乘积 cur_max,
dp[i] = max(dp[i-1],cur_max).
最后返回 max(dp)。
- def maxProduct(nums):
-
- dp = [float('inf')] * len(nums)
- dp[0] = nums[0]
-
- cur_min = nums[0]
- cur_max = nums[0]
-
- for i in range(1, len(dp)):
-
- if nums[i] < 0:
- cur_max, cur_min = cur_min, cur_max
-
- cur_min = min(nums[i], nums[i]*cur_min)
- cur_max = max(nums[i], nums[i]*cur_max)
-
- dp[i] = max(dp[i-1], cur_max)
-
- return max(dp)
416. 分隔等和子集
题目:给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
算法:若 sum(nums) 为奇数则必然不可能,若为偶数则取 target = sum(nums)//2。定义 dp[i] 为是否存在 nums 的子集其和为 i。边界条件为 dp[0] = True (空集的和为 0)。递推关系:遍历 nums 的元素 num,若 dp[j-num] = True 则 dp[j] = True(由 0 <= j-num,j<= target 可得此处 j 的范围是 target,target-1,...,num ,这里必须从大到小取不然会出现元素重复的错误)。
注意递推关系不能用"遍历 nums 的元素 j 若dp[i-j] = True则dp[i] = True。返回 dp[target]",反例为 nums = [1,2,5],显然 dp[0],dp[1],dp[2],dp[3] = True ,按这种递推 dp[4] = True,但实际上不存在和为 target 的子列,错误原因是dp[3] = True的判断中元素1已被使用过一次,判断 dp[4] = True 时其被重复使用了一次。
- def canPartition(nums):
-
- if sum(nums) % 2 != 0:
- return False
- else:
- target = sum(nums)//2
-
- dp = [False] * (target + 1)
- dp[0] = True
-
- for num in nums:
- for j in range(target,num-1,-1):
- if dp[j-num]:
- dp[j] = True
-
- return dp[target]
32. 最长的有效括号
题目:给你一个只包含 '(' 和 ')' 的字符串,找出最长有效(格式正确且连续)括号子串的长度。如输入 ')()())' 返回 4,输入 ')((())))' 返回 6,输入 '()((()))' 返回 8,输入 '())((()))' 返回 6。
算法:定义dp[i]为以第i个字符结尾的最长有效括号子串的长度,边界条件为dp[0]=0(单独半个括号不能构成有效子串)。
如果s[i]是'(',则dp[i]保持为0,因为左括号不能单独结束一个有效子串;如果s[i]是')',检查s[i-1]是否是'(',如果是那么dp[i]为dp[i-2]+2或2(这对括号本身),如果不是在看:若i-dp[i-1]-1(即前一个有效子串的前一个位置)存在且是'(',那么dp[i]可以进一步扩展到dp[i-1] + dp[i-dp[i-1]-2] + 2 (这里+2是因为加上了当前这对括号)。
- def longestValidParentheses(s):
-
- if not s:
- return 0
-
- dp = [0] * len(s)
-
- for i in range(1,len(s)):
- if s[i] == '(':
- dp[i] = 0
- else:
- if s[i-1] == '(':
- if i>=2:
- dp[i] = dp[i-2]+2
- else:
- dp[i] = 2
- else:
- if dp[i-1]>0:
- if i-dp[i-1]-1>=0 and s[i-dp[i-1]-1]=='(':
- dp[i] = dp[i-1] + 2
- if i-dp[i-1]-2>=0:
- dp[i] += dp[i-dp[i-1]-2]
-
- return max(dp)
多维动态规划
62. 不同路径
题目:一个机器人位于一个 m × n 网格的左上角(起始点在下图中标记为 “Start”)。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。
问总共有多少条不同的路径?假设走到起点有一条路径。
![]()
算法:定义 dp[i,j] 为到达 [i,j] 位置的路径数,边界条件为 dp[0,0] = 0,dp[0,i] = 1,dp[i,0] = 1。递推关系:只有 [i,j-1] 和 [i-1,j] 两个格子能一步走到 [i,j] 位置,因此
dp[i,j]=dp[i-1,j]+dp[i,j-1].
- def uniquePaths(m, n):
-
- dp = []
- for i in range(0,m):
- dp.append([0]*n)
-
- for i in range(0, m):
- dp[i][0] = 1
- for j in range(0,n):
- dp[0][j] = 1
-
- for i in range(1,m):
- for j in range(1,n):
- dp[i][j] = dp[i-1][j] + dp[i][j-1]
-
- return dp[m-1][n-1]
64. 最小路径和
题目:给定一个包含非负整数的 m×n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
![]()
算法:定义 dp[i,j] 为到达 [i,j] 的最小的路径上数字之和,边界条件为 dp[i,0],dp[0,j]的取值。递推关系:只有 [i,j-1] 和 [i-1,j] 两个格子能一步走到 [i,j] 位置,因此
dp[i,j] = min(dp[i-1,j],dp[i,j-1]) + grid[i,j].
- def minPathSum(grid):
-
- m, n = len(grid), len(grid[0])
-
- dp = []
- for i in range(0,m):
- dp.append([float('inf')]*n)
- dp[0][0] = grid[0][0]
-
- for j in range(1,n):
- dp[0][j] = dp[0][j-1] + grid[0][j]
- for i in range(1,m):
- dp[i][0] = dp[i-1][0] + grid[i][0]
-
- for i in range(1,m):
- for j in range(1,n):
- dp[i][j] = min(dp[i-1][j],dp[i][j-1]) + grid[i][j]
-
- return dp[m-1][n-1]
5. 最长回文子串
题目:给你一个字符串 s,找到 s 中最长的 回文子串。回文子串指字符串从左往右和从右往左读都是一样的。
算法:定义 dp[i,j] 为索引 i 到索引 j 的片段是否为回文子串,边界条件为长度为 1 的子串全部是回文子串。递推关系:若 s[i]==s[j] 且 dp[i+1,j-1] = True 则 dp[i,j] = True ,即对索引 i 到索引 j 的 s 的片段,若其两端相等,且中间的片段是回文子串,则此片段是回文子串。注意计算 dp[i,j] 时的两层循环外层是对子串长度 ll 进行遍历(从 2 到 len(s)),内层是子串左端索引的遍历(从 0 到 len(s)-ll,由于右端点 j 至少还要占据len(s)-1的位置因此 i 最多只能取到len(s)-ll 的位置)。
- def longestPalindrome(s):
-
- LL = len(s)
- if LL < 2:
- return s
-
- dp = []
- for i in range(0,LL):
- dp.append([False]*LL)
- for i in range(0,LL):
- dp[i][i] = True
-
- pos_sta, len_max = 0, 1
-
- for ll in range(2, LL+1):
- for i in range(0, LL-ll+1):
- j = i + ll -1
- if (s[i]==s[j]) and (ll==2 or dp[i+1][j-1]):
- dp[i][j] = True
- len_max = ll
- pos_sta = i
-
- return s[pos_sta:(pos_sta+len_max)]
1143. 最长公共子序列
题目:给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
算法:定义dp[i][j]为以text1[i-1]和text2[j-1]结尾的两个字符串的最长公共子序列的长度,没有边界条件,dp全部元素初始化成0。递推关系是若text1[i-1]==text2[j-1],则dp[i][j] = dp[i-1][j-1] + 1,若text1[i-1] != text2[j-1],则dp[i][j] = max(dp[i-1][j], dp[i][j-1]),即最长公共子序列要么在text1的前i-1个字符和text2的前j个字符中,要么在text1的前i个字符和text2的前j-1个字符中。
- def longestCommonSubsequence(text1, text2):
-
- m, n = len(text1), len(text2)
-
- dp = []
- for i in range(0,m+1):
- dp.append([0]*(n+1))
-
- for i in range(1, m+1):
- for j in range(1, n+1):
- if text1[i-1]==text2[j-1]:
- dp[i][j] = dp[i-1][j-1]+1
- else:
- dp[i][j] = max(dp[i-1][j],dp[i][j-1])
-
- return dp[m][n]
72. 编辑距离
题目:给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数。
你可以对一个单词进行如下三种操作:1. 插入一个字符,2. 删除一个字符,3. 替换一个字符。
算法:定义dp[i][j]为将word1的前i个字符转换成word2的前j个字符所需要的最少编辑操作次数(0<=i<=len(word1),0<=j<=len(word2)),边界条件是dp[i][0]和dp[0][j],前者表示将的前i个字符变成空字符,需要进行i次删除操作,后者表示将空字符变成长度j的字符串,需要进行j次插入操作。遍历word1和word2,填充dp数组的其余部分。对于每个位置(i, j),有两种情况:1. 若word1[i-1] == word2[j-1],不需要进行任何操作,因此dp[i][j] = dp[i-1][j-1]。2. 考虑三种操作:替换(dp[i-1][j-1] + 1),插入(dp[i][j-1] + 1),删除(dp[i-1][j] + 1),并选择其中的最小值作为dp[i][j]的值。
- def minDistance(word1, word2):
-
- m, n = len(word1), len(word2)
- dp = []
- for i in range(0, m+1):
- dp.append([float('inf')]*(n+1))
-
- for i in range(0,m+1):
- dp[i][0] = i
- for j in range(0, n+1):
- dp[0][j] = j
-
- for i in range(1, m+1):
- for j in range(1, n+1):
- if word1[i-1]==word2[j-1]:
- dp[i][j] = dp[i-1][j-1]
- else:
- dp[i][j] = min(dp[i-1][j-1],dp[i-1][j],dp[i][j-1])+1
-
- return dp[m][n]
评论记录:
回复评论: