算法设计与分析


贪心法

计算机学院    张腾

tengzhang@hust.edu.cn

课程大纲

算法设计与分析分治法动态规划贪心法回溯法分支限界法迭代改进磁带存储排列问题活动选择问题霍夫曼编码最小生成树单源最短路径

磁带存储

磁带特点:读取文件时需将目标文件之前的文件顺序读一遍

磁带存储

设磁带上存储着$n$个文件,长度分别为$L[1, \ldots, n]$,读取第$k$个文件的开销为$c(k) = \sum_{i=1}^k L[i]$

随机读取一个文件的期望开销为$\Ebb[c] = \frac{1}{n} \sum_{k=1}^n \sum_{i=1}^k L[i]$

设$\pi$是一个排列,$\pi(i)$表示磁带上第$i$个文件的实际文件编号,随机读取一个文件的期望开销为$\Ebb[c(\pi)] = \frac{1}{n} \sum_{k=1}^n \sum_{i=1}^k L[\pi(i)]$

问题:文件如何排列可使期望开销最低?

磁带存储

暴力穷举:$\Omega(n!)$

动态规划?

最优子结构性:设$\pi(1), \ldots, \pi(n)$是$\{ \text{文件}1,\ldots, \text{文件}n \}$的最优存储顺序,则$\pi(2), \ldots, \pi(n)$是$\{ \text{文件}1,\ldots, \text{文件}n \}\setminus \{\text{文件}\pi(1)\}$的最优存储顺序

确定$\pi(1)$会产生$n$个规模为$n-1$的子问题;对每个子问题,确定$\pi(2)$会产生$n-1$个规模为$n-2$的子问题;……;对每个子问题,确定$\pi(n-1)$会产生$2$个规模为$1$的子问题;这些子问题各不相同,总数为$n!$,因此动态规划与暴力穷举复杂度相当!

磁带存储 贪心

初始磁带为空,文件集合为$\{ \text{文件}1,\ldots, \text{文件}n \}$

贪心选择:将文件集合中长度最短的文件存入磁带

和动态规划不同,贪心法每轮贪心选择后只产生一个子问题:选文件集合中长度最短的文件

磁带存储 贪心正确性

引理:对$\forall \pi$,若存在$i$使得$L[\pi(i)] \ge L[\pi(i+1)]$,记$a = \pi(i)$$b = \pi(i+1)$,交换文件$a$、文件$b$后得到的新排列为$\pi'$,则$\Ebb[c(\pi')] \le \Ebb[c(\pi)]$

证明:交换文件$a$、文件$b$后

  • 文件$a$的读取开销增加$L[\pi(i+1)]$
  • 文件$b$的读取开销减少$L[\pi(i)]$

