山东大学算法设计与分析复习笔记

名词解释

O、Ω、θ的定义(渐近复杂度符号)

  • O(Big O) :表示上界,说明算法的复杂度不超过某个函数,定义为:
    若存在正数常数c,n0,使当n≥n0时,有

T(n)≤c⋅f(n)

则记为:T(n)=O(f(n))

  • Ω(Omega) :表示下界,说明算法的复杂度至少是某个函数,定义为:
    存在正数常数c,n0,使当n≥n0时,有

T(n)≥c⋅f(n)

则记为:T(n)=Ω(f(n))

  • θ(Theta) :表示紧确界,算法的复杂度是f(n)的渐近界:
    存在正数常数c1,c2,n0,使当n≥n0时,有

c1⋅f(n)≤T(n)≤c2⋅f(n)

则记为:T(n)=Θ(f(n))

P、NP、NPC问题的定义以及归约的定义

归约问题

将3SAT问题归约到团问题

3SAT问题定义

  • 输入 :一个由若干子句(clauses)构成的布尔逻辑表达式(合取范式,CNF),其中每个子句恰好包含3个文字 (literal,即变量或其否定)。
    示例

    (x1∨¬x2∨x3)∧(¬x1∨x4∨¬x5)∧(x2∨x3∨¬x4)

  • 问题:是否存在一组对变量的赋值(真/假),使得整个表达式为真(所有子句均被满足)?

团问题定义:

给定一个无向图G=(V,E)和整数k,判断是否存在大小为k的完全子集(即团)。

  • 归约构造

    设3-SAT实例有 m 个子句 C1,C2,...,Cm每个子句含3个文字。构造图 G:

    • 顶点:为每个子句 Ci中的每个文字创建一个顶点(如子句 (x1∨¬x2∨x3)对应顶点 vi1,vi2,vi3​)。

    • :连接不同子句的顶点,除非它们互为否定(如 x1 和 ¬x1​ 无边)。

  • 参数设置:令 k=m(子句数)。

  • 正确性证明

    • 若3-SAT可满足:存在赋值使每个子句至少一个文字为真。从每个子句选一个真文字对应的顶点,构成集合 V′。因不同子句的顶点无边当且仅当冲突,而赋值无冲突,故 V′ 中任意两点有边 → 形成大小为 k=m的团。

    • 若 G 有大小为 k=m 的团:团中顶点来自不同子句(因同子句内无边)。选择这些顶点对应的文字赋值为真,无冲突(因无边仅存于非互斥文字间) → 满足所有子句。

  • 结论:团问题 ∈ NP,且3-SAT ≤ₚ 团问题 → 团问题是NP完全问题(依据PPT中"若X是NP完全且X≤ₚY,则Y是NP完全")。

将团问题规约到顶点覆盖问题

  • 归约构造

    • 给定团问题实例 (G,k),构造其补图 Gˉ=(V,Eˉ)(Eˉ包含所有 G 中不存在的边)。

    • 设顶点覆盖实例为 (Gˉ,∣V∣−k)

  • 正确性证明

    • 若 G 有大小为 k 的团 V′

      在 Gˉ中,V′ 内任意两点无边 → Gˉ 的每条边至少有一个端点不在 V′ → V∖V′ 覆盖 Gˉ 的所有边,且 ∣V∖V′∣=∣V∣−k。

    • 若 Gˉ有大小为 ∣V∣−k的顶点覆盖 C

      则 V∖C在 Gˉ 中无边 → 在 G 中 V∖C 内任意两点均有边 →V∖C 是 G 的大小为 k 的团。

  • 归约时间:构造补图 Gˉ 需 O(∣V∣2)时间(检查所有顶点对),是多项式时间。

结论:顶点覆盖问题 ∈ NP,且团问题 ≤ₚ 顶点覆盖问题 → 顶点覆盖问题是NP完全问题(依据归约传递性)。

将团问题规约到子图同构问题

子图同构问题:

给定两个图G1​和G2​,判断是否存在一个顶点映射,使得G1​保持结构地"同构"到G2​的子图。

  1. 给定团问题实例

    • 图 G=(V,E)

    • 目标团大小 k

  2. 构造子图同构实例

    • 令 H为完全图 Kk(即 k 个顶点,两两相连)。

    • 问题转化为:判断 H 是否是 G 的子图同构

证明

  • 若 G有大小为 k 的团

    • 则该团本身就是一个完全图 Kk,与 H 同构。

    • 因此,子图同构问题的答案为"是"。

  • 若 H 是 G 的子图同构

    • 则 G 必须包含 k 个顶点,两两相连,即构成一个团。

    • 因此,原团问题的答案为"是"。

