磁带特点:读取文件时需将目标文件之前的文件顺序读一遍
现有$n$个文件,编号分别为$1, \ldots, n$,长度分别为$L[1], \ldots, L[n]$
将这些文件按$\pi(1), \ldots, \pi(n)$的顺序保存到磁带上,其中$\pi$是一个排列,$\pi(i)$表示磁带上第$i$个文件的实际文件编号
读取第$k$个文件的开销为$c(k) = \sum_{i=1}^k L[\pi(i)]$,所有文件的平均读取开销为$\Ebb_{\pi} [c] = \frac{1}{n} \sum_{k=1}^n c(k) = \frac{1}{n} \sum_{k=1}^n \sum_{i=1}^k L[\pi(i)]$
问题:求最优排列$\pi^\star$使平均读取开销$\Ebb_{\pi} [c]$最小
共享资源的任务调度都属于这类问题
暴力穷举:$\Omega(n!)$,动态规划?
最优子结构性:$\pi^\star(1), \ldots, \pi^\star(n)$是所有文件的最优存储顺序,易知$\pi^\star(2), \ldots, \pi^\star(n)$是除文件$\pi^\star(1)$外的所有文件的最优存储顺序
确定$\pi^\star(1)$会产生$n$个规模为$n-1$的子问题;对每个子问题,确定$\pi^\star(2)$会产生$n-1$个规模为$n-2$的子问题;……;对每个子问题,确定$\pi^\star(n-1)$会产生$2$个规模为$1$的子问题;这些子问题各不相同,总数为$n!$,因此动态规划与暴力穷举复杂度相当!
当前存入磁带的文件会出现在后续存入文件的读取开销计算中,故应优先存长度短的文件
贪心法:初始磁带为空,每轮迭代做贪心选择 - 将剩余文件中长度最短的文件存入磁带
和动态规划不同,贪心法每轮贪心选择后只产生一个子问题,时间复杂度与排序相同,$\Theta ( n \lg n )$
贪心法给出的就是$L[1], \ldots, L[n]$的增序排列$\pi^{\text{ins}}$
对任意排列$\pi \ne \pi^{\text{ins}}$,必存在$i$使得$L[\pi(i)] \ge L[\pi(i+1)]$,交换文件$\pi(i)$、$\pi(i+1)$后得到的新排列为$\pi'$,则$\Ebb_{\pi'} [c] \le \Ebb_{\pi} [c]$
证明:交换文件$\pi(i)$、文件$\pi(i+1)$后
平均读取开销变化$\Ebb_{\pi'} [c] - \Ebb_{\pi} [c] = L[\pi(i+1)] - L[\pi(i)] \le 0$
对任意排列,包括最优的$\pi^\star$,都可以通过变化非正的交换操作使其变成$\pi^{\text{ins}}$,因此$\pi^{\text{ins}}$就是使得平均读取开销最小的排列
假设除长度外,还有读取频率$F[1], \ldots, F[n]$
读取第$k$个文件的开销为$c(k) = \sum_{i=1}^k L[\pi(i)] F[\pi(k)]$,所有文件的平均读取开销为
$$ \begin{align*} \quad \Ebb_{\pi} [c] = \frac{1}{n} \sum_{k=1}^n c(k) = \frac{1}{n} \sum_{k=1}^n \sum_{i=1}^k L[\pi(i)] F[\pi(k)] \end{align*} $$
假设所有文件长度相同,则应优先存高频文件,不难猜正确的贪心选择为:将剩余文件中长度/频率最小的文件存入磁带
对任意排列$\pi \ne \pi^{\text{greedy}}$,必存在$i$使得$\frac{L[\pi(i)]}{F[\pi(i)]} \ge \frac{L[\pi(i+1)]}{F[\pi(i+1)]}$,交换文件$\pi(i)$、$\pi(i+1)$后得到的新排列为$\pi'$,则$\Ebb_{\pi'} [c] \le \Ebb_{\pi} [c]$
证明:交换文件$\pi(i)$、文件$\pi(i+1)$后
平均读取开销变化非正:
$$ \begin{align*} \quad \Ebb_{\pi'} [c] - \Ebb_{\pi} [c] & = F[\pi(i)] L[\pi(i+1)] - F[\pi(i+1)] L[\pi(i)] \\ & = F[\pi(i+1)] F[\pi(i)] \left( \frac{L[\pi(i+1)]}{F[\pi(i+1)]} - \frac{L[\pi(i)]}{F[\pi(i)]} \right) \le 0 \end{align*} $$
贪心法:将问题转化为连续决策,每次决策做贪心选择,只产生一个规模更小的子问题
贪心法的优缺点
经典算法
现有$n$个活动,编号分别为$1, \ldots, n$,时长分别为$L[1], \ldots, L[n]$,最后期限为$D[1], \ldots, D[n]$
在安排方案$\pi$中,活动$k$的结束时间为$c_{\pi} (k) = \sum_{i=1}^k L[\pi(i)]$,记延迟$\lambda_{\pi} (k) = \max \{ 0, c_{\pi} (k) - D[k] \}$
问题:求最优排列$\pi^\star$使延迟之和$\sum_{k=1}^n \lambda_{\pi} (k)$最小
贪心选择:
举反例可以排除错误选项,假设有两个活动:
按$L[k]$升序、按$L[k] + D[k]$升序、按$L[k] \cdot D[k]$升序都是先活动$1$、后活动$2$,因此都不是正确的贪心选择
举反例可以排除错误选项,假设有两个活动:
按$D[k]$升序是先活动$1$、后活动$2$,不是正确的贪心选择
问题:延迟之和$\sum_{k=1}^n \lambda_{\pi} (k)$最小 => 最大延迟$\max_k \lambda_{\pi} (k)$最小
假设有两个活动:
除按$D[k]$升序外,其它都是先活动$1$、后活动$2$,都不是正确的贪心选择
按$D[k]$升序是正确的贪心选择
对任意活动排列$\pi$,若存在$i$使得$D[\pi(i)] \ge D[\pi(i+1)]$,交换活动$\pi(i)$、$\pi(i+1)$后得到新排列$\pi'$
其余$n-2$个活动的延迟情况不变,记$M = \sum_{j=1}^{i-1} L[\pi(j)]$
显然$\lambda_{\pi'} (i) \le \lambda_\pi (i+1)$、$\lambda_{\pi'} (i+1) \le \lambda_\pi (i+1)$,故
$$ \begin{align*} \quad \max \{ \lambda_{\pi'} (i), \lambda_{\pi'} (i+1) \} \le \lambda_\pi (i+1) \le \max \{ \lambda_\pi (i), \lambda_\pi (i+1) \} \end{align*} $$
现有$n$个活动$\Scal = \{ a_1, a_2, \ldots, a_n \}$,活动$a_i$的时间段为$[s_i, f_i)$
这些活动使用同一资源且不能共用,如会场等
如果两个活动的时间段不重叠,则称它们是兼容的
输入:$\Scal = \{ a_1 = [s_1, f_1), a_2 = [s_2, f_2), \ldots, a_n = [s_n, f_n) \}$
输出:从$\Scal$中选出最大兼容活动集合
假设活动已按结束时间单调递增排序$f_1 \le f_2 \le \cdots \le f_n$
有$n = 11$个活动
| $i$ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| $s_i$ | 1 | 3 | 0 | 5 | 3 | 5 | 6 | 8 | 8 | 2 | 12 |
| $f_i$ | 4 | 5 | 6 | 7 | 9 | 9 | 10 | 11 | 12 | 14 | 16 |
$\{ a_3, a_9, a_{11} \}$、$\{ a_1, a_4, a_8, a_{11} \}$、$\{ a_2, a_4, a_9, a_{11} \}$是兼容活动集合
后两者都是最大兼容活动集合,最大兼容活动集合不唯一
暴力穷举:$\Omega(2^n)$
设$\Scal_{ij}$表示$a_i$结束后开始、$a_j$开始前结束的活动集合
$$ \begin{align*} \quad \Scal_{ij} & = \{ a_k = [s_k, f_k) \mid f_i \le s_k < f_k \le s_j \} \\[10px] a_1, & ~ \ldots, ~ a_i, ~ \underbrace{\overbrace{a_{i+1}, ~ \ldots, ~ a_{k-1}}^{\Scal_{ik}}, ~ a_k, ~ \overbrace{a_{k+1}, ~ \ldots, ~ a_{j-1}}^{\Scal_{kj}}}_{\Scal_{ij}}, ~ a_j, ~ \ldots, ~ a_n \end{align*} $$
最优子结构性:设$\Acal_{ij}$是$\Scal_{ij}$的最大兼容活动集合并包含$a_k$
若$\Acal_{ik}$不是最大,存在$\Acal'_{ik}$更大,则$\Acal'_{ik} \cup \{a_k\} \cup \Acal_{kj}$更大,矛盾
设$\Acal_{ij}$包含$a_k$,$\Acal_{ij} = \Acal_{ik} \cup \{a_k\} \cup \Acal_{kj}$
令$c[i,j] = |\Acal_{ij}|$表示$\Scal_{ij}$的最大兼容活动集合的大小
$$ \begin{align*} \quad c[i,j] & = c[i,k] + c[k,j] + 1 \\[6px] \Longrightarrow & ~ c[i,j] = \begin{cases} 0, & \Scal_{ij} = \emptyset \\ \max_{a_k \in \Scal_{ij}} \{ c[i,k] + c[k,j] + 1 \}, & \Scal_{ij} \ne \emptyset \end{cases} \end{align*} $$
动态规划:时间复杂度$\Theta(n^3)$,空间复杂度$\Theta(n^2)$
初始化最大兼容活动集合$\Acal = \emptyset$,不断将与$\Acal$兼容的活动加入$\Acal$
贪心选择:选择与$\Acal$兼容的活动中结束时间最早的活动,使剩余可安排时间最大化,给未来留下足够的余地
时间复杂度
自顶向下递归实现,每次选择将问题转化成一个规模更小的问题
def activity_selector_rec(k, a): m = k + 1 while m < n and s[m] < f[k]: # 贪心选择寻找ak之后最早结束的活动 m = m + 1 if m < n: # 若找到 将其加入集合 递归寻找下一个兼容活动 a.append(m) # 将am加入集合 activity_selector_rec(m, a)
尾递归 => 迭代,一重 for 循环时间复杂度$\Theta(n)$
def activity_selector_greedy(): a = [1] # a1直接选择 k = 1 for m in range(1, n): # 从前向后遍历a1之后的活动 if s[m] > f[k]: # 若找到ak之后最早结束的活动am a.append(m) # 将am加入集合 k = m # 从am之后的活动中继续寻找 return a
设贪心法得到的兼容活动集合为
$$ \begin{align*} \quad \Scal^{\text{greedy}} = \{ g_1, ~ g_2, ~ \ldots, g_{i-1}, ~ g_i, ~ g_{i+1}, ~ \ldots, ~ g_k \} \end{align*} $$
任意最大兼容活动集合$\Scal \ne \Scal^{\text{greedy}}$可表示为
$$ \begin{align*} \quad \Scal = \{ g_1, ~ g_2, ~ \ldots, g_{i-1}, ~ c_i, ~ c_{i+1}, ~ \ldots, ~ c_l \} \end{align*} $$
将$c_i$替换为$g_i$得到新的最大兼容活动集合 (替换的合法性?)
$$ \begin{align*} \quad \Scal' = \{ g_1, ~ g_2, ~ \ldots, g_{i-1}, ~ g_i, ~ c_{i+1}, ~ \ldots, ~ c_l \} \end{align*} $$
对任意最大兼容活动集合,都可以通过替换活动使其变成$\Scal^{\text{greedy}}$,因此$\Scal^{\text{greedy}}$就是最大兼容活动集合
最优子结构和可贪心选择是贪心法的两个关键
压缩一个只包含$a$、$b$、$c$、$d$、$e$、$f$的$100$个字符的数据文件
| 字符 | $a$ | $b$ | $c$ | $d$ | $e$ | $f$ |
|---|---|---|---|---|---|---|
| 频率 | $45$ | $13$ | $12$ | $16$ | $9$ | $5$ |
| 定长编码 | $000$ | $001$ | $010$ | $011$ | $100$ | $101$ |
| 变长编码 | $0$ | $101$ | $100$ | $111$ | $1101$ | $1100$ |
问题:最优编码方案?
任何二进制编码都可以表示成一个二叉树,根节点到字符结点路径上遇到的$01$串就是其编码,以表示$a$、$b$、$c$、$d$四字符为例
解码:以右图方案为例,010110 -> abc
输入:字符表$\Ccal$,字符个数$n = |\Ccal| \ge 2$,每个字符$c$的频率$c.f$
输出:最优非前缀编码树
两点说明:
基本想法:维护一个森林,初始为每个字符对应的单结点树,每次合并两棵树,直到所有树合并成一棵树
字符$c$的编码长度是其在编码树$\Tcal$中的深度$d_{\Tcal}(c)$,所有字符的总编码长度$B(\Tcal) = \sum_{c \in \Ccal} c.f \cdot d_{\Tcal}(c)$
每次合并会导致树中结点深度$+1$,总编码长度$B(\Tcal)$的增量为两棵树中字符对应的叶结点的频率和
霍夫曼算法的贪心选择:合并叶结点频率和最低的两棵树
霍夫曼算法来自他在 MIT 攻读博士学位时修读的信息论课程的学期报告:最有效的二进制编码,老师法诺曾提出自顶向下构建树的香农-范诺编码,霍夫曼使用自底向上构建树得到了更优的编码。
霍夫曼算法的贪心选择:合并叶结点频率和最低的两棵树
初级实现:利用列表保存结点,总时间复杂度$\Theta(n^2)$
高级实现:利用二叉堆保存结点,总时间复杂度$\Theta(n \lg n)$
import heapq # python自带的二叉堆库 class HuffmanNode: # 编码树结点类 def __init__(self, symbol, frequency): self.symbol = symbol self.frequency = frequency self.left = None self.right = None def __lt__(self, other): # 比较两个HuffmanNode实例时调用 return self.frequency < other.frequency def get_min_freq(l): # 删除并返回结点列表中频率最小的结点 node, index = l[0], 0 for i in range(1, len(l)): if l[i] < node: node, index = l[i], i return l.pop(index) def merge(left_node, right_node): # 合并两个结点 merged_node = HuffmanNode(None, left_node.frequency + right_node.frequency) # 合并两个结点 merged_node.left, merged_node.right = left_node, right_node return merged_node def build_huffman_tree_elementary(C): # 用列表保存结点 l = [HuffmanNode(symbol, frequency) for symbol, frequency in C.items()] while len(l) > 1: # 最终l中只剩编码树的根结点 left_node = get_min_freq(l) # 寻找频率最小的结点 right_node = get_min_freq(l) # 寻找频率次小的结点 l.append(merge(left_node, right_node)) # 插入合并的结点 return l[0] def build_huffman_tree_advanced(C): # 用二叉堆保存结点 l = [HuffmanNode(symbol, frequency) for symbol, frequency in C.items()] heapq.heapify(l) # 将列表l转换成二叉堆 原地 线性时间 while len(l) > 1: # 最终l中只剩编码树的根结点 left_node = heapq.heappop(l) # 寻找频率最小的结点 right_node = heapq.heappop(l) # 寻找频率次小的结点 heapq.heappush(l, merge(left_node, right_node)) # 插入合并的结点 return l[0] def huffman_codes(node, current_code="", code_map=None): # 递归保存字符的编码 if code_map is None: code_map = {} if node is not None: if node.symbol is not None: # 字符对应的结点 code_map[node.symbol] = current_code huffman_codes(node.left, current_code + "0", code_map) huffman_codes(node.right, current_code + "1", code_map) return code_map C = {"a": 45, "b": 13, "c": 12, "d": 16, "e": 9, "f": 5} for f in [build_huffman_tree_elementary, build_huffman_tree_advanced]: huffman_tree_root = f(C) huffman_code_map = huffman_codes(huffman_tree_root) for symbol, code in huffman_code_map.items(): print(f"{symbol}: {code}") # ---------------------------------------------------- # a: 0 # c: 100 # b: 101 # f: 1100 # e: 1101 # d: 111 # a: 0 # c: 100 # b: 101 # f: 1100 # e: 1101 # d: 111
磁带存储问题和最大兼容活动问题的解的形式都是序列
算法正确性的证明思路:
以此类推:文件编码问题的解的形式是编码树 (满二叉树)
设$x$和$y$是$\Ccal$中频率最低的两个字符,霍夫曼算法算法首轮将$x$和$y$合并成字符$z$,且其频率$z.f = x.f + y.f$,此时字符表变成$\Ccal' = \Ccal \setminus \{ x,y \} \cup \{ z \}$,只剩$n-1$个字符,数学归纳法?
对字符表的字符个数$n$做归纳
当$n = 2$时,只有$2$个字符,显然最优编码为一个字符编码为$0$,另一个编码为$1$,霍夫曼算法的结果亦是如此
归纳假设:假设霍夫曼算法对$n-1$个字符的字符表能输出最优编码树,特别的对$\Ccal'$输出最优编码树$\Tcal'$,将$\Tcal'$中$z$对应的叶结点替换为以$x$、$y$为叶子结点的内部结点,得到编码树$\Tcal$
$\Tcal$是关于$\Ccal$的编码树,其最优性如何?若为最优,如何证明?
$\Tcal'$中$z$对应的叶结点替换为以$x$、$y$为叶子结点的内部结点得到$\Tcal$,总编码长度的变化是个定值
$$ \begin{align*} \quad B(\Tcal) & - B(\Tcal') = x.f \cdot d_{\Tcal}(x) + y.f \cdot d_{\Tcal}(y) - z.f \cdot d_{\Tcal'}(z) \\ & = x.f \cdot (d_{\Tcal'}(z) + 1) + y.f \cdot (d_{\Tcal'}(z) + 1) - (x.f+y.f) \cdot d_{\Tcal'}(z) \\ & = x.f+y.f \end{align*} $$
不仅仅是$\Tcal'$,对任一关于$\Ccal'$的编码树都是如此
$\Tcal'$关于$\Ccal'$最优 => $\Tcal$在关于$\Ccal$且以$x$、$y$为一对兄弟结点的编码树中最优
霍夫曼算法给出的编码树$\Tcal^{\text{greedy}}$中,$x$、$y$也是一对兄弟结点且深度最深
假设$\Tcal$中$x$、$y$不是最深,最深的是$a$、$b$,交换$a$和$x$
$$ \begin{align*} \quad \Delta B & = a.f \cdot (d_{\Tcal}(x) - d_{\Tcal}(a)) + x.f \cdot (d_{\Tcal}(a) - d_{\Tcal}(x)) \\ & = \underbrace{(a.f - x.f)}_{\ge ~ 0} \underbrace{(d_{\Tcal}(x) - d_{\Tcal}(a))}_{\le ~ 0} \le 0 \end{align*} $$
同理,交换$b$和$y$,总编码长度的变化非正
$\Tcal^{\text{greedy}}$在关于$\Ccal$且以$x$、$y$为一对兄弟结点的编码树中最优
最后还需证明:$\Ccal$的最优编码树中,存在以$x$、$y$为一对最深兄弟结点的
假设最优编码树$\Tcal^\star$中,$x$、$y$不是一对最深的兄弟结点,将其与最深的一对兄弟结点$a$、$b$交换,总编码长度的变化非正,因此存在以$x$、$y$为一对最深兄弟结点的最优编码树
输入:带权无向连通图$\Gcal = (\Vcal, \Ecal)$,其中$\Vcal$为边集、$\Ecal$为点集,边上的权重由函数$w: \Ecal \mapsto \Rbb$给出
输出:最小生成树 (minimum spanning tree, MST)
去掉边$(b,c)$,加上边$(a,h)$,也是 MST,MST 不唯一!
唯一性与权重相有关,若所有边权重相同,则任意 ST 都是 MST
定理:若$\Gcal$的边权重各不相同,则其 MST 唯一
证明逆否命题:若$\Tcal$、$\Tcal'$是不同的 MST,则$\Gcal$存在权重相同的边
$\Tcal$、$\Tcal'$不同 => $\Tcal$中存在不属于$\Tcal'$的边,设权重最小的为$e$、$\Tcal'$中存在不属于$\Tcal$的边,设权重最小的为$e'$
$\Tcal'$是树 => $\Tcal' \cup \{ e \}$包含环$\Ccal$
$\Tcal$是树 => $\Ccal$中包含边$e'' \not \in \Tcal$ => $w(e'') \ge w(e') \ge w(e)$
生成树$\Tcal'' = \Tcal' \cup \{ e \} \setminus \{ e'' \}$也是最小生成树 => $w(e) = w(e'')$
连续决策:
问题:如何选择边$(u,v)$?
切割:无向图$\Gcal = (\Vcal, \Ecal)$的切割$(\Scal, \Vcal \setminus \Scal)$是$\Vcal$的一个划分
横跨切割边:边的两个端点分属于$\Scal$和$\Vcal \setminus \Scal$,例如$(a,h)$
尊重:如果集合$\Acal$中没有横跨切割的边,称该切割尊重$\Acal$
轻量边:横跨切割边中权重最小的边,例如$(c,d)$
贪心:每轮对任意尊重当前$\Acal$的切割,选择轻量边$(u,v)$加入
正确性证明:设$\Tcal$是任一包含$\Acal$的 MST,但$(u,v) \not \in \Tcal$
$\Tcal$中存在$u$到$v$的路径$p = \class{yellow}{u \rightsquigarrow w \rightsquigarrow x} \rightsquigarrow \class{blue}{b \rightsquigarrow a \rightsquigarrow v}$
$p$中必有一边横跨切割,不妨设为$(x,b)$
$\Tcal' = \Tcal \setminus \{ (x,b) \} \cup \{ (u,v) \}$也是 MST
$\Acal \cup \{ (u,v) \} \subseteq \Tcal'$
$\Acal$始终是一棵树
$\Acal$始终是一棵树,每次加入连接$\Acal$和$\Acal$之外结点的权重最小边
基于二叉堆的实现,时间复杂度$O((|\Vcal| + |\Ecal|) \lg |\Vcal|)$
def prim(graph, start): min_spanning_tree = [] in_mst = {start} # 已在mst中的结点 # 初始点出发的边组成二叉堆 h = [(weight, start, neighbor) for neighbor, weight in graph[start].items()] heapq.heapify(h) while h: weight, node1, node2 = heapq.heappop(h) # 弹出堆中权重最小的边 if node2 not in in_mst: # 若是横跨切割的轻量边 in_mst.add(node2) min_spanning_tree.append((weight, f"{node1} - {node2}")) for node2_neighbor, weight in graph[node2].items(): if node2_neighbor not in in_mst: # node2出发的横跨切割的边入堆 heapq.heappush(h, (weight, node2, node2_neighbor)) return min_spanning_tree graph = { 'a': {'b': 4, 'h': 8}, 'b': {'a': 4, 'c': 8, 'h': 11}, 'c': {'b': 8, 'd': 7, 'i': 2, 'f': 4}, 'd': {'c': 7, 'e': 9, 'f': 14}, 'e': {'d': 9, 'f': 10}, 'f': {'c': 4, 'd': 14, 'e': 10, 'g': 2}, 'g': {'f': 2, 'h': 1, 'i': 6}, 'h': {'a': 8, 'b': 11, 'g': 1, 'i': 7}, 'i': {'c': 2, 'g': 6, 'h': 7} } mst_weight = 0 for edge in prim(graph, 'a'): mst_weight += edge[0] print(edge) print(f"最小生成树权重和为{mst_weight}") # ------------------------------------------- # (4, 'a - b') # (8, 'a - h') # (1, 'h - g') # (2, 'g - f') # (4, 'f - c') # (2, 'c - i') # (7, 'c - d') # (9, 'd - e') # 最小生成树权重和为37
$\Acal$始终是一个森林
$\Acal$始终是一个森林,每次加入连接$\Acal$中两棵树的权重最小边
基于父图的实现,时间复杂度$O((|\Vcal| + |\Ecal|) \lg |\Vcal|)$
def find(parent, node): # 返回给定结点所在树的根结点 if parent[node] != node: parent[node] = find(parent, parent[node]) return parent[node] def union(parent, size, node1, node2): root1 = find(parent, node1) root2 = find(parent, node2) if root1 != root2: # 结点少的树作为结点多的树的子树 if size[root1] >= size[root2]: parent[root2] = root1 size[root1] += size[root2] else: parent[root1] = root2 size[root2] += size[root1] def kruskal(graph): min_spanning_tree = [] # 保存图中所有边并排序 edges = [] for node, neighbors in graph.items(): for neighbor, weight in neighbors.items(): edges.append((weight, node, neighbor)) edges.sort() # 父图初始化 parent = {node: node for node in graph} size = {node: 1 for node in graph} # 遍历所有边 若为横跨切割的轻量边 合并两个端点所在的树 for edge in edges: weight, node1, node2 = edge if find(parent, node1) != find(parent, node2): union(parent, size, node1, node2) min_spanning_tree.append((weight, f"{node1} - {node2}")) return min_spanning_tree graph = { 'a': {'b': 4, 'h': 8}, 'b': {'a': 4, 'c': 8, 'h': 11}, 'c': {'b': 8, 'd': 7, 'i': 2, 'f': 4}, 'd': {'c': 7, 'e': 9, 'f': 14}, 'e': {'d': 9, 'f': 10}, 'f': {'c': 4, 'd': 14, 'e': 10, 'g': 2}, 'g': {'f': 2, 'h': 1, 'i': 6}, 'h': {'a': 8, 'b': 11, 'g': 1, 'i': 7}, 'i': {'c': 2, 'g': 6, 'h': 7} } mst_weight = 0 for edge in kruskal(graph): mst_weight += edge[0] print(edge) print(f"最小生成树权重和为{mst_weight}") # ------------------------------------------- # (1, 'g - h') # (2, 'c - i') # (2, 'f - g') # (4, 'a - b') # (4, 'c - f') # (7, 'c - d') # (8, 'a - h') # (9, 'd - e') # 最小生成树权重和为37
算法导论 3rd
16.1-4、16.2-7、16.3-3、16-1
求以下背包问题的最优解:
$n=7$,$M=15$,$\pv = [10,5,15,7,6,18,3]$,$\wv = [2,3,5,7,1,4,1]$