山东大学算法设计与分析复习笔记
名词解释
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的子图。
给定团问题实例:
图 G=(V,E)
目标团大小 k
构造子图同构实例:
令 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。
3. 证明最优子结构:
- 设原问题最优解为 {a} ∪ S_sub。
- 若 S_sub 不是子问题最优解,则存在更优解 S_sub',导致 {a} ∪ S_sub' 优于原解,矛盾。
4. 由数学归纳法:算法每一步均保持最优性。
计算最大流
计算时间复杂度
分治例题
归并排序伪代码
function merge_sort(arr):if length(arr) <= 1:return arrmid = length(arr) // 2left = 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 = 0j = 0while i < length(left) and j < length(right):if left[i] <= right[j]:result.append(left[i])i += 1else:result.append(right[j])j += 1// 复制剩余元素while i < length(left):result.append(left[i])i += 1while j < length(right):result.append(right[j])j += 1return result
寻找一个序列中倒置(inversions)的数量,若前面的数比后面的数大,就形成一个倒置,例如74385中有74,73,75,43,85五个(具体案例记不清了,大概是这些数),参考归并排序算法,写出基本思想,伪代码,时间复杂度
-
分治:将序列分成左右两半,递归计算左半部分的倒置数和右半部分的倒置数。
-
合并:在合并左右两部分时,如果左半部分的某个元素
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 // 2left = A[0..mid-1]right = A[mid..n-1]inversions = countInversions(left) + countInversions(right) // 递归计算左右部分的倒置inversions += mergeAndCount(A, left, right) // 合并并统计跨越左右的倒置return inversionsfunction mergeAndCount(A, left, right):i = j = k = 0inversions = 0while i < length(left) and j < length(right):if left[i] <= right[j]:A[k] = left[i]i += 1else:A[k] = right[j]inversions += (length(left) - i) // 左半剩余元素均与 right[j] 形成倒置j += 1k += 1// 处理剩余元素while i < length(left):A[k] = left[i]i += 1k += 1while j < length(right):A[k] = right[j]j += 1k += 1return inversions
最大子数组
DP问题
钢条切割问题
自底向上的记忆化搜索并记录每次最优解的切割点为s
矩阵链乘法
通过动态规划自底向上计算所有可能的矩阵链分割方式,选择乘法代价最小的分割点,逐步构建最优解。枚举所有长度的矩阵链下,遍历选择代价最小的分割点并记录.
-
m[i,k]:左子链 Ai…Ak 的最小代价。
-
m[k+1,j]:右子链 Ak+1…Aj的最小代价。
-
pi−1pkpj:合并左右子链的代价(即两个结果矩阵相乘的代价)。
最大公共子序列问题
贪心例题
01背包
活动选择问题
选择局部最优即每次选择结束时间最早的活动,就能剩下更多时间给后面活动选择.
时间复杂度O(n+nlgn)->O(nlgn) 将活动按结束时间递增排序
注意检查合法性
单源最短路径问题
迪杰斯特拉算法
-
维护一个优先队列(最小堆),每次选择当前距离 s 最近的未访问顶点 u(即 u.d 最小)。
-
松弛(Relax)操作:对 u 的所有邻接顶点 v,检查是否可以通过 u 缩短 v 到 s 的距离(即 v.d>u.d+w(u,v) 时更新 v.d)。(u.Π是前驱节点)
-
逐步扩展已确定最短路径的集合 S,直到所有顶点被处理。
贝尔曼福德算法
记住n-1次松弛,如果第n次还能松那就是有负权边
所有顶点对之间的最短路
按边数分解的(基于矩阵乘法的)动态规划算法
- 计算
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)
。
-
-
检测负权环:
-
计算
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,满足三角不等式)
-
问题描述:在完全图中,找到一个经过所有顶点的最小权重哈密顿回路(满足三角不等式)。
-
算法思想:
-
用 Prim 或 Kruskal 算法构造最小生成树(MST)。
-
对 MST 进行前序遍历,得到访问顺序。
-
跳过重复顶点,直接按前序遍历顺序构造哈密顿回路。
-
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)
-