态规划问题的一般形式就是求最值,求解动态规划的核心问题是穷举。
动态规划三要素:重叠子问题、最优子结构、状态转移方程
转态转移方程:明确 base case -> 明确「状态」-> 明确「选择」 -> 定义 dp 数组/函数的含义。
# 初始化 base case
dp[0][0][...] = base
# 进行状态转移
for 状态1 in 状态1的所有取值:
for 状态2 in 状态2的所有取值:
for ...
dp[状态1][状态2][...] = 求最值(选择1,选择2...)
一、斐波那契数列
1、暴力递归
int fib(int N) {
if (N == 1 || N == 2) return 1;
return fib(N - 1) + fib(N - 2);
}
2、带备忘录的递归解法
int fib(int N) {
if (N < 1) return 0;
// 备忘录全初始化为 0
vector<int> memo(N + 1, 0);
// 进行带备忘录的递归
return helper(memo, N);
}
int helper(vector<int>& memo, int n) {
// base case
if (n == 1 || n == 2) return 1;
// 已经计算过
if (memo[n] != 0) return memo[n];
memo[n] = helper(memo, n - 1) + helper(memo, n - 2);
return memo[n];
}
3、dp 数组的迭代解法
int fib(int N) {
vector<int> dp(N + 1, 0);
// base case
dp[1] = dp[2] = 1;
for (int i = 3; i <= N; i++)
dp[i] = dp[i - 1] + dp[i - 2];
return dp[N];
}
4、细心的读者会发现,根据斐波那契数列的状态转移方程,当前状态只和之前的两个状态有关,其实并不需要那么长的一个 DP table 来存储所有的状态,只要想办法存储之前的两个状态就行了。所以,可以进一步优化,把空间复杂度降为 O(1)
int fib(int n) {
if (n == 2 || n == 1)
return 1;
int prev = 1, curr = 1;
for (int i = 3; i <= n; i++) {
int sum = prev + curr;
prev = curr;
curr = sum;
}
return curr;
}
二、凑零钱问题
# 伪码框架
def coinChange(coins: List[int], amount: int):
# 定义:要凑出金额 n,至少要 dp(n) 个***
def dp(n):
# 做选择,选择需要***最少的那个结果
for coin in coins:
res = min(res, 1 + dp(n - coin))
return res
# 题目要求的最终结果是 dp(amount)
return dp(amount)
根据伪码,我们加上 base case 即可得到最终的答案。显然目标金额为 0 时,所需***数量为 0;当目标金额小于 0 时,无解,返回 -1:
def coinChange(coins: List[int], amount: int):
def dp(n):
# base case
if n == 0: return 0
if n < 0: return -1
# 求最小值,所以初始化为正无穷
res = float('INF')
for coin in coins:
subproblem = dp(n - coin)
# 子问题无解,跳过
if subproblem == -1: continue
res = min(res, 1 + subproblem)
return res if res != float('INF') else -1
return dp(amount)
状态转移方程
面试题 08.11. 硬币
class Solution(object):
def waysToChange(self, n):
"""
:type n: int
:rtype: int
"""
mod = 10**9 + 7
coins = [25, 10, 5, 1]
f = [1] + [0] * n
for coin in coins:
for i in range(coin, n + 1):
f[i] += f[i - coin] # 记录最优解
return f[n] % mod
322.兑换零钱
class Solution:
def coinChange(self, coins, amount: int) -> int:
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for coin in coins:
for x in range(coin, amount + 1):
print('x:',x,'dp[x - coin]:',dp[x - coin])
dp[x] = min(dp[x], dp[x - coin] + 1)
return dp[amount] if dp[amount] != float('inf') else -1
560. 和为K的子数组
class Solution(object):
def subarraySum(self, nums, k):
"""
:type nums: List[int]
:type k: int
:rtype: int
"""
pre,count = 0,0
dic = {0:1}
for i in nums:
pre += i
if pre-k in dic:
count += dic[pre-k]
dic[pre] = dic.get(pre,0)+1
return count
64. 最小路径和
class Solution(object):
def minPathSum(self, grid):
"""
:type grid: List[List[int]]
:rtype: int
"""
dp = [[0 for i in range(len(grid[0]))] for j in range(len(grid))]
for i in range(len(grid)):
for j in range(len(grid[0])):
if i==0 and j==0:
dp[i][j] = grid[0][0]
elif j == 0 and i != 0:
dp[i][j] = dp[i-1][j] + grid[i][j]
elif i == 0 and j != 0:
dp[i][j] = dp[i][j-1] + grid[i][j]
else:
dp[i][j] = min(dp[i][j-1] ,dp[i-1][j]) + grid[i][j] # 较小的加上当前的单元格
return dp[len(grid)-1][len(grid[0])-1]
63. 不同路径 II
class Solution(object):
def uniquePathsWithObstacles(self, obstacleGrid):
"""
:type obstacleGrid: List[List[int]]
:rtype: int
"""
if not obstacleGrid or obstacleGrid[0][0] ==1:
return 0
n = len(obstacleGrid[0])
m = len(obstacleGrid)
dp = [[0 for i in range(len(obstacleGrid[0]))] for j in range(len(obstacleGrid))]
for i in range(m):
if obstacleGrid[i][0] == 1:
break
dp[i][0] = 1
for j in range(n):
if obstacleGrid[0][j] == 1:
break
dp[0][j] = 1
for i in range(1,len(obstacleGrid)):
for j in range(1,len(obstacleGrid[0])):
if obstacleGrid[i][j] == 1:
dp[i][j] = 0 #遇到障碍物
else:
dp[i][j] = dp[i-1][j] + dp[i][j-1]
return dp[m-1][n-1]
62. 不同路径
class Solution(object):
def uniquePaths(self, m, n):
res = [[1 for i in range(n)] for j in range(m) ]
count = 0
for i in range(m):
for j in range(n):
if i ==0 or j ==0:
continue
res[i][j] = res[i-1][j] + res[i][j-1]
return res[m-1][n-1]
剪绳子
'''
给你一根长度为n的绳子,请把绳子剪成整数长的m段(m、n都是整数,n>1并且m>1),
每段绳子的长度记为k[0],k[1],...,k[m]。请问k[0]xk[1]x...xk[m]可能的最大乘积是多少?
例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。
'''
'''
递归法:剪一刀下去,就会产生1,2,3...n-1个值,因此只需要求出f(i)*f(n-i)的最大值即可
'''
class Solution:
def cutRope(self, number):
# write code here
if number < 2:
return 0
if number == 2:
return 1
if number == 3:
return 2
return self.cutRopelong(number)
def cutRopelong(self, number):
if number < 4:
return number
maxnum = 0
for i in range(1, number // 2 + 1):
maxnum = max(self.cutRopelong(i) * self.cutRopelong(number - i), maxnum)
return maxnum
300.最长上升子序列
'''
用dp[i] 表示 从下标0 到下标i 的最长上升子序列的长度,
例如对于样例输入[10,9,2,5,3,7,101,18], 有 dp = [ 1, 1, 1, 2, 2, 3, 4, 4]。
显然dp[0] = 1
对于任意的i 不为零的情况,应该在 i 的左侧找一个下标 j ,其满足两个条件:
1. nums[ j ]比 nums[ i ] 小
2. 它是所有满足条件1里 dp [j] 最大的那个
dp[i] = max(dp[j]) + 1 , j < i and nums[ j ] < nums[ i ]
如果不存在这样的下标j,说明在0 ~ i - 1 的范围内,所有元素都比nums[i] 大,即无法找到一个能和 nums[i] 组成上升子序列的元素,
所以dp[i] = 1, 表示为nums[i] 自身成为一个长度为1 的上升子序列。
'''
class Solution(object):
def lengthOfLIS(self, nums):
if not nums:
return 0
a = [1 for i in range(len(nums))] #单个元素的时候,最长序列为1,保存序列的长度
for i in range(1,len(nums)):
for j in range(0,i):
if nums[i] > nums[j]:
a[i] = max(a[j] + 1, a[i]) # i前边有j个元素的时候,需要确保a[i]的值能取到最大
return max(a)
最长公共字符串/子序列
# 最长公共字符串
def LCstring(string1,string2):
len1 = len(string1)
len2 = len(string2)
res = [[0 for i in range(len1+1)] for j in range(len2+1)]
result = 0
for i in range(1,len2+1):
for j in range(1,len1+1):
if string2[i-1] == string1[j-1]:
res[i][j] = res[i-1][j-1]+1
result = max(result,res[i][j])
return result
# print(LCstring("helloworld","looper"))
# 输出结果为:2
'''
最长公共子序列的长度,比如 hello 和 eillok 的最长公共子序列是4,即ello
'''
'''
用二维数组res[i][j]来记录公共序列的长度,共有三种可能:
当i=0或j=0时,res[i][j] = 0
当A[i] = B[j]的时候 res[i][j]= res[i-1][j-1] + 1
当A[i] != B[j]的时候,res[i][j] = max(res[i-1][j],res[i][j-1])
'''
def LCS2(string1,string2):
len1 = len(string1)
len2 = len(string2)
res = [[0 for i in range(len1+1)] for j in range(len2+1)]
for i in range(1,len2+1):
for j in range(1,len1+1):
if string2[i-1] == string1[j-1]:
res[i][j] = res[i-1][j-1]+1
else:
res[i][j] = max(res[i-1][j],res[i][j-1])
return res[-1][-1]
print(LCS2("helloworld","loop"))
数组中不相邻元素最大和
'''
给定一个只含正数的数组,找到数组满足条件的元素的最大和,条件是:组成最大和的所有元素不能相邻,比如数组 [3,2,7,10] 返回 13(3+10),数组 [3,2,5,10,7] 返回(3+5+7)
分析,这是一道典型的使用动态规划求解的题目(当然能够使用万能的回溯法,但显然不是很棒),最优子结构为:f(i)=max{f(i - 2) + A[i], f(i - 1)}
解释一下,对于第i位,如果选择A[i],则不能选择i-1位,因此有f(i - 2) + A[i],如果不选择A[i],则将该问题放缩到i-1,因此有f(i-1)。
对于边界情况,f(0) = A[0],f(1) = max{A[0], A[1]}
'''
def no_f(B):
opt = [0 for i in range(len(B))]
opt[0] = B[0]
opt[1] = max(B[1],B[0])
for i in range(2,len(B)):
opt[i] = max(B[i]+opt[i-2],opt[i-1])
return opt[len(B)-1]
编辑距离
def edit_distance(str1, str2):
# 初始化矩阵
matrix = [[i+j for j in range(len(str2) + 1)] for i in range(len(str1) + 1)]
for i in range(1,len(str1)+1):
for j in range(1,len(str2)+1):
if str1[i-1] == str2[j-1]:
d = 0
else:
d = 1
# 用d[i-1,j]+1表示增加操作
# d[i,j-1]+1 表示我们的删除操作
# d[i-1,j-1]+temp表示我们的替换操作
matrix[i][j] = min(matrix[i-1][j]+1,matrix[i][j-1]+1,matrix[i-1][j-1]+d)
return matrix[len(str1)][len(str2)]
print(edit_distance('','xwrs'))
120、三角形最小和路径
'''
时间复杂度:O(n^2),其中n是三角形的行数。
空间复杂度:O(n^2)我们需要一个n*n的二维数组存放所有的状态。
'''
class Solution:
def minimumTotal(self, triangle):
n = len(triangle)
f = [[0] * n for _ in range(n)]
f[0][0] = triangle[0][0]
for i in range(1, n):
f[i][0] = f[i - 1][0] + triangle[i][0]
for j in range(1, i):
f[i][j] = min(f[i - 1][j - 1], f[i - 1][j]) + triangle[i][j] #
f[i][i] = f[i - 1][i - 1] + triangle[i][i] # 每行的最后一个元素
return min(f[n - 1])
53、最大子序和
def maxSubArray(nums):
sum = 0
max = nums[0]
for index in range(len(nums)):
if sum < 0:
sum = 0
sum += nums[index]
if sum > max:
max = sum
return max
print(maxSubArray([-2,-1,-3,-4,-1,-2,-1,-5,-4]))
def maxSubArray1(nums):
for i in range(1, len(nums)):
nums[i] = nums[i] + max(nums[i - 1], 0)
return max(nums)
def maxSubArray2(num):
dp = [0] * len(num)
dp[0] = num[0]
for i in range(1, len(num)):
dp[i] = max(num[i], dp[i - 1] + num[i])
# print(dp[i-1]+dp[i])
return max(dp)
121、买卖股票最佳时期
def maxProfit(self, prices):
profit = 0
mindex = 0
for i in range(len(prices)):
if prices[i] < prices[mindex]:
mindex = i
profit = max(profit,prices[i]-prices[mindex])
return profit
122、买卖股票最佳时期II
class Solution:
def maxProfit(self, prices: List[int]) -> int:
profit = 0
for i in range(1,len(prices)):
if prices[i] > prices[i-1]:
profit += prices[i]-prices[i-1]
return profit