结论

Clique(G,k)  ⟺  SubgraphIsomorphism(G,Kk)Clique(G,k)⟺SubgraphIsomorphism(G,Kk​)

即,团问题可以归约到子图同构问题。

贪心算法正确性证明

  1. 定义贪心选择策略(如:选结束最早的活动/单位价值最高的物品)。

  2. 证明贪心选择性质:

  • 设 S 是不含贪心选择 a 的最优解。

  • 构造 S' = (S - {b}) ∪ {a}(b 是 S 中某个元素)。

  • 证明 S' 可行且价值 ≥ S。

  1. 证明最优子结构:
  • 设原问题最优解为 {a} ∪ S_sub。

  • 若 S_sub 不是子问题最优解,则存在更优解 S_sub',导致 {a} ∪ S_sub' 优于原解,矛盾。

  1. 由数学归纳法:算法每一步均保持最优性。

计算最大流

计算时间复杂度

分治例题

归并排序伪代码

复制代码
function merge_sort(arr):
    if length(arr) <= 1:
        return arr

    mid = length(arr) // 2
    left = arr[0:mid]
    right = arr[mid:]

    // 递归排序左右子数组
    sorted_left = merge_sort(left)
    sorted_right = merge_sort(right)

    // 合并两个已排序数组
    return merge(sorted_left, sorted_right)

function merge(left, right):
    result = []
    i = 0
    j = 0

    while i < length(left) and j < length(right):
        if left[i] <= right[j]:
            result.append(left[i])
            i += 1
        else:
            result.append(right[j])
            j += 1

    // 复制剩余元素
    while i < length(left):
        result.append(left[i])
        i += 1

    while j < length(right):
        result.append(right[j])
        j += 1

    return result

寻找一个序列中倒置(inversions)的数量,若前面的数比后面的数大,就形成一个倒置,例如74385中有74,73,75,43,85五个(具体案例记不清了,大概是这些数),参考归并排序算法,写出基本思想,伪代码,时间复杂度

  1. 分治:将序列分成左右两半,递归计算左半部分的倒置数和右半部分的倒置数。

  2. 合并 :在合并左右两部分时,如果左半部分的某个元素 A[i] 大于右半部分的某个元素 A[j],则 A[i] 及其后面的所有左半部分元素都会与 A[j] 形成倒置(因为左右部分已经分别有序)。

    • 因此,倒置数可以增加 mid - i + 1(其中 mid 是左半部分的最后一个位置)。

这种方法的时间复杂度为 O(n log n),与归并排序相同。

复制代码
function countInversions(A):
    n = length(A)
    if n <= 1:
        return 0  // 单个元素无倒置
    mid = n // 2
    left = A[0..mid-1]
    right = A[mid..n-1]
    inversions = countInversions(left) + countInversions(right)  // 递归计算左右部分的倒置
    inversions += mergeAndCount(A, left, right)  // 合并并统计跨越左右的倒置
    return inversions

function mergeAndCount(A, left, right):
    i = j = k = 0
    inversions = 0
    while i < length(left) and j < length(right):
        if left[i] <= right[j]:
            A[k] = left[i]
            i += 1
        else:
            A[k] = right[j]
            inversions += (length(left) - i)  // 左半剩余元素均与 right[j] 形成倒置
            j += 1
        k += 1
    // 处理剩余元素
    while i < length(left):
        A[k] = left[i]
        i += 1
        k += 1
    while j < length(right):
        A[k] = right[j]
        j += 1
        k += 1
    return inversions

最大子数组

DP问题

钢条切割问题

自底向上的记忆化搜索并记录每次最优解的切割点为s

矩阵链乘法

通过动态规划自底向上计算所有可能的矩阵链分割方式,选择乘法代价最小的分割点,逐步构建最优解。枚举所有长度的矩阵链下,遍历选择代价最小的分割点并记录.

  • m[i,k]:左子链 Ai...Ak​ 的最小代价。

  • m[k+1,j]:右子链 Ak+1...Aj的最小代价。

  • pi−1pkpj:合并左右子链的代价(即两个结果矩阵相乘的代价)。

最大公共子序列问题

贪心例题

01背包

活动选择问题

选择局部最优即每次选择结束时间最早的活动,就能剩下更多时间给后面活动选择.

时间复杂度O(n+nlgn)->O(nlgn) 将活动按结束时间递增排序

