动态规划的三要素 : 最优子结构,边界和状态转移函数
最优子结构是指每个阶段的最优状态可以从之前某个阶段的某个或某些状态直接得到(子问题的最优解能够决定这个问题的最优解),边界指的是问题最小子集的解(初始范围),状态转移函数是指从一个阶段向另一个阶段过度的具体形式,描述的是两个相邻子问题之间的关系(递推式)。
重叠子问题,对每个子问题只计算一次,然后将其计算的结果保存到一个表格中,每一次需要上一个子问题解时,进行调用,只要 o(1) 时间复杂度,准确的说,动态规划是利用空间去换取时间的算法.
判断是否可以利用动态规划求解,第一个是判断是否存在重叠子问题。
1、爬楼梯
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
- 边界:F(1)=1,F(2)=2
- 最优子结构:F(3)的最优子结构即 F(1) 和 F(2)
- 状态转移函数:F(n) = F(n-1)+F(n-2)
import timeit
def climb1(n):
'''
常规操作,采用递归的形式求解
:param n: n级台阶
:return: 走到终点有多少种方法
'''
if n <= 2:
return n
return climb1(n-1)+climb1(n-2)
def climb2(n):
'''
动态规划方法求解,代码较长,但是时耗短
:param n:n级台阶
:return:走到终点有多少种方法
'''
if n <= 2:
return n
a = 1 # 边界
b = 2 # 边界
temp = 0
for i in range(3, n + 1):
temp = a + b # 状态转移
a = b # 最优子结构
b = temp # 最优子结构
return temp
if __name__ == '__main__':
print(climb1(12))
print(climb2(12))
tt1 = timeit.repeat("climb1(12)", setup="from __main__ import climb1", number=1000)
print(min(tt1))
tt2 = timeit.repeat("climb2(12)", setup="from __main__ import climb2", number=1000)
print(min(tt2))
结果
233
233
0.027589000000000002
0.000645899999999977
2、三角形最小路径和
例如,给定三角形:
[
[2],
[3,4],
[6,5,7],
[4,1,8,3]
]
自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。
分析
- 边界:dp[0][0]=2
- 最优子结构:
最后一行最小值为:min(4+dp[2][0],1+min(dp[2][0],dp[2][1]),8+min(dp[2][1],dp[2][2]),3+dp[2][2]) - 状态转移方程:在第row行上,如果
i==0 时 ,dp[row][i]=dp[row-1][0]+triangle[row][0]
i ==len(triangle[row]) 时,dp[row][i-1]=dp[row-1][i-1]+triangle[row][i-1]
其它情况下,dp[i]=min(dp[i-1],dp[i])+triangle[row][i]
L = [
[2],
[3,4],
[6,5,7],
[4,1,8,3]
]
def cal_walk_steps():
if len(L) == 1:
return L[0][0]
dp = [[L[0][0]]]
for i in range(1,len(L)):
for j in range(len(L[i])):
dp.append([])
if j == 0:
dp[i].append(L[i][j]+dp[i-1][j])
elif j == len(L[i])-1:
dp[i].append(L[i][j]+dp[i-1][j-1])
else:
dp[i].append(min(dp[i-1][j-1],dp[i-1][j])+L[i][j])
return min(dp[len(L)-1])
print(cal_walk_steps()) #输出11
3、最长回文子串
在一个英文字符串 L 中, 怎么找出最长的回文子串. 例如 L = "caayyhheehhbbbhhjhhyyaac", 那么它最长的回文子串是 "hhbbbhh".
分析
定义状态方程和转移方程:
P[i,j]=0 表示子串[i,j]不是回文串。P[i,j]=1 表示子串[i,j]是回文串。
P[i,j](表示以 i 开始以 j 结束的子串)是回文字符串,那么 P[i+1,j-1]也是回文字符串,即 P[i,j] = 1 if P[i+1,j-1] == 1 and L[i]==L[j] else 0
L = 'caayyhheehhbbbhhjhhyyaac'
def manacher():
#子字符串最大长度
maxlen = 0
#子字符串开始索引
start = 0
#建二维数组记录dp[i][j]是否是回文字符串
dp = [[0 for i in range(len(L))] for j in range(len(L))]
for i in range(len(L)):
#字符串长度为1
dp[i][i] = 1
#字符串长度为2
if i+1 < len(L) and L[i]== L[i+1]:
dp[i][i+1] = 1
maxlen = 2
start = i
#字符串长度大于2,i代表字符串长度
for i in range(3,len(L)+1):
# j代表字符串起始索引
for j in range(len(L)-i+1):
k = i+j-1
if dp[j+1][k-1] == 1 and L[j] == L[k]:
dp[j][k] = 1
if i >maxlen:
#更新子字符串长度及索引
start = j
maxlen = i
if maxlen >= 2:
return L[start:start+maxlen]
return None
print(manacher())
4、找第n个丑数
把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。
分析
设第 i 个丑数为 F(i) 。由题可知,当 N=4 时,丑数为 F(2)*2;因此动态规划的三要素即为:
边界:F(1)=1
最优子结构:F(4)=min(F(2)*2,F(3)*2,F(1)*5)
状态转移函数:F(N)=min(F[t1]*2,F[t2]*3,F[t3]*5)。其中 t1,t2,t3<N-1
关于 t1,t2,t3 的取值,依据 F(t1)*2 >F(N-1) F(t2)*3 >F(N-1) F(t3)*5 >F(N-1)。
def getUglyNumber(n):
if n<=2:
return n
S = [1]
#丑数列 S 中的 S[i]
for i in range(1,n):
#S[i]之前的丑数列中找满足条件的S[j]
#找到某个数S[j]乘以2刚好大于S[i-1],记录索引 t1
for j in range(i):
if S[j]*2 > S[i-1]:
t1 = j
break
#找到某个数S[j]乘以3刚好大于S[i-1],记录索引 t2
for j in range(i):
if S[j]*3 > S[i-1]:
t2 = j
break
#找到某个数S[j]乘以5刚好大于S[i-1],记录索引 t3
for j in range(i):
if S[j]*5 > S[i-1]:
t3 = j
break
S.append(min(S[t1]*2,S[t2]*3,S[t3]*5))
print(S)
return S[-1]
print(getUglyNumber(12))
输出
[1, 2, 3, 4, 5, 6, 8, 9, 10, 12, 15, 16]
16
5、找和最大的连续子序列,输出这个和
给你一个整数list L, 如 L=[2,-3,3,50], 求L的一个连续子序列,使其和最大,输出最大子序列的和。 例如,对于L=[2,-3,3,50], 输出53(分析:很明显,该列表最大连续子序列为[3,50]).
s=[
[12,-7,6,-5],
[12,-7,8,-5],
[12,-7,3,6,-5],
[12,-17,13,-5],
[12,-17,5,8,-5]
]
def zhao(s):
dp=[0]*len(s)
dp[0]=s[0]
#dp[i]表示包含s[i]的 和最大 连续子序列
for i in range(1,len(s)):
#dp[i-1]和s[i]均可能为正数或负数
dp[i]=max(s[i],s[i]+dp[i-1])
print(dp,end=' ')
return max(dp)
for i in s:
print(zhao(i))
输出
[12, 5, 11, 6] 12
[12, 5, 13, 8] 13
[12, 5, 8, 14, 9] 14
[12, -5, 13, 8] 13
[12, -5, 5, 13, 8] 13