总的开销减少$\Ebb[c(\pi)] - \Ebb[c(\pi')] = L[\pi(i)] - L[\pi(i+1)] \ge 0$

磁带存储 贪心正确性

$L[1, \ldots, n]$的增序排列为$\pi^{\text{ins}}$,最优排列$\pi^\star$$\pi^{\text{ins}}$的第一处不同位置$\ge k$,即$\pi^\star[k] \ne \pi^{\text{ins}}[k] = \pi^\star[t]$

$\pi^{\text{ins}}[1]$ $\cdots$ $\pi^{\text{ins}}[k-1]$ $\pi^{\text{ins}}[k]$ $\cdots$  
$=$ $=$ $=$ $\ne$    
$\pi^\star[1]$ $\cdots$ $\pi^\star[k-1]$ $\pi^\star[k]$ $\cdots$ $\pi^\star[t] = \pi^{\text{ins}}[k]$

$\pi^{\text{ins}}$的单调性,$L[\pi^\star[t]] \le L[\pi^\star[k], \ldots, L[\pi^\star[t-1]]$,不断交换$\pi^\star[t]$与其前一个元素直到到达位置$k$,得到新排列$\pi'$,由引理知每次交换期望读取开销不增,故$\pi'$也是最优排列,且与$\pi^{\text{ins}}$的第一处不同位置$\ge k+1$

由归纳法知存在最优排列与$\pi^{\text{ins}}$的第一处不同位置$\ge n$,因此$\pi^{\text{ins}}$就是最优排列

磁带存储 贪心

假设除长度外,还有频率$F[1, \ldots, n]$

随机读取一个文件的期望开销为

$$ \begin{align*} \quad \Ebb[c(\pi)] = \frac{1}{n} \sum_{k=1}^n \sum_{i=1}^k L[\pi(i)] F[\pi(k)] \end{align*} $$

磁带存储 贪心

引理:对$\forall \pi$,若存在$i$使$\frac{L[\pi(i)]}{F[\pi(i)]} \ge \frac{L[\pi(i+1)]}{F[\pi(i+1)]}$,记$a = \pi(i)$$b = \pi(i+1)$,交换文件$a$、文件$b$后得到的新排列为$\pi'$,则$\Ebb[c(\pi')] \le \Ebb[c(\pi)]$

证明:交换文件$a$、文件$b$后

  • 文件$a$的期望读取开销增加$F[a] L[\pi(i+1)] = F[\pi(i)] L[\pi(i+1)]$
  • 文件$b$的期望读取开销减少$F[b] L[\pi(i)] = F[\pi(i+1)] L[\pi(i)]$

$$ \begin{align*} \quad \Ebb[c(\pi)] - \Ebb[c(\pi')] & = F[\pi(i+1)] L[\pi(i)] - F[\pi(i)] L[\pi(i+1)] \\ & = F[\pi(i+1)] F[\pi(i)] \left( \frac{L[\pi(i)]}{F[\pi(i)]} - \frac{L[\pi(i+1)]}{F[\pi(i+1)]} \right) \ge 0 \end{align*} $$

贪心选择:将文件集合中长度/频率最小的文件存入磁带

贪心法

贪心法:分步操作,每步取局部最优,最终得到全局最优解

贪心法只对部分最优化问题有效

经典算法

  • 最小生成树的 Prim 算法、Kruskal 算法
  • 单源最短路径的 Dijkstra 算法
活动选择

现有$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} \}$是兼容活动集合

后两者都是最大兼容活动集合,最大兼容活动集合不唯一

活动选择 最优子结构

$\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_{ij}$中$a_k$开始前的活动集合,则也是$\Scal_{ik}$的最大兼容活动集合
  • 设$\Acal_{kj}$为$\Acal_{ij}$中$a_k$结束后的活动集合,则也是$\Scal_{kj}$的最大兼容活动集合
  • $\Acal_{ij} = \Acal_{ik} \cup \{a_k\} \cup \Acal_{kj}$

若$\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$兼容的活动中结束时间最早的

  • 首次选择$a_1$
  • 其后选择结束时间最早 (贪心) 且开始时间不早于前一个所选活动结束时间 (兼容) 的活动

时间复杂度

  • 若活动已排好序,只需$\Theta(n)$的时间遍历一遍活动集合
  • 若活动未排好序,可先用$\Theta(n \lg n)$的时间重排序

贪心选择使剩余可安排时间最大化,给未来留下足够的余地

活动选择 例子

gantt todayMarker off dateFormat HH axisFormat %H section 选第2个活动 a1: active, 01, 04 a2: done, 03, 05 a3: done, 00, 06 a4: done, 05, 07 section 选第3个活动 a1: active, 01, 04 a4: active, 05, 07 a5: done, 03, 09 a6: done, 05, 09 a7: done, 06, 10 a8: done, 08, 11 section 选第4个活动 a1: active, 01, 04 a4: active, 05, 07 a8: active, 08, 11 a9: done, 08, 12 a10: done, 02, 14 a11: done, 12, 16 section 兼容活动 a1: active, 01, 04 a4: active, 05, 07 a8: active, 08, 11 a11: active, 12, 16
活动选择 实现

自顶向下递归实现,每次选择将问题转化成一个规模更小的问题

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 S = \{ g_1, ~ g_2, ~ \ldots, g_{i-1}, ~ g_i, ~ g_{i+1}, ~ \ldots, ~ g_k \} \end{align*} $$

某个最大兼容活动集合为

$$ \begin{align*} \quad S^\star = \{ 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 S' = \{ g_1, ~ g_2, ~ \ldots, g_{i-1}, ~ g_i, ~ c_{i+1}, ~ \ldots, ~ c_l \} \end{align*} $$

由归纳法$\{ g_1, ~ g_2, ~ \ldots, g_{i-1}, ~ g_i, ~ \ldots, ~ g_k, ~ c_{k+1}, ~ \ldots, ~ c_l \}$是最大兼容活动集合,由贪心法的选择知$k = l$

贪心法的一般步骤

  1. 确定问题的最优子结构
  2. 将问题转化为一系列选择,每次选择后只剩一个子问题
  3. 证明每次做出贪心选择后,若最优解不包含贪心选择,对该最优解进行剪切-粘贴,将其一部分替换为贪心选择,这样构造出的解也是最优解

最优子结构可贪心选择是贪心法的两个关键

文件编码

压缩一个只包含 a、b、c、d、e、f 的 10w 个字符的数据文件

字符 a b c d e f
频率 45 13 12 16 9 5
定长编码 000 001 010 011 100 101
变长编码 0 101 100 111 1101 1100
  • 定长编码:3 * 10w = 30w 个二进制位
  • 变长编码:约 22.4w 个二进制位,节约 25%空间

前缀码:码字互不为前缀,可以保证解码时无歧义

0101100 -> abc

编码树

  • 每个字符对应一个叶子结点
  • 字符的码字由根结点到该字符叶子结点的路径表示

g 100 100 86 86 100->86 0 14 14 100->14 1 58 58 86->58 0 28 28 86->28 1 n1 14 14->n1 0 15 15 14->15 a:45 a:45 58->a:45 0 b:13 b:13 58->b:13 1 c:12 c:12 28->c:12 0 d:16 d:16 28->d:16 1 e:9 e:9 n1->e:9 0 f:5 f:5 n1->f:5 1 e:10 e:10 15->e:10 e:11 e:11 15->e:11

g 100 100 a:45 a:45 100->a:45 0 55 55 100->55 1 a:46 a:46 a:45->a:46 a:47 a:47 a:45->a:47 25 25 55->25 0 30 30 55->30 1 c:12 c:12 25->c:12 0 b:13 b:13 25->b:13 1 14 14 30->14 0 d:16 d:16 30->d:16 1 f:5 f:5 14->f:5 0 e:9 e:9 14->e:9 1

左:定长编码树,右:变长编码树

最优编码树必然是满二叉树(右),每个内部结点有两个子结点

最优前缀码

$\Ccal$为字符表,对$\forall c \in \Ccal$,令$c.f$$c$在文件中出现的频率

$\Tcal$为任意前缀编码树,令$d_{\Tcal}(c)$表示字符$c$对应的叶子结点在$\Tcal$中的深度,也是$c$的码字的长度

采用编码方案$\Tcal$时,文件的编码长度

$$ \begin{align*} \quad B(\Tcal) = \sum_{c \in \Ccal} c.f \times d_{\Tcal}(c) \end{align*} $$

使得$B(\Tcal)$最小的前缀码称为最优前缀码

霍夫曼编码是一种最优前缀码

霍夫曼编码

字符 a b c d e f
频率 45 13 12 16 9 5

g 1) 1) f:5 f:5 e:9 e:9 c:12 c:12 b:13 b:13 d:16 d:16 a:45 a:45

