# 动态规划经典问题整理

5 篇文章 0 订阅
1 篇文章 0 订阅

#### 单序列

1、最长递增子序列

for i in range(1, n):
for j in range(0, i):
if nums[j] < nums[i]:
dp[i] = max(dp[i], dp[j] + 1)
if dp[i] > maxl:
maxl = dp[i]

LIS = [nums[0]]
for num in nums[1:]:
if num > LIS[-1]:
LIS.append(num)
else:
index = bisect_left(LIS, num)
LIS[index] = num
return len(LIS)

如果求单调不递增的子序列还需要num >= LIS[-1] 并且使用bisect_right，见使数组 K 递增的最少操作次数

nums = [-float('inf')] + nums + [float('inf')]
n = len(nums)
dp = [0] * n
for i in range(0, n - 1):
for j in range(i + 1, n):
if nums[j] > nums[i]:
dp[j] = max(dp[i] + 1, dp[j])

path = [1] + [0] * (n - 1)
for i in range(1, n):
for j in range(0, i):
if dp[i] == dp[j] + 1 and nums[i] > nums[j]:
path[i] += path[j]

return path[-1]

同理，根据1的最优方法，我们可以对2的方法进行优化

3、最长连续递增序列

for i in range(1, n):
if nums[i - 1] < nums[i]:
dp[i] = max(dp[i], dp[i - 1] + 1)
if dp[i] > maxl:
maxl = dp[i]

4、最大子序和

for i in range(1, n):
dp[i] = max(dp[i], dp[i - 1] + nums[i])
if dp[i] > maxl:
maxl = dp[i]

5、最大整除子集

for i in range(n):
for j in range(i + 1, n):
if nums[j] % nums[i] == 0:
dp[j] = max(dp[j], dp[i] + 1)

6、最长定差子序列

d = {}
for a in arr:
d[a] = d.get(a - difference, 0) + 1
return max(d.values())

7、爬楼梯

