【算法】算法分析|贪心|分治|动态规划|回溯|分支限界全面解读

算法分析|贪心|分治|动态规划|回溯|分支限界全面解读

文章目录

算法基础

时间复杂度

定义

时间复杂度指程序执行时所用的时间

在解析地分析时间复杂度时,使用以下两种时间单位并计算:

  • 操作计数 (operation count):
    • 找出一个或多个关键操作,确定这些关键操作所需要的执行时间
  • (程序)步计数 (step count):
    • 确定程序总的执行步数
常用记号

紧确界 Θ \Theta Θ

Θ表示算法的渐进紧确上界和下界相同

算法的时间复杂度是Θ(f(n))时,表示该算法的运行时间在最坏情况下以f(n)的速度增长

存在正常数c1和c2,使得对于足够大的n,算法的运行时间在c1 * f(n) 和 c2 * f(n) 之间

上界 O O O

O表示算法的渐进最坏情况上界

算法的时间复杂度是O(f(n)),表示该算法的运行时间在最坏情况下以f(n)的速度增长,但不一定是最紧确的上界。

存在一个正常数c和n0,使得对于足够大的n,算法的运行时间不会超过c * f(n)。

下界 Ω \Omega Ω

Ω表示算法的渐进最坏情况下界

算法的时间复杂度是Ω(f(n)),表示该算法的运行时间在最坏情况下以f(n)的速度增长,但不一定是最紧确的下界。

存在一个正常数c和n0,使得对于足够大的n,算法的运行时间至少是c * f(n)。

非渐进紧确上界 o

o 表示算法的非渐进紧确上界

也就是说 o 的阶高于 f 的阶,不可能等于 f 的阶

非渐进紧确下界 ω \omega ω

ω 表示算法的非渐进紧确下界

也就是说 ω 的阶低于 f 的阶,不可能等于 f 的阶

递归

主定理

T ( n ) = a T ( n b ) + f ( n ) T(n) = aT(\dfrac{n}{b}) + f(n) T(n)=aT(bn)+f(n),其中 a ≥ 1 , b > 1 a , b ∈ N f ( n ) > 0 a \ge 1,b > 1\quad a,b\in N\quad f(n) > 0 a1,b>1a,bNf(n)>0

按照 f ( n ) f(n) f(n) n l o g b a n^{log_b a} nlogba 的渐近性质,分三种情形进行分析:

情形 1

f ( n ) f(n) f(n) 的增长渐近地慢于 n l o g b a n^{log_b a} nlogba ,即 f ( n ) = O ( n l o g b a − ε ) , ε > 0 f(n) = O(n^{log_b a - \varepsilon } ),\quad \varepsilon > 0 f(n)=O(nlogbaε),ε>0

结论: T ( n ) = Θ ( n l o g b a ) T(n) = \Theta(n^{log_b a}) T(n)=Θ(nlogba)

情形 2

f ( n ) f(n) f(n) n l o g b a n^{log_b a} nlogba 几乎有相同的渐近增长率,即 f ( n ) = O ( n l o g b a ) f(n) = O(n^{log_b a} ) f(n)=O(nlogba)

结论: T ( n ) = Θ ( n l o g b a l o g n ) T(n) = \Theta(n^{log_b a}logn) T(n)=Θ(nlogbalogn)

情形 3

f ( n ) f(n) f(n) 多项式地快于 n l o g b a n^{log_b a} nlogba ,即 f ( n ) = O ( n l o g b a + ε ) ε > 0 f(n) = O(n^{log_b a + \varepsilon } )\quad \varepsilon > 0 f(n)=O(nlogba+ε)ε>0 ,并且 a f ( n b ) ≤ c f ( n ) 0 < c < 1 af(\dfrac{n}{b}) \le cf(n)\quad 0 < c < 1 af(bn)cf(n)0<c<1

相当于 f ( n ) f(n) f(n) n l o g b a n^{log_b a} nlogba 阶数高,且 f ( n ) f(n) f(n) a f ( n b ) af(\dfrac{n}{b}) af(bn) 阶数高

结论: T ( n ) = Θ (   f ( n )   ) T(n) = \Theta(\ f(n)\ ) T(n)=Θ( f(n) )

主定理扩展

增加了对 f ( n ) f(n) f(n) 的中带 l o g k n log^k n logkn 的情况的分析