g 2) 2) c:12 c:12 b:13 b:13 14 14 f:5 f:5 14->f:5 0 e:9 e:9 14->e:9 1 d:16 d:16 a:45 a:45

g 3) 3) 14 14 f:5 f:5 14->f:5 0 e:9 e:9 14->e:9 1 d:16 d:16 25 25 c:12 c:12 25->c:12 0 b:13 b:13 25->b:13 1 a:45 a:45

g 4) 4) 25 25 c:12 c:12 25->c:12 0 b:13 b:13 25->b:13 1 30 30 14 14 30->14 0 d:16 d:16 30->d:16 1 f:5 f:5 14->f:5 0 e:9 e:9 14->e:9 1 a:45 a:45

g 5) 5) a:45 a:45 55 55 25 25 55->25 0 30 30 55->30 1 c:12 c:12 25->c:12 0 b:13 b:13 25->b:13 1 14 14 30->14 0 d:16 d:16 30->d:16 1 f:5 f:5 14->f:5 0 e:9 e:9 14->e:9 1

霍夫曼编码

最终霍夫曼编码树为

字符 a b c d e f
频率 45 13 12 16 9 5

g 100 100 a:45 a:45 100->a:45 0 55 55 100->55 1 a:46 a:46 a:45->a:46 a:47 a:47 a:45->a:47 25 25 55->25 0 30 30 55->30 1 c:12 c:12 25->c:12 0 b:13 b:13 25->b:13 1 14 14 30->14 0 d:16 d:16 30->d:16 1 f:5 f:5 14->f:5 0 e:9 e:9 14->e:9 1 a a a:46->a b b a:46->b c c a:47->c d d a:47->d

