本篇博文主要记录动态规划DP这一块的学习。
文章目录
- 印章(蓝桥杯6.27)
- 拿金币(蓝桥杯6.28)
- 最长回文子串(力扣6.29)
- 最大子数组和(力扣6.30)
- 爬楼梯(力扣7.1)
- 杨辉三角(力扣7.2)
- 买卖股票的最佳时机(力扣7.3)
- 比特位计数(力扣7.4)
- 判断子序列(力扣7.5)
- 斐波那契数(力扣7.6)
- 使用最小花费爬楼梯(力扣7.7)
- 除数博弈(力扣7.12)
- 第 N 个泰波那契数(力扣7.12)
- 获取生成数组中的最大值(力扣7.13、简单)
- 括号生成(力扣7.13、中等)
- 跳跃游戏(力扣7.14 难度中等)
- 跳跃游戏 II(力扣7.14 难度中等)
- 不同路径(力扣7.15 难度中等)
- 不同路径2(力扣7.15 难度中等)
- 最小路径和(7.16)
- 解码方法(7.16)
- 三角形最小路径和(7.19)
- 单词拆分(7.20)
印章(蓝桥杯6.27)
问题描述:共有n种图案的印章,每种图案的出现概率相同。小A买了m张印章,求小A集齐n种印章的概率。(1≤n,m≤20)
输入格式:一行两个正整数n和m
输出格式:一个实数P表示答案,保留4位小数。
思考1:
概率论?!分类讨论
1)当m<n时,集齐的概率为0
2)当m>n时,买的m张印章,每张有n种可能,分母:n的m次方;分子:(C_m^n) * (C_n m-n)(m张印章中,有n张是图案不同的,剩下的m-n张随意)。
思考2:
动态规划解题步骤:
1)设置状态,即数组
dp[i][j] = 印章数目为i、集齐j种印章的概率
dp[1][1] = 1
dp[i][1] = (1/n)^(i-1)
i < j时,dp = 0
2)确定状态转移方程
找状态之间的关系,从手里有(i-1)枚印章到 i 枚印章时,有两种情况,第一种,拿到的这枚印章种类手里已经有了,此时 dp[i][j] 表示手里已经有 j 种印章了,因此上个状态也只有 j 种印章,即 dp[i-1][j] 。dp[i][j] = dp[i-1][j] * j/n;第二种,拿到的这枚印章种类手里还没有,所以上个状态表示为dp[i-1][j-1],一共有n种印章,前面已经取了(j-1)种,而现在取的和前面没有重复,因此取的是n-(j-1)中的其中一个。
3)代码实现
n,m = map(int,input().split())
dp = [[0 for i in range(n+1)]for i in range(m+1)]
for i in range(1,m+1):
for j in range(1,n+1):
if (i < j):
dp[i][j] = 0
elif (j == 1):
dp[i][j] = (1/n)**(i-1)
else:
dp[i][j] = (dp[i-1][j])*(j*1.0/n) + (dp[i-1][j-1])*((n-j+1)*1.0/n)
print("%.4f"%(dp[m][n]))
拿金币(蓝桥杯6.28)
问题描述:有一个N x N的方格,每一个格子都有一些金币,只要站在格子里就能拿到里面的金币。你站在最左上角的格子里,每次可以从一个格子走到它右边或下边的格子里。请问如何走才能拿到最多的金币。
输入格式:第一行输入一个正整数n。以下n行描述该方格。金币数保证是不超过1000的正整数。
输出格式:最多能拿金币数量。
思考1:
n*n的方格应该用一个数组表示,dp[i][j]表示i行j列的金币数量,想要求出能拿金币最多的数量,每次移动做选择的时候,比较右边还是下边的金币数量大,哪边大就往哪边移动,直到不能移动为止(没有向右也没有向下的选择,即边界处)
思考2:
状态转移方程
dp[i][j] = max((dp[i][j-1]+dp[i][j]),(dp[i-1][j]+dp[i][j]))
代码
n = int(input())
rect = []
for i in range(n):
rect.append(list(map(int, input().split())))
#将边界的数单独算出来
for i in range(1,n):
rect[0][i] =rect[0][i] + rect[0][i-1]
for i in range(1,n):
rect[i][0] = rect[i][0] + rect[i-1][0]
for i in range(1,n):
for j in range(1,n):
rect[i][j] = max(rect[i][j-1],rect[i-1][j]) + rect[i][j]
print(rect[-1][-1])
最长回文子串(力扣6.29)
思考1:
1)关于判断是否为回文数,之前写过博客,可直接s==s[::-1]
。
2)字串可通过双循环得到
3)回文字串的长度最大!?
但这种判断肯定复杂度很高,而且这道题是动态规划那一p的,所以这个思路应该不太对。
思考2:
1)s[0…j]是回文子串则s[1…j-1]必定是回文子串,而且s[0]==s[j]
2)dp[i][j]为“s[i…j]是否为回文字符串”,如果是回文字符串,则dp[i][j]=true,且dp[i][j]=dp[i+1][j-1] && s[i]==s[j](状态转移方程)
3)问题边界,有1个字符时,dp=true;有两个字符,即i+1=j时,如果s[i]==s[j],则dp[i][j]=true
代码如下:
class Solution:
def longestPalindrome(self, s: str) -> str:
n = len(s)
if n < 2:
return s
max_len = 1
begin = 0
# dp[i][j] 表示 s[i..j] 是否是回文串
dp = [[False] * n for _ in range(n)]
for i in range(n):
dp[i][i] = True
# 递推开始
# 先枚举子串长度
for L in range(2, n + 1):
# 枚举左边界,左边界的上限设置可以宽松一些
for i in range(n):
# 由 L 和 i 可以确定右边界,即 j - i + 1 = L 得
j = L + i - 1
# 如果右边界越界,就可以退出当前循环
if j >= n:
break
if s[i] != s[j]:
dp[i][j] = False
else:
if j - i < 3:
dp[i][j] = True
else:
dp[i][j] = dp[i + 1][j - 1]
# 只要 dp[i][L] == true 成立,就表示子串 s[i..L] 是回文,此时记录回文长度和起始位置
if dp[i][j] and j - i + 1 > max_len:
max_len = j - i + 1
begin = i
return s[begin:begin + max_len]
最大子数组和(力扣6.30)
思考1:
dp[i][j]表示nums中从 i 到 j 位置的和。
dp[i][i]表示nums本身
状态转移方程:
i < j 时,dp[i][j] = dp[i][j-1] + nums[j]
返回dp中最大的值。
思考2:
二维数组表示最大值反而复杂了,一维数组即可。
dp[i]:表示以 nums[i] 结尾的连续子数组的最大和。
如果dp[i - 1] > 0,那么可以把nums[i]直接接在dp[i - 1]表示的那个数组的后面,得到和更大的连续子数组;
如果dp[i - 1] <= 0,那么nums[i]加上前面的数dp[i - 1]以后值不会变大。此时单独的一个nums[i]的值,就是dp[i]。
代码:
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
n = len(nums)
dp = [0] * n
dp[0] = nums[0]
for i in range(1,n):
dp[i] = max(dp[i-1]+nums[i],nums[i])
return max(dp)
爬楼梯(力扣7.1)
思考1:
设置状态和边界条件:
dp[i]表示到i阶有dp[i]种方法;dp[1]=1,dp[2] = 2,dp[3]=3
状态转移方程:
dp[i] = dp[i-1] + dp[i-2]
class Solution:
def climbStairs(self, n: int) -> int:
dp = [0] * n
dp[0] = 1
ways = 0
if n == 1:
ways = 1
elif n == 2:
ways = 2
else:
dp[1] = 2
for i in range(2,n):
dp[i] = dp[i-1] + dp[i-2]
ways = dp[n-1]
return ways
杨辉三角(力扣7.2)
思考:
二维数组dp[i][j]表示第i行第j列的数字,每行的第一个和最后一个数字都是1。
状态转移方程:
当j=0或i时,dp[][]=1
其他情况下,dp[i][j] = dp[i-1][j-1] + dp[i-1][j]
二维数组的创建?!
class Solution:
def generate(self, numRows: int) -> List[List[int]]:
dp = [[0] * i for i in range(1,numRows+1)]
dp[0][0] = 1
if numRows==1:
dp = [[1]]
elif numRows==2:
dp = [[1],[1,1]]
else:
for i in range(numRows):
for j in range(i+1):
if (j==0)or(j==i):
dp[i][j] = 1
else:
dp[i][j] = dp[i-1][j-1] + dp[i-1][j]
return dp
官方答案:
class Solution:
def generate(self, numRows: int) -> List[List[int]]:
ret = list()
for i in range(numRows):
row = list()
for j in range(0, i + 1):
if j == 0 or j == i:
row.append(1)
else:
row.append(ret[i - 1][j] + ret[i - 1][j - 1])
ret.append(row)
return ret
买卖股票的最佳时机(力扣7.3)
思考2:
法一:暴力求解法
前i天的最大收益 = max{前i-1天的最大收益,第i天的价格-前i-1天中的最小价格}
直接max(prices[j] - prices[i])
prices = [7,1,5,3,6,4]
n = len(prices)
ans = 0
for i in range(n):
for j in range(i+1,n):
ans = max(ans,prices[j]-prices[i])
print(ans)
法二:
(有地方不是很懂)
找出最低点买入,最高点卖出
max(prices) - min(prices)
class Solution:
def maxProfit(self, prices: List[int]) -> int:
inf = int(1e9)
minprice = inf
maxprofit = 0
for price in prices:
maxprofit = max(price - minprice, maxprofit)
minprice = min(price, minprice)
return maxprofit
比特位计数(力扣7.4)
判断子序列(力扣7.5)
思考1:
dp[i]表示s中到第i个子序列是否为t的子序列。
t[i] = s[i] in t
dp[i] = dp[i-1] and t[i]?
思考2:
方法一(官方答案)
初始化两个指针i 和 j,分别指向 s 和 t 的初始位置。每次贪心地匹配,匹配成功则 i 和 j 同时右移,匹配 s 的下一个位置,匹配失败则 j 右移,i不变,尝试用 t 的下一个字符匹配 s。最终如果 i 移动到 s 的末尾,就说明 s 是 t 的子序列。
class Solution:
def isSubsequence(self, s: str, t: str) -> bool:
n, m = len(s), len(t)
i = j = 0
while i < n and j < m:
if s[i] == t[j]:
i += 1
j += 1
return i == n
斐波那契数(力扣7.6)
class Solution:
def fib(self, n: int) -> int:
dp = [0] * (n+1)
if n < 2:
return n
dp[0] = 0
dp[1] = 1
for i in range(2,n+1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
官方
class Solution:
def fib(self, n: int) -> int:
if n < 2:
return n
p, q, r = 0, 0, 1
for i in range(2, n + 1):
p, q = q, r
r = p + q
return r
使用最小花费爬楼梯(力扣7.7)
思考1:
dp[i]表示达到下标i的最小花费。
dp[i] = min(dp[i-1]+cost[i-1], dp[i-2] + cost[i-2])
class Solution:
def minCostClimbingStairs(self, cost: List[int]) -> int:
n = len(cost)
dp = [0] * (n+1)
dp[0] = dp[1] = 0
for i in range(2,n+1):
dp[i] = min(dp[i-1]+cost[i-1], dp[i-2] + cost[i-2])
return dp[n]
除数博弈(力扣7.12)
思考1:
n为奇数的时候,爱丽丝必输;n为偶数的时候,爱丽丝必赢。
思考2:(某网友)
1.将所有的小于等于 N 的解都找出来,基于前面的,递推后面的。
2.如果 i 的约数里面有存在为 False 的(即输掉的情况),则当前 i 应为 True;如果没有,则False。
代码:
class Solution:
def divisorGame(self, N: int) -> bool:
target = [0 for i in range(N+1)]
target[1] = 0 #若爱丽丝抽到1,则爱丽丝输
if N<=1:
return False
else:
target[2] = 1 #若爱丽丝抽到2,则爱丽丝赢
for i in range(3,N+1):
for j in range(1,i//2):
# 若j是i的余数且target[i-j]为假(0)的话,则代表当前为真(1)
if i%j==0 and target[i-j]==0:
target[i] = 1
break
return target[N]==1
第 N 个泰波那契数(力扣7.12)
思考:
跟斐波那契数列差不多,比较简单
class Solution:
def tribonacci(self, n: int) -> int:
if n == 0:
return 0
if 0 < n < 3:
return 1
q = 0
r = 1
t = 1
for i in range(2,n):
p,q,r = q,r,t
t = p+q+r
return t
获取生成数组中的最大值(力扣7.13、简单)
思考:
生成数组的规则已经明确,比较num[i+1]和num[i-1],最大值即为num[i] + max(num[i+1],num[i-1])
分奇偶?
思考2:
直接max(nums)
找出规律,一个nums[i]与nums[i//2]的式子即可表示
代码:
class Solution:
def getMaximumGenerated(self, n: int) -> int:
if n == 0:
return 0
nums = [0] * (n + 1)
nums[1] = 1
for i in range(2, n + 1):
nums[i] = nums[i // 2] + i % 2 * nums[i // 2 + 1]
return max(nums)
括号生成(力扣7.13、中等)
思考:
输入:n=1
输出:[“()”]
输入:n=2
输出:[“(())”,“()()”]
输入:n=3
输出:[“((()))”,“(()())”,“(())()”,“()(())”,“()()()”]
如何生成n=4的输出,大括号往上套,1+1+3+3(重复一个)+4
括号放一侧,2+2+2+2+1
如何更换以前的列表内容!
思考2:
把括号()作为一个整体插入,要比前面的思考方式简单,用集合还可以直接去重
代码:
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
if n == 1:
return list({'()'})
res = set()
for i in self.generateParenthesis(n - 1):
for j in range(len(i) + 2):
res.add(i[0:j] + '()' + i[j:])
return list(res)
跳跃游戏(力扣7.14 难度中等)
思考1:
数组的所有和等于数组的长度len
思考2:
如果x+nums[x]>=y,那么位置y可以到达。
可以用 x + nums[x] 更新 最远可以到达的位置
如果最远可以到达的位置>=数组的最后一个位置,那么就返回true
官方代码:
class Solution:
def canJump(self, nums: List[int]) -> bool:
n, rightmost = len(nums), 0
for i in range(n):
if i <= rightmost:
rightmost = max(rightmost, i + nums[i])
if rightmost >= n - 1:
return True
return False
跳跃游戏 II(力扣7.14 难度中等)
思考1:
直接在前一题的基础上改,但注意的是不访问最后一个元素。
代码:
class Solution:
def jump(self, nums: List[int]) -> int:
n, rightmost = len(nums), 0
end = 0
j = 0
for i in range(n-1):
if i <= rightmost:
rightmost = max(rightmost, i + nums[i])
if i == end:
end = rightmost
j += 1
return j
不同路径(力扣7.15 难度中等)
思考1:
二维数组dp[i][j]表示到达改位置最多的路径,dp[0][0]到达dp[m-1][n-1],边界条件,i<m,j<n 而且dp[0][j]或者dp[i][0]都是1。
状态转移dp[i][j]=dp[i][j-1]+dp[i-1][j]过来的
其实也就是杨辉三角
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
dp = [[1 for i in range(n+2)]for i in range(m+2)]
for i in range(2,m+1):
for j in range(2,n+1):
dp[i][j] = dp[i][j-1] + dp[i-1][j]
return dp[m][n]
不同路径2(力扣7.15 难度中等)
最小路径和(7.16)
跟蓝桥杯拿金币是一摸一样的。
class Solution:
def minPathSum(self, grid: List[List[int]]) -> int:
m = len(grid)
n = len(grid[0])
for i in range(1,n):
grid[0][i] = grid[0][i-1] + grid[0][i]
for j in range(1,m):
grid[j][0] = grid[j-1][0] + grid[j][0]
for i in range(1,m):
for j in range(1,n):
grid[i][j] = grid[i][j] + min(grid[i-1][j],grid[i][j-1])
return grid[-1][-1]
解码方法(7.16)
由于只需要输出方法的总数,所以应该并不需要去做数字和字母的一一映射,只需要把数字拆分,看可以有几种拆分方法,拆分出来的数字只能在1-26之间。
dp[i]表示到i位置有几种拆分方法,状态转移dp[i] = dp[i-1] * ds[i]。
ds[i]如果不是最后一个位置,则当为0时,ds[i]为1,当为1时,ds[i]为2,当为2时,判断i+1位置的数字是否为0到6之间,是的话,ds[i]为2。
class Solution:
def numDecodings(self, s: str) -> int:
n = len(s)
f = [1] + [0] * n
for i in range(1, n + 1):
if s[i - 1] != '0':
f[i] += f[i - 1]
if i > 1 and s[i - 2] != '0' and int(s[i-2:i]) <= 26:
f[i] += f[i - 2]
return f[n]
三角形最小路径和(7.19)
思考1:
dp[i]表示到第i行的最小路径和,假设第i行的最小值取在(i,j)dp[i+1] = dp[i] + min(triangle[i+1][j],triangle[i+1][j+1])
自己写的代码:
class Solution:
def minimumTotal(self, triangle: List[List[int]]) -> int:
n = len(triangle)
dp =[0] * n
dp[0] = triangle[0]
for i in range(1,n):
j = (triangle[i-1]).index(min(triangle[i-1]))
dp[i] = dp[i-1] + min(triangle[i][j],triangle[i][j+1])
return dp[n-1]
上面的代码思路有两个很大的问题:
一个是在求j的时候,忽略了j应该是上一行最小值的相邻结点。
另一个就是题目是让求最小路径和,每行最小值再求和这种思路不对。
思考2:
看完一个博主的讲解视频后,发现自己前面的思路很有问题。
边界条件:
当j=0的时候,triangle[i][j] = triangle[i][j] + triangle[i-1][j]
当j=i的时候,triangle[i][j] = triangle[i][j] + triangle[i-1][j-1]
其他时候,triangle[i][j] = triangle[i][j] + min(triangle[i-1][j],triangle[i-1][j-1])
class Solution:
def minimumTotal(self, triangle: List[List[int]]) -> int:
n = len(triangle)
if n == 0:
return 0
for i in range(1,n):
for j in range(i+1):
if j == 0:
triangle[i][j] = triangle[i][j] + triangle[i-1][j]
elif j == i:
triangle[i][j] = triangle[i][j] + triangle[i-1][j-1]
else:
triangle[i][j] = triangle[i][j] + min(triangle[i-1][j],triangle[i-1][j-1])
return min(triangle[n-1])
单词拆分(7.20)
某博主的代码
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
n=len(s)
dp=[False]*(n+1)
dp[0]=True
for i in range(n):
for j in range(i+1,n+1):
if(dp[i] and (s[i:j] in wordDict)):
dp[j]=True
return dp[-1]