T ( n ) = { Θ ( n l o g b a ) f ( n ) = O ( n l o g b a − ε ) Θ ( n l o g b a l o g k + 1 n ) f ( n ) = Θ ( n l o g b a l o g k n ) Θ ( f ( n ) ) f ( n ) = Ω ( n l o g b a + ε ) a f ( n b ) ≤ c f ( n ) , 0 < c < 1 T(n) = \left\{\begin{matrix} \Theta(n^{log_b^a}) & f(n) = O(n^{log_b^a - \varepsilon}) \\ \Theta(n^{log_b^a}log^{k+1}n) & f(n) = \Theta(n^{log_b^a}log^k n) \\ \Theta(f(n)) & f(n) = \Omega(n^{log_b^a + \varepsilon}) & af(\dfrac{n}{b}) \le cf(n), 0 < c < 1 \end{matrix}\right. T(n)= Θ(nlogba)Θ(nlogbalogk+1n)Θ(f(n))f(n)=O(nlogbaε)f(n)=Θ(nlogbalogkn)f(n)=Ω(nlogba+ε)af(bn)cf(n),0<c<1

代入法(归纳法)
  • 猜测解的形式,确定好猜测的是上界还是下界
  • 根据上下界,确定稍后化简的方向
    • 通常猜测上界 O ( f ( n ) ) O(f(n)) O(f(n))
    • 如若猜测的是上界,后序证明步骤中要使用 ≤ \le
  • 为猜测的解添加一个常数项 c c c
  • 将带有未知常数项的解带入递归方程,替换掉等号右侧的 T ( g ( n ) ) T(g(n)) T(g(n))
    • 注意使用不等号,方向取决于上下界
    • T ( g ( n ) ) T(g(n)) T(g(n)) 替换成 c f ( g ( n ) ) cf(g(n)) cf(g(n))
  • 化简等号右边,拆分出 c f ( n ) cf(n) cf(n) 作为最高次项
  • 使用不等号消掉剩余项,仅保留最高次项 c f ( n ) cf(n) cf(n)
  • 被消掉的剩余项需保证正负匹配不等号
    • n n n 具有一个下限,上至无穷大
    • 此时可以得到常数的一个范围
  • 检验初始条件满足假设
    • 通常递归的初始值是 T ( 1 ) T(1) T(1),将 1 代入 c f ( n ) cf(n) cf(n),查看是否存在 c c c 满足条件
    • 如若 T ( 1 ) T(1) T(1) 无论如何都不可能不满足条件,可以将递归的初始值定义为 T ( 2 ) T(2) T(2) 或者 T ( 3 ) T(3) T(3),再次检验,这时 T ( 1 ) T(1) T(1) 单独定义为一个常数值
  • 难以搞定的小偏差
    • 比如尝试证明 T ( n ) ≤ c n T(n) \le cn T(n)cn 时,得到 T ( n ) ≤ c n + 1 T(n) \le cn + 1 T(n)cn+1
    • 这时无法再通过不等式放缩得到 T ( n ) ≤ c n T(n) \le cn T(n)cn
    • 可以修改原假设,假设 T ( n ) ≤ c n − d T(n) \le cn - d T(n)cnd
    • 带有低阶项的新假设,可以使原式按照同方向放缩到 T ( n ) ≤ c n − d T(n) \le cn - d T(n)cnd
  • 使用指数来简化表达形式
    • 例如使 m = l o g n m = logn m=logn T ( n ) = T ( 2 m ) T(n) = T(2^m) T(n)=T(2m)
    • 此时替换 T ( 2 m ) T(2^m) T(2m) S ( m ) S(m) S(m)
递归树

将递归树展开,并做渐进分析

减法式递归

T ( n ) = T ( 1 ) + T ( n − 1 ) + c n T(n) = T(1) + T(n-1) + cn T(n)=T(1)+T(n1)+cn

            c(n)
            / \ 
           /   \
       c(n-1)  Θ(1)
         / \          
        /   \      
    c(n-2)  Θ(1)
     / \    
    /   \   
c(n-3)  Θ(1)
  |
 ...

Θ ( ∑ k = 1 n k ) = n ( n + 1 ) 2 = Θ ( n 2 ) \Theta (\sum^n_{k=1}k) = \frac{n(n+1)}{2} = \Theta(n^2) Θ(k=1nk)=2n(n+1)=Θ(n2)

除法式递归

T ( n ) = 2 T ( n 2 ) + c n T(n) = 2T(\dfrac{n}{2}) + cn T(n)=2T(2n)+cn ,其中 c > 0 c > 0 c>0,为常数

                    T(n)
                    /  \
                   /    \ 
                  /      \
            T(n/2)         T(n/2)
             / \             / \
            /   \           /   \   
           /     \         /     \
        T(n/4)  T(n/4)  T(n/4)  T(n/4)
         /  \    /  \    /  \    /  \
       ...   ...   ...   ...   ...   ...  
        |    |   |   |   |   |   |   |
T(1) ... T(1) ... T(1) ... T(1) ... T(1) ... T(1)  
  • 树深度: h = l o g n h=logn h=logn
  • 每层代价: c n cn cn
  • 可得总代价: c n l o g n cnlogn cnlogn
  • 时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)

一般性总结

T ( n ) = a T ( n b ) + f ( n ) T(n) = aT(\dfrac{n}{b}) + f(n) T(n)=aT(bn)+f(n)

树深度: h = l o g b n h=log_bn h=logbn

T ( n ) T(n) T(n) 的渐近阶由 f ( n ) f(n) f(n) n l o g b a n^{log_b a} nlogba 中阶较高的决定

迭代法

与递归树展开异曲同工,就是将递归一步步展开,然后求和

这里使用的指数,方便了展开和求和

  • 先设一个n,通常是指数,例如对于 a T ( n b ) aT(\dfrac{n}{b}) aT(bn) n = b k n = b^k n=bk ,其中 k k k 为正整数
  • 如果函数分段,且间断点未知,可以设一个值
  • 不断展开递归式,直到等式右边无 T ( n ) T(n) T(n)
  • 由于 n = b k n = b^k n=bk, 所以 k = l o g b n k = log_b n k=logbn,代入得到 T ( n ) T(n) T(n) 的渐进阶
  • 例如: T ( n ) = 2 T ( n 2 ) + Θ ( n ) T(n) = 2T(\dfrac{n}{2}) + \Theta(n) T(n)=2T(2n)+Θ(n)

假设 n = 2 k n = 2^k n=2k k k k 是整数

T ( 2 k ) ≤ 2 ⋅ T ( 2 k − 1 ) + a ⋅ 2 k 2 ⋅ T ( 2 k − 1 ) ≤ 2 2 ⋅ T ( 2 k − 2 ) + 2 ⋅ a ⋅ 2 k − 1 2 2 ⋅ T ( 2 k − 2 ) ≤ 2 3 ⋅ T ( 2 k − 3 ) + 2 2 ⋅ a ⋅ 2 k − 2 ⋯ 2 k − 1 ⋅ T ( 2 1 ) ≤ 2 k ⋅ T ( 2 0 ) + x k − 1 a ⋅ 2 0 2 k − 1 ⋅ T ( 2 ) ≤ 2 k ⋅ T ( 1 ) + 2 k − 1 a ⋅ 2 \begin{align*} T(2^k) \quad &\le \quad 2\cdot T(2^{k-1}) + a\cdot 2^k \\ 2\cdot T(2^{k-1}) \quad &\le \quad 2^2\cdot T(2^{k-2}) + 2\cdot a \cdot 2^{k-1} \\ 2^2\cdot T(2^{k-2}) \quad &\le \quad 2^3\cdot T(2^{k-3}) + 2^2\cdot a\cdot 2^{k-2} \\ \cdots \\ 2^{k-1}\cdot T(2^{1}) \quad &\le \quad 2^k \cdot T(2^{0}) + x^{k-1}a\cdot 2^0 \\ 2^{k-1}\cdot T(2) \quad &\le \quad 2^{k}\cdot T(1) + 2^{k-1}a\cdot 2 \\ \end{align*} T(2k)2T(2k1)22T(2k2)2k1T(21)2k1T(2)2T(2k1)+a2k22T(2k2)+2a2k123T(2k3)+22a2k22kT(20)+xk1a202kT(1)+2k1a2

将上面所有不等式左右分别相加,并约分得到

T ( 2 k ) ≤ 2 k + k ⋅ a ⋅ 2 k T(2^k) \le 2^k + k \cdot a \cdot 2^k T(2k)2k+ka2k

因为 2 k = N 2^k = N 2k=N,所以 k = l o g 2 N k = log_2N k=log2N,代入得到

T ( N ) ≤ N + l o g 2 N ⋅ a ⋅ N = O ( N l o g N ) T(N) \le N + log_2N \cdot a \cdot N = O(NlogN) T(N)N+log2NaN=O(NlogN)

NP完全问题

P & NP
  • P 问题: 多项式时间内可解决的问题
  • NP 问题: 多项式时间内可验证的问题
  • P ⊆ \subseteq NP
NP-hard
  • 所有 NP 问题都可以多项式归约到 NP-hard 问题
  • NP-hard 问题不一定是 NP 问题
  • NP-hard 问题中,属于 NP 问题的称为 NP-complete 问题
NP-complete
  • NP 中最难的问题

  • 特点:

    • 是 NP 问题
    • 任何一个 NP 问题都可以多项式归约到 NP-complete 问题
    • NP-complete 问题都可以多项式归约到彼此
    • 解决了一个 NP-complete 问题,就解决了 NP 中所有问题
  • 经典问题:

    • Packing problems: set-packing, independent set.
    • Covering problems: set-cover, vectex-cover.
    • Constraint satisfaction problems: SAT, 3-SAT.
    • Sequencing problems: hamiltonian-cycle, TSP.
    • Partitioning problems: 3D-matching,3-color.
    • Numerical problems: subset-sum, knapsack.
  1. Set-Packing(集合覆盖问题):

    这是一个集合优化问题,目标是从给定的一组集合中选择最大数量的不相交集合。也就是说,要选择尽可能多的集合,使得它们之间没有共同的元素。

  2. Independent Set(独立集问题):

    在一个给定的图中,独立集是一组顶点的集合,其中任意两个顶点都不相邻。独立集问题就是寻找图中具有最大顶点数的独立集。

  3. Set-Cover(集合覆盖问题)

    这是一个集合优化问题,目标是从给定的一组集合中选择最小数量的集合,使得它们的并集包含了所有元素。也就是说,要选择尽可能少的集合,使得它们的并集包含了所有的元素。

  4. Vertex-Cover(顶点覆盖问题):

    在一个给定的图中,顶点覆盖是指选择一组顶点,使得每条边都至少与其中一个顶点相邻。顶点覆盖问题就是寻找图中具有最小顶点数的顶点覆盖集。

  5. SAT(可满足性问题):

    这是一个经典的约束满足问题,它涉及到判断一个布尔逻辑公式是否存在满足所有约束的解。SAT问题是一个NP完全问题,意味着在一般情况下很难找到高效的解决方法。

  6. 3-SAT(3可满足性问题):

    这是SAT问题的一个特殊情况,其中每个子句都包含最多3个文字。3-SAT问题也是一个NP完全问题,但在实践中有许多高效的启发式算法可以处理。

  7. Hamiltonian Cycle(哈密顿回路问题):

    在一个给定的图中,哈密顿回路是指通过每个顶点恰好一次的闭合路径。哈密顿回路问题是要确定是否存在一个包含图中所有顶点的哈密顿回路。

  8. TSP(旅行商问题):
    在一个给定的图中,旅行商问题是要找到一条经过每个顶点恰好一次的最短路径。这是一个经典的组合优化问题,也是一个NP完全问题。

  9. 3D-Matching(三维匹配问题):

    在一个给定的三元组集合中,3D-Matching问题是要找到最大的互不相交三元组子集。每个三元组由三个元素组成,而三元组子集中的任意两个三元组都不共享元素。

  10. 3-Color(三色问题):

    在图论中,三色问题是要找到一种给图中的每个顶点染上三种颜色的方法,使得任意两个相邻的顶点具有不同的颜色。

  11. Subset-Sum(子集和问题):

    给定一个集合和一个目标值,子集和问题是要确定是否存在集合的一个子集,使得子集中元素的和等于目标值。

  12. Knapsack(背包问题):

    背包问题是一个组合优化问题,涉及到在有限容量的背包中选择一组物品,使得它们的总价值最大化,同时不超过背包的容量限制。

贪心

算法概述

  • 不回溯,多步求解,局部优化
  • 不同的贪心策略得到不同的算法
  • 常常采纳使目标函数有最大增量的策略为贪心策略

算法证明

  • 假设存在最优解
  • 贪心规则替换
  • 反证法证明
  • 形成子问题

假设存在最优解

假设 I = { i 1 , i 2 , … , i n } I=\{i_1, i_2,\dots ,i_n\} I={i1,i2,,in} 是【优化值】的最优解,且【贪心标准】 f i 1 ≤ f i 2 ≤ ⋯ ≤ f i n f_{i1} \le f_{i2} \le \dots \le f_{in} fi1fi2fin

  • 这一步是假设了一个存在的全局最优的答案,这个答案不是通过贪心算法获得的答案,后续将逐步使用贪心算法获得值替换
  • 这个全局最优答案的每个子项的排序,应当按照证明所要采取的贪心算法所“贪心”的标准来排序,这样后续替换顺序才能够对应

贪心规则替换

以贪心规则取得【贪心标准】最靠前的元素记为 1,该贪心元素一定满足其【贪心标准】 f 1 ≤ f i 1 f_1 \le f_{i1} f1fi1 ,此时 I = { 1 , i 2 , … , i n } I=\{1, i_2,\dots ,i_n\} I={1,i2,,in} 仍为【优化值】的最优解

  • 这一步是对刚刚全局最优解最前面的值进行替换,替换成了我们贪心算法所能取得的最优值
  • 由于最优解各值的顺序已经按照贪心标准排序,而我们新选择的贪心值只可能比最优解“更贪心”,因此不会导致解失效
  • 由于元素替换是一对一的,因此替换行为对【优化值】无影响,替换后的解还是等效的最优解

反证法证明

命题: I ′ = { i 2 , … , i n } I'=\{i_2,\dots ,i_n\} I={i2,,in} 是【优化值】-【替换值】的最优解

反证假设:在【优化值】-【替换值】下存在更优解 I ∗ I^* I 使得 ∣ I ∗ ∣ > ∣ I ′ ∣ |I^*| > |I'| I>I

则必存在 ∣ I ∗ ∪ { 1 } ∣ > ∣ I ′ ∪ { 1 } ∣ |I^* \cup \{1\}| > |I' \cup \{1\}| I{1}>I{1}

∣ I ∗ ∪ { 1 } ∣ > I = { 1 , i 2 , … , i n } |I^* \cup \{1\}| > I= \{1,i_2, \dots , i_n\} I{1}>I={1,i2,,in}

这与” I = { i 1 , i 2 , … , i n } I=\{i_1, i_2,\dots ,i_n\} I={i1,i2,,in} 是【优化值】的最优解“相矛盾

所以假设不成立

  • 反证法证明了去处了贪心解替换值的最优解在子问题上依然是最优解
  • 此处使用反证法的意义:传递解的最优性,保证每次替换时问题的一致性
  • 这样每一次循环都有:替换后的解依然是最优解,去掉已替换后的那部分依然是其子问题的最优解

形成子问题

I ′ I' I 为【优化值】-【替换值】问题的最优解

循环替换直至 I ′ I' I 为空

  • 每一次循环都得到了一个贪心值,这个贪心值保留了解的最优性
  • 问题的规模逐渐变小,本质上就是开始假设的最优解逐个被贪心解替换,并证明替换行为不影响最优性且能够传递

算法实例

活动安排问题

假设𝑺 = (𝟏, 𝟐, . . . , 𝒏) 为n项活动集合,𝒔𝒊和𝒇𝒊分别为活动𝒊的开始和结束时间( 活动 𝒊,𝒋 相容 ↔ 𝒔𝒊 ≥ 𝒇𝒋 或者 𝒔𝒋 ≥ 𝒇𝒊 )求最大的两两相容活动集A

每个活动都要求使用同一资源

同一时间内只有一个活动能使用这一资源

活动在半开时间区间[si, fi)内占用资源

  • 输入的活动以其完成时间的非减序排列
  • 每次选择具有最早完成时间的相容活动加入集合中
  • 贪心选择的意义:使剩余的可安排时间段极大化

伪代码

输入: S , F : s i , f i i = 1 , 2 , … , n , f 1 ≤ ⋯ ≤ f n S,F:\quad s_i,f_i\quad i=1,2,\dots , n,f_1 \le \dots \le f_n S,F:si,fii=1,2,,n,f1fn

greedySelect(S, F)
    n ← Length(S)
    A ← {1}
    j ← 1
    for i ← 2 to n do:
        if S[i] >= F[j]
            A ← A ∪ {i}
            j ← i
    return A

// 结束时间
t = max{F[k] : k ∈ A}
机器调度问题

现有n个任务和足够多台机器, 假定任何时间一台机器只能执行一个任务。设任务 i 的开始时间为 si, 完成时间为 fi, si < fi。[si, fi] 为处理任务i 的时间区间。称两个任务 i, j 重叠是指两个任务的时间区间有重叠

  • 例如:区间[1,4]与区间[2,5]重叠, 而与区间[4,7]不重叠

可行的任务分配是指该分配中没有将重叠的任务分配给同一台机器

最优分配指占用机器数最少的可行分配

  • 可用时间: 用过的机器上最近执行的任务的完成时间,该时间及以后可执行下一任务
  • min-堆: 存放每台机器的可用时间
  • 贪心准则: 尽可能使用已用过的机器
  • 调度方法:
    • 如果一个新任务的起始时间 ≥ 这些机器的最小可用时间
    • 则安排该任务在这台机器上执行
    • 否则使用一台新机器
  • 时间复杂度为 Θ ( n l o g n ) \Theta (nlogn) Θ(nlogn) (排序和堆操作)

证明

  • 任何可行解使用的机器数 ≥ 最大重叠任务数; 所以优化调度使用的机器数 ≥ 最大重叠任务数.
  • 贪心解使用的机器数不超过最大重叠任务数:任何时候当算法使用一台新机器时, 当前这些机器上的任务一定是彼此重叠的

对于活动安排问题机器调度问题,上述贪心算法却总能求得的整体最优解

伪代码

// 输入: tasks 
// 任务列表,每个任务是一个元组 (s:开始时间, f:完成时间)

def task_scheduling(tasks):
    // 按照任务的开始时间对任务进行排序
    tasks ← sort(tasks)
    
    // 创建一个最小堆来存储每台机器的可用时间
    machines ← []
    
    // 将第一个任务分配给第一台机器,并将其完成时间作为该机器的可用时间
    add(machines, sorted_tasks[0].f)
    
    // 遍历剩余的任务
    for i ← 1 to length(sorted_tasks) do:
        s ← tasks[i].s
        f ← tasks[i].f
        
        // 查找可用时间最早的机器
        minf ← min(machines)
        
        // 如果任务的起始时间大于等于最早可用机器的完成时间,则将任务分配给该机器
        if s >= minf:
            pop(machines)              // 从堆中移除最小值
            machines ← machines ∪ {f}  // 将任务的完成时间作为该机器的可用时间
        else:
            // 否则,分配给一台新机器
            add(machines, f)
    
    // 返回最终占用的机器数量
    return length(machines)
找零问题
  • 每次尽可能找最多的最大面额的零钱
// input: amount - 需要找零的金额
//        denominations - 零钱的面额列表,按降序排列

def make_change(amount, denominations):
    change = []  // 存储找零的零钱列表
    
    for d in denominations:
        while amount >= d:
            change.append(d)  // 将当前面额的零钱加入找零列表
            amount -= d  // 减去已找到的金额
        
        if amount == 0:
            break  // 如果金额已经找完,则退出循环
    
    if amount != 0:
        return None  // 无法找零,返回空值
    
    return change  // 返回找零的零钱列表
货船装箱

n个大小一样的集装箱,只考虑重量不同,货船总载重一定,求最大装载数量

  • 贪心策略:轻者优先
  • 将集装箱按照重量从小到大排序装箱,直到装不下为止

伪代码

input: n - 集装箱数量
       w - 集装箱重量列表
       c - 货船载重

def ship_loading(n, w, c):
    sort(w)  // 按照重量从小到大排序
    
    count = 0  // 记录装载的集装箱数量
    weight = 0  // 记录当前货船的载重
    
    for i ← 1 to n do:
        if weight + w[i] <= c:
            count += 1
            weight += w[i]
        else:
            break
    
    return count
ACT 最小平均完成时间

给定 n 个任务,1,2,…,n,假定任务 i 要求的执行时间是 ti。如果按顺序 1,…,n 执行这些任务,任务 i 的完成时间 ci 为 t1+t2+…+ti。定义一个任务顺序的平均完成时间 ACT 为 (c1+c2+…+cn)/n,不同的顺序可能有不同的平均完成时间。

试证明:在 n!个可能的任务顺序中,按最小执行时间优先的贪心策略得到的任务顺序有最小的平均完成时间。

证明:

  • 按最小执行时间优先的贪心策略得到的任务顺序等同于按任务执行时间从小到大的排列得到的任务顺序。下面证明在此顺序下 ACT 值最小。
  • 设在某任务顺序中,顺序号 i > j i > j i>j,但 t i < t j t_i < t_j titj,则交换作业 i , j i,j i,j 的顺序,得到一个新的任务顺序
  • 设原顺序的平均完成时间为 A C T ACT ACT,改变后的平均完成时间为 A C T ′ ACT' ACT,下面证明 A C T ′ < A C T ACT'< ACT ACT<ACT
    • n A C T = ( n t 1 + ⋯ + ( n − j + 1 ) t j + ⋯ + ( n − i + 1 ) t i + ⋯   ) nACT=(nt_1+ \cdots +(n-j+1)t_j+\cdots+(n-i+1)t_i+\cdots) nACT=(nt1++(nj+1)tj++(ni+1)ti+)
    • n A C T ′ = ( n t 1 + ⋯ + ( n − j + 1 ) t i + ⋯ + ( n − i + 1 ) t j + ⋯   ) nACT'=(nt_1+\cdots+(n-j+1)t_i+\cdots+(n-i+1)t_j+\cdots ) nACT=(nt1++(nj+1)ti++(ni+1)tj+)
    • n A C T − n A C T ′ = ( i − j ) t j − ( i − j ) t i = ( i − j ) ( t j − t i ) > 0 nACT-nACT'= (i-j)t_j-(i-j)t_i=(i-j)(t_j-t_i)>0 nACTnACT=(ij)tj(ij)ti=(ij)(tjti)>0
    • 所以 A C T ′ < A C T ACT'<ACT ACT<ACT
    • 也就是每消除一个逆序, A C T ACT ACT值减小
    • 所以当无逆序时,即任务按执行时间从小到大排列时, A C T ACT ACT 值最小
背包问题 ※

密度贪心法 O(nlogn)

输入:

物品价格 P = { p i ∣ i = 1 , … , n } P=\{ p_i | i = 1, \dots , n\} P={pii=1,,n}

物品体积 W = { w i ∣ i = 1 , … , n } W=\{w_i | i = 1, \dots , n\} W={wii=1,,n}

背包容量 C C C

输出:

购买的物品解集 X = { x − i ∣ i = 1 , … , n , x i = 0 , 1 } X = \{x-i | i = 1, \dots , n, \quad x_i = 0, 1\} X={xii=1,,n,xi=0,1}

densityGreedySelect(P, W, C)
    for i ← 1 to n do:
        di = pi / wi   //计算密度
        D ← D ∪ {di}
    D ← sort(D, P, W)  //将物品按密度从大到小排序
    for i ← 1 to n do:
        if (C >= wi)
            xi = 1     //装入
        else
            xi = 0     //不装入
    return X

k-优化算法 O( n k + 1 n^{k+1} nk+1 )

例如 2-优化:{1}, {2}, {3}, {4}, {1,2}, {1,3}, {1,4}, {2,3}, {2,4}, {3,4}

对于不同的子集是排列组合问题

子集数目 ∑ j = 0 k C n k ≈ c ⋅ n k \sum^k_{j=0}C^k_n \approx c \cdot n^k j=0kCnkcnk,每一个子集贪心所用时间为 O(n)

  • 对物品按照密度从大到小排序
  • 先将一些物品装入背包, 然后对其余物品用贪心法
    • 预先装入的物品数不超过 k
    • 对所有预装物品数不超过 k 的剩余物品子集执行贪心过程,并从中找到有最大效益值的解作为 k-优化算法的解

连续背包

按价值密度非递增的顺序检查物品

若剩余容量能容下正在考察的物品则将其装入,否则往背包中装入此物品的一部分

证明:

  • 按价值密度从大到小排序,保证 q 1 ≥ q 2 ≥ ⋯ ≥ q n q_1 \ge q_2 \ge \dots \ge q_n q1q2qn
  • X = { x 1 , x 2 , … , x k , … , x n } X=\{x_1, x_2, \dots ,x_k, \dots, x_n\} X={x1,x2,,xk,,xn} 为当前最优解
  • Y = { y 1 , y 2 , … , y k , … , y n } Y=\{y_1, y_2, \dots ,y_k, \dots, y_n\} Y={y1,y2,,yk,,yn} 为贪心解,且形式为 { 1 , 1 , … , y k , … , 0 , 0 } \{1,1,\dots, y_k, \dots, 0, 0\} {1,1,,yk,,0,0},其中 0 ≤ y k ≤ 1 0\le y_k \le 1 0yk1
  • 假设 m m m 是满足 x i ≠ y i x_i \ne y_i xi=yi 的最小下标,则 m < k m < k m<k x m < y m x_m < y_m xm<ym
  • x m = y m x_m = y_m xm=ym,这时由于 x m x_m xm 值的改变导致 X X X 的体积增大,因此需要从 X X X 的后面部分中减少一定量物品来与 x m x_m xm 交换空间,使得 X X X 的体积不变
  • 此时由于密度递减,交换后的解 X X X 的价值增加,从而获得了一个更优的解,这与 X X X 是最优解矛盾,假设不成立
  • 因此不存在任何一个 i i i 使得 x i ≠ y i x_i \ne y_i xi=yi
  • X = Y X = Y X=Y,最优解即为贪心解
拓扑排序问题

用有向图来形象地表示这些子工程之间的先后关系

这种有向图称为 “顶点活动网络”,又称 “AOV” 网,这些子工程也可称为“活动“

拓扑排序是一个 有向无环图 G=(V, E) 的所有顶点的线性序列,满足:

  • 每个顶点出现且只出现一次
  • 如果图G包含边(u, v),则结点u在拓扑排序中处于结点v的前面
  • 贪心策略:从当前尚不在拓扑排序序列的顶点中选择一顶点 v,其所有前驱节点 u 都在已产生的拓扑序列中(或无前驱顶点),并将 v 加入到拓扑序列中
  • v 的确定:用减节点入度的方法,入度变成0的顶点为要加到拓扑序列中的顶点

Kahn算法 O(n+e)

计算每个顶点的入度
将入度为 0 的顶点入栈
While(栈不空)
{
    任取一入度为 0 的顶点放入拓扑序列中;
    将与其相邻的顶点的入度减 1;
    如有新的入度为 0 的顶点出现,将其放入栈中; 
}
如有剩余的顶点未被删除,说明该图有环路

环路

如果程序失败,则有向图含有环路.

  • 设V为算法结束时算法输出的节点构成的集合
  • 证明:当失败时|V|<n,且剩下的顶点不能加入已排好的序列V中
  • 至少有一节点q1不在V中,和一条边(q2,q1)且q2不在V中,否则q1可加入V中
  • 同理,有边 (q3, q2) 且q3不在V中。若q3=q1 则q1q2q3 是有向图中的一个环路,若q3≠q1,则必存在q4,(q4, q3) 是有向图的边且q4不在V中,否则q3应在V中。若q4为q1,q2,q3 中的任何一个,则该有向图含有环
  • 因为有向图有有限个节点,重复上述步骤,一定能找到一个环路
二分覆盖问题

二分图是一个无向图,它的 n 个顶点分为两个不交叉集合 A 和 B,且任一条边的两个顶点不在同一个集合

A的一个子集 A’ 覆盖集合B,当且仅当 B 中每一顶点至少和 A’ 中一顶点相连

覆盖子集 A’ 的大小指 A’ 中的顶点数目

二分覆盖问题就是在二分图中寻找最小覆盖的问题,等价于集合覆盖问题,是 NP 难问题,使用贪心算法可以快速得到非常接近的解

  • 通俗地讲,就是 A,B 两个点集中的点只与对方点集中的点有连接,现在要在 A 中找一个最小的子集,这个 A 的子集能够让 B 中每个点都与它有连接,不让B存在孤立节点
  • 举例:联系学者办会,5个学生负责联络7个学者,每个学生都认识学者中的部分,怎么用最少的学生联系到全部7个学者?
    • 学生:A
    • 学者:B
    • 最少的学生集合:A’
  • 贪心策略:选择覆盖 B 中那些尚未被覆盖的顶点数最多的 A 的节点

伪代码

O( ∣ A ∣ 2 + n 2 |A|^2 + n^2 A2+n2) 邻接矩阵 O( ∣ A ∣ 2 + n + e |A|^2 + n + e A2+n+e) 邻接表

for all i ∈ A, New[i]=degree[i];		// A是上面的节点(例如:学生)
for all i ∈ B, covered[i]=false; 		// B是需要被覆盖的节点(例如:学者)
A’ = Ø;
while (for some i∈A, New[i]>0) {
	选取 v 为 A-A’ 中 New[i] 值最大的节点
	A’=A’+ {v};
	for 所有被 v 覆盖的 B 中结点 j {
		covered[j] = true
		for 所有覆盖结点 j 的 A 中的顶点 k
			New[k]=New[k]-1
		} 
	}
if 有B中顶点未被覆盖 
	return “失败”
else 
	找到一个覆盖
单源最短路 ※

从任意节点 u 到另一节点 v 的最短路径是所有从 u 到 v 的路径中权重最小的路径

如果一个图包含一个权重为的循环,则有些节点之间将不存在最短路径

Dijkstra O( n 2 n^2 n2)

贪心准则:从一条最短路径还没有到达的顶点中,选择一个可以产生最短路径的目的顶点

伪代码:关于优先队列,顺序关键字为上一次更新点与其邻接点的距离

d[s] ← 0			// 出发点
    for each v ∈ V – {s} 
        do d[v] ← ∞
S ← ∅					// 路径
Q ← V 				// Q 是维护 V-S 的优先队列
while Q ≠ ∅		// 对于顶点 v ∈ V 执行|V|次
    do u ← EXTRACT-MIN(Q)			// 找到Q中最小值
        S ← S ∪ {u}						// 加入新点
        for each v ∈ Adj[u]		// 遍历新点的邻居
            do if d[v] > d[u] + w(u , v)		// 松弛从新点出发的所有路径
            then d[v] ← d[u] + w(u , v)
最小生成树

Prim

设 G=(V, E) 是一个连通带权图, S 是顶点集 V 的一个非空真子集。

首先置 S={s},然后,只要 S 是 V 的真子集,就作如下的贪心选择:

  • 选取满足 u ∈ S, v ∈ V - S,且 w(u, v) 最小的边,将顶点 v 添加到S 中
  • 这个过程一直进行到 S = V 时为止
哈夫曼编码问题 ※

前缀码: 对每一个字符规定一个 0, 1 串作为其代码,并要求任一字符的代码都不是其它字符代码的前缀

表示最优前缀码的二叉树总是一棵完全二叉树,即树中任一结点都有2个儿子结点

  • 将字符序列中每个字符视为一个节点,按照频率从小到大排序
  • 创建一个新的空节点,将最小频率的节点连接到新节点的左侧,将频率第二的节点连接到新节点右侧,然后将空节点赋值为两个字符节点频率的和
  • 从字符节点序列中去除掉这两个节点,并将新节点添加到字符序列中,加入排序
  • 重复上述步骤,直到节点序列中仅剩一个节点,即二叉树的头节点
  • 为树编码,对每条边标记为左0右1,从根节点到达每个叶子节点所经过的01序列为该叶子节点的编码
  • 代价计算: ∑ i ∈ s d ( i ) f i − ( n − 1 ) \sum_{i \in s} d(i)f_i - (n - 1) isd(i)fi(n1),频率 * 编码长度 - (n - 1)

分治

算法概述

  • 把一个难以直接求解的大问题分割成一些规模较小的相同的问题
  • 分解出的各个子问题是相互独立的,且可以合并为原问题的解
  • 注: 不独立的各子问题一般用动态规划较好
  • 平衡子问题: 将一个问题分成大小相等 的 k 个子问题,使子问题的规模大致相同
  • 递归是分治策略的一种表达形式和实现方式,在每层递归中:
    • 分解——分
    • 求解——治
    • 合并——合
  • 递归各参数意义
    • a a a:子问题数量
    • b b b:每个子问题的规模
    • f ( n ) f(n) f(n):分解合并不同子问题时需要的额外成本

分治法基本思想:

  1. 将要求解的较大规模的问题分割成k个更小规模的子问题
  2. 这k个子问题分别求解。如果子问题的规模仍然不够小,则再划分为k个子问题,如此递归的进行下去,直到问题规模足够小,很容易求出其解为止
  3. 将求出的小规模的问题的解合并为一个更大规模的问题的解,自底向
    上逐步求出原来问题的解

分治法可解问题的特征:

  1. 该问题的规模缩小到一定的程度就可以容易地解决
  2. 该问题可以分解为若干个规模较小的相同问题
  3. 分解出的子问题的解可以合并为原问题的解
  4. 分解出的各个子问题是相互独立的

算法实例

伪币问题

一个装有16个硬币的袋子,其中有一个是伪造的,并且那个伪造的硬币比真的硬币要轻一些

  • 假设函数f(n)可以实现所需功能
  • 可以将待解决的大问题分解成小问题
  • 列出边界条件

伪币寻找:二等分

f(A,n) 
    memcpy(A1,A,n/2)		//将A的前n/2个数组放到一起
    memcpy(A2,A+n/2,n/2)	//将A的后n/2个数组放到一起
    B1=sum(A1,n/2)
    B2=sum(A2,n/2)
    if(B1<B2) 				//sum为定义的将数组内的元素加在一起
        f(A1)
    else
        f(A2)

伪币寻找:三等分

f(A,n)
    a1 = floor(n/3);			// 对数组进行三等分,不能整除时向下取整
    memcpy(A1,A,a1);			// 将A的前floor(n/3)个数组放到一起
    memcpy(A2,A+a1,a1);			// 将A的中间a1个数组放到一起
    memcpy(A3,A+2*a1,n-2*a1);	// 将A的剩余数组放到一起
    B1=sum(A1,a1);
    B2=sum(A2,a1);
    B3=sum(A3,n-2*a1);
    if(B1==B2)
        f(A3);
    elseif(B1<B2)
        f(A1);
    else
        f(A2);

找出伪币:对于n个硬币,重量未知,最多需多少次完成?

需要天平比较次数为: ⌈ l o g 3 ( 硬币总数 ) ⌉ + 1 \left \lceil log_3(硬币总数)\right \rceil + 1 log3(硬币总数)+1

金块问题

有一袋子金块,假设有一台天平,用最少的比较次数找出最重和最轻的金块

相当于在n个数中找出最大和最小的数

分治法求解

  • 将金块分成两组 A 和 B
  • 分别找出 A 和 B 中最重和最轻的金块
  • 将第二步中找出的金块比较,得到最重的和最轻的金块
Max-min(A[0,n-1], max, min)
    if n<1
        max ← min ← a[0]
        return;
    else
        m ← n/2 
        Max-min(A[0,m-1], max1, min1)
        Max-min(A[m,n-1], max2, min2)
        max ← max(max1, max2)
        min ← min(min1, min2)
    return
矩阵相乘

[ C 11 C 12 C 21 C 22 ] = [ A 11 A 12 A 21 A 22 ] ⋅ [ B 11 B 12 B 21 B 22 ] ⇔ { C 11 = A 11 B 11 + A 12 B 21 C 12 = A 11 B 12 + A 12 B 22 C 21 = A 21 B 11 + A 22 B 21 C 22 = A 21 B 12 + A 22 B 22 \begin{bmatrix} C_{11} & C_{12} \\ C_{21} & C_{22} \end{bmatrix} = \begin{bmatrix} A_{11} & A_{12} \\ A_{21} & A_{22} \end{bmatrix} \cdot \begin{bmatrix} B_{11} & B_{12} \\ B_{21} & B_{22} \end{bmatrix} \Leftrightarrow \left\{\begin{matrix} C_{11} = A_{11}B_{11} + A_{12}B_{21} \\ C_{12} = A_{11}B_{12} + A_{12}B_{22} \\ C_{21} = A_{21}B_{11} + A_{22}B_{21} \\ C_{22} = A_{21}B_{12} + A_{22}B_{22} \end{matrix}\right. [C11C21C12C22]=[A11A21A12A22][B11B21B12B22] C11=A11B11+A12B21C12=A11B12+A12B22C21=A21B11+A22B21C22=A21B12+A22B22

大整数乘法

X Y = ( A ⋅ 1 0 n 2 + B ) ( C ⋅ 1 0 n 2 + D ) = A C ⋅ 1 0 n + ( A D + B C ) ⋅ 1 0 n 2 + B D = A C ⋅ 1 0 n + ( ( A + B ) ( C + D ) − A C − B D ) ⋅ 1 0 n 2 + B D \begin{align} XY &= (A \cdot 10^{\frac{n}{2}} + B)(C \cdot 10^{\frac{n}{2}}+D) \\ &= AC \cdot 10^n + (AD + BC)\cdot 10^{\frac{n}{2}} + BD \\ &= AC \cdot 10^n + ((A+B)(C+D)-AC-BD)\cdot 10^{\frac{n}{2}} + BD \end{align} XY=(A102n+B)(C102n+D)=AC10n+(AD+BC)102n+BD=AC10n+((A+B)(C+D)ACBD)102n+BD

二分搜索

二分搜索: 给定已按升序排好序的 n 个元素 a[0:n-1],现要在这 n 个元素中找出一特定元素 x

  • 若 x = a[mid] ,则 x 在 L 中的位置就是 mid
  • 如果 x < a[mid],因为 a 是递增排序的,假如 x 在 a 中的话,x 必然排在 a[mid] 的前面,所以在 a[mid] 的前面查找 x 即可
  • 如果 x > a[mid],同理在 a[mid] 的后面查找 x 即可
  • 最坏情况下: O ( l o g   n ) O(log\ n) O(log n)
BinarySearch(a, x, left, right)
    while right >= 1
        m = (left - right) / 2
        if x == a[mid]
            return mid
        if x < a[mid]
            right = mid - 1
        else
            left = mid + 1
棋盘覆盖 ※

在一个 2 k × 2 k 2^k \times 2^k 2k×2k 个方格组成的棋盘中,恰有一个方格与其它方格不同,称该方格为一特殊方格,且称该棋盘为一特殊棋盘

用图示的4种不同形态的L型骨牌覆盖给定的特殊棋盘上除特殊方格以外的所有方格,且任何 2 个L 型骨牌不得重叠覆盖

▢        ▢    ▢ ▢    ▢ ▢
▢ ▢    ▢ ▢      ▢    ▢
  • 如果是一个2×2的方格,必定可以用一个L型的骨牌填满
  • 将棋盘横纵四分,均分成四个子棋盘,特殊方格在其中的一个棋盘中,使用一个方向合适的骨牌放在横纵分割线交点处,即另外三个棋盘的会合处,原问题转化为 4 个较小规模的棋盘覆盖问题
  • 递归地使用这种分割,直至棋盘简化为棋盘 2×2
  • T ( n ) = 4 T ( n 4 ) + c = O ( n ) T(n) = 4T(\dfrac{n}{4}) + c = O(n) T(n)=4T(4n)+c=O(n)
循环赛日程表

设有n=2k个运动员要进行网球循环赛,现在要设计一 个满足以下要求的比赛日程表:

(1) 每个选手必须与其他 个选手各赛一次

(2) 每个选手一天只能赛一次

(3) 循环赛一共进行 n-1 天

  • 将比赛表设计成一个 n 行 n-1 列 的二维表,其中 第 i 行第 j 列 的元素表示和第 i 个选手在第 j 天比赛的选手号
  • 分治策略: 所有的选手分为两半,n个选手的比赛日程表就可以通过为n/2个选手设计的比赛日程表来决定
  • 递归地使用这种一分为二的策略对选手进行分割,直到只剩下2个选手
Day1Day2Day3Day4Day5Day6Day7Day8
12345678
21436587
34127756
43218865
56781134
65872243
78563312
87654421
归并排序 ※
MergeSort(E, n)
    // 对E中的n个元素进行排序,k为全局变量
    if n >= k
        i = n / k
    j = n - i
	 
    // 令A包含E中的前i个元素,B包含E中剩余的j个元素
    sort(A, i)
    sort(B, j)
	 
    // 把A和B合并到E
    merge(A, B, E, i, j)

时间复杂度

t ( n ) = { d n < k t ( n k ) + t ( n − n k ) + c n n ≥ k t(n) = \left\{\begin{matrix} d & n < k\\ t(\dfrac{n}{k}) + t(n - \dfrac{n}{k}) + cn & n \ge k \end{matrix}\right. t(n)={dt(kn)+t(nkn)+cnn<knk

当𝑘 = 2时,分治法通常具有最佳性能,则有以下递推公式:

t ( n ) = { d n ≤ 1 t ( ⌊ n 2 ⌋ ) + t ( ⌈ n 2 ⌉ ) + c n n > 1 t(n) = \left\{\begin{matrix} d & n \le 1\\ t(\left \lfloor \dfrac{n}{2}\right \rfloor ) + t(\left \lceil \dfrac{n}{2} \right \rceil) + cn & n >1 \end{matrix}\right. t(n)={dt(2n)+t(2n)+cnn1n>1

算法复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)

逆序对的个数

逆序:给定自然数 1, ⋯ , n 的一个排列,如果 j > i 但 j 排 在 i 的前面则称 (j, i) 为该排列的一个逆序

  • 计算序列分为两个子序列

  • 分别计算两个子序列的逆序对数

    • 每层递归中,逆序对数的计算发生在合并中
    • 合并时考虑左右两个序列,存在左 > 右则为逆序
    • 接下来按合并方法,右序列的数将“插入”左序列数的左边,这个过程所跨过的左序列中的数都是逆序对
    • 对应的逆序对数量就是从当前左数到左序列末尾所有数的个数
  • 在递归中,每次需要加上的总逆序对数:

    • 当前左右序列的逆序对数递归结果
    • 跨越左右序列的逆序对数,即上述合并过程的结果
int MergeSort(int r[], int low, int high) {
    if (low >= high) {
        return 0;  // 递归结束条件:子序列长度为1
    }

    int mid = (low + high) / 2;
    int count = 0;  // 逆序对个数

    count += MergeSort(r, low, mid);         // 左子序列逆序对个数
    count += MergeSort(r, mid + 1, high);     // 右子序列逆序对个数
    count += Merge(r, low, mid, high);        // 合并并计算跨越左右子序列的逆序对个数

    return count;
}

int Merge(int r[], int low, int mid, int high) {
    int i = low, j = mid + 1, k = 0;
    int count = 0;  // 跨越左右子序列的逆序对个数
    int r1[high - low + 1];  // 临时数组存放合并结果

    while (i <= mid && j <= high) {
        if (r[i] <= r[j]) {
            r1[k++] = r[i++];
        } else {
            count += mid - i + 1;  // 左子序列剩余元素都大于r[j],因此构成逆序对
            r1[k++] = r[j++];
        }
    }

    while (i <= mid) {
        r1[k++] = r[i++];
    }

    while (j <= high) {
        r1[k++] = r[j++];
    }

    for (int idx = 0; idx < k; idx++) {
        r[low + idx] = r1[idx];  // 将合并结果复制回原数组
    }

    return count;
}
快速排序 ※

简便理解:随机选择一张85分试卷,A找85以上试卷,B找85以下试卷; A再外包C和D:随机选择一张80分试卷,C找80以上试卷,D找80以下试卷

  • n个元素被分成三段:左段left、右段right和中段middle
    • 中段middle仅包含一个元素;
    • 左段left中各元素都小于等于中段元素;
    • 右段right中各元素都大于等于中段元素。
  • left 和 right 中的元素可以独立排序,不必对 left 和 right的排序结果进行合并。
  • middle 中的元素称为支点(pivot)。
算法过程
伪代码
Partition(A, p, r):
    x ← A[p]  // pivot = A[p]
    i ← p + 1
    j ← r
    while i <= j:
        while i <= j and A[j] > x:
            j ← j - 1
        while i <= j and A[i] < x:
            i ← i + 1
        if i <= j:
            swap(A[i], A[j])
            i ← i + 1
            j ← j - 1
    swap(A[p], A[j])
    return j
QuickSort(A, p, r)
	if (p < r)
		q ← Partition(A, p, r)
		QuickSort(A, p, q - 1)
		QuickSort(A, q + 1, r)
时间复杂度
  • 对于输入序列 A [ p . . r ] A[p..r] A[p..r],算法 Partition 的计算时间为 O ( r − p − 1 ) O(r-p-1) O(rp1)
  • 最坏情况: T ( n ) = T ( 0 ) + T ( n − 1 ) + c n = O ( n 2 ) T(n) = T(0) + T(n-1) + cn = O(n^2) T(n)=T(0)+T(n1)+cn=O(n2)
  • 最好情况: T ( n ) = 2 T ( n 2 ) + c n = O ( n l o g n ) T(n) = 2T(\dfrac{n}{2}) + cn = O(nlogn) T(n)=2T(2n)+cn=O(nlogn)
  • 平均情况: T ( n ) = T ( α n ) + T ( ( 1 − α ) n ) + c n = O ( n l o g n ) T(n) = T(\alpha n) + T((1-\alpha)n) + cn = O(nlogn) T(n)=T(αn)+T((1α)n)+cn=O(nlogn)
最坏情况复杂度
  • 最坏情况: 每次将序列划分成两个长度分别为 0 和 n-1 的子序列
  • 数列已排好序 (顺序或倒序)
    • 每一趟没有达到把数列分成左右两部分的目的,无法发挥分治法
    • 每次划分只减少一个元素,需要进行 n-1 趟划分

∑ k = 1 n c ⋅ ( n − k ) = Θ ( n 2 ) \sum_{k=1}^n c\cdot (n-k) = \Theta (n^2) k=1nc(nk)=Θ(n2)

T ( n ) = T ( 1 ) + T ( n − 1 ) + c n = O ( n 2 ) T(n) = T(1) + T(n-1) + cn = O(n^2) T(n)=T(1)+T(n1)+cn=O(n2)

                 cn
                / \ 
               /   \
           c(n-1)  T(1)
             / \          
            /   \      
        c(n-2)  T(1)
         / \    
        /   \   
    c(n-3)  T(1)
     ...
    /
  T(1) 
最好情况复杂度
  • 最好情况: 每次将序列划分成两个长度分别为 n/2 的子序列

T ( n ) = 2 T ( n 2 ) + O ( n ) = O ( n l o g n ) T(n) = 2T(\dfrac{n}{2}) + O(n) = O(nlogn) T(n)=2T(2n)+O(n)=O(nlogn)

                     cn
                    /  \
                   /    \ 
                  /      \
            c(n/2)         c(n/2)
             / \             / \
            /   \           /   \   
           /     \         /     \
        c(n/4)  c(n/4)  c(n/4)  c(n/4)
         /  \    /  \    /  \    /  \
       ...   ...   ...   ...   ...   ...  
        |    |   |   |   |   |   |   |
Θ(1) ... Θ(1) ... Θ(1) ... Θ(1) ... Θ(1) ... Θ(1)  
排序算法比较
方法最坏复杂性平均复杂性
冒泡排序 n 2 n^2 n2 n 2 n^2 n2
基数排序 n 2 n^2 n2 n 2 n^2 n2
插入排序 n 2 n^2 n2 n 2 n^2 n2
选择排序 n 2 n^2 n2 n 2 n^2 n2
堆排序 n l o g n nlogn nlogn n l o g n nlogn nlogn
归并排序 n l o g n nlogn nlogn n l o g n nlogn nlogn
快速排序 n 2 n^2 n2 n l o g n nlogn nlogn
寻找第k小 ※

问题:对于给定的 n 个元素的数组 a[0…n-1],要求从中找出第 k 小的元素

  • 数组 a[p…q] 被划分为两个子数组 a[p…r–1] 和 a[r+1…q],使a[p…r–1] 中每个元素都不大于a[r+1…q] 中每个元素(类似快速排序
    • 如果 k = r–p+1,支点元素就是第 k 个元素;
    • 如果 k < r–p+1,下一次搜索在 a[p…r–1] 中进行;
    • 反之,在 a[r+1…q] 中进行
Select(A, p, q, k)
    // k_th smallest of A[p..q]
    if p = q
        return A[p]
    r ← Partition(A, p, q)
    i ← r – p + 1		// i = rank(A[r])

    // 如果 k = r–p+1,支点元素就是第 k 个元素
    if k = i
        return A[r]

    // 如果 k < r–p+1,下一次搜索在 a[p..r–1] 中进行
    if k < i
        return Select(A, p, r – 1, k)

    // 反之,在 a[r+1..q] 中进行
    else
        return Select(A, r + 1, q, k – i)

中间的中间

为保证选出的支点不是最大或最小元素,一种选择支点元素的方法是使用“中间的中间(median-of-median)” 规则:

  • 将数组 a 中的 n 个元素分成 n/r 组,r 为某一整常数
  • 对每组中的 r 个元素进行排序,寻找每组的中位数
  • 对n/r个中位数递归使用选择算法选择中间数作为支点
平面点对

给定平面上 n 个点的集合 S,找其中的一对点,使得在 n个点组成的所有点对中,该点对间的距离最小。严格来讲,最接近点对可能多于一对,为简便起见,我们只找其中的一对作为问题的解

分治策略

  • 分(Divide):将点集P依据横坐标分为左右两个子集PL和PR
  • 治(Conquer):分别计算PL和PR两个点集的最近点对k1,k2
  • 合(Combine):考虑PL和PR两个点集交界处的最近点对,因为交界处可能有比k1和k2距离更近的两个点
输入: 点集P, X和Y为横、纵坐标数组
输出: 最近的两个点及距离

MinDistance(P, X, Y)
	若 |P|≤3,直接计算其最小距离
	排序X, Y
	做中垂线 l 将 P 划分为 PL 和 PR
	MinDidtance (PL, XL, YL)
	MinDistance (PR, XR, YR)
	𝛿=min(𝛿𝐿, 𝛿𝑅)		//𝛿𝐿,𝛿𝑅为子问题的距离
	检查距中垂线 l 不超过 𝛿 两侧各1个点的距离,若小于 𝛿,修改 𝛿 为这个值

边界

  • f ( n ) f(n) f(n) 为检查跨边界点对和排序中更高的复杂度
  • 缩小边界法: 假设前面求解的最小距离为 δ \delta δ,那么对于左边的点,只需要在以它为中心 δ × ( 2 δ ) \delta \times (2\delta) δ×(2δ) 的正方形内取值
  • 以空间换时间: 先将点集中的元素以它们的纵坐标进行一遍排序,然后存储在数组中,在合的步骤中以 O ( n ) O(n) O(n) 的复杂度提取并比较
  • 边界处理的时间复杂度: O ( n ) O(n) O(n)

时间复杂度

T ( n ) = { 1 , n = 2 3 , n = 3 2 T ( n 2 ) + O ( n ) n > 3 T(n) = \left\{\begin{matrix} 1, & n = 2 \\ 3, & n = 3 \\ 2T(\dfrac{n}{2}) + O(n) & n > 3 \end{matrix}\right. T(n)= 1,3,2T(2n)+O(n)n=2n=3n>3

T ( n ) = O ( n l o g 2 n ) T(n) = O(nlog^2n) T(n)=O(nlog2n)

平面点集凸包

问题:给定大量离散点的集合Q,求一个最小的凸多边形,使得Q中的点都能够处于该多边形内、或者边上
应用场景:图形处理中的形状识别,字形识别,碰撞检测等

分治法

  • 对点集内点进行排序,连接最大横坐标点 X m a x X_{max} Xmax 和最小纵坐标点 X m i n X_{min} Xmin,记该连线为 d 1 d_1 d1,并以 d d d 为分界线
    将点集划分上下两部分,分别称之为上包 Q u p Q_{up} Qup 和下包 Q d o w n Q_{down} Qdown,并分别对 Q u p Q_{up} Qup Q d o w n Q_{down} Qdown 进行递归处理
  • 先以上子集为例:从 Q u p Q_{up} Qup 中找到一点 P m a x P_{max} Pmax ,该点满足到直线 d 1 d_1 d1 距离最远,该步骤可以由点到直线距离公式求解
  • 分别连接 P m a x X m i n P_{max}X_{min} PmaxXmin, P m a x X m a x P_{max}X_{max} PmaxXmax,将连线分别记为 d 2 , d 3 d_2,d_3 d2,d3。将 d 2 , d 3 d_2,d_3 d2,d3 外侧的点也分别看作“上包”, d 2 d_2 d2 外的点与 d 2 d_2 d2 构成 Q u p Q_{up} Qup 的子问题, d 3 d_3 d3 外的点与 d 3 d_3 d3 也构成 Q u p Q_{up} Qup 的子问题
  • 重复前几步,对下包 Q d o w n Q_{down} Qdown 也进行同样操作,直到所有点都在凸包内部或边上

Graham扫描法

  • 将所有点放在二维坐标系中,纵坐标最小的点一定是凸包上的点(如P0)。将所有点平移,使P0作为原点
  • 计算其他各个点相对于P0的幅角α,按α从小到大对各个点排序。当α相同时,距离P0更近的点排在前面
  • 已知P0和P1是凸包上的点,入栈,用直线相连。按顺序找到下一个角度上最外的点,观察它在前两个点连线左边还是右边
  • 若新点在直线左边,将其入栈,并用直线连接前一个点和新点;若该新点不是同一条直线上的最外一个点,则用最外一个点替换该点
  • 若在前两个点连线右边,前一个点出栈,换成新点入栈,连线新点和栈中再前一个点,找下一个点
  • 时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)

动态规划

算法概述

最优子结构
  • 最优性原理:待求解问题的一个最优策略序列子策略序列总是最优的
  • 最优子结构性质:问题的最优解是由其子问题的最优解来构造
  • 无后效性:某阶段的状态一旦确定,此后的演变不再受此前各状态和决策的影响
重叠子问题
  • 把子问题答案保存起来,以减少重复计算,直接查表
自顶向下
  • 带备忘录的递归,从 f(n) 往 f(1) 方向延伸求解
memo[0,1] ← 1
memo[2:n] ← 0
𝒏 ← 𝑰𝒏𝒑𝒖𝒕()

𝒇𝒊𝒃𝒐𝒏𝒂𝒄𝒄𝒊(𝒏)
	if memo(𝒏) != 0
		return memo(𝒏)
	return 𝒇𝒊𝒃𝒐𝒏𝒂𝒄𝒄𝒊(𝒏-1) + 𝒇𝒊𝒃𝒐𝒏𝒂𝒄𝒄𝒊(𝒏-2)
自底向上
  • 从较小问题的解,由交叠性质,逐步决策出较大问题的解,从 f(1) 往 f(n) 方向求解
  • 策略:把可能的值填入表格,不着急做决定,待触底后回溯

伪代码:斐波那契

𝒏 ← 𝑰𝒏𝒑𝒖𝒕()
if 𝒏 ≤ 𝟏
	return 𝟎
if 𝒏 == 𝟐
	return 𝟏
𝒂 ← 𝟎, 𝒃 ← 𝟏, 𝒕𝒆𝒎𝒑 ← 𝟎
for 𝒊 ← 𝟑 𝒕𝒐 𝒏 do :
	𝒕𝒆𝒎𝒑 ← 𝒂 + 𝒃
	𝒂 ← 𝒃
	𝒃 ← 𝒕𝒆𝒎𝒑
return 𝒕𝒆𝒎𝒑

算法思路

四大核心: 最优子结构,状态转移方程,边界,重叠子问题

设计思路:

  1. 最优性原理
  2. 边界情况
  3. 递归方程
  4. 重叠问题
  5. 回溯方法

算法实例

0/1背包问题 ※
  • 问题:背包容量不足以装入所有物品,面临选择
  • 目标函数:效益值最大(一组非负数之和)
  • 优化解:使得效益值最大的一种放法 ( z 1 , … , z n ) (z_1, \dots,z_n) (z1,,zn)
  • 优化原理:无论优化解 ( z 1 , … , z n ) (z_1, \dots,z_n) (z1,,zn) 是否放物品 1,相对剩余背包容量,优化解 ( z 1 , … , z n ) (z_1, \dots,z_n) (z1,,zn) 对物品 2 , … , n 2,\dots,n 2,,n 的放法也是优化解(最优子结构性质)

最优性原理证明

命题:0-1背包问题Knap(1,n,c)满足最优性原理

证明:

  • ( y 1 , y 2 , … , y n ) (y_1, y_2,\dots,y_n) (y1,y2,,yn) K n a p ( 1 , n , c ) Knap(1,n,c) Knap(1,n,c) 的一个最优解
  • ( y 2 , … , y n ) (y_2,\dots,y_n) (y2,,yn) K n a p ( 2 , n , c − w 1 y 1 ) Knap(2,n,c-w_1y_1) Knap(2,n,cw1y1) 子问题的一个最优解
  • 否则,设 ( z 2 , … , z n ) (z_2,\dots,z_n) (z2,,zn) K n a p ( 2 , n , c − w 1 y 1 ) Knap(2,n,c-w_1y_1) Knap(2,n,cw1y1) 的最优解,则 ( y 1 , z 2 , … , y n ) (y_1, z_2,\dots,y_n) (y1,z2,,yn) K n a p ( 1 , n , c ) Knap(1,n,c) Knap(1,n,c) 的一个更优解
  • 与假设矛盾,则原假设成立

状态转移方程

f ( i , y ) = { m a x { f ( i − 1 , y ) ,   f ( i − 1 , y − w i ) + v i } y ≥ w i f ( i − 1 , y ) 0 ≤ y < w i f(i,y) = \left\{\begin{matrix} max\{f(i-1,y),\ f(i-1,y-w_i)+v_i\} & y \ge w_i\\ f(i-1,y) & 0\le y < w_i \end{matrix}\right. f(i,y)={max{f(i1,y), f(i1,ywi)+vi}f(i1,y)ywi0y<wi

dp[i][j] = max(上方单元格的价值, 剩余空间价值 + 当前商品价值)
         = max(dp[i - 1][j], dp[i - 1][c - w[i]] + v[i])

dp[j]    = max(dp[j], dp[c - w[i]] + v[i])
  • dp[i][j]相当于在前i个物品中选择,背包容量为j的最大价值

边界条件
f ( 1 , y ) = { v 1 y ≥ w 1 0 0 ≤ y < w 1 f(1,y) = \left\{\begin{matrix} v_1 & y \ge w_1\\ 0 & 0\le y < w_1 \end{matrix}\right. f(1,y)={v10yw10y<w1

回溯最优解 O(n)

  • 从物品 n n n 开始考虑,首先看 d p [ n ] [ c ] dp[n][c] dp[n][c],即物品 n n n,价值 c c c ,依次向前逐个考虑
  • 对于物品 i i i,此时容量为 j j j,对应 d p [ i ] [ j ] dp[i][j] dp[i][j],与 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i1][j] 做比较,若相等说明物品 i i i 没有被放入背包,不相等则说明物品 i i i 已被放入背包
  • d p [ i ] [ j ] ≠ d p [ i − 1 ] [ j ] dp[i][j] \ne dp[i-1][j] dp[i][j]=dp[i1][j],则找到满足条件的 k k k,使得 d p [ i − 1 ] [ k ] = d p [ i ] [ j ] − v [ i ] dp[i-1][k] = dp[i][j]-v[i] dp[i1][k]=dp[i][j]v[i] j − k ≥ w [ i ] j - k \ge w[i] jkw[i],通常直接取 k = j − w [ i ] k = j - w[i] k=jw[i]
  • 对物品 i − 1 i-1 i1,容量 k k k,重复上述步骤

注意

  • 贪心算法处理连续背包问题可以得到最优解,动态规划无法处理连续背包问题
递归法
  • 适用于手推递归解题,方向从后向前,序号从大往小装

  • f ( i , y ) f(i,y) f(i,y) 表示从物品 i i i 到最后一个物品中选择,背包容量为 y y y 的最大价值

  • 每个 f ( i , y ) f(i,y) f(i,y) 中的 i 写成实数,y 为变量,范围从 0 0 0 ∑ j = i n w j \sum_{j = i}^n w_j j=inwj 也就是所有已经考察过的物品重量和

  • 递归方程:
    f ( i , y ) = { m a x ( f ( i + 1 , y ) , f ( i + 1 , y − w i ) + p i ) y ≥ w i f ( i + 1 , y ) y < w i f(i,y) = \left\{\begin{matrix} max(f(i + 1,y),f(i + 1,y - w_i) + p_i) & y \ge w_i\\ f(i + 1,y) & y < w_i \end{matrix}\right. f(i,y)={max(f(i+1,y),f(i+1,ywi)+pi)f(i+1,y)ywiy<wi

  • 例题:n=5, P=[6,3,5,4,6],w=[2,2,6,5,4],c=10

f ( 5 , y ) = { 6 y ≥ 4 0 y < 4 f(5,y) = \left\{\begin{matrix} 6 & y \ge 4\\ 0 & y < 4 \end{matrix}\right. f(5,y)={60y4y<4

f ( 4 , y ) = { m a x ( f ( 5 , y ) , f ( 5 , y − 5 ) + 4 ) y ≥ w 4 f ( 5 , y ) y < w 4 = { 6 4 ≤ y < 5 0 y < 4 6 5 ≤ y < 9 10 y ≥ 9 = { 0 y < 4 6 4 ≤ y < 9 10 y ≥ 9 f(4,y) = \left\{\begin{matrix} max(f(5,y),f(5,y-5)+4) & y \ge w_4\\ f(5,y) & y < w_4 \end{matrix}\right. \quad = \left\{\begin{matrix} 6 & 4 \le y < 5 \\ 0 & y < 4 \\ 6 & 5 \le y < 9 \\ 10 & y \ge 9 \end{matrix}\right. \quad = \left\{\begin{matrix} 0 & y < 4\\ 6 & 4 \le y < 9 \\ 10 & y \ge 9 \end{matrix}\right. f(4,y)={max(f(5,y),f(5,y5)+4)f(5,y)yw4y<w4= 606104y<5y<45y<9y9= 0610y<44y<9y9

f ( 3 , y ) , f ( 2 , y )  同理 f(3,y),f(2,y)\ 同理 f(3,y),f(2,y) 同理

  • 回溯求优化解:
    • c = 10
    • f (1, 10) = 15, f (2, 10) = 11, 所以 x1 = 1, c = 10 - 2 = 8
    • f (2, 8) = 9, f (3, 8) = 6, 所以 x2 = 1, c = 8 - 2 = 6
    • f (3, 6) = 6, f (4, 6) = 6, 所以 x3 = 0, c = 6
    • f (4, 6) = 6, f (5, 6) = 6, 所以 x4 = 0, c = 6
    • f (5, 6) = 6 ≠ \ne = 0, 所以 x5 = 1, 所以优化解为:x = [1,1,0,0,1],优化值为 15
元祖法
  • P , Q P,Q P,Q 中元素为元组,其定义为原 dp 矩阵中每行的跳跃点,例如 ( a , b ) (a,b) (a,b) 中的 a a a表示背包的重量, b b b表示当前背包重量下可装入的最大价值
  • P ( i − 1 ) = P ( i ) + Q P(i-1) = P(i) + Q P(i1)=P(i)+Q
  • 如果 P ( i ) P(i) P(i) Q Q Q 中各取一个元组, ( a , b ) (a,b) (a,b) ( c , d ) (c,d) (c,d) 满足 a ≤ c a \le c ac b > d b > d b>d,则说明更少或相同的重量能够装入更大价值的物品,则删掉 ( c , d ) (c,d) (c,d) 再合并成新的 P P P
  • 求解过程
    • 从后往前,序号从大往小装(从前往后也一样,顺序一致就行)
    • 先指出新物品元组,例如 (w4, v4) = (5, 4)
    • 构建新的 Q Q Q,是 P P P 中每个元素逐位加(5, 4),例如 Q = {(5, 4), (9, 10)}
    • 合并 P P P Q Q Q,去除价值小但重量大的元组,并按照重量从小到大排序,例如去掉(5, 4)P(4) = [(0, 0), (4, 6), (9, 10)]
    • 重复步骤直到P(1)

n = 5, w = [2, 2, 6, 5, 4], v = [6, 3, 5, 4, 6], c = 10。

(w5, v5) = (4, 6)

P(5) = [(0, 0), (4, 6)];

(w4, v4) = (5, 4), Q = {(5, 4), (9, 10)}

P(4) = [(0, 0), (4, 6), (9, 10)];

(w3,v3) = (6, 5), Q = {(6, 5), (10, 11)}

P(3) = [(0, 0), (4, 6), (9, 10), (10, 11)];

(w2, v2) = (2, 3), Q = {(2, 3), (6, 9)}

P(2) = [(0, 0), (2, 3), (4, 6), (6, 9), (9, 10), (10, 11)]

(w1, v1) = (2, 6), Q = {(2, 6), (6, 12), (8, 15)}

P(1) = [(0, 0), (2, 6), (4, 9), (6, 12), (8, 15)]

完全背包问题

每种物品都就可以选择任意多个,只要背包装得下,每件物品可以选择任意多件

  • 对于第 i 种物品,我们有 k 种选择, 0 ≤ k ⋅ w i ≤ c 0 \le k \cdot w_i \le c 0kwic,即可以选择 0 , 1 , 2 , … , k 0,1,2,\dots,k 0,1,2,,k 个第 i i i 种物品

  • 递推式:
    g ( i , c ) = m a x { g ( i − 1 , c − w i ⋅ k ) + v i ⋅ k } ( k ∈ N , 0 ≤ k ⋅ w i ≤ c ) g(i,c)=max\{g(i-1,c-w_i\cdot k)+v_i \cdot k\}\quad (k \in N, 0 \le k \cdot w_i \le c) g(i,c)=max{g(i1,cwik)+vik}(kN,0kwic)

  • 边界条件:
    g ( 0 , c ) = 0 , g ( i , 0 ) = 0 g(0,c) = 0,g(i,0) = 0 g(0,c)=0,g(i,0)=0

  • 问题优化:
    g ( i , c − w i ⋅ 2 ) + v i ⋅ 2 = g ( i , ( c − w i ) − w i ) + v i ⋅ 2 g(i,c-w_i\cdot 2) + v_i\cdot 2 = g(i,(c-w_i)-w_i) + v_i \cdot 2 g(i,cwi2)+vi2=g(i,(cwi)wi)+vi2

    • 同一物品不同数量的选择,本质上与不同物品的更新策略与最大值筛选方式一致
    • 对于同一个物品,max函数所考察的另一个数量较少的最优值,其根据状态转移方程的定义,一定是在前面已经更新过的
    • 这样通过在同一层dp内更新,可以实现将同一物品不同数量的选择,转化为不同物品的更新策略与最大值筛选方式一致
    • 因此,完全背包问题可以转化为多重背包问题,从而使用多重背包问题的解法
    • 从而有新的状态转移方程

g ( i , c ) = m a x { g ( i , c ) c < w [ i ] m a x (   g ( i , c ) , g ( i , c − w [ i ] ) + v [ i ] ) c ≥ w [ i ] g(i,c) = max \left\{\begin{matrix} g(i,c) & c < w[i] \\ max(\ g(i,c),\quad g(i,c-w[i]) + v[i]) & c \ge w[i] \end{matrix}\right. g(i,c)=max{g(i,c)max( g(i,c),g(i,cw[i])+v[i])c<w[i]cw[i]

  • 注意:

    • 使用新的状态转移方程,遍历顺序应为从小到大
    • 遍历前,需要整行拷贝自上一行
    • 这种方法可以减少一层循环,降低时间复杂度
  • 最优性原理证明:

    • 已知完全背包的解为 ( x 1 , x 2 , … , x n ) (x_1, x_2, \dots,x_n) (x1,x2,,xn) ,xi表示第 i 件物品的选取数量, g ( x 1 , x 2 , … , x i ) g(x_1, x_2, \dots,x_i) g(x1,x2,,xi) 为将前 i 种物品按照 ( x 1 , x 2 , … , x i ) (x_1, x_2, \dots,x_i) (x1,x2,,xi) 方案放入容量为 c 的背包所取得的价值
    • 假设 ( x 1 , x 2 , … , x i ) (x_1, x_2, \dots,x_i) (x1,x2,,xi) 不是子问题的最优解,则存在另一组解 ( y 1 , y 2 , … , y i ) (y_1, y_2, \dots, y_i) (y1,y2,,yi) ,使得 g ( y 1 , y 2 , … , y i ) > g ( x 1 , x 2 , … , x i ) g(y_1, y_2, \dots, y_i) > g(x_1, x_2, \dots,x_i) g(y1,y2,,yi)>g(x1,x2,,xi)
    • g ( y 1 , y 2 , … , y i , x i + 1 , … , x n ) > g ( x 1 , x 2 , … , x n ) g(y_1, y_2, \dots, y_i,x_{i+1}, \dots,x_n) > g(x_1, x_2, \dots,x_n) g(y1,y2,,yi,xi+1,,xn)>g(x1,x2,,xn)
    • 因此 ( x 1 , x 2 , … , x n ) (x_1, x_2, \dots,x_n) (x1,x2,,xn) 不是原问题的最优解,与已知矛盾
    • 所以 ( x 1 , x 2 , … , x i ) (x_1, x_2, \dots,x_i) (x1,x2,,xi) 必然是子问题的最优解
  • 完全背包无后效性

    • 后续子问题的解只与背包的剩余空间有关:前 i 种物品如何选择,都不会影响后面物品的选择
    • 即子问题的任意解,都不会影响后续子问题的解,满足无后效性
  • 伪代码:

function knapsack(n, m)
    f[n][m] = 0
    for i = 1 to n
        for j = 0 to m
            f[i][j] = f[i-1][j]
            if j >= w[i]
                f[i][j] = max(f[i][j], f[i][j-w[i]] + v[i])
    return f[n][m]

read n, m
for i = 1 to n
    read w[i], v[i]
result = knapsack(n, m)
print result
多重背包

物品有指定的数量限制,第 i i i 种物品最多有 m i m_i mi 件可用

  • 转化成 0/1 背包: i i i 件物品最多选 m i m_i mi 件,可把第 i i i 种物品转化为 m i m_i mi 件体积和价值相同的物品
  • 相比完全背包:,只是 k 多了一个限制条件 0 ≤ k ≤ m i 0 \le k \le m_i 0kmi
  • k取值: ( 0 ≤ k ≤ m i a n d 0 ≤ k ⋅ w i ≤ c ) (0 \le k \le m_i\quad and \quad0 \le k \cdot w_i \le c) (0kmiand0kwic)
  • 递推式: g ( i , c ) = m a x { g ( i − 1 , c − w i ⋅ k ) + v i ⋅ k } ( k ∈ N , k ∈ [ 0 , m i ] , 0 ≤ k ⋅ w i ≤ c ) g(i,c)=max\{g(i-1,c-w_i\cdot k)+v_i \cdot k\}\quad (k \in N,k \in [0,m_i], 0 \le k \cdot w_i \le c) g(i,c)=max{g(i1,cwik)+vik}(kN,k[0,mi],0kwic)
  • 边界条件: g ( 0 , c ) = 0 , g ( i , 0 ) = 0 g(0,c) = 0,g(i,0) = 0 g(0,c)=0,g(i,0)=0
  • 伪代码:
for (int i = 0; i < n; i++){ 
	// 考虑第i个物品,分两种情况:

	// 1. mi ⋅ wi ≥ C,可以当做完全背包问题来处理
	if (mi ⋅ wi ≥ C) {
		for (int j = wi; j ≤ c ; j++) { 
			f [j] = max(f[j], f[j - wi] + vi);
		}
	} 

	// 2. mi ⋅ wi < C, 需要在 f[j-wi ⋅ k] + vi ⋅ k中找到最大值, 0 ≤ k ≤ mi
	else {
		for (int j = wi; j ≤ c ; j++) { 
			int k = 1; 
			while (k ≤ mi && j ≥ wi ⋅ k ){ 
				f[j] = max(f[j], f[j - wi ⋅ k] + vi ⋅ k); 
				k++; 
			} 
		} 
	}
}
矩阵乘法链 ※

对矩阵乘法链 M 1 × ⋯ × M q M_1 \times \cdots \times M_q M1××Mq M i M_i Mi 的维数为 r i × r i + 1 ( 1 ≤ i ≤ n ) r_i \times r_{i+1}\quad (1 \le i\le n) ri×ri+1(1in),求优化的乘法顺序,使得计算该乘法链所用的乘法数最少

  • 矩阵乘法可结合
  • 长度为 q q q 的矩阵乘法链有 Ω ( 2 q ) \Omega(2^q) Ω(2q) 的可能乘法顺序
  • 矩阵 A m × n A_{m\times n} Am×n 与 矩阵 B n × p B_{n\times p} Bn×p 相乘需要做 m ⋅ n ⋅ p m\cdot n \cdot p mnp 个元素乘法
  • r r r 的定义: M 1 : M 5 M_1:M_5 M1:M5 的维度分别为 10x5, 5x1, 1x10, 10x2, 2x10, [   r = ( 10 , 5 , 1 , 10 , 2 , 10 )   ] [\ r = (10, 5, 1, 10, 2, 10)\ ] [ r=(10,5,1,10,2,10) ]
  • c c c的定义: c ( i , j ) c(i, j) c(i,j) 为计算 M i M_i Mi M j M_j Mj 所需乘法次数的最小值
  • 递推式:

c ( i , j ) = { 0 j = i r i r i + 1 r i + 2 j = i + 1 m i n i ≤ k < j { c ( i , k ) + c ( k + 1 , j ) + r i r k + 1 r j + 1 } j > i + c(i,j) = \left\{\begin{matrix} 0 & j = i \\ r_{i}r_{i+1}r_{i+2} & j = i + 1 \\ min_{i \le k < j} \{c(i,k) + c(k+1,j) + r_{i}r_{k+1}r_{j+1}\} & j > i + \end{matrix}\right. c(i,j)= 0riri+1ri+2minik<j{c(i,k)+c(k+1,j)+rirk+1rj+1}j=ij=i+1j>i+

  • 回溯优化值: K a y ( i , j ) Kay(i,j) Kay(i,j) 为达到最小值的 k k k,即断开位置
  • 复杂度: T ( q ) = ∑ 0 ≤ s < q − 1 [ s ⋅ ( q − s ) ] = O ( q 3 ) T(q)= \sum_{0 \le s < q-1} [s \cdot (q - s)] = O(q^3) T(q)=0s<q1[s(qs)]=O(q3)

伪代码

function OptimalMatrixChainOrder(r):
    n = length(r) - 1
    let c[1..n][1..n] be a new 2D array of size n x n
    let s[1..n][1..n] be a new 2D array of size n x n

    for i = 1 to n
        c[i][i] = 0

    for L = 2 to n
        for i = 1 to n - L + 1
            j = i + L - 1
            c[i][j] = infinity

            for k = i to j - 1
                cost = c[i][k] + c[k+1][j] + r[i] * r[k+1] * r[j+1]
                if cost < c[i][j]
                    c[i][j] = cost
                    s[i][j] = k

    return c and s
All-Pair最短路 ※

单源最短路径求解节点 S 1 S_1 S1 到其他 N − 1 N-1 N1 个节点的最短路径

All-pair最短路求解以一对节点为基础,即 N N N 个节点需要计算 C N 2 C^2_N CN2

  • c c c 的定义: c i j ( k ) c_{ij}(k) cij(k) 表示 节点 i i i j j j 的中间节点编号不超过 k k k 的最短路长度
  • 递归式: c i j ( k ) = m i n { c i j ( k − 1 ) , c i k ( k − 1 ) + c k j ( k − 1 ) } c_{ij}(k) = min\{c_{ij}(k-1),c_{ik}(k-1)+c_{kj}(k-1)\} cij(k)=min{cij(k1),cik(k1)+ckj(k1)}

Floyd-Warshall O( n 3 n^3 n3)

  • 只需要使用一个矩阵,每次迭代更新
n = |V|
c = W	// 初始化为邻接矩阵
for k = 1 to n
    for i = 1 to n
        for j = 1 to n
            c[i][j] = min(c[i][j], c[i][k] + c[k][j])
return c

前驱矩阵 Π ( k ) = [ π i j ( k ) ] \Pi(k)=[\pi_{ij}(k)] Π(k)=[πij(k)]

在 Floyd-Warshall 算法中,可以在计算矩阵 C(k) 的同时计算前驱矩阵 Π ( k ) = [ π i j ( k ) ] \Pi(k)=[\pi_{ij}(k)] Π(k)=[πij(k)]

p i j ( k ) p_{ij}(k) pij(k) 为节点 i i i j j j 之间的中间节点编号不超过 k k k 的最短路径, π i j ( k ) \pi_{ij}(k) πij(k) 为路径 p i j ( k ) p_{ij}(k) pij(k) j j j 的前驱节点

  • 对于初始状态,连通的点对中的终点的前驱结点为起点,其余点的前驱节点置为0
  • 对于每一次 k 的更新,如果没有插入 k 使得路径更短,则继承上一次的前驱结点
  • 如果成功插入 k 获得了更短的路径,则新路径的前驱结点继承从 k 到终点的路径的前驱结点

π i j ( k ) = { 0 W i j = 0 , ∞ i 0 < W i j < ∞ π i j ( k − 1 ) c i j ( k ) = c i j ( k − 1 ) π k j ( k − 1 ) c i j ( k ) < c i j ( k − 1 ) \pi_{ij}(k) = \left\{\begin{matrix} 0 && W_{ij} = 0,\infin \\ i && 0 < W_{ij} < \infin \\ \pi_{ij}(k-1) && c_{ij}(k) = c_{ij}(k-1) \\ \pi_{kj}(k-1) && c_{ij}(k) < c_{ij}(k-1) \end{matrix}\right. πij(k)= 0iπij(k1)πkj(k1)Wij=0,0<Wij<cij(k)=cij(k1)cij(k)<cij(k1)

TSP旅行商问题 ※

欧拉路: 给定无孤立结点图 G,若存在一条路,经过图中每边一次且仅一次

欧拉回路: 存在一条回路,经过图中每边一次且仅一次

欧拉定理: 偶点组成的连通图一定可以一笔画,只有两个奇点的连通图把奇点作为起点终点一笔画,其他情况的图都不能一笔画出,奇点数除以二可知需几笔画成

旅行商问题: 给定一系列城市和每对城市之间的距离, 求访问每一座城市一次并回到起始城市的最短回路

问题: 求图 G = (V, E) 的最小成本周游路线

TSP 最优性原理证明

  • s , s 1 , … , s p , s s,s_1,\dots,s_p,s s,s1,,sp,s 是从 s s s 出发的一条路径长度最短的简单回路
  • 假设从 s s s 到下一个城市 s 1 s_1 s1 的最短路径已经求出,则问题转化为求 从 s 1 s_1 s1 s s s 的最短路径,显然 s 1 , … , s p , s s_1,\dots,s_p,s s1,,sp,s 一定构成一条从 s 1 s_1 s1 s s s 的最短路径
  • 如若不然,设 s 1 , r 1 , r 2 , … , r q , s s_1,r_1,r_2,\dots,r_q,s s1,r1,r2,,rq,s 是一条从 s 1 s_1 s1 出发到 s s s 的最短路径且经过 n − 1 n-1 n1 个不同城市, s , s 1 , r 1 , r 2 , … , r q , s s,s_1,r_1,r_2,\dots,r_q,s s,s1,r1,r2,,rq,s 是从 s s s 出发的最短简单回路 且比 s , s 1 , r 1 , r 2 , … , r q , s s,s_1,r_1,r_2,\dots,r_q,s s,s1,r1,r2,,rq,s 要短,从而导致矛盾

求解方法

  • 起点是第一个城市,保持不变
  • 最优性原理使得被从起点处拆开,这样环可以被拓展,拆开后的环相当于从一个点到起点的最短路径,这样就可以用动态规划求解
  • 对于一个子结构,他的两个维度特征是:从哪里开始,经过了哪些点,回到起点
  • i 表示开始的点,是点的序号
  • j 是一个二进制数,表示要经过的点(不包含起点),所以 j 也是一个具体点的集合
  • dp[i][j]
  • i起点不在 j 中,j 是某些点的集合,dp 的值隐含着这些点的一个最优排列,使得经过他们从 i起点的问题里,路径长度已经得到了最优化
  • dp[i][j] :从 i 出发经过集合 j 里面所有节点并回到起点的最小距离
  • 每次更新,相当于对一个新的点 kk 和前面所有小于 ki 有一个距离,计为 value[k][i]
  • 对于这些 i 又有一个 dp[i][j],其中的 j 不含 i,不含起点,但是包含所有小于 k 的其他点
  • 也就是说遍历了所有 ivalue[k][i] + dp[i][j] 的最小值,就是从 k 出发经过 j 里面所有节点并回到起点的最小距离

TSP伪代码

for i = 1 to n-1:
    dp[i][0] = w[i][0]

for j = 1 to (2^(n-1)) - 1:
    for i = 1 to n-1:
        if i not in S(j):
            dp[i][j] = infinity
            for each k in S(j):
                dp[i][j] = min(dp[i][j], c[i][k] + dp[k][j - {k}])

dp[0][(2^(n-1)) - 1] = infinity
for each k in S((2^(n-1)) - 1):
    dp[0][(2^(n-1)) - 1] = min(dp[0][(2^(n-1)) - 1], c[0][k] + dp[k][(2^(n-1)) - 2])

return dp[0][(2^(n-1)) - 1]

时间复杂度

T ( n ) = ∑ 1 ≤ k ≤ n − 2 ( k − 1 ) C n − 2 k = O ( n 2 n ) T(n) = \sum_{1 \le k \le n-2} (k-1)C_{n-2}^k = O(n2^n) T(n)=1kn2(k1)Cn2k=O(n2n)

最大无交叉子集
  • 给定一个上下两边有 n 个针脚的布线通道和一个排列 C。 顶部的针脚 𝑖与底部的针脚 𝐶𝑖 相连,其中 1 ≤ i ≤ n,数组 𝑖, 𝐶𝑖 称为网组。n 总共有 n 个网组需连接或连通
  • 假定有两个或更多的布线层,其中有一个为优先层。优先层的连线 更细,电阻也要小得多。我们的任务是尽可能把更多的网组布设在优先层。当且仅当两个网组没有交叉时,它们可布设在同一层
  • 因此我们的任务等价于寻找一个最大无交叉子集 (maximum noncrossing subset, MNS)

s i j = { 0 i f j = 0 0 i f i = 1 , j < c 1 1 i f i = 1 , j ≥ c 1 s i − 1 , j i f i > 1 , j < c i m a x { s i − 1 , j ,   s i − 1 , c i − 1 + 1 } i f i > 1 , j ≥ c i s_{ij} = \left\{\begin{matrix} 0 & if & j = 0\\ 0 & if & i = 1, j < c_1\\ 1 & if & i = 1, j \ge c_1\\ s_{i-1,j} & if & i > 1, j < c_i\\ max\{s_{i-1,j},\ s_{i-1,c_i-1}+1 \} &if & i > 1, j\ge c_i \end{matrix}\right. sij= 001si1,jmax{si1,j, si1,ci1+1}ifififififj=0i=1,j<c1i=1,jc1i>1,j<cii>1,jci

最长公共子序列

给定两个序列 X = {x1 , x2 , ⋯, xm} 和 Z = {z1 , z2 , ⋯, zk }。如 果存在一个严格递增(无需连续)下标序列 {i1 , i2 , ⋯, ik }, 使得对于所有 j = 1, 2, ⋯ , k 有 zj = xij,则称序列 Z 是 X 的子序列

给定 2 个序列 X 和 Y,当序列 Z 同时是X和Y的子序列时, 称 Z 是 X 和 Y 的公共子序列

例:X = {A, B, C, B, D, A, B},Y = {B, A, C, D, A, C, B}和 Z = {B, C, D, B}。

其中X和Y还可能有其他的公共子序列,如{B, C, D},{D, A, B}等,其中最长的为{A, C, D, A, B},称为最长公共子 序列。缩写LCS(Longest Common Subsequence)

  • 无需连续
  • 最优子结构性质:2 个序列的最长公共子序列包含了这 2 个序列的前缀的最长公共子序列
  • c i j c_{ij} cij 记录序列 X i X_i Xi Y i Y_i Yi 的最长公共子序列的长度

c i j = { 0 i = 0 , j = 0 c i − 1 , j − 1 + 1 i , j > 0 ; x i = y i m a x { c i , j − 1 , c i − 1 , j } i , j > 0 ; x i ≠ y i c_{ij} = \left\{\begin{matrix} 0 & i = 0, j = 0\\ c_{i-1,j-1} + 1 & i,j > 0; x_i = y_i \\ max\{c_{i,j-1},c_{i-1,j} \} & i,j > 0; x_i \ne y_i \end{matrix}\right. cij= 0ci1,j1+1max{ci,j1,ci1,j}i=0,j=0i,j>0;xi=yii,j>0;xi=yi

void LCSLength(int m,int n,char *x,char *y,int **c,int **b)
	int i,j;
	for (i = 1; i <= m; i++)
		c[i][0] = 0
	for (i = 1; i <= n; i++)
		c[0][i] = 0
	for (i = 1; i <= m; i++)
		for (j = 1; j <= n; j++)
			if (x[i] == y[j])
				c[i][j] = c[i-1][j-1]+1
				b[i][j] = 1						// b 用来记录构造方式
			else if (c[i-1][j] >= c[i][j-1]) 
				c[i][j] = c[i-1][j]
				b[i][j] = 2
			else 
				c[i][j] = c[i][j-1]
				b[i][j] = 3
子集和数问题

设 S = {s1 , s2 , ⋯, sn } 为 n 个正数的集合。试找出满足以下条件的和数最大的子集 J

∑ 1 ≤ i ≤ n s i ≥ c , ∑ i ∈ J s i ≤ c \sum_{1 \le i \le n} s_i \ge c,\sum_{i \in J}s_i \le c 1insic,iJsic,其中 c 是任意给定的常数。

  • 该问题可以转化为 w 和 v 值相等的背包问题
  • 递归关系:

f ( n , y ) = { s n y ≥ s n 0 y < s n f(n,y) = \left\{\begin{matrix} s_n & y \ge s_n\\ 0 & y < s_n \end{matrix}\right. f(n,y)={sn0ysny<sn

f ( i , y ) = { m a x { f ( i + 1 , y ) , f ( i + 1 , y − s i ) + s i } y ≥ s i f ( i + 1 , y ) 0 ≤ y < s i f(i,y) = \left\{\begin{matrix} max\{f(i+1,y),f(i+1,y-s_i)+s_i\} & y \ge s_i\\ f(i+1,y) & 0 \le y < s_i \end{matrix}\right. f(i,y)={max{f(i+1,y),f(i+1,ysi)+si}f(i+1,y)ysi0y<si

  • 例题:s = [20,18,15], c= 34

f ( 3 , y ) = { 15 y ≥ 15 0 y < 15 f(3,y) = \left\{\begin{matrix} 15 & y \ge 15\\ 0 & y < 15 \end{matrix}\right. f(3,y)={150y15y<15

f ( 2 , y ) = { 33 y ≥ 33 18 18 ≤ y < 33 15 15 ≤ y < 18 0 y < 15 f(2,y) = \left\{\begin{matrix} 33 & y \ge 33 \\ 18 & 18 \le y < 33 \\ 15 & 15 \le y < 18 \\ 0 & y < 15 \end{matrix}\right. f(2,y)= 3318150y3318y<3315y<18y<15

回溯求解 f ( 1 , 34 ) = 33 , f ( 2 , 34 ) = 33 x 1 = 0 f ( 2 , 34 ) = 33 , f ( 3 , 34 ) = 15 x 2 = 1 f ( 3 , 34 − 18 ) = f ( 3 , 16 ) = 15 ≠ 0 x 3 = 1 解的集合 X = [ 0 , 1 , 1 ] \begin{matrix} 回溯求解 & \\ f(1,34)=33, f(2,34)=33 & x_1 = 0\\ f(2,34) = 33, f(3,34)=15 & x_2 = 1\\ f(3,34-18)=f(3,16)=15\ne 0 & x_3 = 1\\ 解的集合 & X = [0,1,1] \\ \end{matrix} 回溯求解f(1,34)=33,f(2,34)=33f(2,34)=33,f(3,34)=15f(3,3418)=f(3,16)=15=0解的集合x1=0x2=1x3=1X=[0,1,1]

DP公式
  • 货箱装载问题
    { m a x ∑ 1 ≤ i ≤ n x i s . t . ∑ 1 ≤ i ≤ n w i x i ≤ c \left\{\begin{matrix} max\sum_{1 \le i \le n } x_i\\ s.t. \sum_{1 \le i \le n }w_ix_i \le c \end{matrix}\right. {max1inxis.t.1inwixic
  • 背包问题
    { m a x ∑ 1 ≤ i ≤ n v i x i s . t . ∑ 1 ≤ i ≤ n w i x i ≤ c \left\{\begin{matrix} max\sum_{1 \le i \le n } v_ix_i\\ s.t. \sum_{1 \le i \le n }w_ix_i \le c \end{matrix}\right. {max1invixis.t.1inwixic
  • 子集和数问题
    { m a x ∑ 1 ≤ i ≤ n w i x i s . t . ∑ 1 ≤ i ≤ n w i x i ≤ c \left\{\begin{matrix} max\sum_{1 \le i \le n } w_ix_i\\ s.t. \sum_{1 \le i \le n }w_ix_i \le c \end{matrix}\right. {max1inwixis.t.1inwixic

回溯

算法概述

算法原理
  • 在包含问题的所有解的解空间树中,按照深度优先策略,从根结点出发搜索解空间树
  • 搜索至任一节点时,通过约束函数判断该节点为根的子树是否包含问题的解
  • 如果包含解,则继续进入该子树,按深度优先进行搜索
  • 如果肯定不包含解,则逐层向其祖先节点回溯
子集树和排列树

子集树

  • 从 n 个元素的集合 S 中找出满足某种性质的子集,相应的解空间树称为子集树
  • 空间复杂度: Ω ( 2 n ) \Omega(2^n) Ω(2n)
                  A
               /     \ 
           1 /         \ 0
           /             \
          B               C
         / \             / \
      1 /   \ 0       1 /   \ 0
       /     \         /     \
      D       E       F       G
    1/ \0   1/ \0   1/ \0   1/ \0 
    H   I   J   K   L   M   N   O
  (111)           (011)        (000)

排列树

  • 当问题是确定 n 个元素为满足某种性质的排列时,相应的解空间树称为排列树
  • 空间复杂度: Ω ( n ! ) \Omega(n!) Ω(n!)
              /```````a,b,c  ```````\
           a /            |            \ c
            /           b |             \
           /              |              \
         b,c             a,c             a,b
         / \             / \             / \
      b /   \ c       a /   \ c       a /   \ b
       /     \         /     \         /     \
      c       b       c       a       b       a
      |       |       |       |       |       |
     abc     acb     bac     bca     cab     cba
状态空间树
  • 状态空间树在搜索过程中动态产生,是解空间树的子树
  • 约束函数在扩展结点处剪去不满足约束的子树, 从而降低复杂度
  • 三种节点:
    • 活结点: 已展开了部分子节点, 但所有子结点尚未全部展开的结点
    • 死结点: 被限界或已展开了所有子结点的结点
    • 扩展结点: 当前正在展开子结点的活结点
界限函数
  • 约束函数:剪去不满足条件的子树,得到可行解
  • 限界函数:剪去不是最优解的子树,得到最优解
  • 使用0/1背包举例:
    • 约束函数:用cw(i)表示到第i层时的总重量,则当cw(i-1)+wi >Wmax时,停止搜索第i层及下面的层
    • 限界函数:用cv(i)表示到第i层时的总价值,r(i)表示剩余物品总价值,Vbest表示目前的最大价值。则当cv(i)+r(i)≤Vmax时,停止搜索第i层及下面的层
核心思想
  • 针对所给问题,定义问题的解空间
  • 确定易于搜索的解空间树(子集树 & 排列树)
  • 剪枝避免无效搜索

算法实例

n-皇后问题 ※

在 𝑛 × 𝑛 格的棋盘上放置 𝑛 个皇后,使任何2个 皇后不放在同一行或同一列或同一斜线上

解向量:(𝑥1 , 𝑥2 , ⋯ , 𝑥𝑛 ). 问题的解可以 表示为一个8元组(“𝑥1”, … ,“𝑥8”),其中 “𝑥𝑖” 是放在第𝑖行的皇后所在的列号

约束条件

  • x i , i = 1 , 2 , … , n x_i,\quad i = 1,2,\dots,n xi,i=1,2,,n
  • x i ≠ x j a n d ∣ x i − x j ∣ ≠ ∣ i − j ∣ x_i \ne x_j \quad and \quad |x_i - x_j| \ne |i - j| xi=xjandxixj=ij ,不同列,不处于同一斜线

解空间树

  • 基本结构是一个n叉树,是几叉看是几皇后,下面拿4皇后举例
  • 每个节点上标注一个数对,为当前皇后的位置 i, j
  • 头节点无标注,可以写 初始
  • 第一层子树的标注为 1,1 1,2 1,3 1,4
  • 第二层子树的标注为 2,1 2,2 2,3 2,4,每个第一层的子树都有四个子节点,第二层总共16个节点
  • 第一层没有被删除的,第二层开始有被删除的节点,画一个大叉
  • 第二层还活着的节点继续向下扩展,第三层的节点标注为 3,1 3,2 3,3 3,4
  • 注意删除的节点也要画出来,然后打叉,被打叉了的就不再往下画了
  • 每个节点扩展后都是四个子节点
  • 最后成为解的节点可以画个对号表示

伪代码

bool Place(int k)
	for (int j = 1; j < k; j++)
		if ((abs(k - j) == abs(x[j] - x[k])) || (x[j] == x[k])) 	// 在同一斜线或同一列上
			return false;
	return true;

void Backtrack(int t)
	if (t>n)
		sum++;	// 到达叶子节点
	else
		for (int i = 1; i <= n; i++) 
			x[t] = i;
			if (Place(t))	// 剪枝
				Backtrack(t + 1); 
货厢装船 ※

有两艘船和 n 个货箱;第一艘船的载重量是 𝑐1,第二艘船的载重量是 𝑐2 , 𝑤𝑖 是货箱 i 的重量且 ∑𝑤𝑖 ≤ 𝑐1 + 𝑐2 ,1 ≤ i ≤ n,是否有一种可将所有 n 个货箱全部装走的方法

  • 限界方法1:剪去不可行

    如果 cw + w(i) > c1,则杀死该(左)子节点

  • 限界方法2:剪去非最优

    设 bestw 为当前最优装箱重量,r 为未装货箱的总重量,如cw + r ≤ bestw,则停止展开该节点

设 𝑥 = (𝑥1, … , 𝑥𝑘)为当前 E 节点, cw + r > bestw 成立;

// 展开左子节点: 
    如果 c𝑤 + 𝑤𝑘 > c1, 则停止展开该左子节点, r ← r - 𝑤𝑘, 并展开右子节点;
    否则, 𝑥𝑘 ← 1, cw ← cw + 𝑤𝑘, r ← r - w(k),
    𝑥 = (𝑥1, … , 𝑥𝑘) 为新的 E 节点;

// 展开右子节点: 
    如果 cw + r ≤ bestw, 停止展开该右子节点, 并回溯到最近的一个活节点;
    否则, 令𝑥𝑘 = 0, 𝑥 = (𝑥1, … , 𝑥𝑘), 为新的 E 节点

// 回溯:
    从 i = k-1 开始, 找 𝑥𝑖 = 1 的第一个 i; 修改 cw 和 r的值:
        cw ← cw - 𝑤𝑖, r ← r - 𝑤𝑖。
    如果 k = n 且 c𝑤 + 𝑤𝑛 > c1, 则:
        如果 cw > bestw, bestw ← cw, 𝑥𝑛 ← 0; 
        否则, 回溯.
    否则 (即 c𝑤 + 𝑤𝑛 ≤ c1), cw ← cw + 𝑤𝑛, 
        如果cw > bestw, bestw ← cw, 𝑥𝑛 ← 1; 
        否则, 回溯.
    当回溯过程到达根节点时算法结束.
    因为左子节点和父节点有相同的cw + r 值, 所以算法在展开左子节点时不检查限界条件2.
0/1背包 ※
  • x = ( x 1 , x 2 , ⋯   , x k − 1 ) x = (x_1,x_2,\cdots, x_{k-1}) x=(x1,x2,,xk1) 为当前状态空间树的节点
  • 当前效益值定义为 c p = x 1 p 1 + x 2 p 2 + ⋯ + x k − 1 p k − 1 cp = x_1p_1+x_2p_2+\cdots+x_{k-1}p_{k-1} cp=x1p1+x2p2++xk1pk1
  • bound(x) x x x 对应的子问题的优化效益值的上界。即假设物品可拆分, 且按照单位重量价值降序排列, 通过贪心法得到的效益值
  • bound(x)cp + 剩余物品的连续背包问题的优化效益值
  • bestp 是算法目前获得的最优效益值
  • 如果 bound(x) ≤ bestp 则停止展开 x x x

解空间树

  • 解空间树为子集树的形式,基本结构为二叉树
  • 左子树1,右子树0,0/1标注在两条边的线段旁
  • 每个节点上标注一个数对,为当前背包重量和价值(cw, cp),或按照展开顺序标注 1,2,3···
  • 头节点为(0,0)
  • 左枝限界条件: cw + w[i] ≤ c
  • 右枝限界条件: bound(X) > bestp

伪代码

  • cw: 当前背包重量
  • cp: 当前背包价值,cp = x[1]p[1] + ⋯ + x[k-1]p[k-1]
  • bestp: 当前最优价值
  • bound(X) = 当前背包价值 + 子树贪心上界
Backtrack (int i)
	if (i > n)			// 到达叶子节点
		if (cp > bestp)
			bestp ← cp
	if (cw + w[i] ≤ c)	// 左子树,判断容量约束
		cw ← cw + w[i]
		cp ← cp + p[i]
		Backtrack (i + 1)
		cw ← cw - w[i]
		cp ← cp - p[i]
	if (Bound (i + 1) > bestp)	// 右子树,判断贪心上界
		Backtrack (i + 1)

Bound (int i)	// 计算剩余物品贪心上界
	cleft ← c - cw
	bound ← cp
	while (i ≤ n and w[i] ≤ cleft)
		cleft ← cleft - w[i]
		bound ← bound + p[i]
		i ← i + 1
	if (i ≤ n)
		bound ← bound + p[i] / w[i] * cleft
	return bound
最大完备子集 ※

给定无向图 G = (V, E),其中V是顶点集,E 是边集

如 V={1, 2, 3, 4, 5},E = {(1, 2), (1, 4), (1, 5), (2, 3), (2, 5), (3, 5), (4, 5)}

给定补图 G’ = (V, E’),其中 E’ = {(1, 3), (2, 4), (3, 4)}

完全图 G 就是指图 G 的每两个顶点之间都有连边。例如,G∪G’ 是一个完全图

如果 U 是图 G 的一个顶点子集,并且对于 U 中的任意两个顶点 u 和 v,都有边 (u, v) ∈ E,则称 U 是 G 的一个完全子图

{1, 2}、{3, 2, 5}、{1, 2, 5}、{1, 4, 5} 等都是G的完全子图

一个完全子图 U 的尺寸是指图 U 中顶点的数量。例如,{1, 2, 5} 的尺寸是 3

如果一个完全子图 U 不被包含在 G 的一个更大的完全子图中,称它是图 G 的一个集团

{1, 2} 是图 G 的一个完全子图,但不是一个团,因为它包含于 G 的更大的完全子图 {1, 2, 5}之中

最大集团是具有最大尺寸的集团,简称最大团

如果 U ∈ V 且对任意 u, v ∈ U 有(u, v) 不属于 E,则称 U 是 G 的空子图。如 {1, 3}, {2, 4}, {3, 4}。

G 的空子图 U 是 G 的独立集当且仅当 U 不包含在 G 的更大的空子图中

如 {2, 4} 是 G 的一个空子图,同时也是 G 的一个最大独立集

虽然 {1, 2} 也是 G’ 的空子图,但它不是 G’ 的独立集,因为它包含在 G’ 的空子图 {1, 2, 5} 中

G 的最大独立集是 G 中所含顶点数最多的独立集,如 {1, 3}

如果 U 是 G 的完全子图,例如 {1, 2, 5},则它也是 G’ 的空子图,反之亦然。

G 的团与 G’ 的独立集之间存在一一对应的关系

例如,{2, 3}, {1, 2, 5} 都是 G’ 的独立集,它们同时是 G 的团

特殊地,U 是 G 的最大团当且仅当 U 是 G’ 的最大独立集。 {1, 2, 5} 是 G’ 的最大独立集。{1, 4, 5} 和 {2, 3, 5} 也是 G’ 的最大独立集

求最大团和最大独立集两个问题都是 NP-难度问题

问题总结

  • 最大团问题: 给定无向图 G = (V, E),求包含顶点数最多的子图,子图满足其任意两个顶点间在原图中都有边
  • 最大独立集问题: 给定无向图 G = (V, E),求包含顶点数最多的独立集,即求相对于完全图的补图 G’ 的最大团
  • 子集树: 元组长度等于图的顶点数,分量取值 {0, 1}, x i = 1 x_i = 1 xi=1 表示顶点 i i i 在所考虑的集团中
  • 优化问题:
    • 剪去不可行: 检查根节点到状态空间树的某一状态节点的路径上对应的图的顶点子集是否构成一个完全子图?如果不是则不再展开。
    • 剪去非最优: 如果剩余未考虑的顶点数加上团中顶点数 (cn+n-i) 不大于当前解的顶点数 (bestn),则不再展开

伪代码

void Backtrack(int i)
	if (i > n)
		for (int j = 1; j <= n; j++)
			bestx[j] = x[j]
		bestn = cn
		return

	int ok = 1
	for (int j = 1; j < i; j++)
		// i不与j相连
		if (x[j] == 1 && !adjacent(i, j))
			ok = 0
			break

	// 尝试 x[i] = 1
	if (ok)
		// 把i加入完备子图
		x[i] = 1
		cn++
		Backtrack(i + 1)
		x[i] = 0
		cn--

	// 尝试 x[i] = 0
	if (cn + n - i > bestn)
		x[i] = 0
		Backtrack(i + 1)

int maxClique(int v[])
	x = int[n + 1]	// 初始化
	cn = 0
	bestn = 0
	bestx = v
	Backtrack(1)	// 寻找最大完备子图
	return bestn
TSP旅行商问题 ※

设G=(V,E)是一个带权图

  • 图中各边的费用(权)为正数
  • 图中的一条周游路线是包括V中的每个顶点在内的一条回路
  • 周游费用是这条边上所有边的费用之和

旅行商问题要在图G中找出费用最小的周游路线。

  • 排列树: 从根结点到解空间树的任一叶结点的路径,定义了图 G 的一条周游路线

  • 优化问题:

    • 剪去不可行: 当前节点和路径上的下一个节点之间必有边
    • 剪去非最优: 当前路径长度要保证 S < bestc
    • 启发式: 优先产生边长最小的子节点, 有望获得更好的限界效果
  • 遍历排列:

    • 这里从顺序排列开始,从前往后遍历,逐个数字与后面的交换,然后递归,最后再交换回来,实现对排列的遍历
    • 由于 Backtrack(2) 从第二个开始遍历,相当于默认了一个“起点”为 1,这样可以避免同一环路不同起点的重复计算

伪代码 O ( n 2 l o g n ) O(n^2logn) O(n2logn)

  • G: 图,G[i][j] 表示顶点 i 到顶点 j 的距离
  • bestc: 当前最优解,初始为无穷大
  • bestx: 当前最优解路径,初始为顶点集合 V
  • S: 当前路径长度
  • n: 顶点个数
  • x: 解向量,x[i] 表示路径中第 i 个顶点对应的顶点编号
TSP(int V[])
	bestc ← ∞
    bestx ← V  			// 存储最优路径,初始为顶点集合 V
    S ← 0  				// 路径长度
    n ← length(V)  		// 顶点个数
    x ← [0] * (n + 1)   // x 是路径,是所有点的排列
    for i in range(1, n + 1):
        x[i] ← i

    Backtrack(2)
    return bestc


Backtrack(i):
    // 到达叶子节点
    if i == n:
        // 判断 (n-1)→n,n→1 有边,可以闭环
        if G[x[n - 1]][x[n]] != ∞ and G[x[n]][x[1]] != ∞:
            // 新路径更短
            if S + G[x[n]][x[1]] + G[x[n - 1]][x[n]] < bestc or bestc == ∞:
                bestx ← copy(x)
                bestc ← S + G[x[n - 1]][x[n]] + G[x[n]][x[1]]
    else:
        for j in range(i, n + 1):  // 遍历后面未到的点
            if G[x[i - 1]][x[j]] != ∞:  // 判断是否有通路
                if S + G[x[i - 1]][x[j]] < bestc or bestc == ∞:  // 新路径不长于目前最优解
                    swap(x[i], x[j])
                    S += G[x[i - 1]][x[i]]
                    Backtrack(i + 1)
                    S -= G[x[i - 1]][x[i]]
                    swap(x[i], x[j])
子集和数问题

已知正数 M 和集合 W = { w i ∣ w i > 0 , 1 ≤ i ≤ n } W = \{w_i | w_i > 0, 1\le i\le n\} W={wiwi>0,1in},要求找出 W 的所有子集,使得子集内所有元素之和等于 M

例如:n = 4,M = 31,W = (11, 13, 24, 7)。则满足要求的子 集是 (11, 13, 7) 和 (24, 7)

子集和数的判定问题

  • 子集树

  • 存在性问题

  • 用长度固定的元组来设计一种回溯算法

  • 解向量的元素 x i x_i xi 取 1 或 0 值,表示子集是否包含 w i w_i wi

  • 限界函数:

剩下的数全加上都不够
∑ 1 ≤ i ≤ k w i x i + ∑ k + 1 ≤ i ≤ n w i < M \sum_{1\le i \le k} w_ix_i + \sum_{k+1 \le i \le n}w_i < M 1ikwixi+k+1inwi<M

剩下的数一个不加不够,加上一个剩下最小的就超了
∑ 1 ≤ i ≤ k w i x i ≠ M , ∑ 1 ≤ i ≤ k w i x i + w k + 1 > M ( w k + 1 ≤ w k + 2 ≤ ⋯ ≤ w n ) \sum_{1 \le i \le k}w_ix_i \ne M,\quad \sum_{1 \le i \le k} w_ix_i + w_{k+1} > M \qquad (w_{k+1} \le w_{k+2} \le \cdots \le w_n) 1ikwixi=M,1ikwixi+wk+1>M(wk+1wk+2wn)

伪代码

  • M: 子集和数
  • W: 集合
  • X: 解向量,X[i] = 1 表示集合 W[i] 在子集中
  • n: 集合元素个数
  • S: 当前解的和
  • r: 剩余数的和
// 子集和数问题求解算法
// 输入:正数 M,集合 W = {w_i | w_i > 0, 1 ≤ i ≤ n}
// 输出:满足子集和数为 M 的所有子集

subsetSum(M, W):
    n ← length(W)  // 集合 W 的元素个数
    X ← [0] * n  // 初始化解向量为全0的列表

    // 回溯搜索函数
    backtrack(k, S, r):
        if S == M:
            // 找到和数为 M 的子集
            printSubset(X)
            return

        if S + r < M or S + W[k] > M:
            // 剪枝条件:剩余数全加上或加上最小剩余数都不满足要求
            return

        // 展开左子节点
        if S + W[k] + W[k + 1] > M:
            // 停止展开左子节点,判断展开右子节点
            X[k] ← 1
            backtrack(k + 1, S + W[k], r - W[k])
        
        // 展开右子节点
        if S + r >= M and S + W[k + 1] <= M:
            X[k] ← 0
            backtrack(k + 1, S, r - W[k])

    // 初始化递归调用
    backtrack(0, 0, sum(W))

// 打印解向量对应的子集
printSubset(X):
    subset ← []
    for i in range(len(X)):
        if X[i] == 1:
            subset.append(W[i])
    print(subset)

最大子集和数问题

子集树 + 优化问题

顶点覆盖问题

已知 G 为一个无向图。给定一个 G 的一个顶点子集 U,当且仅当 G 中的每一条边必有一个顶点属于 U 或者两个顶点都属于 U 时,称 U 是 G 的一 个顶点覆盖(vertex cover),U 中顶点的数量是覆盖的大小(size)

给定一个无向图,顶点覆盖问题要求找到一个顶点集合,使得图中的每条边的至少一个端点都属于该集合。这个顶点集合被称为顶点覆盖

问题分析

  • 最优解应满足覆盖条件的最少顶点最小权值和
  • 解空间树:子集树

伪代码 O ( n 2 l o g n ) O(n^2logn) O(n2logn)

  • bestw: 当前最优解的权值
  • solution: 最优解的顶点集合
  • s: 当前解的权值
  • c: 当前解的顶点集合,c[i] = 1 表示顶点i在顶点覆盖集合中
  • e: 图的邻接矩阵,e[i][j] = 1 表示顶点ij之间有边
  • n: 图的顶点数
bestw ← infinity  // 初始化最优解的权值为正无穷大
solution ← []  // 初始化最优解的顶点集合为空

// 入口函数
def vertexCover():
    Backtrack(1, 0)  // 从顶点1开始进行回溯搜索
    return solution


// 回溯函数,递归实现
def Backtrack(i, s):
    if s >= bestw:
        return  // 剪枝:当前权值已不小于最优解的权值

    if i > n:
        if cover():  // 检查当前顶点集合是否是顶点覆盖
            bestw ← s  // 更新最优解的权值
            solution ← copy(c)  // 更新最优解的顶点集合
        return

    c[i] ← 0  // 不选择顶点i
    Backtrack(i + 1, s)  // 继续搜索下一个顶点

    c[i] ← 1  // 选择顶点i
    Backtrack(i + 1, s + w[i])  // 继续搜索下一个顶点


// 检查当前顶点集合是否是顶点覆盖
def cover():
    for i in range(1, n + 1):
        if c[i] == 0:  // 如果顶点i不在顶点覆盖集合中
            t ← 0
            j ← 1
            while j < i:  // 检查与顶点i相邻且编号小于i的顶点j
                if e[j][i] == 1 and c[j] == 1:
                    t ← 1
                j ← j + 1

            j ← i + 1
            while j <= n:  // 检查与顶点i相邻且编号大于等于i的顶点j
                if e[i][j] == 1 and c[j] == 1:
                    t ← 1
                j ← j + 1

            if t == 0:
                return 0  // 顶点i的所有相邻顶点都不在顶点覆盖集合中,返回0
    return 1  // 所有顶点的相邻顶点都在顶点覆盖集合中,返回1

分支限界

算法概述

基本思想

求解过程

  • 定义解空间,确定树结构
  • 按BFS或最佳优先方法搜索:
    • 每个活节点仅有一次机会变成扩展节点;
    • 由扩展节点生成所有一步可达的新节点;
    • 在新节点中,删除不可能导出最优解的节点,限界函数加速搜索; 限界策略
    • 将余下的新节点加入活动表/队列中(先进先出、优先队列);
    • 从活动表中选择节点再扩展; 分支策略
    • 直至活动表为空

活节点表

  • FIFO:先进先出
  • LIFO:后进先出
  • 优先队列
    • 最小耗费:小根堆构建活节点表
    • 最大收益:大根堆构建活节点表
LC-检索 「最小成本检索」
  • 状态空间树上任一节点的成本函数 c(x) 定义为:

    • “从x展开状态空间树能得到的最小成本值”
  • 对每个x,成本c(x)是一个可能不会达到的下限,可采用贪心法或启发式法求得

    • 例如对于背包问题
    • 某一状态的成本,就是当前所有没被放进背包的物品的价值和
    • 相当于假设了最好情况,接下来的所有物品都能放进背包,这样成本最低,就是成本下限c(x)
  • 对于一个已经到达了叶子节点的状态,其成本就是最终的成本,也就是最小成本,计为cost

    • cost是一个全局变量,初始值为无穷大
    • 每次到达新的叶子节点时,更新cost为当前最小成本
  • c(x) 可用于剪枝

    • 一个待展开节点的成本已经是估值的最小成本,其真值不可能更小
    • 如果一个待展开节点的成本 ≥ 最小成本,即c(x)≥cost,则剪枝
    • 这种思想与前面背包问题的回溯算法中,根据贪心法得到的上界bound(x) 进行剪枝的思想是一致的,只是这里的c(x)是下界
  • 每次拓展新节点时

    • 判断是否超过背包容量,如果超过则剪枝
    • 判断是否超过最小成本,如果超过则剪枝
    • 被剪枝的节点不用画出来,也就是不会被放入队列
  • 优先扩展最小成本的节点,可尽早找到最优解或最小成本解

    • 优先队列:小根堆构建活节点表
    • 每次从活节点表中取出成本最小的节点进行扩展

算法实例

最小罚款额作业调度

令三元组 ( p i , d i , t i ) (p_i,d_i,t_i) (pi,di,ti) = (罚款额,截止期,需要的处理时间)。用LC-分枝限界法求解最小罚款额作业调度问题

  • ( p i , d i , t i ) (p_i,d_i,t_i) (pi,di,ti) = ( 罚款额, 截止期, 需要的处理时间 )

  • 定长元组 X = ( x 1 , x 2 , ⋯   , x n ) X = (x_1, x_2, \cdots, x_n) X=(x1,x2,,xn) 表示可行作业解, x i = 1 x_i = 1 xi=1 表示作业 i i i 在解中

  • 求可行作业解 X X X,使得罚款额 ∑ i = 1 n ( 1 − x i ) p i \sum_{i = 1}^n (1 - x_i)p_i i=1n(1xi)pi 最小

  • 下界:

    • 状态空间树的节点 x = ( x 1 , x 2 , ⋯   , x k ) x = (x_1, x_2, \cdots, x_k) x=(x1,x2,,xk)
    • 当前结点 x x x 处已发生成本值 c ( x ) = ∑ i = 1 k ( 1 − x i ) p i c(x) = \sum_{i = 1}^k(1 - x_i)p_i c(x)=i=1k(1xi)pi
    • 成本不包括尚未生成的作业的罚款额,仅包括已损失的作业的罚款额
  • 到达某一节点,根据当前 x = ( x 1 , x 2 , ⋯   , x k ) x = (x_1, x_2, \cdots, x_k) x=(x1,x2,,xk) ,寻找一个最优的活动安排顺序

    • 如果无法找到使得该状态下每个作业都能在截止期前完成的安排顺序,则剪枝
    • 算法不在意作业的安排顺序,只在意作业是否能在截止期前完成
  • U U U 为当前可行解成本

    • U U U 是一个全局变量,初始值为无穷大
    • 每次到达新的叶子节点时,更新 U U U 为当前最小成本
    • 如果待展开的 c ( x ) ≥ U c(x) \ge U c(x)U,则剪枝
  • 限界条件:

    • 显性约束:截止期 d i d_i di
    • 隐性约束: c ( x ) ≥ U c(x) \ge U c(x)U
TSP旅行商问题 ※

给定一系列城市和每对城市之间的距离,求解访问每一座城市一次并回到起始城市的最短回路

  • 在旅行商问题中,每个节点只能出、入一次,构成哈密尔顿环
  • A 是邻接矩阵, 𝑎𝑖𝑗 表示节点 i 和 j 之间边的费用
  • A 的第 𝑖 行元素表示从 𝑖 节点出发到其他节点的费用(距离),A 的第 𝑗 列表示其他节点到 𝑗 节点的费用
  • 设 𝑓 = (𝑓1 , ⋯ , 𝑓𝑛 ) 为一条从节点 1 开始的周游路线,𝑓𝑖来自邻接矩阵的第 i 行;所有 𝑓𝑖 不同列
基本概念

归约矩阵和归约数

  • 行归约数: 邻接矩阵每行最小值的和

  • 行归约矩阵: 邻接矩阵每个元素减去自身每行最小值

  • 列归约数: 行归约矩阵每列最小值的和

  • 列归约矩阵: 行归约矩阵每个元素减去自身每列最小值

  • 归约矩阵: 邻接矩阵每个元素减去自身每行最小值,再减去此时每列最小值

  • 归约数: 行归约数与列归约数的和
    A = [ ∞ 30 6 4 30 ∞ 5 10 6 5 ∞ 20 4 10 20 ∞ ] R = [ ∞ 26 2 0 25 ∞ 0 5 1 0 ∞ 15 0 6 16 ∞ ] A ′ = [ ∞ 26 2 0 25 ∞ 0 5 1 0 ∞ 15 0 6 16 ∞ ] A=\begin{bmatrix} \infty & 30 & 6 & 4\\ 30 & \infty & 5 & 10\\ 6 & 5 & \infty & 20\\ 4 & 10 & 20 & \infty \end{bmatrix}\quad R=\begin{bmatrix} \infty & 26 & 2 & 0\\ 25 & \infty & 0 & 5\\ 1 & 0 & \infty & 15\\ 0 & 6 & 16 & \infty \end{bmatrix}\quad A'=\begin{bmatrix} \infty & 26 & 2 & 0\\ 25 & \infty & 0 & 5\\ 1 & 0 & \infty & 15\\ 0 & 6 & 16 & \infty \end{bmatrix} A= 306430510652041020 R= 2510260620160515 A= 2510260620160515
    定理

  • 已知有向赋权图 G = (V, E) 和它的一条哈密尔顿回路 f,A 是 G 的邻接矩阵,A’ 是 A 的归约矩阵,归约常数为 h。分别记以 A 和 A’ 计算的回路费用为 W(f)、W’(f),有:
    W ( f ) = W ′ ( f ) + h W(f) = W'(f) + h W(f)=W(f)+h

  • 已知有向赋权图 G = (V, E) 的最短的哈密尔顿回路 f,A 是 G 的邻接矩阵, Q 是 A 的归约矩阵。以 Q 为邻接矩阵构造图 G’,则 f 也是 G’ 的最短的哈密尔顿回路

归约解法理解
  • 归约矩阵相当于在原来的邻接矩阵上打洞

    • 使用行归约和列归约,获得一个归约矩阵A
    • 打洞后的矩阵中,每行每列都至少有一个 0,对应得到一个归约数h
    • 打洞后的矩阵对回路长度影响多少,就是归约数
  • 归约数本身就是一个最下限的下限

    • 假设有洞的边刚好对应一个回路
    • 那么归约矩阵对应一个点全重合的图,回路长度为 0,加上归约数,就是原图的最小回路长度
    • 当然这需要有洞的边刚好构成回路,概率极小,所以归约数是一个最下限的下限
  • 每个新扩展节点,都需对其归约矩阵进行修改

    • 假设某个扩展节点,是从 i 扩展到 j
    • 那么在这个设定下,就不再会从 i 扩展到别的节点,或其他节点扩展到 j
    • 所以,将矩阵中 i 行和 j 列全修改为 ∞
    • 又由于避免过早形成回路,导致缺点,所以将 A(j, 0)的点修改为 ∞
    • 修改前的上一个节点归约矩阵为A,修改后的新节点归约矩阵为A'
    • 修改后的归约矩阵 A'将计算得到一个新的归约数 h'
  • 更精确的下界是由归约数和最小边权和得到的

    • 每一次扩展,都会加上一条边,其在被扩展前的归约矩阵中对应一个边的权 A(i,j)
    • 新节点的下界 = 旧节点的下界 + 新归约数 h' + 扩展边的权A(i,j)
  • 归约数 + 边的权的双重作用

    • 作为下界,与已求得的最小成本解相比,可以剪枝
    • 作为优先队列的优先级,可以优先扩展最小成本的节点,尽早找到最优解
伪代码
function BnB(adjMatrix):
    n <- numCities
    
    // 初始化根节点
    root <- createNode(0, adjMatrix, 0, [])
    pq <- createPriorityQueue()
    enqueue(pq, root)
    
    // 初始化最优解
    bestCost <- infinity
    bestPath <- []
    
    while pq not empty:
        currNode <- dequeue(pq)
        
        // 检查是否达到叶节点
        if currNode.level == n:
            currCost <- currNode.cost + adjMatrix[currNode.city, 0]
            if currCost < bestCost:
                bestCost <- currCost
                bestPath <- currNode.path
                
        // 扩展节点
        for i from 1 to n:
            if adjMatrix[currNode.city, i] != infinity and i not in currNode.path:
                childMatrix <- copyMatrix(currNode.matrix)
                updateMatrix(childMatrix, currNode.city, i)
                childCost <- currNode.cost + adjMatrix[currNode.city, i]
                childPath <- currNode.path + [i]
                childNode <- createNode(currNode.level + 1, childMatrix, childCost, childPath)
                
                // 计算归约数
                reduction <- calculateReduction(childNode.matrix)
                lowerBound <- childCost + reduction
                
                // 剪枝条件
                if lowerBound < bestCost:
                    enqueue(pq, childNode)
    
    return bestCost, bestPath

function createNode(lvl, matrix, cost, path):
    node <- newNode()
    node.level <- lvl
    node.matrix <- matrix
    node.cost <- cost
    node.path <- path
    return node

function createPriorityQueue():
    pq <- newPriorityQueue()
    return pq

function enqueue(pq, node):
    // 根据节点的下界估计值作为优先级将节点插入优先队列
    priority <- node.lowerBound
    pq.insert(node, priority)

function dequeue(pq):
    node <- pq.removeMin()
    return node

function copyMatrix(matrix):
    // 复制矩阵
    newMatrix <- newMatrix()
    // 省略复制逻辑
    return newMatrix

function updateMatrix(matrix, row, col):
    // 更新矩阵,将行row和列col的元素设为无穷大
    matrix[row, :] <- infinity
    matrix[:, col] <- infinity

function calculateReduction(matrix):
    reduction <- 0
    for i from 0 to n:
        minRowValue <- min(matrix[i, :])
        minColValue <- min(matrix[:, i])
        reduction <- reduction + minRowValue + minColValue
    return reduction

算法对比

五大算法

贪心法分治法动态规划回溯分支限界
分步骤,决策,状态分部分,治,合分规模,递归,回溯分枝(深度优先),剪枝,回溯扩展(广度优先/最佳优先),删除
决策节点,贪心策略问题划分,优化时间复杂度,确保最差情况下的时间复杂度规模可变最优子问题,子问题间递归关系解空间,限界函数,回溯,数据预处理

分支限界与回溯法

区别回溯法分支限界法
形象说明类似深度优先搜索类似广度优先搜索
求解目标找出解空间树中满足约束条件的所有解尽快地找出满足约束条件的一个解
搜索方法深度优先方法广度优先或最佳优先方法
扩展方式每次只产生一个节点每一个活节点只有一次机会成为扩展节点,并一次性产生所有儿子节点
存储要求内存容量有限时,回溯法成功的可能性更大分治限界法的存储空间比回溯法大得多
  • 9
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值