for i in range(2, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
for i in range(2, n):
dp[i] = (i - 1) * (dp[i - 1] + dp[i - 2])
for i in range(2, len(cost) + 1):
dp[i] = min(dp[i - 2] + cost[i - 2], dp[i - 1] + cost[i - 1])

10、比特位计数

for i in range(1, n + 1):
if i & 1 == 0:
dp[i] = dp[i >> 1]
else:
dp[i] = dp[i - 1] + 1

11、旋转数字

class Solution:
def rotatedDigits(self, N: int) -> int:
ans, dp = 0, [0, 0, 1, -1, -1, 1, 1, -1, 0, 1] + [0] * (N - 9)
for i in range(N + 1):
dp[i] = -1 in (dp[i // 10], dp[i % 10]) and -1 or dp[i // 10] | dp[i % 10]
ans += dp[i] == 1
return ans

12、打家劫舍 II

a, b = nums[0], max(nums[0], nums[1])
for i in range(2, len(nums) - 1):
a = max(a + nums[i], b)
b, a = a, b
res1 = b
a, b = nums[1], max(nums[1], nums[2])
for i in range(3, len(nums)):
a = max(a + nums[i], b)
b, a = a, b
return max(b, res1)

13、解决智力问题

n = len(questions)
dp = [0] * (n + 1)
for i, q in enumerate(questions):
dp[i + 1] = max(dp[i + 1], dp[i])
j = min(i + q[1] + 1, n)
dp[j] = max(dp[j], dp[i] + q[0])
return dp[n]

14、栅栏涂色

if n == 1: return k
dp = [0] * (n + 1)
dp[1], dp[2] = k, k * k
for i in range(3, n + 1):
dp[i] = dp[i - 1] * (k - 1) + dp[i - 2] * (k - 1)
return dp[-1]

15、带因子的二叉树

arr.sort()
for i, a in enumerate(arr):
dp[a] = 1
for j in range(i):
if a % arr[j] == 0 and a // arr[j] in set(arr):
dp[a] += dp[arr[j]] * dp[a // arr[j]]

return sum(count for num, count in dp.items())


16、分隔数组以得到最大和

n = len(arr)
dp = [0] * (n + 1)
for i in range(1, n + 1):
maxl = 0
for j in range(i - 1, max(i - k, 0) - 1, - 1):
maxl = max(maxl, arr[j])
dp[i] = max(dp[i], dp[j] + maxl * (i - j))
return dp[-1]

17、可被三整除的最大和

dp = [0] * div
for n in nums:
nw = [0] * div
for d in range(div):
nw[d] = dp[d] + n
for t in nw:
dp[t % div] = max(dp[t % div], t)
return dp[0]

18、检查数组是否存在有效划分

dp = [True] + [False] * len(nums)
for i in range(1, len(nums)):
dp[i + 1] |= dp[i - 1] and nums[i] == nums[i - 1]
dp[i + 1] |= dp[i - 2] and nums[i] == nums[i - 1] and nums[i] == nums[i - 2]
dp[i + 1] |= dp[i - 2] and nums[i] - nums[i - 1] == 1 and nums[i - 1] - nums[i - 2] == 1
return dp[-1]

19、活字印刷 （动态规划 + 组合数学）

dp, cv, alllen = [1] + [0] * len(tiles), Counter(tiles).values(), 0
for v in cv:
alllen += v
for i in range(alllen, 0, -1):
dp[i] += sum(dp[i - j] * math.comb(i, j) for j in range(1, min(i,v) + 1))
return sum(dp) - 1

20、填充书架

for i in range(1, len(books) + 1):
tmp_width, j, h = 0, i, 0
while j > 0:
tmp_width += books[j - 1][0]
if tmp_width > shelf_width:
break
h = max(h, books[j - 1][1])
dp[i] = min(dp[i], dp[j - 1] + h)
j -= 1
return dp[-1]

21、统计特殊子序列的数目

class Solution:
def countSpecialSubsequences(self, nums: List[int]) -> int:
mod = 10**9 + 7
f0 = f1 = f2 = 0
for num in nums:
if num == 0:
f0 = (f0 * 2 + 1) % mod
elif num == 1:
f1 = (f1 * 2 + f0) % mod
else:
f2 = (f2 * 2 + f1) % mod
return f2

#### 双序列

1、最长公共子序列

for i in range(1, len(text1) + 1):
for j in range(1, len(text2) + 1):
if text1[i - 1] == text2[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])

2、 不相交的线

for i in range(1, len(nums1) + 1):
for j in range(1, len(nums2) + 1):
if nums1[i - 1] == nums2[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
else:
dp[i][j] = max(dp[i][j - 1], dp[i - 1][j])

3、最长重复子数组

m, n = len(nums1), len(nums2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
maxl = 0
for i in range(1, m + 1):
for j in range(1, n + 1):
if nums1[i - 1] == nums2[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
if dp[i][j] > maxl:
maxl = dp[i][j]
return maxl

m, n = len(nums1), len(nums2)
maxl = 0
for d in range(1 - m, n):
l = 0
for i in range(max(0, -d), min(m, n - d)):
if nums1[i] == nums2[i + d]:
l += 1
maxl = max(maxl, l)
else: l = 0
return maxl

还可以使用二分查找+哈希的方法使得时间复杂度降为O((m + n) *log(min(m, n)))

4、 通配符匹配

for i in range(1, len(p) + 1):
for j in range(1, len(s) + 1):
if p[i - 1] == '?':
dp[i][j] = dp[i - 1][j - 1]
elif p[i - 1] == '*':
dp[i][j] = dp[i - 1][j] or dp[i - 1][j - 1] or dp[i][j - 1]
elif p[i - 1] == s[j - 1]:
dp[i][j] = dp[i - 1][j - 1]
else:
dp[i][j] = False

5、正则表达式匹配

for i in range(1, len(p) + 1):
for j in range(1, len(s) + 1):
if p[i - 1] == s[j - 1] or p[i - 1] == '.':
dp[i][j] = dp[i - 1][j - 1]
elif p[i - 1] == '*':
if p[i - 2] == s[j - 1] or p[i - 2] == '.':
dp[i][j] = dp[i - 2][j] or dp[i][j - 1]
else:
dp[i][j] = dp[i - 2][j]
else:
dp[i][j] = False

6、最长等差数列

dp = dict()
for cur in range(1, len(nums)):
for prev in range(cur):
diff = nums[cur] - nums[prev]
dp[(cur, diff)] = dp.get((prev, diff), 1) + 1
return max(dp.values())
index = {x: i for i, x in enumerate(A)}
longest = collections.defaultdict(lambda: 2)

ans = 0
for k, z in enumerate(A):
for j in range(k):
i = index.get(z - A[j], None)
if i is not None and i < j:
cand = longest[j, k] = longest[i, j] + 1
ans = max(ans, cand)

return ans if ans >= 3 else 0

8、判断子序列

for i in range(1, len(s) + 1):
for j in range(1, len(t) + 1):
if s[i - 1] == t[j - 1]:
dp[i][j] = dp[i - 1][j - 1]
else:
dp[i][j] = dp[i][j - 1]

9、子序列的数目

for i in range(1, len(t) + 1):
for j in range(i, len(s) + 1):
if t[i - 1] == s[j - 1]:
dp[j] = dp[i - 1][j - 1] + dp[i][j - 1]
else:
dp[j] = dp[i][j - 1]


类似于判断子序列，可以采用双指针进行求解，本题是一个长串匹配多个短串，可以采用动态的方法构造状态转移矩阵，减少长串指针的空跳

m, set_s = len(s), set(s)
f = [defaultdict(lambda: m) for _ in range(m + 1)]

for i in reversed(range(m)):
for c in set_s:
if s[i] == c:
f[i][ord(c) - ord('a')] = i
else:
f[i][ord(c) - ord('a')] = f[i + 1][ord(c) - ord('a')]

res = ""
for t in dictionary:
match = True
j = 0
for i in range(len(t)):
if f[j][ord(t[i]) - ord('a')] == m:
match = False
break
j = f[j][ord(t[i]) - ord('a')] + 1
if match:
if len(t) > len(res) or (len(t) == len(res) and t < res):
res = t
return res

11、交错字符串

for i in range(len(s1) + 1):
for j in range(len(s2) + 1):
if i > 0 and s1[i - 1] == s3[i + j - 1]:
dp[i][j] = dp[i][j] or dp[i - 1][j]
if j > 0 and s2[j - 1] == s3[i + j - 1]:
dp[i][j] = dp[i][j] or dp[i][j - 1]

12、编辑距离

for i in range(1, len(word1)+1):
for j in range(1, len(word2)+1):
if word1[i - 1] == word2[j - 1]:
dp[i][j] = dp[i - 1][j -1]
else:
dp[i][j] = min(dp[i-1][j] + 1, dp[i][j-1] + 1, dp[i-1][j-1] + 1)

13、卖木头块

for i in range(1, m + 1):
for j in range(1, n + 1):
dp[i][j] = mp[i, j]
for k in range(1, j + 1):
dp[i][j] = max(dp[i][j], dp[i][j - k] + dp[i][k])
for l in range(1, i + 1):
dp[i][j] = max(dp[i][j], dp[i - l][j] + dp[l][j])

#### 区间动态规划

1、猜数字大小 II

for j in range(2, n + 1):
for i in reversed(range(1, j)):
f[i][j] = min(max(f[i][x - 1], f[x + 1][j]) + x for x in range(i, j))
return f[1][n]


2、最少回文分割

#求取区间是否是回文串
for l in range(n-1, -1, -1):
for r in range(l + 1, n):
if s[l] == s[r]:
if l + 1 == r:
f[l][r] = True
else:
f[l][r] = f[l + 1][r - 1]
#求取最小分割
for i in range(1, len(s)):
dp[i] = 0 if f[0][i] else i
for j in range(1, i + 1):
if f[j][i]:
dp[i] = min(dp[i], dp[j - 1] + 1)

3、戳气球

for i in reversed(range(n)):
for j in range(i + 2, n + 2):
for k in range(i + 1, j):
val = nums[i] * nums[k] * nums[j]
dp[i][j] = max(dp[i][j], dp[i][k] + dp[k][j] + val)
nums = [int(num) for num in re.split("[+\-*]", input)]
opts = re.sub('[0-9]', "", input)
opt_m = {'+':(lambda x, y:x+ y), '-':(lambda x, y:x - y), '*':(lambda x, y:x * y)}
dp = [[[] for _ in range(len(nums))] for _ in range(len(nums))]

for j in range(len(nums)):
for i in reversed(range(j + 1)):
if i == j:
dp[i][j].append(nums[i])
else:
for k in range(i, j):
dp[i][j].extend([opt_m[opts[k]](x,y) for x, y in product(dp[i][k], dp[k + 1][j])])

5、预测赢家

length = len(nums)
dp = [[0] * length for _ in range(length)]
for i, num in enumerate(nums):
dp[i][i] = num
for i in range(length - 2, -1, -1):
for j in range(i + 1, length):
dp[i][j] = max(nums[i] - dp[i + 1][j], nums[j] - dp[i][j - 1])
return dp[0][length - 1] >= 0
P, N = [0], len(nums)
for x in nums: P.append(P[-1] + x)
def average(i, j):
return (P[j] - P[i]) / float(j - i)

dp = [average(i, N) for i in range(N)]
for k in range(K-1):
for i in range(N):
for j in range(i+1, N):
dp[i] = max(dp[i], average(i, j) + dp[j])

return dp[0]
n = len(A)
dp = [[inf] * n for _ in range(n)]
for i in range(n - 1):
dp[i][i + 1] = 0
for i in reversed(range(n)):
for j in range(i + 2, n):
for k in range(i + 1, j):
dp[i][j] = min(dp[i][j], dp[i][k] + A[i] * A[k] * A[j] + dp[k][j])
return dp[0][-1]

#### 状态转移动态规划

dp0, dp1, ans = arr[0], 0, arr[0]
for i in arr[1:]:
dp1 = max(dp1 + i, dp0)
dp0 = max(dp0 + i, i)
ans = max(ans, dp0, dp1)
return ans
dp0, dp1 = nums[0], max(nums[0], nums[0] ** 2)
maxl = max(dp0, dp1)
for i in range(1, len(nums)):
dp1 = max(dp1 + nums[i], max(dp0, 0) + nums[i] ** 2)
dp0 = max(dp0 + nums[i], nums[i])
maxl = max(maxl, dp0, dp1)
return maxl

3、最大交替子数组和

ans, dp0, dp1 = nums[0], nums[0], -inf
for num in nums[1:]:
dp0, dp1 = max(num, dp1 + num), dp0 - num
ans = max(ans, dp0, dp1)
return ans
#dp1[k] 总计k列，最后一列满的种数
#dp2[k] 总计k列，最后一列差一个满
dp1, dp2 = [0] * (max(3, n + 1)), [0] * (max(3, n + 1))
dp2[1], dp2[2] = 0, 2
dp1[1], dp1[2] = 1, 2
for i in range(3, n + 1):
dp2[i] = dp2[i - 1] + 2 *dp1[i - 2]
dp1[i] = dp1[i - 1] + dp1[i - 2] + dp2[i - 1]
return dp1[n] % (10 ** 9 + 7)

cnt[i-1][j-1]为以s[i-1]t[j-1]结尾的恰好只有一个字符不同的子串对数目，same[i][j]记录以s[i]t[j]结尾的连续相同的字符数量。

class Solution:
def countSubstrings(self, s: str, t: str) -> int:
m, n = len(s), len(t)
dp = [(0, 0)]*(n+1) # 记录 (以s[i]和t[j]结尾的 满足条件的子字符串对数目, 连续相同的位数)
tot = 0   # 总的满足条件的子字符串对数目
for i in range(m):
last = dp[0]
for j in range(n):
if s[i] == t[j]:
cur = (dp[j][0], dp[j][1] + 1)
tot += dp[j][0]
else:
cur = (dp[j][1] + 1, 0)
tot += dp[j][1] + 1
dp[j], last = last, cur
dp[n] = last
return tot
class Solution:
def checkRecord(self, n: int) -> int:
mod = 10 ** 9 + 7
dp = [[0] * 2  for _ in range(3)]
dp[0][0] = 1
for i in range(n):
tp = [[0] * 2  for _ in range(3)]
#以A为结尾
for j in range(3):
tp[0][1] = (tp[0][1] + dp[j][0]) % mod

#以P为结尾
for j in range(3):
for k in range(2):
tp[0][k] = (tp[0][k] + dp[j][k]) % mod

#以L为结尾
for j in range(1, 3):
for k in range(2):
tp[j][k] = (tp[j][k] + dp[j - 1][k]) % mod

dp = tp
return sum([sum(p) for p in dp]) % mod

代码如下：

class Solution:
def checkRecord(self, n: int) -> int:
MOD = 10**9 + 7
mat = [
[1, 1, 0, 1, 0, 0],
[1, 0, 1, 1, 0, 0],
[1, 0, 0, 1, 0, 0],
[0, 0, 0, 1, 1, 0],
[0, 0, 0, 1, 0, 1],
[0, 0, 0, 1, 0, 0],
]

def multiply(a: List[List[int]], b: List[List[int]]) -> List[List[int]]:
rows, columns, temp = len(a), len(b[0]), len(b)
c = [[0] * columns for _ in range(rows)]
for i in range(rows):
for j in range(columns):
for k in range(temp):
c[i][j] += a[i][k] * b[k][j]
c[i][j] %= MOD
return c

def matrixPow(mat: List[List[int]], n: int) -> List[List[int]]:
ret = [[1, 0, 0, 0, 0, 0]]
while n > 0:
if (n & 1) == 1:
ret = multiply(ret, mat)
n >>= 1
mat = multiply(mat, mat)
return ret

res = matrixPow(mat, n)
ans = sum(res[0])
return ans % MOD



#### 背包问题

1、零钱兑换

for i in range(amount + 1):
for coin in coins:
if i - coin >= 0:
dp[i] = min(dp[i], 1 + dp[i - coin])

2、零钱兑换II  （单序列完全背包，正向遍历， 无放入顺序）

for coin in coins:
for i in range(amount + 1):
if i - coin >= 0:
dp[i] = dp[i] + dp[i - coin]
dp = [1] + (total) * [0]
for cost in (cost1, cost2):
for i in range(1, total + 1):
if i - cost >= 0:
dp[i] += dp[i - cost]
return sum(dp)

res = 0
for i in range(total // cost1 + 1):
res += math.ceil((total-cost1 * i) // cost2 + 1)

3、组合总和Ⅳ （单序列完全背包，正向遍历， 有放入顺序）

for i in range(target + 1):
for num in nums:
if i - num >= 0:
dp[i] = dp[i] + dp[i - num]


4、 一和零（双序列01背包，逆向遍历）

for s in strs:
c0, c1 = s.count('0'), s.count('1')
for i in reversed(range(c0, m + 1)):
for j in reversed(range(c1, n + 1)):
dp[i][j] = max(dp[i][j], dp[i - c0][j - c1] + 1)

5、分割等和子集 （单序列01背包）

for i in range(1, len(nums)):
for j in range(1, target + 1):
dp[i][j] = dp[i - 1][j] | dp[i - 1][j - nums[i]]
size, n = 1 << len(nums), len(nums)
dp, s = [False] * size, [0] * size
dp[0] = True
for i in range(size):
if not dp[i]: continue
for j in range(n):
if i & (1 << j) != 0:
continue
next_i = i | (1 << j)
if dp[next_i]: continue
if (s[i] % side) + nums[j] <= side:
s[next_i] = s[i] + nums[j]
dp[next_i] = True
else: break
return dp[-1]

7、完全平方数

for i in range(1, n + 1):
j = 1
while j * j <= n:
if i - j * j >= 0:
dp[i] = min(dp[i - j * j] + 1, dp[i])
j += 1
else:
break

8、目标和

for num in nums:
for j in range((s + t) // 2, num - 1, -1):
dp[j] += dp[j - num]

9、单词拆分

for i in range(len(s)):
for word in wordDict:
if s[i + 1 - len(word): i + 1] == word:
dp[i + 1] = dp[i + 1] or dp[i + 1 - len(word)]

10、分汤（双序列完全背包，正向遍历）

for i in range(1, n + 1):
for j in range(1, n + 1):
dp[i][j] = 0.25 * (dp[max(i - 4, 0)][j] + dp[max(i - 3, 0)][j - 1] +
dp[max(i - 2, 0)][max(j - 2, 0)] + dp[i - 1][max(j - 3, 0)])

#### 路径动态规划

for i in range(0, m):
for j in range(0, n):
if i == 0 or j == 0:
dp[i][j] = matrix[i][j]
elif matrix[i][j] == 1:
dp[i][j] = min(dp[i-1][j-1], dp[i][j-1], dp[i-1][j]) + 1
c += dp[i][j]

2、最大正方形

for i in range(0, m):
for j in range(0, n):
if i == 0 or j == 0:
dp[i][j] = int(matrix[i][j])
elif matrix[i][j] == '1':
dp[i][j] = min(dp[i-1][j-1], dp[i][j-1], dp[i-1][j]) + 1
max_c = max(max_c, dp[i][j])
for i in range(1, len(triangle)):
for j in range(len(triangle[i])):
if j == 0:
triangle[i][j] = triangle[i -1][0] + triangle[i][j]
elif j == len(triangle[i]) - 1:
triangle[i][j] = triangle[i -1][j - 1] + triangle[i][j]
else:
triangle[i][j] = min(triangle[i -1][j - 1], triangle[i -1][j]) + triangle[i][j]

4、最小路径和

for i in range(len(grid)):
for j in range(len(grid[i])):
if i == 0 and j > 0:
dp[j] = dp[j - 1] + grid[i][j]
elif j == 0 and i > 0:
dp[j] = dp[j] + grid[i][j]
elif j > 0 and  i > 0:
dp[j] = min(dp[j - 1] + grid[i][j], dp[j] + grid[i][j])

5、不同路径 II

for i in range(0, m):
for j in range(0, n):
if obstacleGrid[i][j] == 1:
dp[j] = 0
elif i > 0 and j > 0:
dp[j] = dp[j] + dp[j - 1]
elif j > 0 and i == 0:
dp[j] = dp[j - 1]

6、地下城游戏（不同路径的逆向遍历）

m, n = len(dungeon), len(dungeon[0])
dp = [[[0, 0] for _ in range(n)] for _ in range(m)]
for i, j in product(reversed(range(m)), reversed(range(n))):
if i == m - 1 and j == n - 1:
dp[i][j] = min(0, dungeon[i][j])
elif i == m - 1:
dp[i][j] = min(0, dp[i][j + 1] + dungeon[i][j])
elif j == n - 1:
dp[i][j] = min(0, dp[i + 1][j] + dungeon[i][j])
else:
dp[i][j] = min(0, max(dp[i][j + 1], dp[i + 1][j]) + dungeon[i][j])
return -dp[0][0] + 1

7、01 矩阵

for i in range(m):
for j in range(n):
if mat[i][j] == 0:
dist[i][j] = 0
if i > 0:
dist[i][j] = min(dist[i][j], dist[i - 1][j] + 1)
if j > 0:
dist[i][j] = min(dist[i][j], dist[i][j - 1] + 1)
for i in range(m - 1, -1, -1):
for j in range(n - 1, -1, -1):
if i < m - 1:
dist[i][j] = min(dist[i][j], dist[i + 1][j] + 1)
if j < n - 1:
dist[i][j] = min(dist[i][j], dist[i][j + 1] + 1)
for i in range(m):
for j in range(n):
if grid[i][j] == 1:
if j == 0 or j == n - 1:
dp[i][j] = 1
else:
dp[i][j] = min(dp[i - 1][j - 1], dp[i - 1][j], dp[i - 1][j + 1]) + 1
else:
dp[i][j] = 0 

9、出界的路径数

for k in range(maxMove):
for i in range(m):
for j in range(n):
if k == 0:
dp[i][j][k] += 1 if i - 1 < 0 else 0
dp[i][j][k]+= 1 if i + 1 >= m else 0
dp[i][j][k]+= 1 if j - 1 < 0 else 0
dp[i][j][k] += 1 if j + 1 >= n else 0
else:
dp[i][j][k] += dp[i - 1][j][k - 1] if i > 0 else 0
dp[i][j][k] += dp[i + 1][j][k - 1] if i + 1 < m else 0
dp[i][j][k] += dp[i][j - 1][k - 1] if j > 0 else 0
dp[i][j][k] += dp[i][j + 1][k - 1] if j + 1 < n else 0
return sum(dp[startRow][startColumn])

10、矩阵中最长的连续1线段

m, n = len(mat), len(mat[0])
#用dp[i][j][k]记录，k分别表示水平、垂直、对角、反对角四个状态
dp = [[[0]*4 for _ in range(n+2)] for _ in range(m+2)]
ans = 0
for i in range(1, m+1):
for j in range(1, n+1):
if mat[i-1][j-1] == 1:
dp[i][j][0] = dp[i-1][j][0] + 1
dp[i][j][1] = dp[i][j-1][1] + 1
dp[i][j][2] = dp[i-1][j-1][2] + 1
dp[i][j][3] = dp[i-1][j+1][3] + 1
ans = max(ans, max(dp[i][j]))
return ans

11、K 站中转内最便宜的航班

dp = [float('inf') for _ in range(n)]
dp[src] = 0
for i in range(K+1):
tmp = dp[:]
for u, v, w in flights:
dp[v] = min(dp[v],tmp[u] + w)
return dp[dst] if dp[dst] != float('inf') else -1

也可以使用堆的方法进行遍历

graph = collections.defaultdict(dict)
for start,end,cost in flights:
graph[start][end] = cost

queue, v = [(0,0,src)], set()
while queue:
cost, k, pos = heapq.heappop(queue)
if pos == dst: return cost
if (pos, k) in v: continue
if k > K: continue
for nxpos, nxcost in graph[pos].items():
if (nxpos, k + 1) not in v:
heapq.heappush(queue,(cost + nxcost, k + 1, nxpos))
return -1

12、网格图中递增路径的数目

class Solution:
def countPaths(self, grid: List[List[int]]) -> int:
m, n = len(grid), len(grid[0])
dp = [[1 for i in range(n)] for j in range(m)]
s = []
mod = 10 ** 9 + 7
for i in range(m):
for j in range(n):
s.append([grid[i][j], i, j])
s.sort()
while s:
_, i, j = s.pop()#从尾部弹出则是从大到小遍历
for x, y in [[i + 1, j], [i - 1, j], [i, j + 1], [i, j - 1]]:
if 0 <= x < m and 0 <= y < n and grid[x][y] > _:
dp[i][j] += dp[x][y]
dp[i][j] %= mod
return sum(sum(i) for i in dp) % mod

class Solution:
def countPaths(self, grid: List[List[int]]) -> int:
m, n = len(grid), len(grid[0])
@cache
def dfs(i, j):
res = 1
for dx, dy in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
ni, nj = i + dx, j + dy
if 0 <= ni < m and 0 <= nj < n and grid[ni][nj] > grid[i][j]:
res = (res + dfs(ni, nj)) % (10 ** 9 + 7)
return res
res = 0
for i, j in product(range(m), range(n)):
res = (res + dfs(i, j)) % (10 ** 9 + 7)
return res

注：有些问题只适合于记忆化深搜，例如：将整数按权重排序，如果利用dp的方法，如果要通过所有测试用例，dp长度至少为300 * 最高值 + 3， 而且运行时间远大于记忆化深搜。

#### 决策动态规划

# dp[i][kk][0]表示第i天第k笔交易不持有股票, dp[i][kk][1]表示第i天第k笔交易持有股票
for i in range(1, n):
for kk in range(1, k + 1):
dp[i][kk][0] = max(dp[i-1][kk][0], dp[i-1][kk][1] + prices[i])
dp[i][kk][1] = max(dp[i-1][kk][1], dp[i-1][kk-1][0] - prices[i])
return dp[n-1][k][0]
#a0最后一位为0最少需要翻转次数，a1最后一位为1最少需要翻转次数
for c in s:
if c == '0':
a1 = min(a0, a1) + 1
else:
a1 = min(a0, a1)
a0 = a0 + 1


3、乘积最大子数组

maxF, minF, ans = nums[0], nums[0], nums[0]
length = len(nums)
for i in range(1, length):
mx, mn = maxF, minF # 只用两个变量来维护i−1时刻的状态,优化空间
maxF = max(mx * nums[i], nums[i], mn * nums[i])
minF = min(mn * nums[i], nums[i], mx * nums[i])
ans = max(maxF, ans)
return ans
class Solution:
def getMaxLen(self, nums: List[int]) -> int:
length = len(nums)
positive, negative = [0] * length, [0] * length
if nums[0] > 0:
positive[0] = 1
elif nums[0] < 0:
negative[0] = 1

maxLength = positive[0]
for i in range(1, length):
if nums[i] > 0:
positive[i] = positive[i - 1] + 1
negative[i] = (negative[i - 1] + 1 if negative[i - 1] > 0 else 0)
elif nums[i] < 0:
positive[i] = (negative[i - 1] + 1 if negative[i - 1] > 0 else 0)
negative[i] = positive[i - 1] + 1
else:
positive[i] = negative[i] = 0
maxLength = max(maxLength, positive[i])

return maxLength


5、丑数 II

dp = (n + 1) * [0]
n2, n3, n5 = 1, 1, 1
dp[1] = 1
for i in range(2, n + 1):
dp[i] = min(2 * dp[n2], 3 * dp[n3], 5 * dp[n5])
if dp[i] == 2 * dp[n2]: n2 += 1
if dp[i] == 3 * dp[n3]: n3 += 1
if dp[i] == 5 * dp[n5]: n5 += 1
return dp[n]

6、 优美的排列

f = [0] * (1 << n)
f[0] = 1
for mask in range(1, 1 << n):
for i in range(n):
if mask & (1 << i) and (num % (i + 1) == 0 or (i + 1) % num == 0):
return f[(1 << n) - 1]

7、只有两个键的键盘

if n == 1: return 0
dp = [[inf] * (n // 2 + 1) for _ in range(n + 1)]
dp[1][0], dp[1][1] = 0, 1
for i in range(2, n + 1):
for k in range(1, i // 2 + 1):
if i - k >= 0:
dp[i][k] = min(dp[i][k], dp[i - k][k] + 1)
if k == i // 2 and i % 2 == 0:
dp[i][k] = min(dp[i][k], min(dp[k]) + 2)
return min(dp[-1])

f = [0] * (n + 1)
for i in range(2, n + 1):
f[i] = float("inf")
j = 1
while j * j <= i:
if i % j == 0:
f[i] = min(f[i], f[j] + i // j)
f[i] = min(f[i], f[i // j] + j)
j += 1
return f[n]

由于两个素数的和小于其乘积，所以可优化成采用分解质因数的方法

8、新 21 点  （前缀和 + 动态规划）

dp = [1] + [0] * n
for i in range(1, n + 1):
if i <= n - k:
dp[i] = dp[i - 1] + 1
continue
dp[i] = dp[i - 1] - (dp[i - maxPts - 1] if i - maxPts > 0 else 0)
dp[i] = dp[i - 1] + dp[i] / maxPts
return dp[-1] - (dp[-2] if n >= 1 else 0)

9、计算分配糖果的不同方式 （斯特林数）

for i in range(1, k + 1):       #盒子
for j in range(i + 1, n + 1):   #糖果数
#新的糖果，单独一个盒子
dp[i][j] = dp[i-1][j-1]
#新的糖果，加入其他的盒子
dp[i][j] += dp[i][j-1] * i

10、石子游戏 II（前缀和 + 动态规划）

class Solution:
def stoneGameII(self, piles: List[int]) -> int:
pre = [0]
for p in piles:
pre.append(pre[-1] + p)

n = len(piles)
@cache
def dfs(i, M):
if i + 2 * M >= n:
return pre[-1] - pre[i]
maxl = 0
for j in range(1, 2 * M + 1):
maxl = max(maxl, pre[-1] - pre[i] - dfs(j + i, max(M, j)))
return maxl

return dfs(0, 1)

#### 动态规划与树结合

1、打家劫舍 III

class Solution:
def rob(self, root: TreeNode) -> int:
def dfs(p):
if not p: return 0, 0
la, lb = dfs(p.left)
ra, rb = dfs(p.right)
return max(lb + rb + p.val, ra + la), ra + la
return max(dfs(root))
class Solution:
def longestZigZag(self, root: TreeNode) -> int:
maxl = 0
def dfs(p):
nonlocal maxl
if not p: return -1, -1
l1, r1 = dfs(p.left)
l2, r2 = dfs(p.right)
maxl = max(maxl, r1 + 1, l2 + 1)
return r1 + 1, l2 + 1
dfs(root)
return maxl
class Solution:
def maxSumBST(self, root: Optional[TreeNode]) -> int:
maxl = 0
def dfs(p):
min1, max1, s1 = dfs(p.left) if p.left else (inf, -inf, 0)
min2, max2, s2 = dfs(p.right) if p.right else (inf, -inf, 0)
if max1 < p.val < min2:
nonlocal maxl
maxl = max(maxl, s1 + s2 + p.val)
return min(min1, p.val), max(max2, p.val), s1 + s2 + p.val
return -inf, inf, 0
dfs(root)
return maxl

4、 统计可能的树根数目 （换根dp）

（1）先从任意一个根出发，求解所有点

（2）再换根，计算变化的点

class Solution:
def rootCount(self, edges: List[List[int]], guesses: List[List[int]], k: int) -> int:
m = defaultdict(list)
for f, t in edges:
m[f].append(t)
m[t].append(f)

s = defaultdict(int)
for f, t in guesses:
s[f, t] += 1

t = 0
def dfs(i, last):
nonlocal t
for j in m[i]:
if j == last: continue
t += s[i, j]
dfs(j, i)

dfs(0, -1)
res = 0
def dfs2(i, last, t):
nonlocal res
if t >= k: res += 1
for j in m[i]:
if j == last: continue
dfs2(j, i, t - s[i, j] + s[j, i])

dfs2(0, -1, t)
return res

5、 树中距离之和  （换根dp）

class Solution:
def sumOfDistancesInTree(self, n: int, edges: List[List[int]]) -> List[int]:
m = defaultdict(list)
for f, t in edges:
m[f].append(t)
m[t].append(f)

dises, sizes  = [0] * n, [0] * n
#节点到所有孩子节点的距离，以及节点与所有孩子节点的个数
def dfs(i, last):
dis, size = 0, 1
for j in m[i]:
if j == last: continue
child_dis, child_size = dfs(j, i)
dis += child_dis + child_size
size += child_size
dises[i], sizes[i] = dis, size
return dis, size

dfs(0, -1)

res = [0] * n
res[0] = dises[0]
def dfs2(i, last, dis, size):
sum_child_size, sum_child_dis = 0, 0
for j in m[i]:
if j == last: continue
sum_child_size += sizes[j]
sum_child_dis += dises[j]

for j in m[i]:
if j == last: continue
parent_dis = dis + 2 * (sum_child_size - sizes[j]) + sum_child_dis - dises[j] + size
parent_size = size + sum_child_size - sizes[j] + 1
res[j] = parent_dis + dises[j]
dfs2(j, i,  parent_dis, parent_size)

dfs2(0, -1, 0, 1)

return res

#### 数位动态规划

class Solution:
def numDupDigitsAtMostN(self, n: int) -> int:
s = str(n)
@cache
def f(i: int, mask: int, is_limit: bool, is_num: bool) -> int:
if i == len(s):
return int(is_num)
res = 0
if not is_num:  # 可以跳过当前数位
res = f(i + 1, mask, False, False)
up = int(s[i]) if is_limit else 9
for d in range(0 if is_num else 1, up + 1):  # 枚举要填入的数字 d
if mask >> d & 1 == 0:  # d 不在 mask 中
res += f(i + 1, mask | (1 << d), is_limit and d == up, True)
return res
return n - f(0, 0, True, False)
class Solution:
def findIntegers(self, n: int) -> int:
s = str(bin(n))[2:]
@cache
def f(i: int, pre1: bool, is_limit: bool) -> int:
if i == len(s):
return 1
up = int(s[i]) if is_limit else 1
res = f(i + 1, False, is_limit and up == 0)  # 填 0
if not pre1 and up == 1:  # 可以填 1
res += f(i + 1, True, is_limit)  # 填 1
return res
return f(0, False, True)

#### 倍增

1、Pow(x, n)  -- 快速幂算法

class Solution:
def myPow(self, x: float, n: int) -> float:
if n == 0: return 1
n, f = abs(n), (n < 0)
p = [x] * n.bit_length()
for i in range(n.bit_length() - 1):
p[i + 1] = p[i] * p[i]
res = 1
for i in range(n.bit_length()):
if (n >> i) & 1:
res *= p[i]
if f: return 1 / res
return res

2、在传球游戏中最大化函数值  -- (求解第k个祖先)

class Solution:
def getMaxFunctionValue(self, receiver: List[int], k: int) -> int:
m = k.bit_length() - 1
pa = [[(p, p)] + [None] * m for p in receiver]
for i in range(m):
for x in range(n):
p, s = pa[x][i]
pp, ss = pa[p][i]
pa[x][i + 1] = (pp, s + ss)  # 合并节点值之和

ans = 0
for i in range(n):
x = sum = i
for j in range(m + 1):
if (k >> j) & 1:  # k 的二进制从低到高第 j 位是 1
x, s = pa[x][j]
sum += s
ans = max(ans, sum)
return ans

#### 状态压缩与优化

1、取余法：（70. 爬楼梯

dp = [1, 2]
for i in range(2, n):
dp[i % 2] = dp [(i -1) % 2] + dp[(i - 2) % 2]
return dp[(n - 1) % 2]


2、常数级别压缩： （70. 爬楼梯

a, b = 0, 1
for i in range(n):
a, b = b, a + b
return b

3、二维压缩成一维：（72. 编辑距离

s, t = (word1, word2) if len(word1) > len(word2) else (word2, word1)
dp = [j for j in range(len(t) + 1)]
for i in range(1, len(s)+1):
dpij, dp[0] = dp[0], i
for j in range(1, len(t) + 1):
temp = dp[j]
if s[i - 1] == t[j - 1]:
dp[j] = dpij
else:
dp[j] = min(dp[j] + 1, dp[j - 1] + 1, dpij + 1)
dpij = temp
return dp[-1]

class Solution:
def isThereAPath(self, grid: List[List[int]]) -> bool:
m, n = len(grid), len(grid[0])
@cache
def dfs(i, j):
if i <= 0 and j < 0 or i < 0 and j <= 0: return {0}
if i < 0 or j < 0: return set()
res = set()
for e in dfs(i - 1, j) | dfs(i, j - 1):
if grid[i][j]:
else:
return res

return 0 in dfs(m - 1, n - 1)

class Solution:
def isThereAPath(self, grid: List[List[int]]) -> bool:
m, n = len(grid), len(grid[0])
@cache
def dfs(i, j):
if i <= 0 and j < 0 or i < 0 and j <= 0: return 1 << (m + n)
if i < 0 or j < 0: return 0
if grid[i][j]:
return (dfs(i - 1, j) << 1) | (dfs(i, j - 1) << 1)
else:
return (dfs(i - 1, j) >> 1) | (dfs(i, j - 1) >> 1)

return ((dfs(m - 1, n - 1) >> (m + n)) & 1) == 1

5、使用26个字母空间

（1）2370. 最长理想子序列

f = [0] * 26
for c in s:
c = ord(c) - ord('a')
f[c] = 1 + max(f[max(c - k, 0): c + k + 1])
return max(f)

（2）不同的子序列 II

对于相同的子序列，只会考虑其最后一次出现的位置（下标序列的字典序最大）

class Solution:
def distinctSubseqII(self, s: str) -> int:
mod = 10 ** 9 + 7
dp = [0] * 26
for c in s:
i = ord(c) - ord('a')
for j in range(26):
if i == j: continue
dp[i] = (dp[j] + dp[i]) % mod
dp[i] = (dp[i] + 1) % mod
return sum(dp) % mod

6、使用线段树 （2407. 最长递增子序列 II

class Solution:
def lengthOfLIS(self, nums: List[int], k: int) -> int:
tmp = SegmentTree([0] * (max(nums) + 1))
for x in nums:
note = tmp.query(max(0, x-k), x-1)
tmp.update(x, note + 1)
return tmp.query(0, max(nums))

class SegmentTree:
def __init__(self, data, merge=max):
self.data = data
self.n = len(data)
self.tree = [None] * (4 * self.n)
self._merge = merge
if self.n:
self._build(0, 0, self.n-1)

def query(self, ql, qr):
return self._query(0, 0, self.n-1, ql, qr)

def update(self, index, value):
self.data[index] = value
self._update(0, 0, self.n-1, index)

def _build(self, tree_index, l, r):
if l == r:
self.tree[tree_index] = self.data[l]
return
mid = (l+r) // 2
left, right = 2 * tree_index + 1, 2 * tree_index + 2
self._build(left, l, mid)
self._build(right, mid+1, r)
self.tree[tree_index] = self._merge(self.tree[left], self.tree[right])

def _query(self, tree_index, l, r, ql, qr):
if l == ql and r == qr:
return self.tree[tree_index]

mid = (l+r) // 2
left, right = tree_index * 2 + 1, tree_index * 2 + 2
if qr <= mid:
return self._query(left, l, mid, ql, qr)
elif ql > mid:
return self._query(right, mid+1, r, ql, qr)

return self._merge(self._query(left, l, mid, ql, mid),
self._query(right, mid+1, r, mid+1, qr))

def _update(self, tree_index, l, r, index):
if l == r == index:
self.tree[tree_index] = self.data[index]
return
mid = (l+r)//2
left, right = 2 * tree_index + 1, 2 * tree_index + 2
if index > mid:
self._update(right, mid+1, r, index)
else:
self._update(left, l, mid, index)
self.tree[tree_index] = self._merge(self.tree[left], self.tree[right])

7、提高状态空间的重用性

（1）2143.在两个数组的区间中选取数字中，如下方法设置状态空间运行结果为超时

class Solution:
def countSubranges(self, nums1: List[int], nums2: List[int]) -> int:
n = len(nums1)
@cache
def dfs(i, l, is_start, r, is_end):
if i == n:
return int(l == r) if is_start else 0
res = 0
if not is_start:
res += dfs(i + 1, l, False, r, False)
res += dfs(i + 1, l + nums1[i], True, r , False)
res += dfs(i + 1, l, True, r + nums2[i], False)
elif is_start and not is_end:
res += dfs(i + 1, l + nums1[i], True, r , False)
res += dfs(i + 1, l, True, r + nums2[i], False)
res += dfs(i + 1, l, True, r, True)
else:
res += dfs(i + 1, l, True, r, True)
return res
return dfs(0, 0, False, 0, False)

这是因为(l, r) == (1, 1) 与 (l, r) == (6, 6)的状态是等价的，但是状态空间没有复用，导致重复计算，所以可以按如下方法改正，状态空间优化成一个，结果通过：

class Solution:
def countSubranges(self, nums1: List[int], nums2: List[int]) -> int:
n = len(nums1)
@cache
def dfs(i, d, is_start, is_end):
if i == n: return int(d == 0) if is_start else 0
if is_end: return int(d == 0)
res = 0
if not is_start:
res += dfs(i + 1, d, False, False)
res += dfs(i + 1, d + nums1[i], True, False)
res += dfs(i + 1, d - nums2[i], True, False)
elif is_start and not is_end:
res += dfs(i + 1, d + nums1[i], True, False)
res += dfs(i + 1, d - nums2[i], True, False)
res += dfs(i + 1, d, True, True)
else:
res += dfs(i + 1, d, True, True)
return res
return dfs(0, 0, False, False) % (10 ** 9 + 7)

（2）在盈利计划中，如下方法超时：

class Solution:
def profitableSchemes(self, n: int, minProfit: int, group: List[int], profit: List[int]) -> int:
mod = 10 ** 9 + 7
@cache
def dfs(i, n, p):
if i == len(group):
return p >= minProfit
res = 0
if group[i] <= n:
res = (res + dfs(i + 1, n - group[i], p + profit[i])) % mod
return (res + dfs(i + 1, n, p)) % mod

return dfs(0, n, 0) % mod

这是由于当前盈利值达到最低要求后，后续结果只与剩余人数有关，所以按照最小盈利值对状态进行重用，如下所示。

class Solution:
def profitableSchemes(self, n: int, minProfit: int, group: List[int], profit: List[int]) -> int:
mod = 10 ** 9 + 7
@cache
def dfs(i, n, p):
if i == len(group):
return int(p == minProfit)
res = 0
if group[i] <= n:
res = (res + dfs(i + 1, n - group[i], min(minProfit, p + profit[i]))) % mod
return (res + dfs(i + 1, n, p)) % mod

return dfs(0, n, 0) % mod

8、缩短计算步数

如下解法运行超时，因为当怪物需要的能量比较大时，计算次数变多。

class Solution:
def minimumTime(self, power: List[int]) -> int:
n, mp = len(power), max(power)
@cache
def dfs(g, v, inc):
if v == (1 << n) - 1: return 0
res, gg, step = inf, g - inc, 0
while gg <= mp:
gg += inc
step += 1
for i in range(n):
if (v >> i) & 1 == 0 and gg >= power[i]:
res = min(res, dfs(inc + 1, v | (1 << i), inc + 1) + step)
return res
return dfs(1, 0, 1)

根据题意，结果只与打败怪物的顺序有关，与积攒能量的过程无关，所以可以优化成下面的方法，减少重复计算，同时优化状态空间。

class Solution:
def minimumTime(self, power: List[int]) -> int:
n, mp = len(power), max(power)
@cache
def dfs(v, inc):
if v == (1 << n) - 1: return 0
res = inf
for i in range(n):
if (v >> i) & 1 == 0:
res = min(res, dfs(v | (1 << i), inc + 1) + ceil(power[i] / inc))
return res
return dfs(0, 1)

（2）好二进制字符串的数量

class Solution:
def goodBinaryStrings(self, minLength: int, maxLength: int, oneGroup: int, zeroGroup: int) -> int:
@cache
def dfs(i, p):
res = 0
if minLength <= i <= maxLength and p == 0:
res = 1
if i == maxLength: return res
if p <= 0:
res += dfs(i + 1, -((-p + 1) % zeroGroup))
if p >= 0:
res += dfs(i + 1, (p + 1) % oneGroup)
return res % (10 ** 9 + 7)
return dfs(0, 0) % (10 ** 9 + 7)

由于要求具有连续相同字符，所以不需要计算使用每个字符的状态，如下所示，通过减少状态空间和缩短步数来达到优化的目的。

class Solution:
def goodBinaryStrings(self, minLength: int, maxLength: int, oneGroup: int, zeroGroup: int) -> int:
@cache
def dfs(i):
res = 0
if minLength <= i <= maxLength:
res = 1
if i == maxLength: return res
if i + oneGroup <= maxLength:
res += dfs(i + oneGroup)
if i + zeroGroup <= maxLength:
res += dfs(i + zeroGroup)
return res % (10 ** 9 + 7)
return dfs(0) % (10 ** 9 + 7)

直接使用暴力dp超时，因为n最大超过10^9, 每次减一需要的时间比较长，所以可以对减一部分进行优化，由于n 变为 二分之一或者三分之一步数总是比连续减一的步数小，所以可以写成如下代码：

class Solution:
@lru_cache(None)
def minDays(self, n: int) -> int:
if n == 0: return 0
if n == 1: return 1
return 1 + min(self.minDays(n//2) + n % 2, self.minDays(n//3) + n % 3)

9、剪枝法

（1） 出租车的最大盈利

如下使用动态规划的方法，会超时

class Solution:
def maxTaxiEarnings(self, n: int, rides: List[List[int]]) -> int:
rides.sort()
@cache
def dfs(j):
idx = bisect_left(rides, [j, 0, 0])
if idx >= len(rides): return 0
maxl = 0
for i in range(idx, len(rides)):
start, end, tip = rides[i]
maxl = max(maxl, dfs(end) + end - start + tip)
return maxl
return dfs(0)

这是因为当两个区间不相交时，两个区间都取到时收益最大，所以在新的起始点大于最小终止点时，结束计算，如下所示：

class Solution:
def maxTaxiEarnings(self, n: int, rides: List[List[int]]) -> int:
rides.sort()
@cache
def dfs(j):
idx = bisect_left(rides, [j, 0, 0])
if idx >= len(rides): return 0
maxl = 0
s0, e0, t0 = rides[idx]
for i in range(idx, len(rides)):
start, end, tip = rides[i]
e0 = min(e0, end)
if start >= e0: break
maxl = max(maxl, dfs(end) + end - start + tip)
return maxl
return dfs(0)

也可以利用区间结束点的顺序和二分查找法对其进行优化：

class Solution:
def maxTaxiEarnings(self, n: int, rides: List[List[int]]) -> int:
rides = sorted(rides, key=lambda x: x[1])
dp = [[0, 0]]
for s, e, p in rides:
i = bisect.bisect(dp, [s + 1]) - 1
if dp[i][1] +  p + e - s > dp[-1][1]:
dp.append([e, dp[i][1] + p + e - s])
return dp[-1][1]

（2）石子游戏 V

class Solution:
def stoneGameV(self, stoneValue: List[int]) -> int:
pre = [0]
for v in stoneValue:
pre.append(pre[-1] + v)

@cache
def dfs(i, j):
if i == j: return 0
maxl = 0
for k in range(i, j):
left = pre[k + 1] - pre[i]
right = pre[j + 1] - pre[k + 1]
if left < right:
if maxl >= 2 * left: continue
maxl = max(maxl, dfs(i, k) + left)
elif right < left:
if maxl >= 2 * right: continue
maxl = max(maxl, dfs(k + 1, j) + right)
else:
if maxl >= 2 * right: continue
maxl = max(maxl, dfs(i, k) + left, dfs(k + 1, j) + right)
return maxl
return dfs(0, len(stoneValue) - 1)

class Solution:
def maximumPoints(self, edges: List[List[int]], coins: List[int], k: int) -> int:
m = defaultdict(set)
for f, t in edges:

def dfs(p, last):
for np in list(m[p]):
if np == last:
m[p].remove(np)
continue
dfs(np, p)
dfs(0, -1)

@cache
def dp(p, mm):
r1 = coins[p] // mm - k
r2 = coins[p] // mm // 2
for np in m[p]:
if r1 < r2 and mm < 2 ** 13:
r2 += dp(np, 2 * mm)
r1 += dp(np, mm)
return max(r1, r2)

return dp(0, 1)

10、逆向思维求解  -- 好分区的数目

如果直接求解好分区数量，但是背包容量太大，会超时，代码如下：

class Solution:
def countPartitions(self, nums: List[int], k: int) -> int:
sm = sum(nums)
@cache
def dfs(i, s):
if i == len(nums):
return 1 if sm - s >= k and s >= k else 0
return dfs(i + 1, s + nums[i]) + dfs(i + 1, s)
return dfs(0, 0) % (10 ** 9 + 7)

class Solution:
def countPartitions(self, nums: List[int], k: int) -> int:
sm = sum(nums)
if sm < k * 2: return 0
mod = (10 ** 9 + 7)
@cache
def dfs(i, s):
if i == len(nums): return 1
res = dfs(i + 1, s)
if s >= nums[i]:
res += dfs(i + 1, s - nums[i])
return res % mod
return (pow(2, len(nums), mod) - dfs(0, k - 1) * 2) % mod

class Solution:
def countPartitions(self, nums: List[int], k: int) -> int:
if sum(nums) < k * 2: return 0
MOD = 10 ** 9 + 7
f = [0] * k
f[0] = 1
for x in nums:
for j in range(k - 1, x - 1, -1):
f[j] = (f[j] + f[j - x]) % MOD
return (pow(2, len(nums), MOD) - sum(f) * 2) % MOD

11、问题转化 -- K 次调整数组大小浪费的最小总空间

class Solution:
def minSpaceWastedKResizing(self, nums: List[int], k: int) -> int:
n = len(nums)

@cache
def dfs(i, j, k):
if i == len(nums): return 0
if nums[i] <= nums[j]:
minl = dfs(i + 1, j, k) + nums[j] - nums[i]
else: minl = inf
if k > 0:
for jj in range(j + 1, n):
if nums[jj] >= nums[i]:
minl = min(minl, dfs(i + 1, jj, k - 1) + nums[jj] - nums[i])
return minl

return min(dfs(0, j, k) for j in range(n))

可以理解成将数组分成k + 1组，求每组最大值乘以这一段的长度再减去这一段的元素和，找到总体最小值，代码如下：

class Solution:
def minSpaceWastedKResizing(self, nums: List[int], k: int) -> int:
n = len(nums)
pre = [0]
for num in nums:
pre.append(pre[-1] + num)

@cache
def diff(i, j):
return (j - i + 1) * max(nums[i: j + 1]) -  pre[j + 1] + pre[i]

@cache
def dfs(i, j, k):
minl = diff(i, j)
if k > 0:
for t in range(i, j):
minl = min(minl, dfs(i, t, k - 1) + diff(t + 1, j))

return minl

return dfs(0, n - 1, k)


12、降低参数维度

（1）减少不必要的参数 -- 两个子序列的最大点积

class Solution:
def maxDotProduct(self, nums1: List[int], nums2: List[int]) -> int:
@cache
def dfs(i, j, f):
if i == len(nums1) or j == len(nums2): return 0 if f else -inf
return max(dfs(i + 1, j, f), dfs(i, j + 1, f), dfs(i + 1, j + 1, True) + nums1[i] * nums2[j])
return dfs(0, 0, False)

可以改变max的判断来减少维度，运行时间为628ms

class Solution:
def maxDotProduct(self, nums1: List[int], nums2: List[int]) -> int:
@cache
def dfs(i, j):
if i == len(nums1) or j == len(nums2): return -inf
return max(dfs(i + 1, j), dfs(i, j + 1), dfs(i + 1, j + 1) + nums1[i] * nums2[j], nums1[i] * nums2[j])
return dfs(0, 0)

（2）将参数移至返回值   -- 完成任务的最少工作时间段

class Solution:
def minSessions(self, tasks: List[int], sessionTime: int) -> int:
@cache
def dfs(v, s):
if v.bit_count() == n:
return 0
minl = inf
for i in range(n):
if (v >> i) & 1 == 1: continue
minl = min(minl, dfs(v | (1 << i), s - tasks[i]))
else:
minl = min(minl, dfs(v | (1 << i), sessionTime - tasks[i]) + 1)
return minl
return dfs(0, sessionTime) + 1

当任务已使用时间放到返回值时，运行时间为1196ms

class Solution:
def minSessions(self, tasks: List[int], sessionTime: int) -> int:
@cache
def dfs(v):
if v.bit_count() == n:
return 1, 0
minl, mins = inf, inf
for i in range(n):
if (v >> i) & 1 == 1: continue
l, s = dfs(v | (1 << i))
if tasks[i] + s <= sessionTime:
minl, mins = min((minl, mins), (l, s + tasks[i]))
else:
minl, mins = min((minl, mins), (l + 1, tasks[i]))
return minl, mins
return dfs(0)[0]

（3） 执行操作使两个字符串相等

根据题意，使用动态规划进行求解，代码如下，用时368ms

class Solution:
def minOperations(self, s1: str, s2: str, x: int) -> int:
if abs(s1.count('1') - s2.count('1')) & 1 == 1:
return -1
@cache
def dfs(i, t, is_prev):
if i == len(s1):
return inf if t > 0 or is_prev else 0
if (s1[i] == s2[i]) == (not is_prev):
return dfs(i + 1, t, False)

minl = min(dfs(i + 1, t + 1, False) + x, dfs(i + 1, t, True) + 1)
if t > 0:
minl = min(minl, dfs(i + 1, t - 1, False))

return minl

return dfs(0, 0, False)

可以证明不同字符之间只能有一次操作，所以可以在不同字符下标处进行转移，缩短计算步数。

class Solution:
def minOperations(self, s1: str, s2: str, x: int) -> int:
if s1 == s2:
return 0
p = [i for i, (x, y) in enumerate(zip(s1, s2)) if x != y]

if len(p) % 2:
return -1
f0, f1 = 0, x
for i, j in pairwise(p):
f0, f1 = f1, min(f1 + x, f0 + (j - i) * 2)
return f1 // 2

13、转化转移方程，减少循环 -- 扣分后的最大得分

该题直接使用暴力法超时，代码如下：

class Solution:
def maxPoints(self, points: List[List[int]]) -> int:
m, n = len(points), len(points[0])
dp = points[0]
for i in range(1, m):
temp = [0] * n
for j in range(n):
for jj in range(n):
temp[j] = max(temp[j], points[i][j] + dp[jj] - abs(j - jj))
dp = temp
return max(dp)

最终代码如下：

class Solution:
def maxPoints(self, points: List[List[int]]) -> int:
m, n = len(points), len(points[0])
dp = points[0]
for i in range(1, m):
temp = [0] * n
maxl2 = [-inf]
for j in reversed(range(n)):
maxl2.append(max(maxl2[-1], dp[j] - j))

maxl2 = maxl2[::-1]
maxl = -inf
for j in range(n):
maxl = max(maxl, dp[j] + j)
temp[j] = max(temp[j], points[i][j] - j + maxl, points[i][j] + j + maxl2[j])
dp = temp
return max(dp)

14、 使用二分法进行优化 -- 最多可以参加的会议数目 II

class Solution:
def maxValue(self, events: List[List[int]], k: int) -> int:
events.sort(key=lambda e: e[1])
n = len(events)
f = [[0] * (k + 1) for _ in range(n + 1)]
for i, (start, end, val) in enumerate(events):
p = bisect_left(events, start, hi=i, key=lambda e: e[1])  # hi=i 表示二分上界为 i（默认为 n）
for j in range(1, k + 1):
# 为什么是 p 不是 p+1：上面算的是 >= start，-1 后得到 < start，但由于还要 +1，抵消了
f[i + 1][j] = max(f[i][j], f[p][j - 1] + val)
return f[n][k]

15、使用单调栈进行优化 -- 工作计划的最低难度

代码如下：

class Solution:
def minDifficulty(self, a: List[int], d: int) -> int:
n = len(a)
if n < d:
return -1

f = [[inf] * n for _ in range(d)]
f[0] = list(accumulate(a, max))
for i in range(1, d):
st = []  # (下标 j，从 f[i-1][left[j]] 到 f[i-1][j-1] 的最小值)
for j in range(i, n):
mn = f[i - 1][j - 1]  # 只有 a[j] 一项工作
while st and a[st[-1][0]] <= a[j]:  # 向左一直计算到 left[j]
mn = min(mn, st.pop()[1])
f[i][j] = mn + a[j]  # 从 a[left[j]+1] 到 a[j] 的最大值是 a[j]
if st:  # 如果这一段包含 <=left[j] 的工作，那么这一段的最大值必然不是 a[j]
f[i][j] = min(f[i][j], f[i][st[-1][0]])  # 答案和 f[i][left[j]] 是一样的
st.append((j, mn))  # 注意这里保存的不是 f[i][j]
return f[-1][-1]

16、改变递推顺序 --  通过给定词典构造目标字符串的方案数

class Solution:
def numWays(self, words: List[str], target: str) -> int:
m, n = len(words[0]), len(target)
mp = defaultdict(lambda: defaultdict(int))
for word in words:
for i, c in enumerate(word):
mp[c][i] += 1
mod = 10 ** 9 + 7
@cache
def dfs(i, j):
if i == n: return 1
res = 0
for k in range(j, m - (n - i - 1)):
if mp[target[i]][k] > 0:
res = (res + mp[target[i]][k] % mod * dfs(i + 1, k + 1) % mod) % mod
return res % mod
return dfs(0, 0)

class Solution:
def numWays(self, words: List[str], target: str) -> int:
m, n = len(words[0]), len(target)
mp = defaultdict(lambda: defaultdict(int))
for word in words:
for i, c in enumerate(word):
mp[c][i] += 1

@cache
def dfs(i, j):
if i == n: return 1
if m - j < n - i: return 0
return (mp[target[i]][j] * dfs(i + 1, j + 1) + dfs(i, j + 1)) % (10 ** 9 + 7)
return dfs(0, 0)

17、改变状态空间  -- 每个人戴不同帽子的方案数

class Solution:
def numberWays(self, hats: List[List[int]]) -> int:
@cache
if mask.bit_count() == len(hats): return 1
res = 0
if (mask >> h) & 1 == 0:
res = (res + dfs(mask | (1 << h))) % (10 ** 9 + 7)
return res
return dfs(0)

class Solution:
def numberWays(self, hats: List[List[int]]) -> int:
mp = defaultdict(list)
s = set()
for i, hat in enumerate(hats):
for a_hat in hat:
mp[a_hat].append(i)
s = list(s)
@cache
if mask.bit_count() == len(hats): return 1
if i == len(s): return 0
res = 0
for j in mp[s[i]]:
if (mask >> j) & 1 == 0:
res = (res + dfs(i + 1, mask | (1 << j))) % (10 ** 9 + 7)
res = (res + dfs(i + 1, mask)) % (10 ** 9 + 7)
return res

return dfs(0, 0)

18、使用前缀和进行优化 -- 生成数组

class Solution:
def numOfArrays(self, N: int, M: int, K: int) -> int:
dp = [[[0 for _ in range(M + 1)] for _ in range(K + 1)] for _ in range(N + 1)]

for k in range(1, M + 1):
dp[1][1][k] = 1

for i, j, k in itertools.product(range(1, N + 1), range(1, K + 1), range(M + 1)):
dp[i][j][k] += dp[i - 1][j][k] * k
dp[i][j][k] += sum(dp[i - 1][j - 1][1:k])

return sum(dp[N][K][1:]) % (10 ** 9 + 7)

class Solution:
def numOfArrays(self, n: int, m: int, k: int) -> int:
# 不存在搜索代价为 0 的数组
if k == 0:
return 0

f = [[[0] * (m + 1) for _ in range(k + 1)] for __ in range(n + 1)]
mod = 10**9 + 7
# 边界条件，所有长度为 1 的数组的搜索代价都为 1
for j in range(1, m + 1):
f[1][1][j] = 1
for i in range(2, n + 1):
# 搜索代价不会超过数组长度
for s in range(1, min(k, i) + 1):
# 前缀和
presum_j = 0
for j in range(1, m + 1):
f[i][s][j] = (f[i - 1][s][j] * j + presum_j) % mod
presum_j += f[i - 1][s - 1][j]

# 最终的答案是所有 f[n][k][..] 的和
# 即数组长度为 n，搜索代价为 k，最大值任意
ans = sum(f[n][k][j] for j in range(1, m + 1)) % mod
return ans

#### 求解最优路径

1、直接记录路径：（最大整除子集

dp= [[x] for x in nums]
for i in range(len(nums)):
for j in range(i + 1, len(nums)):
if nums[j] % nums[i] == 0 and len(dp[i]) + 1 > len(dp[j]):
dp[j] = dp[i] + [nums[j]]
return max(dp, key=len)

2、回溯： （最大整除子集

maxl, temp = max(dp), None
res, l = [], maxl
for i in reversed(range(n)):
if l == dp[i]:
if l == maxl or temp is not None and temp % nums[i] == 0:
temp = nums[i]
res.insert(0, nums[i])
l = l - 1

#### 参考资料

​​​​​​动态规划解决01背包问题 - Christal_R - 博客园

• 6
点赞
• 66
收藏
觉得还不错? 一键收藏
• 2
评论
05-29
02-18 1万+
10-24 4788
11-11 4979
07-21
12-15
02-23 7535
03-05 4272
05-13 1万+
04-24 1086
10-28 2344
05-29 1574
02-14 1280

### “相关推荐”对你有帮助么？

• 非常没帮助
• 没帮助
• 一般
• 有帮助
• 非常有帮助

1.余额是钱包充值的虚拟货币，按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载，可以购买VIP、付费专栏及课程。