霍夫曼算法 贪心选择

g 100 100 a:45 a:45 100->a:45 0 55 55 100->55 1 a:46 a:46 a:45->a:46 a:47 a:47 a:45->a:47 25 25 55->25 0 30 30 55->30 1 c:12 c:12 25->c:12 0 b:13 b:13 25->b:13 1 14 14 30->14 0 d:16 d:16 30->d:16 1 f:5 f:5 14->f:5 0 e:9 e:9 14->e:9 1

编码长度$B(\Tcal) = \sum_{c \in C} c.f \times d_{\Tcal}(c)$

$B(\Tcal)$也等于编码树中所有结点频率值的和

证明依赖以下事实:

  • 父结点频率值等于两个子结点频率值值和
  • 祖先结点频率值等于其后代叶子结点频率值的和
  • 对任意字符$c$,其深度$d_{\Tcal}(c) = |\text{祖先结点}|+1$

满二叉树:$|\text{内部结点}| = |\text{叶子结点}| - 1 = |\Ccal| - 1$

内部结点均是由其它两个结点合并出来的

贪心选择:每次选频率最低的两个结点合并

霍夫曼算法 贪心选择

$x$$y$$\Ccal$中频率最低的两个字符,则存在$\Ccal$的一个最优前缀码,在对应的编码树中,$x$$y$是一对最深的兄弟叶子结点

证明:反设$\Tcal$是最优编码树,$x$$y$不是一对兄弟叶子结点

$x$$y$$\Tcal$中最深的一对兄弟叶子结点$a$$b$交换会如何?

g 1 2 1->2 x x 1->x y y 2->y 3 2->3 a a 3->a b b 3->b

g 1 2 1->2 a a 1->a y y 2->y 3 2->3 x x 3->x b b 3->b

g 1 2 1->2 a a 1->a b b 2->b 3 2->3 x x 3->x y y 3->y

霍夫曼算法 贪心选择

