Files
hsa/Html/apps/static/68bacdfadf5aeae0912f7f18-第五章:动态规划-动态规划原理.html
2025-11-17 14:04:52 +08:00

240 lines
9.9 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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]) -&gt; int:
&quot;&quot;&quot;
返回长度 n 的最大收益(记忆化递归)。
TODO:
1) 处理 n==02) 命中 memo 直接返回3) 枚举第一刀长度 i4) 写回 memo[n]。
&quot;&quot;&quot;
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) -&gt; int:
&quot;&quot;&quot;
返回长度 n 的最大收益(自底向上)。
TODO:
1) 初始化 dp[0]=02) for x in 1..ndp[x] = max_{1..x}( price[i] + dp[x-i] )
&quot;&quot;&quot;
&quot;&quot;&quot;
以下内容无需修改注意将你实现的rod_cut_topdown代码复制过来
&quot;&quot;&quot;
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&quot;Function {func.__name__} timed out after {seconds} seconds&quot;)
# 设置信号处理
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) -&gt; 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&quot;{'n':&lt;5} {'递归(ms)':&lt;10} {'记忆化(ms)':&lt;12} {'自底向上(ms)':&lt;15}&quot;)
print(&quot;-&quot; * 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 = &quot;超时&quot;
recursive_time = &quot;&gt;1000&quot;
# 记忆化递归
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&quot;结果不一致 for n={n}&quot;
# 输出结果
print(f&quot;{n:&lt;5} {recursive_time:&lt;10} {topdown_time:&lt;12.4f} {bottomup_time:&lt;15.4f}&quot;)
if __name__ == &quot;__main__&quot;:
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]) -&gt; tuple[list[list[int]], list[list[int]]]:
&quot;&quot;&quot;
返回 (m, s)m[i][j] 为最小代价s[i][j] 为最优断点。
TODO: 长度 n = len(p)-1按区间长度 L=2..n 填表。
&quot;&quot;&quot;
...
def print_optimal_parens(s: list[list[int]], i: int, j: int) -&gt; str:
&quot;&quot;&quot;根据断点矩阵 s 输出最优括号化方案&quot;&quot;&quot;
if i == j:
return f&quot;A{i}&quot;
else:
return f&quot;({print_optimal_parens(s, i, s[i][j])}&quot; \
f&quot;{print_optimal_parens(s, s[i][j]+1, j)})&quot;
#测试验证部分
if __name__ == &quot;__main__&quot;:
# 经典测试案例
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&quot;矩阵维度数组: {p}&quot;)
print(f&quot;矩阵数量: {n}&quot;)
print(f&quot;计算得到的最小标量乘法次数: {result}&quot;)
print(f&quot;预期的最小标量乘法次数: {expected_result}&quot;)
print(f&quot;测试{'通过' if result == expected_result else '失败'}&quot;)
print(f&quot;最优括号化方案: {print_optimal_parens(s, 1, n)}&quot;)
# 额外测试案例
p2 = [40, 20, 30, 10, 30]
m2, s2 = matrix_chain_order(p2)
print(&quot;\n第二个测试案例:&quot;)
print(f&quot;矩阵维度数组: {p2}&quot;)
print(f&quot;最小标量乘法次数: {m2[1][4]}&quot;) # 预期结果为 26000
print(f&quot;最优括号化方案: {print_optimal_parens(s2, 1, 4)}&quot;)
</code></pre>
<h4>综合讨论</h4>
<ul>
<li>何时选“记忆化 Top-Down”何时选“自底向上表格法”</li>
<li>如何从“纯递归”快速判断是否值得改造成 DP</li>
</ul>
</body>
</html>