cdn fix more

This commit is contained in:
Cai
2025-11-17 14:04:52 +08:00
parent 7dc4714ae9
commit 0852e12118
25 changed files with 22539 additions and 0 deletions

View 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]) -&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>