$a$$x$交换后的新树为$\Tcal'$$d_{\Tcal'}(a) = d_{\Tcal}(x)$$d_{\Tcal'}(x) = d_{\Tcal}(a)$

不难证明$\Tcal'$也是最优前缀树,同理再将$b$$y$交换后依然还是

$$ \begin{align*} \quad \Delta B(\Tcal) & = a.f \times d_{\Tcal'}(a) + x.f \times d_{\Tcal'}(x) - a.f \times d_{\Tcal}(a) - x.f \times d_{\Tcal}(x) \\ & = a.f \times d_{\Tcal}(x) + x.f \times d_{\Tcal}(a) - a.f \times d_{\Tcal}(a) - x.f \times d_{\Tcal}(x) \\ & = \underbrace{(a.f - x.f)}_{\ge ~ 0} \underbrace{(d_{\Tcal}(x) - d_{\Tcal}(a))}_{\le ~ 0} \le 0 \end{align*} $$

g 1 2 1->2 x x 1->x y y 2->y 3 2->3 a a 3->a b b 3->b

g 1 2 1->2 a a 1->a y y 2->y 3 2->3 x x 3->x b b 3->b

$$ \begin{align*} \quad \qquad \quad \Tcal \qquad \qquad \qquad \qquad \quad \quad \Tcal' \end{align*} $$

最优子结构

$\Ccal' = \Ccal \setminus \{ x,y \} \cup \{ z \}$$z.f = x.f + y.f$,其它字符频率不变

$\Tcal'$$\Ccal'$的任一最优前缀码树,将$\Tcal'$$z$对应的叶子结点替换为以$x$$y$为孩子的内部结点,则得到的树$\Tcal$$\Ccal$的一个最优前缀码树

g 1 2 1->2 a 1->a b 2->b z z 2->z

g 1 2 1->2 a 1->a b 2->b 3 2->3 x x 3->x y y 3->y

$$ \begin{align*} \quad \qquad \qquad \qquad \qquad \Tcal' \qquad \qquad \qquad \qquad \quad ~ \Tcal \end{align*} $$

最优子结构

证明:反设$\Tcal$对应的前缀码不是$\Ccal$的最优前缀码

最优前缀码树为$\Tcal''$,于是$B(\Tcal'') < B(\Tcal)$

$\Tcal''$中的$x$$y$和它们的父节点整体替换成$z$,得到$\Tcal'''$

注意编码长度等于树中所有结点频率值的和

$B(\Tcal''') = B(\Tcal'') - x.f - y.f$$B(\Tcal) = B(\Tcal') + x.f + y.f$

两式相加可得$B(\Tcal''') = B(\Tcal'') - B(\Tcal) + B(\Tcal') < B(\Tcal')$

$\Tcal'$不可能是$\Ccal'$的最优前缀码树,矛盾!

小结

前一个命题表明选择频率最低的两个字符可以构造最优前缀码树

后一个命题表明将频率最低的两个字符$x$$y$合并成$z$后加入字符集合,由此构造出的最优前缀码树再将$z$对应的叶子结点作为内部结点分解成$x$$y$两个叶子结点,依然还是最优前缀码树

问题描述

输入:带权无向图$\Gcal = (\Vcal, \Ecal)$,其中$\Vcal$为边集、$\Ecal$为点集,边上的权重由函数$w: \Ecal \mapsto \Rbb$给出

输出:最小权重生成树$\Tcal$

思路

初始化边集$\Acal = \emptyset$,之后每轮选择一条边$(u,v)$加入$\Acal$

在此过程中保证$\Acal$始终是某棵最小生成树的子集

问题:如何选择边$(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$的最小生成树,但$(u,v) \not \in \Tcal$

不妨设$\Tcal$$u$$v$的路径$p = \class{yellow}{u \rightsquigarrow w \rightsquigarrow x} \rightsquigarrow \class{blue}{b \rightsquigarrow a \rightsquigarrow v}$

$(u,v)$横跨切割,$p$中也必有一边横跨切割,不妨设为$(x,b)$

当前切割尊重$\Acal$,故$(x,b) \not \in \Acal$

$\Tcal' = \Tcal \setminus \{ (x,b) \} \cup \{ (u,v) \}$

$w(u,v) \le w(x,b)$$\Tcal'$也是最小生成树

$\Acal \cup \{ (u,v) \} \subseteq \Tcal'$

g ⓖ->ⓦ ⓦ->ⓧ ⓤ->ⓦ ⓤ->ⓥ ⓧ->ⓨ ⓧ->ⓑ ⓥ->ⓐ ⓐ->ⓑ ⓑ->ⓒ ⓑ->ⓓ

最小生成树 算法

Kruskal 算法:$\Acal$始终是一个森林

  • 初始时$\Acal$是所有单结点树构成的森林
  • 每次加入连接$\Acal$中不同两棵树的权重最小的边
  • 两棵树作为切割的两方,其它树随意属于某一方

Prim 算法:$\Acal$始终是一棵树

  • 初始时$\Acal$是某个单结点树
  • 每次加入连接$\Acal$$\Acal$之外结点的权重最小的边
  • $\Acal$作为切割的一方,$\Acal$之外的结点作为切割另一方
Kruskal 算法

$\Acal$始终是一个森林,每次加入连接$\Acal$中两棵树的权重最小边

Prim 算法

$\Acal$始终是一个树,每次加入连接$\Acal$$\Acal$之外结点的权重最小边

作业

算法导论 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]$