分治法:面对规模为 n 的问题,从顶层出发,将其拆为若干个更小的子问题,分别求解后合并。
动态规划(DP):当同一子问题重复出现时,与其一再从顶层“把问题拆碎”,不如记录子问题的答案,并按规模从小到大把所有需要的子问题一次性求出来。
与AI教师讨论,如何区分“相同规模的子问题”和“同一(可复用的)子问题”
给定一根长度为 n 的绳子,价格表 price[i] 表示长度为 i 的一段可以卖出的价格。允许将绳子切成多段出售,目标是使总收益最大。
设 R(n) 为长度 n 的最大收益:
纯递归分治会对同一规模的子问题多次求解,子问题规模组合数呈指数级,时间复杂度为 O(n^n) 量级。
思想:用哈希表/数组 memo[n] 记录 R(n)。当再次需要 R(n) 时,直接返回已存结果,避免重复计算。
代码任务 A:实现记忆化递归(Top-Down)
def rod_cut_topdown(price: dict[int, int], n: int, memo: list[int]) -> int:
"""
返回长度 n 的最大收益(记忆化递归)。
TODO:
1) 处理 n==0;2) 命中 memo 直接返回;3) 枚举第一刀长度 i;4) 写回 memo[n]。
"""
pass
将规模从小到大推进:dp[x] 表示长度 x 的最优收益。
代码任务 B:实现自底向上(Bottom-Up)
def rod_cut_bottomup(price: dict[int, int], n: int) -> int:
"""
返回长度 n 的最大收益(自底向上)。
TODO:
1) 初始化 dp[0]=0;2) for x in 1..n:dp[x] = max_{1..x}( price[i] + dp[x-i] )
"""
"""
以下内容无需修改,注意将你实现的rod_cut_topdown代码复制过来
"""
import time
import random
import signal
from functools import wraps
#超时异常定义
class TimeoutError(Exception):
pass
#超时装饰器
def timeout(seconds):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# 定义超时处理函数
def handle_timeout(signum, frame):
raise TimeoutError(f"Function {func.__name__} timed out after {seconds} seconds")
# 设置信号处理
signal.signal(signal.SIGALRM, handle_timeout)
signal.alarm(seconds) # 触发超时
try:
result = func(*args, **kwargs)
return result
finally:
signal.alarm(0) # 取消超时
return wrapper
return decorator
#带超时的纯递归实现(用于对比)
@timeout(1) # 1秒超时
def rod_cut_recursive(price: dict[int, int], n: int) -> int:
if n == 0:
return 0
max_rev = -float('inf')
for i in range(1, n + 1):
if i in price:
max_rev = max(max_rev, price[i] + rod_cut_recursive(price, n - i))
return max_rev
#对比实验
def compare_algorithms():
# 生成测试用的价格表(随机生成1到10的价格)
max_length = 50
price = {i: random.randint(1, 10) for i in range(1, max_length + 1)}
print(f"{'n':<5} {'递归(ms)':<10} {'记忆化(ms)':<12} {'自底向上(ms)':<15}")
print("-" * 50)
# 测试n从10到50的情况
for n in range(10, 51, 5):
# 纯递归(带超时处理)
try:
start = time.time()
recursive_result = rod_cut_recursive(price, n)
recursive_time = (time.time() - start) * 1000
except TimeoutError:
recursive_result = "超时"
recursive_time = ">1000"
# 记忆化递归
memo = [-1] * (n + 1)
start = time.time()
topdown_result = rod_cut_topdown(price, n, memo)
topdown_time = (time.time() - start) * 1000
# 自底向上
start = time.time()
bottomup_result = rod_cut_bottomup(price, n)
bottomup_time = (time.time() - start) * 1000
# 验证结果一致性(仅当递归未超时)
if isinstance(recursive_result, int):
assert recursive_result == topdown_result == bottomup_result, f"结果不一致 for n={n}"
# 输出结果
print(f"{n:<5} {recursive_time:<10} {topdown_time:<12.4f} {bottomup_time:<15.4f}")
if __name__ == "__main__":
compare_algorithms()
完成上面的代码,讨论实验对比结果。
dp[i]、dp[i][j])。dp[0]=0)。choice[i][j])。矩阵乘法有严格的维度匹配要求:只有前一个矩阵的列数 = 后一个矩阵的行数,才能相乘。
即:
矩阵 A:维度为 m × k(共 m 行、k 列)
矩阵 B:维度为 k × n(共 k 行、n 列)
矩阵 C = A×B,维度为 m × n
产生标量乘法次数为 m×n×k
给定矩阵链 A₁A₂…Aₖ,维度数组 p[0..k] 满足 Aᵢ 大小为 p[i-1] × p[i]。目标:只改变乘法括号化顺序,最小化标量乘法次数。
令 m[i][j] 表示从 Aᵢ…Aⱼ 的最小乘法次数(1-index)。则:
令 s[i][j] 存储从 Aᵢ…Aⱼ 的最优断点。
def matrix_chain_order(p: list[int]) -> tuple[list[list[int]], list[list[int]]]:
"""
返回 (m, s),m[i][j] 为最小代价,s[i][j] 为最优断点。
TODO: 长度 n = len(p)-1;按区间长度 L=2..n 填表。
"""
...
def print_optimal_parens(s: list[list[int]], i: int, j: int) -> str:
"""根据断点矩阵 s 输出最优括号化方案"""
if i == j:
return f"A{i}"
else:
return f"({print_optimal_parens(s, i, s[i][j])}" \
f"{print_optimal_parens(s, s[i][j]+1, j)})"
#测试验证部分
if __name__ == "__main__":
# 经典测试案例
p = [30, 35, 15, 5, 10, 20, 25]
expected_result = 15125
# 计算最优解
m, s = matrix_chain_order(p)
n = len(p) - 1
result = m[1][n]
# 输出测试结果
print(f"矩阵维度数组: {p}")
print(f"矩阵数量: {n}")
print(f"计算得到的最小标量乘法次数: {result}")
print(f"预期的最小标量乘法次数: {expected_result}")
print(f"测试{'通过' if result == expected_result else '失败'}")
print(f"最优括号化方案: {print_optimal_parens(s, 1, n)}")
# 额外测试案例
p2 = [40, 20, 30, 10, 30]
m2, s2 = matrix_chain_order(p2)
print("\n第二个测试案例:")
print(f"矩阵维度数组: {p2}")
print(f"最小标量乘法次数: {m2[1][4]}") # 预期结果为 26000
print(f"最优括号化方案: {print_optimal_parens(s2, 1, 4)}")