动态规划都是从一张表格开始,DP三要素:
输入: [10,9,2,5,3,7,101,18]
输出:4
解释:最长上升子序列 [2,3,7,101] (不唯一,但长度唯一)
最长上升子序列是求长度而不是具体的子序列,因此我们的dp table的具体含义可以设置为在当前位置 i 处的最长递升子序列为dp[i];当我们考虑dp时,由于假设的是 x < i x < i x<i处的dp[x]是已知的,确定dp[i]时存在两种情况:
最后求得max(dp[i])即在当前位置的最长递升子序列
strs = '2 7 1 5 6 4 3 8 9'
str1 = list(map(int,strs.split(' ')))
init = [1 for i in range(len(str1))] //table 初始化,至少为1
for i in range(2,len(str1) + 1):
for j in range(i-1):
if(str1[i-1] > str1[j]):
init[i-1] = max(init[i-1],init[j] + 1)
lis = max(init)
>>init :[1,2,1,2,3,2,2,4,5]
当前解法的复杂度为 O ( n 2 ) O(n^2) O(n2),利用二分法可以将复杂度降低至 O ( n l o g n ) O(nlogn) O(nlogn)
设置一个数组low,该数组的元素更新原则是寻找第一个大于 L ( i ) L(i) L(i)的元素,将 L ( i ) L(i) L(i)替换该元素。
def binary_search(strs, R, x):
L = 0
while (L < R):
mid = (L + R) // 2
if (strs[mid] < x):
L = mid + 1
elif(strs[mid] > x):
R = mid - 1
else:
L = mid
return L
strs = [1, 4, 7, 6, 5, 9, 10, 3]
Low = [-1 for i in range(len(strs))]
Low[0] = strs[0]
ans = 1
for i in range(1, len(strs)):
if strs[i] > Low[ans - 1]:
Low[ans] = strs[i]
ans += 1
print(Low)
else:
Low[binary_search(Low, ans, strs[i])] = strs[i]
print(Low)
给定两个字符串s1和s2,计算出将s1转换为s2所使用最少的操作数,可以使用以下三种操作:
1.插入一个字符;
2.删除一个字符;
3.替换一个字符;
举例:将‘rad’ -> ‘apple’
思路:若想知道‘rad’ -> ‘apple’ 的距离,则可以通过’ra’ -> ‘apple’的距离推测出来,若想知道’ra’ -> ‘apple’ 的距离,则可以通过‘r’ -> ‘apple’ 的距离推测出来;
* | a | p | p | l | e | |
---|---|---|---|---|---|---|
* | 0 | 1 | 2 | 3 | 4 | 5 |
r | 1 | 1 | 2 | 3 | 4 | 5 |
a | 2 | 1 | 2 | 3 | 4 | 5 |
d | 3 | 2 | 2 | 3 | 4 | 5 |
由于有三种操作方法:
故状态转移方程为:
dp[i][j] = min(dp[i-1][j] + 1,dp[i][j-1] + 1, dp[i-1][j-1] + str1[i] != str2[j])
def editDistance(str1, str2):
len1, len2 = len(str1) + 1, len(str2) + 1
dp = [[0 for i in range(len2)] for j in range(len1)]
for i in range(len1):
dp[i][0] = i
for j in range(len2):
dp[0][j] = j
for i in range(1, len1):
for j in range(1, len2):
dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + (str1[i - 1] != str2[j - 1]))
return dp[-1][-1]
while True:
try:
print(editDistance(input(), input()))
except:
break
讲解
有从1到N共N层的楼。现手中有k个鸡蛋。现在确定这栋楼存在楼层 0 < = F < = N 0<=F<=N 0<=F<=N,在这层楼将鸡蛋扔下去,鸡蛋恰好没摔碎(高于 F 的楼层都会碎,低于 F 的楼层都不会碎)。问,在最坏的情况下,至少要扔几次鸡蛋才能确定F?
解决动态规划问题的基本框架:
【状态】:每次都会变话的参数为鸡蛋的个数k 和 需要测试的楼层数;
【选择】:会改变状态的行为,在哪一层扔鸡蛋。
【穷举】:从第一层到第N层,每一次都会有一个操作数,即从这些操作数中选出最小的;
动态规划最重要的就是确定状态如何转移:该题中我们选择在第 i i i层进行扔鸡蛋,会产生两种不同的状态:
def superEggDrop(k,N):
memo = dict()
def dp(k,N):
if k == 1:
return N
if N == 0:
return 0
if (k,N) in memo:
return memo[(k,N)]
res = float('INF')
#穷举所有的可能性
for i in range(1,N+1):
res = min(res, max(dp(k,N-i),dp(k-1,i-1)) + 1)
mome[(k,N)] = res
return res
return dp(k,N)
//二分法:
def superEggDrop(K, N):
memo = dict()
def dp(K, N):
if K == 1: return N
if N == 0: return 0
if (K, N) in memo:
return memo[(K, N)]
# for 1 <= i <= N:
# res = min(res,
# max(
# dp(K - 1, i - 1),
# dp(K, N - i)
# ) + 1
# )
res = float('INF')
# 用二分搜索代替线性搜索
lo, hi = 1, N
while lo <= hi:
mid = (lo + hi) // 2
broken = dp(K - 1, mid - 1) # 碎
not_broken = dp(K, N - mid) # 没碎
# res = min(max(碎,没碎) + 1)
if broken > not_broken:
hi = mid - 1
res = min(res, broken + 1)
else:
lo = mid + 1
res = min(res, not_broken + 1)
memo[(K, N)] = res
return res
return dp(K, N)
一旦涉及到子序列和最值,则一般都是考察动态规划;动态规划的核心就是定义dp table,找状态转移关系
一、两种思路
for i in range(2,n+1):
for j in range(i-1):
if str1[i-1] > str2[j]:
dp[i-1] = max(dp[i-1], dp[j] + 1)
int n = arr.length;
int[][] dp = new dp[n][n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (arr[i] == arr[j])
dp[i][j] = dp[i][j] + ...
else
dp[i][j] = 最值(...)
}
}
该思路中有包含这两种情况:
在子串 s [ i . . j ] s[i..j ] s[i..j] 中,最长回文子序列的长度为 dp[i][j], 动态规划的核心思想就是寻找状态转移,而状态转移的核心思想就是归纳思维,简单的说就是从已知的部分推出未知的部分。假如知晓 d p [ i + 1 ] [ j − 1 ] dp[i+1][j-1] dp[i+1][j−1] 就可以通过通过str[i] = str[j] ? 推测出来:
代码实现为:
if (s[i] == s[j]):
dp[i][j] = dp[i+1][j-1] + 2
else:
dp[i][j] = max(dp[i+1][j],dp[i][j-1])
base_case:
def longestPalindSubseq(s):
n = len(s)
dp = [0 for i in range(n)]
for i in range(n):
dp[i][i] = 1
for i in range(n-2,-1,-1):
for j in range(i+1,n):
if (s[i] == s[j]):
dp[i][j] = dp[i+1][j-1] + 2
else:
dp[i][j] = max(dp[i+1][j],dp[i][j-1])
return dp[0][n-1]
有一排石头堆,用数组piles表示,piles[i]表示第i堆石头有多少个。两个人轮流拿石头,一次拿一堆,但是只能拿最左边或者最右边的石头堆。等所有石头被拿完后,谁拥有的石头数多谁就获胜;
动态规划两大核心:状态与选择
该题中选择是选择最左边的石头还是最右边的石头;通过选择发生的变化就是状态的转移,该题中的状态是开始的索引 i ,结束的索引 j, 当前轮到的人;
确定dp数组的含义:
创建一个存储元组的二维数组dp table,其中dp[i][j]表示从i开始到j结束的范围内从左开始和从右开始的最高分数;
dp[i][j].fir 表示,对于 piles[i…j] 这部分石头堆,先手能获得的最高分数。
dp[i][j].sec 表示,对于 piles[i…j] 这部分石头堆,后手能获得的最高分数。
举例理解一下,假设 piles = [3, 9, 1, 2],索引从 0 开始
dp[0][1].fir = 9 意味着:面对石头堆 [3, 9],先手最终能够获得 9 分。
dp[1][3].sec = 2 意味着:面对石头堆 [9, 1, 2],后手最终能够获得 2 分。
状态转移方程:
dp[i][j].fir = max(piles[i] + dp[i+1][j].sec,piles[j] + dp[i][j-1].sec)
解释:在从 i j i~j i j中先手选石头堆有两种选择,从左边开始和从右边开始,其中从左边开始的结果就是piles[i] + i + 1 j i+1 ~ j i+1 j石头堆中的后手(这是由于两人轮流进行),从左边开始推理类似。再从左边开始和右边开始之间选取最大值;此时fir的状态转移方程已出现;下面是sec的状态转移方程
if 先手选择左边:
dp[i][j].sec = dp[i+1][j].fir
if 先手选择右边:
dp[i][j].sec = dp[i][j-1].fir
设定dp的初始值:
if 0<=i == j <=n:
dp[i][j].fir = piles[i]
dp[i][f].sec = 0
只有一堆石头时先手必赢
遍历dp:
根据状态转移方程,计算dp[i][j]时需要用到dp[i+1][j] 和dp[i][j-1],
遍历的顺序不止一种,但都是斜着遍历的;整个程序代码如下:
//返回最后先手和后手的得分差
def stoneGame(piles):
n = len(piles)
dp = [[(0,0) for i in range n] for j in range(n)]
for i in range(n):
dp[i][i] = (piles[i],0)
for i in range(n-2,-1,-1):
for j in range(i+1,n):
left = piles[i] + dp[i+1][j][1]
right = piles[j] + dp[i][j-1][1]
//状态转移方程
if (left > right):
dp[i][j][0] = left
dp[i][j][1] = dp[i+1][j][0]
else:
dp[i][j][0] = right
dp[i][j][1] = dp[i][j-1][0]
res = dp[0][n-1]
return res[0] - res[1]
贪心算法是特殊的动态规划,具有更多的约束条件,所谓的贪心算法就是:每一步都做出一个局部最优的选择,最终的结果就是最优的,但只有一部分问题拥有这个性质;
给出一系列形如[start,end]的闭区间,算出最多有几个互不相交的区间
该题的目的是求出最多的区间,解题思路如下:
解题代码如下:
def intervalSchedule(invts):
if (len(invts) == 0):
return 0
sorted(invts, key = lambda x:x[1])//key 是一个函数对象,参数就是列表中每一个元素
count = 1
x_end = invts[0][1]
for i in invts[1:]:
start = i[0]
if start >= x_end:
count += 1
x_end = i[1]
return count
题目
给定一个数组,他的第 i i i个元素表示股票第 i i i天的价格,设计一个算法来实现获取的最大利润,其中你可以有k次交易(即可以有k次买入卖出),注意:股票不得在买进之前卖出
利用‘状态机’框架来完成,所谓的‘状态机’也就是dp table,使用动态规划首先确定状态和选择:
选择: buy(买入), sell(卖出), rest(啥也不干)
状态: i(天数),k(允许交易的次数),rest(0:表示没有持有股票,1:持有股票)
明确状态之后开始穷举所有的状态:
dp[ i ][ k ][0 or 1]
0<=i<=n-1, 1<=k<=K
其中n为天数,大K为最多交易数,总共的状态为nK2
for i in range(0,n):
for k in range(K):
for s in {0,1}:
dp[i][k][s] = max{buy, sell, rest}
>>dp[2][3][1] : 在第三天,我手里有股票,现在进行了三次交易
>>最终欲求的是dp[n-1][K][0]
确定状态转移方程:
股票问题归根结题就两种状态,一种是手里有股票,一种是手里没股票:
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
手里没股票有两种可能:
1.前一天就没股票,今天保持原样;
2.前一天有股票,但今天我把他卖了;
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
手里有股票有两种可能:
1.前一天就有,今天保持原样;
2.前一天没有,今天刚买入
确定base case:
dp[-1][k][0] = 0: 还没开始,利润必定为0
dp[-1][k][1] = -inf ,还没开始是不可能买入的,所以用负无穷表示不可能
dp[i][0][1] = -inf, k = 0,表示不可交易,则不可能会有股票
dp[i][0][0] = 0: 不可交易当然利润为0
状态转移方程:
//base case:
dp[-1][k][0] = dp[i][0][0] = 0
dp[-1][k][1] = dp[i][0][1] = -inf
//状态转移方程
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
股票问题之-- k = 1
给定一个数组,他的第 i i i个元素表示股票第 i i i天的价格,设计一个算法来实现获取的最大利润,其中你可以有1次交易(即可以有k次买入卖出),注意:股票不得在买进之前卖出
将k=1代入状态转移方程中:
dp[i][k][0] = max(dp[i-1][1][0], dp[i-1][1][1] + prices[i])
dp[i][k][1] = max(dp[i-1][1][1], dp[i-1][0][0] - prices[i]) = max(dp[i-1][1][1], - prices[i])
此时k对整个状态转移方程已经没有影响了,所以可以进一步简化状态转移方程:
dp[i][k][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][k][1] = max(dp[i-1][1], - prices[i])
代码如下:
n = len(prices)
dp = [[0 for i in range(n)] for j in range(2)]
for i in range(n):
if(i -1 == -1):
dp[i][0] = 0
dp[i][1] = -prices[i]
else:
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1],-prices[i])
//dp[i]的状态实际上只跟dp[i-1]有关,所以可以继续简化,将空间复杂度降为O(1)
dp_i_0 = 0
dp_i_1 = -inf
for i in range(n):
//dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp_i_0 = max(dp_i_0,dp_i_1 + prices[i])
//dp[i][1] = max(dp[i-1][1],-prices[i])
dp_i_1 = max(dp_i_1,-prices[i])
return dp_i_0
股票问题之-- k = + inf
若K为无穷大,则可以认为k对整个状态转移方程式没有影响的
dp[i][k][0] = max(dp[i-1][k][0],dp[i-1][k-1][1] + princes[i])
dp[i][k][1] = max(dp[i-1][k][1],dp[i-1][k-1][0] - prince[i])//此时k = k-1
由于k对整个状态转移方程已经没有影响了,故可以简化方程为:
dp[i][k][0] = max(dp[i-1][0],dp[i-1][1] + princes[i])
dp[i][k][1] = max(dp[i-1][1],dp[i-1][0] - prince[i])
解题代码为:
n= len(prices)
dp_i_0 = 0
dp_i_1 = -inf
for i in range(n):
tmp = dp_i_0
dp_i_0 = max(dp_i_0,dp_i_1 + princes[i])
dp_i_1 = max(dp_i_1,tmp - princes[i])
return dp_i_0
股票问题之-- k = + inf + cooldown
也就是每次卖出之后必须等一天才能继续交易
dp[i][k][0] = max(dp[i-1][0],dp[i-1][1] + princes[i])
dp[i][k][1] = max(dp[i-1][1],dp[i-2][0] - prince[i])
//解释:第 i 天选择 buy 的时候,要从 i-2 的状态转移,而不是 i-1 。
解题代码为:
n= len(prices)
dp_i_0 = 0
dp_i_1 = -inf
dp_pre_0 = 0
for i in range(n):
tmp = dp_i_0
dp_i_0 = max(dp_i_0,dp_i_1 + princes[i])
dp_i_1 = max(dp_i_1,dp_pre_0 - princes[i])
dp_pre_0 = temp
return dp_i_0
股票问题之-- k = + inf with fee
每次交易需要手续费
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i] - fee)
//解释:相当于买入股票的价格升高了。
//在第一个式子里减也是一样的,相当于卖出股票的价格减小了
解题代码:
n= len(prices)
dp_i_0 = 0
dp_i_1 = -inf
for i in range(n):
tmp = dp_i_0
dp_i_0 = max(dp_i_0,dp_i_1 + princes[i])
dp_i_1 = max(dp_i_1,tmp - princes[i] - fee)
return dp_i_0
股票问题之-- k = 2
原始状态转移方程在这种情况就无法简化了:
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
解题代码:
n = len(prices)
max_k = 2
dp = [[0 for i in range(n)] for j in range(2)]
for i in range(n):
for k in range(max_k,0,-1):
if(i -1 == -1):
dp[i][k][0] = 0
dp[i][k][1] = -prices[i]
else:
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1],dp[i-1][k-1][0] - prices[i])
return dp[n-1][max_k ][0]
股票问题之-- k = any int
若简单的利用上一题的解法,则容易超内存,这是由于传入的k会非常大,这样分析,当有n天的股票时,一次完整的交易(买进,卖出)至少需要两天,所以k最大可以达到n/2,若k>n/2时则与k 为无穷是等效的。所以该题分为两部分,一部分是小于n/2,一部分是大于n/2
if k >n/2:
--k = + inf 解法--
else:
--k = 2 的解法--
总结,该题型的核心为:
//base case:
dp[-1][k][0] = dp[i][0][0] = 0
dp[-1][k][1] = dp[i][0][1] = -inf
//状态转移方程
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
一个强盗,从一排房子走过,在每间房子前都有两种选择:抢或者不抢。如果选择抢,则不能抢相邻的下一间房子,只能从下下间房子开始做选择。若步枪,则可以从下一间房子继续做选择,当走过最后一间房子后,就没有得抢。
动态规划问题核心:状态和选择。该题的状态就是房子的索引,选择就是抢或者不抢;
def dp(num,start):
if start>=len(num):
return 0
else:
res = max(dp(num,start+1),num[start] + dp(num,start+2))
return res
常规的递归解法,缺点是存在大量的重复计算,因此可以引入备忘录dp table来记录已计算过得值;
def rob(num):
memo = [-1 for i in range(len(num))]
return dp(num,0)
def dp(num,start):
if start>= len(num):
return 0
elif memo[start] != -1:
return memo[start]
res = max(dp(num,start+1), num[start] + dp(num,start+2))
memo[start] = res
return res
递归 + 备忘录 = 自顶向下的动态规划算法
若这一排的房子是首位相接的一个环,则怎么抢可以抢到最多的钱,原则与上题一致
两种选择:第一间抢最后一间不抢,最后一间抢第一间不抢
def rob (nums):
n = len(nums)
if n == 1:
return nums[0]
else:
return max(robRange(nums,0,n-2),robRange(nums,1,n-1))
def robRange(nums, start, end):
n = len(nums)
dp_i_1 = dp_i_2 = 0
dp dp_i = 0
for i in range(end,start-1,-1):
dp_i = max(dp_i_1,nums[i] + dp_i_2)
dp_i_2 = dp_i_1
dp_i_1 = dp_i
return dp_i
若房子是按照二叉树排列,且相连的两座房子是不能连续被抢
memo = {}
def rob(TreeNode root):
if root == NULL:
return 0
if root in memo.keys():
return memo[root]
do_it = root.val +
(0 if root.left == NULL else rob(root.left.left) + rob(root.left.right))
not_do = rob(root.left) + rob(root.right)
res = max(do_it,not_do)
memo[root] = res
return res
有一个键盘,只有四个按钮: A, ctrl + A, ctrl + C, ctrl + V,其中A表示在屏幕中打印一个‘A’,ctrl + A 表示全选屏幕,ctrl + C, ctrl + V分别表示复制和粘贴,请问经过N此操作之后A的个数最多可以达到多少个?
动态规划的基本思想:状态 + 选择;
该题的选择有四种,实际上也可以看成三种,‘A’,‘ctrl + A 和 ctrl + C’, ‘ctrl + V’;
该题的状态有三种:剩下的次数n,屏幕上’A’的个数a_num,和剪切板上‘A’的个数copy
确定了状态和选择后可以写出状态转移方程:
dp(n-1,a_num+1,copy)//按下‘A’
dp(n-1,a_num + copy,copy)//按下‘ctrl + V’
dp(n-2,a_num,a_num)//按下‘ctrl + A 和 ctrl + C’
基本框架确定好后,再加上备忘录dp table来避免重复计算子问题!解题代码如下:
dp max_A(n):
memo = {}
def dp(n,a_num,copy):
if (n,a_num,copy) in memo.keys():
return memo[(n,a_num,copy)]
if n<=0:
return a_num
memo[(n,a_num,copy)] = max(
dp(n - 1, a_num + 1, copy), # A
dp(n - 1, a_num + copy, copy), # C-V
dp(n - 2, a_num, a_num) # C-A C-C
)
return memo[(n,a_num,copy)]
return dp(N,0,0)
这种状态选择导致算法复杂度过高,消耗时间过长,选择一种的新的状态,实际上最优按键序列一定是只有两种情况的:
两种情况最后一个按键一定是‘A’或者‘C+V’,所以我们用一个变量,‘j’作为若干‘C+V’的起点,则’j-2’代表‘C+A,和C+C’的操作了;整个思路类似于 ‘LIS’。
解题代码为:
def maxA(N):
dp = [0 for i in range(N+1)]
for i in range(1,N+1):
dp[i] = dp[i-1] + 1
if i>3:
for j in range(3,i+1):
dp[i] = max(dp[i], dp[j-3]*(i-j + 1))
return dp[N]