cdn fix more
This commit is contained in:
240
Html/apps/static/68bacdfadf5aeae0912f7f18-第五章:动态规划-动态规划原理.html
Normal file
240
Html/apps/static/68bacdfadf5aeae0912f7f18-第五章:动态规划-动态规划原理.html
Normal file
@@ -0,0 +1,240 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Document</title>
|
||||
<!-- 你的其他样式 -->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
||||
<script>
|
||||
MathJax.config = {
|
||||
tex: {
|
||||
inlineMath: [['$', '$'], ['\(', '\)']]
|
||||
},
|
||||
svg: {
|
||||
fontCache: 'global'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<h1>动态规划原理</h1>
|
||||
<h2>动态规划原理</h2>
|
||||
<h3>自顶而下的分治 vs. 自底向上的动态规划</h3>
|
||||
<p>分治法:面对规模为 <em>n</em> 的问题,从<strong>顶层</strong>出发,将其拆为若干个更小的子问题,分别求解后<strong>合并</strong>。</p>
|
||||
<p>动态规划(DP):当<strong>同一子问题重复出现</strong>时,与其一再从顶层“把问题拆碎”,不如记录子问题的答案,并按规模<strong>从小到大</strong>把所有需要的子问题一次性求出来。
|
||||
<br></p>
|
||||
<h4>什么才算“同一”子问题</h4>
|
||||
<p>与AI教师讨论,如何区分“相同规模的子问题”和“同一(可复用的)子问题”</p>
|
||||
<h3>切绳(Rod Cutting)问题</h3>
|
||||
<p>给定一根长度为 <em>n</em> 的绳子,价格表 <code>price[i]</code> 表示长度为 <em>i</em> 的一段可以卖出的价格。允许将绳子切成多段出售,目标是使总收益最大。</p>
|
||||
<h4>分治法与时间复杂度</h4>
|
||||
<p>设 <code>R(n)</code> 为长度 <em>n</em> 的最大收益:</p>
|
||||
<section>
|
||||
<eqn> R(0) = 0, R(1)=price[1] </eqn>
|
||||
</section>
|
||||
<section>
|
||||
<eqn> R(n) = max_{1 ≤ i ≤ n} ( price[i] + R(n - i) ) </eqn>
|
||||
</section>
|
||||
<p><strong>纯递归分治</strong>会对同一规模的子问题多次求解,子问题规模组合数呈指数级,时间复杂度为 <strong>O(n^n)</strong> 量级。</p>
|
||||
<h4>记忆化(自顶向下)</h4>
|
||||
<p>思想:用哈希表/数组 <code>memo[n]</code> 记录 <code>R(n)</code>。当再次需要 <code>R(n)</code> 时,直接返回已存结果,避免重复计算。</p>
|
||||
<ul>
|
||||
<li><strong>复杂度</strong>:时间 <strong>O(n^2)</strong>(外层 n,内层枚举切第一刀 i),空间 <strong>O(n)</strong>。</li>
|
||||
</ul>
|
||||
<p><strong>代码任务 A:实现记忆化递归(Top-Down)</strong></p>
|
||||
<pre><code class="language-python">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
|
||||
</code></pre>
|
||||
<h4>自底向上(反向记忆化)</h4>
|
||||
<p>将规模从小到大推进:<code>dp[x]</code> 表示长度 <code>x</code> 的最优收益。
|
||||
<strong>代码任务 B:实现自底向上(Bottom-Up)</strong></p>
|
||||
<pre><code class="language-python">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()
|
||||
</code></pre>
|
||||
<p>完成上面的代码,讨论实验对比结果。</p>
|
||||
<h3>动态规划的“组成”:如何写出一个 DP</h3>
|
||||
<h4><strong>四大组成</strong></h4>
|
||||
<ul>
|
||||
<li><strong>状态空间</strong>:用最少的下标(或维度)刻画子问题(如 <code>dp[i]</code>、<code>dp[i][j]</code>)。</li>
|
||||
<li><strong>状态转移</strong>:写出“从更小状态到当前状态”的递推/转移式。</li>
|
||||
<li><strong>边界条件</strong>:初始已知的最小规模答案(如 <code>dp[0]=0</code>)。</li>
|
||||
<li><strong>解的恢复(部分题目可能不需要)</strong>:若需输出方案/路径,记录子问题选择来源(从那个子问题的答案转移而来)(如 <code>choice[i][j]</code>)。</li>
|
||||
</ul>
|
||||
<h4><strong>两大性质</strong>:</h4>
|
||||
<ul>
|
||||
<li><strong>最优子结构</strong>:全局最优由若干子问题的最优解组合而成;</li>
|
||||
<li><strong>重复子问题</strong>:不同路径会遇到同一个(或等价的)子问题。</li>
|
||||
</ul>
|
||||
<h3>例题:矩阵连乘(Matrix-Chain Multiplication, MCM)</h3>
|
||||
<h4>问题描述</h4>
|
||||
<p>矩阵乘法有严格的维度匹配要求:只有前一个矩阵的列数 = 后一个矩阵的行数,才能相乘。
|
||||
即:
|
||||
矩阵 A:维度为 m × k(共 m 行、k 列)
|
||||
矩阵 B:维度为 k × n(共 k 行、n 列)
|
||||
矩阵 C = A×B,维度为 m × n
|
||||
产生标量乘法次数为 <code>m×n×k</code>
|
||||
给定矩阵链 <code>A₁A₂…Aₖ</code>,维度数组 <code>p[0..k]</code> 满足 <code>Aᵢ</code> 大小为 <code>p[i-1] × p[i]</code>。目标:只改变乘法<strong>括号化顺序</strong>,最小化标量乘法次数。</p>
|
||||
<h4>递推与 DP 表</h4>
|
||||
<p>令 <code>m[i][j]</code> 表示从 <code>Aᵢ…Aⱼ</code> 的最小乘法次数(1-index)。则:</p>
|
||||
<section>
|
||||
<eqn> m[i][i] = 0 </eqn>
|
||||
</section>
|
||||
<section>
|
||||
<eqn> m[i][j] = min_{i ≤ k < j} ( 尝试推导一下这里的转移式 ) </eqn>
|
||||
</section>
|
||||
<h4>记录断点</h4>
|
||||
<p>令 <code>s[i][j]</code> 存储从 <code>Aᵢ…Aⱼ</code> 的最优断点。
|
||||
<eq>$ s[i][j] = k \text{ if } m[i][j] == 与上面的式子一样 $</eq></p>
|
||||
<h4>代码任务</h4>
|
||||
<pre><code class="language-python">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)}")
|
||||
|
||||
</code></pre>
|
||||
<h4>综合讨论</h4>
|
||||
<ul>
|
||||
<li>何时选“记忆化 Top-Down”,何时选“自底向上表格法”?</li>
|
||||
<li>如何从“纯递归”快速判断是否值得改造成 DP?</li>
|
||||
</ul>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user