注意检查合法性

单源最短路径问题

迪杰斯特拉算法

  1. 维护一个优先队列(最小堆) ,每次选择当前距离 s 最近的未访问顶点 u(即 u.d 最小)。

  2. 松弛(Relax)操作:对 u 的所有邻接顶点 v,检查是否可以通过 u 缩短 v 到 s 的距离(即 v.d>u.d+w(u,v) 时更新 v.d)。(u.Π是前驱节点)

  3. 逐步扩展已确定最短路径的集合 S,直到所有顶点被处理。

贝尔曼福德算法

记住n-1次松弛,如果第n次还能松那就是有负权边

所有顶点对之间的最短路

按边数分解的(基于矩阵乘法的)动态规划算法

  1. 计算 D[1] = W(初始权重矩阵)。

    • 通过类似矩阵乘法的方式,计算 D[2] = D[1] * D[1]D[4] = D[2] * D[2],直到 D[m](其中 m ≥ n-1)。

    • 每次"乘法"操作的时间复杂度是 O(n³),共进行 O(log n) 次,总时间复杂度 O(n³ log n)

  2. 检测负权环

    • 计算 D[n],如果 D[n] != D[n-1],说明存在负权环(因为最短路径可以无限绕环降低权重)。

Floyd-Warshall 算法(按顶点编号分解)

循环1-n作为中间节点时,对矩阵进行松弛更新

矩阵更新D(1)表示允许以1作为中间节点时的矩阵

近似算法

Makespan(负载均衡)问题
  • 问题描述:将 nn 个任务分配到 mm 台机器上,最小化所有机器的最大负载(完成时间)。

  • 算法思想(贪心策略)

    • List Scheduling:按顺序将每个任务分配给当前负载最小的机器。

    • Longest Processing Time First (LPT):先按任务处理时间从大到小排序,再用 List Scheduling 分配。

顶点覆盖问题

    • 问题描述:在无向图中找到一个最小的顶点集合,覆盖所有的边。

    • 算法思想(基于极大匹配)

      • 每次都挑选一条未覆盖的边(步骤3)。
      • 为了覆盖这条边,将它的两个端点(u和v)加入集合C(步骤4)。
      • 移除所有被u或v覆盖的边,减少未覆盖边(步骤5)。
      • 重复上述过程直到所有边都被覆盖。

旅行商问题(TSP,满足三角不等式)

  • 问题描述:在完全图中,找到一个经过所有顶点的最小权重哈密顿回路(满足三角不等式)。

  • 算法思想

    1. 用 Prim 或 Kruskal 算法构造最小生成树(MST)。

    2. 对 MST 进行前序遍历,得到访问顺序。

    3. 跳过重复顶点,直接按前序遍历顺序构造哈密顿回路。

    复制代码
       MST-PRIM(G, w, r)
       1  for each u ∈ G.V          # 初始化所有顶点的键值和父节点
       2      u.key = ∞             # 键值表示连接到当前树的最小边权
       3      u.π = NIL             # 父节点初始为空
       4  r.key = 0                 # 任选根节点 r,键值设为 0
       5  Q = G.V                   # 优先队列 Q 包含所有顶点
       6  while Q ≠ ∅
       7      u = Extract-Min(Q)    # 取出键值最小的顶点 u(首次为 r)
       8      for each v ∈ G.Adj[u] # 遍历 u 的邻接顶点 v
       9          if v ∈ Q and w(u,v) < v.key
       10             v.π = u        # 更新 v 的父节点为 u
       11             v.key = w(u,v) # 更新 v 的键值为边权 w(u,v)
相关推荐
阿阳微客12 分钟前
Steam 搬砖项目深度拆解:从抵触到真香的转型之路
前端·笔记·学习·游戏
WarPigs2 小时前
Unity性能优化笔记
笔记·unity·游戏引擎
木子.李3472 小时前
排序算法总结(C++)
c++·算法·排序算法
闪电麦坤953 小时前
数据结构:递归的种类(Types of Recursion)
数据结构·算法
Gyoku Mint4 小时前
机器学习×第二卷:概念下篇——她不再只是模仿,而是开始决定怎么靠近你
人工智能·python·算法·机器学习·pandas·ai编程·matplotlib
纪元A梦4 小时前
分布式拜占庭容错算法——PBFT算法深度解析
java·分布式·算法
echo haha5 小时前
第7章 :面向对象
笔记
njsgcs5 小时前
chili3d 笔记16 emscripten配置 |用cnpm i 安装 |hello world 编译
笔记