1 题目
通过测试
def minStickers1(stickers, target):
n = len(stickers)
maps = [[0] * 26 for _ in range(n)] # [第一个贴纸][字母出现次数] 0下标对应a,1下标对应b [2][25]
for i in range(n): # 第i个贴纸
s = list(stickers[i])
for j in s: # 每个字符
maps[i][ord(j) - ord('a')] += 1
dp = {}
dp[""] = 0 # 空字符的时候,返回0张贴纸
return process1(dp, maps, target)
# dp 缓存,如果目标t已经算过了,直接返回dp中的值
# t 剩余的目标
# 0...N 每一个字符串所含字符的词频统计
def process1(dp, maps, rest):
if rest in dp: # 已经算过答案就直接拿
return dp[rest]
# 如果没有算过,以下是正式的递归调用过程
ans = float("inf") # 设置为无穷大,ans是搞定rest使用的最少的贴纸数量
n = len(maps) # 贴纸数量,n种贴纸
tmap = [0] * 26 # tmap取代替rest
target = list(rest)
for c in target: # target是目标字符串,tmap指定位置词频
tmap[ord(c) - ord('a')] += 1 # 要组成字符的词频
# map代表所有贴纸 ->去搞定 tmap代表剩余字符 这两全变成了词频统计的形式
# 枚举当前第一张贴纸是谁
for i in range(n):
if maps[i][ord(target[0]) - ord('a')] == 0: # 这里有错误
continue
sb = []
for j in range(26):
if tmap[j] > 0: # j这个字符是target需要的
for k in range(max(0, tmap[j] - maps[i][j])): # 剩余多少,在sb里添多少个作为剩余
sb.append(chr(ord('a') + j))
s = ''.join(sb)
tmp = process1(dp, maps, s)
if tmp != -1:
ans = min(ans, 1 + tmp)
dp[rest] = -1 if ans == float("inf") else ans
return dp[rest]
arr = ['aaaa', 'bbaa', 'ccddd']
s = "abcccccdddddbbbaaaaa"
print(minStickers1(arr, s))
2 关于递归一些问题
- 什么暴力递归可以继续优化?
- 有重复调用同一个子问题的解,这种递归可以优化
- 如果每一个子问题都是不同的解,无法优化也不用优化
- 暴力递归和动态规划的关系
- 某一个暴力递归,有解的重复调用,就可以把这个暴力递归优化成动态规划
- 任何动态规划问题,都一定对应着某一个有解的重复调用的暴力递归,但不是所有的暴力递归,都一定对应着动态规划
- 如何找到某个问题的动态规划方式?
- 设计暴力递归:重要原则+4种常见模型
- 分析有没有重复解:套路解决
- 用记忆化搜索->用严格表结构实现动态规划:套路解决
- 看看能否继续优化:套路解决
- 设计暴力递归过程的原则:
- 每一个可变参数的类型,一定不要比int类型更加复杂
- 1可以违反,让类型突破到一维线性结构,那必须是唯一可变参数
- 如果发现原则1被违反,但不违反原则2,只需要做到记忆化搜索即可
- 可变参数的个数,能少则少
- 设计暴力递归过程的原则:
- 一定要b自己找到不违反原则情况下的暴力尝试
- 如果你找到的暴力尝试,不符合原则,马上舍弃,找新的
- 5%才有可能突破原则
- 常见的4种尝试模型:
- 从左到右的尝试模型->从左向右考虑每个位置要还是不要->数字字符串转字母,背包问题
- 范围上的尝试模型->纸牌赢
- 多样本位置全对应的尝试模型
- 寻找业务限制的尝试模型
- 暴力递归到动态规划的套路:
- 你已经有了一个不违反原则的暴力递归,而且的确存在解的重复调用
- 找到哪些参数的变化会影响返回值,对每一个列出变化范围
- 参数间的所有的组合数量,意味着表大小
- 记忆化搜索的方法就是缓存
- 规定好严格表的大小 ,分析位置依赖顺序,然后从基础填写到最终解
- 对于有枚举行为的决策过程,进一步优化
- 动态规划进一步优化:
- 空间压缩
- 状态化简
- 四边形不等式
3 多样本位置全对应的尝试模型
题目:两个字符串的最长公共子序列问题
中间可以不连续
任何一个格子来源于左上角的格子
可能性2,有可能值来自左边
可能性3,值来自上方
可能性4,前面的值+1
# 一个样本做行,一个样本做列的模型
def lcse(str1,str2):
dp = [[0] * len(str2) for _ in range(len(str1))]
dp[0][0] = 1 if str1[0] == str2[0] else 0
for i in range(1,len(str1)):
# 从第一行开始赋值,如果左边是0,那么字母相等就赋值1,如果前面已经赋值过了,就写前面的值
dp[i][0] = max(dp[i - 1][0],1 if str1[i] == str2[0] else 0)
for j in range(1, len(str2)):
# 从列开始填
dp[0][j] = max(dp[0][j - 1], 1 if str1[0] == str2[j] else 0)
for i in range(1,len(str1)):
for j in range(1,len(str2)):
# 可能性2和可能性3
dp[i][j] = max(dp[i - 1][j],dp[i][j - 1])
# 观察法,观察位置得出的结论
if(str1[i] == str2[j]):
dp[i][j] = max(dp[i][j],dp[i - 1][j - 1] + 1)
return dp[len(str1) - 1][len(str2) - 1]
4 寻找业务限制的尝试模型
# a 洗一杯的时间 固定变量
# b 自己挥发干净的时间 固定变量
# drinks 每一个员工喝完的时间 固定变量
# drinks[0....index-1]都已经干净了,不用操心了
# drinks[index...]都想变干净,这是我要操心的
# washLine 表示洗的机器何时可用
# drinks[index...]变干净,最少的时间点返回
# 主函数调用 process(drinks,3,10,0,0) 3时间洗完,10挥发,从0开始洗,机器从0时可用
def process(drinks,a,b,index,washLine): # 从左向右尝试模型
if index == len(drinks) - 1:
return min(
max(washLine,drinks[index]) + a # 即使机器空着,也得等员工喝完咖啡才能洗
,drinks[index] + b
) # 找花费最少时间
# 剩不止一杯咖啡
# wash是我当前的一杯咖啡杯,洗完的时间
wash = max(washLine,drinks[index]) + a # 洗,index一杯,结束的时间点
# index+1...变干净的最早时间,后面的杯子也得洗完
next1 = process(drinks,a,b,index + 1,wash)
# index...
p1 = max(wash,next1) # 既要自己洗完,其他也要洗完
# 可能性二,就决定挥发了
dry = drinks[index] + b # 挥发,index一杯,结束的时间点
next2 = process(drinks,a,b,index + 1,washLine) # 挥发过程,机器还空着,可以送去洗咖啡杯
p2 = max(dry,next2)
return min(p1,p2)
arr = [1,1,5,5,7,10,12,12,12,12,12,15]
a = 3
b = 10
print(process(arr,a,b,0,0))
改动态规划
def dp(drinks,a,b):
if a >= b: # 挥发时间比洗的时间短,不用洗了
return drinks[len(drinks - 1)] + b
# a < b
limit = 0 # 咖啡机什么时间可用
N = len(drinks)
for i in range(len(drinks)):
limit = max(limit,drinks[i] + a)
dp = [[0] * (limit + 1) for _ in range(N)]
# N - 1,所有的值
for washLine in range(limit + 1):
dp[N - 1][washLine] = min(
max(washLine, drinks[N - 1]) + a # 即使机器空着,也得等员工喝完咖啡才能洗
, drinks[N - 1] + b
# 找花费最少时间
)
for index in range(N - 2,-1,-1):
for washLine in range(limit + 1):
# dp[index][washLine] = ?
p1 = float("inf")
wash = max(washLine,drinks[index]) + a
if wash <= limit:
p1 = max(wash,dp[index + 1][wash])
p2 = max(drinks[index] + b,dp[index + 1][washLine])
dp[index][washLine] = min(p1,p2)
return dp[0][0]
arr = [1,1,5,5,7,10,12,12,12,12,12,15]
a = 3
b = 10
print(dp(arr,a,b))
6 练习题目
象棋,马从(0,0)处走,走向(x,y)的位置一共有多少种方法?
def ways(x,y,k): # (x,y)位置,k是马只能走几步
return f(x,y,k)
def f(x,y,k):
if k == 0: # 如果只能走0步,走到(0,0)是一种方法,原地走是0种方法
return 1 if x == 0 and y == 0 else 0
if x < 0 or x > 9 or y < 0 or y > 8: # 越界
return 0
# 有步数要走,x,y也是棋盘上的位置 下面递归见图,马可以走8个点
return f(x + 2,y - 1,k - 1) + f(x + 2, y + 1, k - 1) + f(x + 1, y + 2, k - 1) + f(x - 1, y + 2, k - 1) + f(x - 2, y + 1, k - 1) + f(x - 2, y - 1, k - 1) + f(x - 1, y - 2, k - 1) + f(x + 1, y - 2, k - 1)
x = 2
y = 3
k = 3
print(ways(x,y,k))
改动态规划
三个可变参数递归,把所有返回值装到一张表,就是三维表
把所有可变参数组合都列全了,把缓存结构化了就是动态规划
def ways(x,y,k): # (x,y)位置,k是马只能走几步
dp = [[[0] * (k + 1) for _ in range(9)] for _ in range(10)] # x范围0-9,给它10个空间,给k个空间是因为,k只会减小
dp[0][0][0] = 1 # 其余位置都是0
for level in range(1,k + 1):
# level 层
for i in range(10): # x可能性
for j in range(9): # y的可能性
dp[i][j][level] = getValue(dp,i + 2,j - 1,level - 1) + getValue(dp,i + 2, j + 1, level - 1) + getValue(dp,i + 1, j + 2, level - 1) + getValue(dp,i - 1, j + 2, level - 1) + getValue(dp,i - 2, j + 1, level - 1) + getValue(dp,i - 2, j - 1, level - 1) + getValue(dp,i - 1, j - 2, level - 1) + getValue(dp,i + 1, j - 2, level - 1)
return dp[x][y][k]
def getValue(dp,x,y,k):
if x < 0 or x > 9 or y < 0 or y > 8: # 越界
return 0
return dp[x][y][k]
x = 2
y = 3
k = 3
print(ways(x,y,k))
7 斐波那契数列
从左往右尝试模型
改为矩阵乘法
def f(n):
if n < 1:
return 0
if n == 1 or n == 2:
return 1
# [1,1]
# [1,0]
base = [[1,1],[1,0]]
res = matrixPower(base,n - 2) # 矩阵平方
return res[0][0] + res[1][0]
def matrixPower(m,p): # 求矩阵的p次方
res = [[0] * len(m[0]) for _ in range(len(m))]
for i in range(len(res)):
res[i][i] = 1 # 单位矩阵
tmp = m # 矩阵1次方
while p != 0:
if (p & 1) != 0: # 二进制向右移动
res = muliMateix(res,tmp)
tmp = muliMateix(tmp,tmp)
p >>= 1
return res
def muliMateix(m1,m2): # 两个矩阵相乘
res = [[0] * len(m2[0]) for _ in range(len(m1))]
for i in range(len(m1)):
for j in range(len(m2[0])):
for k in range(len(m2)):
res[i][j] += m1[i][k] * m2[k][j]
return res
print(f(19))
7.1 奶牛问题
7.2 达标字符串
给定一个数N,想象只由0和1两种字符,组成的所有长度为N的字符串,如果某个字符串,任何0字符的左边都有1紧挨着,认为这个字符串达标,返回有多少达标字符串?
# 类斐波那契
def getnum1(n):
if n < 1:
return 0
return process(1,n) # 第0位置固定为1了
def process(i,n):
if i == n - 1:
return 2
if i == n:
return 1
return process(i+1,n)+process(i+2,n)
def getnum2(n):
if n < 1:
return 0
if n == 1:
return 1
pre = 1
cur = 1
tmp = 0
for i in range(2,n + 1):
tmp = cur
cur += pre
pre = tmp
return cur
def getnum3(n):
if n < 1:
return 0
if n == 1 or n == 2:
return n
base = [[1,1],[1,0]]
res = matrixPower(base,n - 2)
return 2 * res[0][0] + res[1][0]
def matrixPower(m,p): # 求矩阵的p次方
res = [[0] * len(m[0]) for _ in range(len(m))]
for i in range(len(res)):
res[i][i] = 1 # 单位矩阵
tmp = m # 矩阵1次方
while p != 0:
if (p & 1) != 0: # 二进制向右移动
res = muliMateix(res,tmp)
tmp = muliMateix(tmp,tmp)
p >>= 1
return res
def muliMateix(m1,m2): # 两个矩阵相乘
res = [[0] * len(m2[0]) for _ in range(len(m1))]
for i in range(len(m1)):
for j in range(len(m2[0])):
for k in range(len(m2)):
res[i][j] += m1[i][k] * m2[k][j]
return res
print(getnum1(5))
print(getnum2(5))
print(getnum3(5))
7.3 瓷砖排放问题
用斐波那契方法:严格没有条件转移表达式