第 1 章 算法和算法分析
1. 掌握算法的定义及特征
算法的定义: 算法是解决问题的一系列明确指令的集合。它是计算机科学的核心,任何计算或处理过程都可以被视为某种形式的算法。
算法的特征:
- 有穷性:算法必须在有限的步骤内完成,即其每一个步骤都必须有确定的结束点。
- 确定性:算法的每一个步骤都必须明确,无二义性。
- 可行性:算法的每一步都应该是可行的,即能通过基本操作在有限时间内完成。
- 输入:一个算法应该有零个或多个输入。
- 输出:一个算法至少有一个输出。
2. 掌握好的算法的标准
一个好的算法通常需要满足以下标准:
- 正确性:算法必须准确无误地解决问题,产生正确的输出。
- 时间复杂度:算法的运行时间应该尽可能地短,即算法应高效。
- 空间复杂度:算法所需的存储空间应尽可能少。
- 可读性:算法应易于理解和维护。
- 健壮性:算法应能处理异常情况,即应对非法输入或其他意外情况有良好的应对能力。
3. 了解算法与程序的区别
- 算法是解决问题的方法和步骤的集合,是一种理论描述。
- 程序是用某种编程语言实现的算法,是算法的具体实现和执行过程。
4. 掌握算法复杂性分析的相关知识
算法复杂性分析是评估算法在资源(如时间和空间)使用上的效率。主要包括:
- 时间复杂度:衡量算法运行时间随输入规模增长的变化。常用大O符号表示,如 𝑂(𝑛)O(n)、𝑂(log𝑛)O(logn)、𝑂(𝑛2)O(n2) 等。
- 空间复杂度:衡量算法所需内存空间随输入规模增长的变化。同样用大O符号表示。
5. 了解算法的度量标准
度量算法性能的常用标准包括:
- 最坏情况时间复杂度:算法在最坏情况下的运行时间。
- 平均情况时间复杂度:算法在所有可能输入上的平均运行时间。
- 最优情况时间复杂度:算法在最优情况下的运行时间。
- 空间复杂度:评估算法所需的存储空间。
- 渐近分析:通过忽略低阶项和常数项,关注输入规模趋近无穷时的行为,来简化复杂度分析,如 𝑂(𝑛)O(n)、𝑂(𝑛2)O(n2)。
第 2 章 分治法
1. 掌握递归的定义及特点
递归的定义: 递归是一种直接或间接调用自身的编程技术。通过递归,一个问题可以分解为若干个更小的相同问题的子问题。
递归的特点:
- 基准条件:递归必须有一个或多个基准条件,这些条件不再递归调用自身,直接返回结果。
- 递归条件:定义函数在何种情况下调用自身。
- 递归层次:递归的层次必须是有限的,即递归调用必须朝着基准条件前进。
2. 掌握分治的定义及特点
分治的定义: 分治法(Divide and Conquer)是一种算法设计范式,通过将问题分解为更小的子问题,递归求解子问题,再将子问题的解合并以得到原问题的解。
分治的特点:
- 分解(Divide):将原问题分解为若干个规模较小、结构相似的子问题。
- 解决(Conquer):递归地求解这些子问题。若子问题规模足够小,则直接求解。
- 合并(Combine):将子问题的解合并,得到原问题的解。
3. 掌握使用分治法的条件
使用分治法的条件通常包括:
- 问题可以分解:原问题可以分解为多个相同或相似的子问题。
- 子问题独立:子问题之间相互独立,不会相互影响。
- 子问题易合并:子问题的解可以合并成原问题的解。
- 递归求解:子问题的规模足够小,可以通过递归求解。
4. 掌握分治的基本思想和设计步骤
分治法的基本思想:
- 将一个大问题分解成若干个规模较小的子问题。
- 递归地解决每个子问题。
- 将每个子问题的解合并,得到原问题的解。
分治法的设计步骤:
- 分解:将问题分解为若干个子问题。
- 解决:递归地解决每个子问题。
- 合并:将子问题的解合并,得到原问题的解。
5. 掌握使用分治法求解经典问题的步骤和伪码实现
棋盘覆盖问题
问题描述:
给定一个 2𝑘×2𝑘的棋盘,其中一个方格被覆盖,使用L形骨牌覆盖其余部分。
步骤:
- 将棋盘分成四个2𝑘-1×2𝑘-1 的子棋盘。
- 在中心位置放置一个 L 形骨牌,使其覆盖三个子棋盘的一个角。
- 递归地覆盖每个子棋盘。
伪码:
合并排序
问题描述:
对一个数组进行排序。
步骤:
- 将数组分成两半。
- 递归地排序每一半。
- 合并两个有序数组。
伪码:
快速排序
问题描述:
对一个数组进行排序。
步骤:
- 选择一个基准元素。
- 将数组分成两部分,左边部分小于基准,右边部分大于基准。
- 递归地排序两部分。
伪码:
线性时间的选择(快速选择)
问题描述:
在无序数组中找到第k小的元素。
步骤:
- 选择一个基准元素。
- 将数组分成两部分,左边部分小于基准,右边部分大于基准。
- 根据基准的位置递归地选择需要的部分。
伪码:
第 3 章 动态规划
1. 掌握动态规划的定义及特点。
2. 掌握使用动态规划的条件。
3. 掌握动态规划的基本思想和设计步骤。
4. 掌握使用动态规划求解经典问题的步骤和伪码实现,包括:矩阵链乘,多段
图,0-1 背包,最优二叉搜索树等。
1.动态规划的定义及特点
定义: 动态规划是一种算法设计方法,用于求解具有重叠子问题和最优子结构性质的问题。通过将问题分解为子问题,逐步解决子问题并合并其结果,最终解决原问题。
特点:
重叠子问题: 问题可以分解为重复出现的子问题。
最优子结构: 原问题的最优解包含子问题的最优解。
子问题无关性: 各子问题互相独立。
2.使用动态规划的条件
问题具有最优子结构性质: 即一个问题的最优解可以通过子问题的最优解来构建。
子问题重叠: 递归算法会反复求解相同的子问题。
3.动态规划的基本思想和设计步骤
定义子问题: 明确子问题的含义和数量。
递归关系: 找出子问题之间的关系,建立递推公式。
底层情况: 找到最小子问题的解。
计算子问题: 采用自底向上或自顶向下(带备忘录)的方式计算子问题的解。
4.使用动态规划求解经典问题的步骤和伪码实现
矩阵链乘法
问题描述:
给定一系列矩阵,计算它们的乘法顺序,使得计算量最小。
步骤:
- 定义 m[i][j] 为计算矩阵 A_i 到 A_j 的最小乘法次数。
- 初始化:m[i][i] = 0。
- 递推关系:m[i][j] = min(m[i][k] + m[k+1][j] + p[i-1]*p[k]*p[j]),其中 i ≤ k < j。
- 最后,m[1][n] 即为所求。
伪码:
多段图最短路径
问题描述:
给定一个多段图,找到从起点到终点的最短路径。
步骤:
- 定义 d[i] 为从节点 i 到终点的最短距离。
- 初始化:d[终点] = 0。
- 递推关系:d[i] = min(cost(i, j) + d[j]),其中 j 是 i 的后继节点。
- 计算 d[起点]。
伪码:
0-1 背包问题
问题描述:给定一组物品,每个物品有重量和价值,在总重量不超过背包容量的情况下,求最大价值。
步骤:
- 定义 dp[i][w] 为前 i 个物品在重量不超过 w 的情况下的最大价值。
- 初始化:dp[0][w] = 0。
- 递推关系:dp[i][w] = max(dp[i-1][w], dp[i-1][w-weight[i]] + value[i])。
- 最后,dp[n][W] 即为所求。
伪码
最优二叉搜索树
问题描述:给定一组有序键及其访问概率,构建一棵二叉搜索树,使得搜索的期望代价最小。
步骤:
- 定义 e[i][j] 为包含键 k_i 到 k_j 的最优二叉搜索树的期望代价。
- 初始化:e[i][i-1] = 0。
- 递推关系:e[i][j] = min(e[i][r-1] + e[r+1][j] + sum(p[i] to p[j])),其中 i ≤ r ≤ j。
- 最后,e[1][n] 即为所求。
伪码:
第 4 章 贪心算法
1. 掌握贪心算法的定义及特点。
2. 掌握使用贪心算法的条件。
3. 掌握贪心算法的基本思想和求解步骤。
4. 掌握贪心算法和动态规划方法的异同。
5. 掌握使用贪心算法求解经典问题的步骤和伪码实现,包括:活动安排,一般
背包,哈夫曼编码,单源最短路径,最小生成树等。
1. 贪心算法的定义及特点
定义:贪心算法是一种在每一步选择中都采取在当前状态下最好或最优(即局部最优解)的选择,从而希望结果是全局最优的算法策略。
特点:
- 每步选择都采取当前状态下的最优选择(局部最优解)。
- 不进行回溯,一旦做出选择就不会改变。
- 不能保证最终能得到全局最优解,但在某些问题上表现良好。
2. 使用贪心算法的条件
- 问题具有贪心选择性质:即每一步的最优解可以导致最终的全局最优解。
- 无后效性:即某个状态确定后,之后的过程不会影响到它之前的状态。
3. 贪心算法的基本思想和求解步骤
- 基本思想:每一步都选择当前情况下的最优解,期望通过局部最优解达到全局最优解。
- 求解步骤:
- 制定贪心策略,选择当前最优解。
- 确定每一步的选择是否符合问题的最优解性质。
- 通过局部最优解推导全局最优解。
4. 贪心算法和动态规划方法的异同
相同点
- 目标一致:都用于解决优化问题,目标是找到最优解。
- 步骤相似:都通过构建一个解的序列来达到最优解。
- 问题分解:都涉及将问题分解成子问题来处理。
不同点
1. 方法论
- 贪心算法:
- 局部最优选择:每一步都做出在当前看来是最优的选择,不考虑全局,只关注当前的选择。
- 不回溯:一旦做出选择,就不会回溯进行更改。
- 简单高效:通常实现简单,运行时间较短。
- 适用条件:适用于贪心选择性质和最优子结构性质的问题,即通过局部最优解可以得到全局最优解。
- 动态规划:
- 全局最优选择:通过考虑所有可能的子问题解,最终构建出全局最优解。
- 重叠子问题:利用已经计算过的子问题解,避免重复计算。
- 回溯:有时需要回溯来找到最优解的路径。
- 复杂性:通常实现较为复杂,运行时间较长,但适用范围广。
- 适用条件:适用于具有最优子结构和重叠子问题性质的问题。
2. 实现方式
- 贪心算法:
- 步骤:选择当前最优 -> 更新问题 -> 重复直到解决问题。
- 例子:找零问题、最小生成树(Kruskal、Prim算法)、活动选择问题。
- 动态规划:
- 步骤:定义状态 -> 建立状态转移方程 -> 计算最优解。
- 例子:最长公共子序列、背包问题、最短路径(Floyd-Warshall算法)、矩阵链乘法。
3. 时间复杂度
- 贪心算法:通常时间复杂度较低,O(n log n) 或 O(n)。
- 动态规划:时间复杂度较高,通常为多项式时间,如 O(n^2) 或 O(n^3)。
例子对比:
最小生成树
- 贪心算法:Kruskal 和 Prim 算法。
- 动态规划:不常用于这种问题,因为贪心算法更适合。
最短路径
- 贪心算法:Dijkstra 算法(适用于非负权图)。
- 动态规划:Floyd-Warshall 算法(适用于任意权图,包括负权图)。
背包问题
- 贪心算法:适用于分数背包问题,但不适用于0/1背包问题。
- 动态规划:适用于0/1背包问题,通过构建DP表来解决。
都是求解最优
- 相同点:都是求解最优化问题的方法。
- 不同点:
- 贪心算法每一步选择都采取当前最优解,不考虑未来的影响,没有回溯。
- 动态规划则是通过保存中间状态,具有回溯性质,通过分析每一步的选择来进行状态转移和最优化决策。
5. 使用贪心算法求解经典问题的步骤和伪码实现
1. 活动安排
问题描述:给定一系列活动,每个活动都有一个开始时间和结束时间,选择尽可能多的活动,使得没有两个活动的时间重叠。
贪心策略:
- 按照活动的结束时间排序。
- 选择第一个活动。
- 选择下一个与当前选择的活动不冲突的活动。
- 重复步骤3,直到没有更多活动可以选择。
伪代码:
function activitySelection(S, F, n):
A = sorted(zip(S, F), key=lambda x: x[1]) # 按结束时间排序活动
res = [A[0]] # 选择第一个活动
last = A[0][1] # 设置最后选择的活动的结束时间
for i in range(1, n):
if A[i][0] >= last: # 如果活动与当前选择的活动不冲突
res.append(A[i]) # 选择该活动
last = A[i][1] # 更新最后选择的活动的结束时间
return res # 返回选择的活动列表
2. 一般背包
问题描述:给定一组物品,每个物品有一定的价值和重量,确定每种物品的数量,使得总重量不超过背包容量,且总价值最大。
贪心策略:
- 按照单位重量价值(价值/重量)排序。
- 从单位重量价值最大的物品开始,选择可以放入背包的物品。
- 重复步骤2,直到背包容量耗尽。
伪代码:
function knapsack(W, w, v, n):
I = sorted(zip(w, v), key=lambda x: x[1]/x[0], reverse=True) # 按单位重量价值排序物品
val = 0 # 初始化总价值为0
for wt, val_ in I:
if W >= wt: # 如果物品可以完全放入背包
W -= wt # 更新背包剩余容量
val += val_ # 增加总价值
else: # 如果只能部分放入
val += val_ * (W / wt) # 增加部分价值
break # 背包已满,退出循环
return val # 返回总价值
3. 哈夫曼编码
问题描述:构建一棵带权路径长度最短的二叉树,即哈夫曼树,用于数据压缩。
贪心策略:
- 创建一个优先队列,将所有字符按照其频率排序。
- 从队列中取出频率最小的两个节点,合并它们并将合并后的节点重新插入队列。
- 重复步骤2,直到队列中只剩一个节点。
伪代码:
function huffmanCoding(freq):
q = PriorityQueue() # 创建优先队列
for c, f in freq.items():
q.put((f, Node(c))) # 将所有字符和其频率加入队列
while q.qsize() > 1:
f1, l = q.get() # 取出频率最小的两个节点
f2, r = q.get() # 取出频率次小的节点
q.put((f1 + f2, Node(None, l, r))) # 合并节点并放回队列
return q.get()[1] # 返回哈夫曼树的根节点
class Node:
def __init__(self, c, l=None, r=None):
self.c = c # 字符
self.l = l # 左子节点
self.r = r # 右子节点
4. 单源最短路径
问题描述:在一个有向图或无向图中,找到从源点到其他所有顶点的最短路径。
贪心策略:
- 初始化距离数组,将起点到自身的距离设为0,其余为∞。
- 使用优先队列选择当前距离最短的顶点。
- 更新该顶点邻接点的距离。
- 重复步骤2和3,直到所有顶点都被处理。
伪代码:
function dijkstra(G, s):
d = {v: float('inf') for v in G} # 初始化距离为无穷大
d[s] = 0 # 起点到自身的距离为0
pq = PriorityQueue() # 创建优先队列
pq.put((0, s)) # 将起点加入队列
while not pq.empty():
cd, cv = pq.get() # 取出当前距离最短的顶点
if cd > d[cv]:
continue # 如果当前距离大于已知最短距离,跳过
for n, w in G[cv].items(): # 遍历当前顶点的邻接点
dist = cd + w # 计算新的距离
if dist < d[n]: # 如果新距离小于已知距离
d[n] = dist # 更新距离
pq.put((dist, n)) # 将邻接点加入队列
return d # 返回所有顶点的最短距离
5. 最小生成树
这里以Prim算法为例:
问题描述:
在一个带权连通图中,构造一颗包含所有顶点的树,使得树的所有边的权值之和尽可能小。
贪心策略:
- 将所有边按权重升序排序。
- 初始化森林,每个顶点各自成为一棵树。
- 从最小权重的边开始,检查边的两个顶点是否属于不同的树。
- 如果是,将这条边添加到最小生成树中,并合并这两个顶点的树。
- 重复步骤3和4,直到最小生成树包含所有顶点。
伪代码:
function kruskal(G):
E = sorted(G['E'], key=lambda x: x[2]) # 按权重排序边
p, r = {}, {} # 初始化父节点和秩
def find(v):
if p[v] != v:
p[v] = find(p[v]) # 路径压缩
return p[v]
def union(v1, v2):
r1, r2 = find(v1), find(v2) # 找到两个顶点的根
if r1 != r2:
if r[r1] > r[r2]:
p[r2] = r1 # 合并树
else:
p[r1] = r2
if r[r1] == r[r2]: r[r2] += 1 # 更新秩
for v in G['V']:
p[v], r[v] = v, 0 # 初始化父节点和秩
mst = []
for v1, v2, w in E:
if find(v1) != find(v2):
union(v1, v2) # 合并树
mst.append((v1, v2, w)) # 添加边到最小生成树
return mst # 返回最小生成树的边集合
第 5 章 搜索
1. 树搜索的动机。
2. 爬山法、最佳优先搜索策略(Best-First 搜索)、分支界限策略的基本思想和求解步骤。
3. 掌握使用分支界限搜索算法求解经典问题的步骤和伪码实现,包括:人员分配问题,旅行商问题。
4. A*算法求解最短路径问题。
1.树搜索的动机
树搜索的动机主要来源于解决具有多个可能状态空间的问题,特别是那些需要探索多种路径以找到最优解或满足特定条件解的问题。在现实世界中,很多复杂问题如路径规划、游戏策略制定、资源分配等都可以抽象成树结构来处理。树搜索算法旨在高效地遍历这棵树,找到从初始状态到目标状态的最佳路径或满足特定目标的状态。
2.基本搜索策略
爬山法
-
基本思想:从一个初始解开始,逐步移动到相邻的、评价更好的解(即向着目标函数值增加的方向),直到无法找到更好的位置为止。这种方法简单,但容易陷入局部最优解。
-
求解步骤:
- 选择一个初始解作为起点。
- 寻找当前解的所有邻居,判断哪个邻居的评价更好。
- 移动到评价最好的邻居位置。
- 重复步骤2-3,直到没有更好的邻居或者达到预设停止条件。
最佳优先搜索(Best-First 搜索)
-
基本思想:每次扩展最有希望的节点(基于某个启发式评估函数)。该方法尝试平衡探索(未探索区域)与利用(已有信息)。
-
求解步骤:
- 使用启发式函数评估初始节点集中的每个节点。
- 选择评估值最高的节点进行扩展。
- 将新生成的子节点加入节点集,并重新评估。
- 重复步骤2-3,直到找到目标或节点集为空。
分支界限策略
-
基本思想:通过系统地枚举解空间的部分(分支),同时剪枝(界限)掉不可能产生最优解的子树,从而有效地寻找全局最优解。
-
求解步骤:
- 生成初始解空间的根节点。
- 选择一个节点进行扩展,根据某种准则评估其孩子节点。
- 剪枝:如果孩子节点不可能导致比当前最优解更好的解,则剪掉这个子树。
- 递归地对剩下的孩子节点执行步骤2-3,直到找到目标或所有子树被剪枝完毕。
3.分支界限搜索算法实例
人员分配问题
问题描述:
人员分配问题涉及将n个任务分配给n个人,以使总的完成时间或成本最小。每个人完成每个任务的时间或成本不同。
步骤:
- 初始化:开始时,所有任务都未分配,所有人都未分配任务。
- 界限计算:计算当前部分解的界限(最小成本)。
- 分支:从当前部分解生成新节点,尝试为下一个任务分配不同的人。
- 剪枝:如果当前解的成本超过已知的最优解,则剪枝。
- 更新:如果找到更优的解,则更新最优解。
- 终止:当所有任务都已分配并且没有更多节点可扩展时,算法终止。
伪码:
# 定义分支界限算法
function bnb(PT):
n = len(PT) # 获取任务数量(即矩阵的大小)
bestSol = ∞ # 初始化最佳解为无穷大
bestAssign = [] # 初始化最佳分配为空
# 计算当前分配的界限函数
function bd(cA, lvl):
bCost = sum(PT[cA[i]][i] for i in range(lvl)) # 计算当前已分配任务的总成本
# 计算未分配任务的最小成本总和
return bCost + sum(min(PT[i][j] for i in range(n) if i not in cA) for j in range(lvl, n))
# 分支界限递归函数
function rec(cA, lvl, cCost):
nonlocal bestSol, bestAssign # 使用外部变量bestSol和bestAssign
if lvl == n: # 如果所有任务都已分配
if cCost < bestSol: # 如果当前成本小于最佳解
bestSol = cCost # 更新最佳解
bestAssign = cA[:] # 更新最佳分配
return
# 遍历所有人,尝试不同的分配
for i in range(n):
if i not in cA: # 如果当前人未分配任务
nCost = cCost + PT[i][lvl] # 计算新的总成本
if nCost < bestSol: # 如果新成本小于最佳解
cA.append(i) # 将当前人添加到分配中
rec(cA, lvl + 1, nCost) # 递归调用
cA.pop() # 回溯,移除当前人
rec([], 0, 0) # 从空分配和零成本开始递归
return bestSol, bestAssign # 返回最佳解和最佳分配
旅行商问题(TSP)
问题描述:
旅行商问题(TSP)是找到一个销售员访问n个城市的最短路径,要求每个城市只访问一次,最后回到起点城市。
步骤:
- 初始化:从起点城市开始。
- 界限计算:计算当前部分路径的界限(当前路径长度加上从当前城市到未访问城市的最短距离)。
- 分支:从当前城市生成新节点,尝试访问不同的未访问城市。
- 剪枝:如果当前路径长度超过已知的最优解,则剪枝。
- 更新:如果找到更优的路径,则更新最优解。
- 终止:当所有城市都已访问并且没有更多节点可扩展时,算法终止。
伪码:
# 定义分支界限旅行商问题算法
function bnbTSP(D):
n = len(D) # 获取城市数量(即距离矩阵的大小)
bestP = ∞ # 初始化最佳路径成本为无穷大
bestT = [] # 初始化最佳路径为空
# 计算当前路径的界限函数
function bd(cT, lvl):
cCost = sum(D[cT[i]][cT[i+1]] for i in range(lvl)) # 计算当前路径的总成本
# 计算从当前城市到未访问城市的最小距离总和
return cCost + min(D[cT[lvl]][j] for j in range(n) if j not in cT)
# 分支界限递归函数
function rec(cT, lvl, cCost):
nonlocal bestP, bestT # 使用外部变量bestP和bestT
if lvl == n - 1: # 如果所有城市都已访问
fCost = cCost + D[cT[lvl]][cT[0]] # 计算回到起点的总成本
if fCost < bestP: # 如果总成本小于最佳路径成本
bestP = fCost # 更新最佳路径成本
bestT = cT[:] + [cT[0]] # 更新最佳路径
return
# 遍历所有城市,尝试访问不同的未访问城市
for i in range(1, n):
if i not in cT: # 如果当前城市未访问
nCost = cCost + D[cT[lvl]][i] # 计算新的路径成本
if nCost < bestP: # 如果新路径成本小于最佳路径成本
cT.append(i) # 将当前城市添加到路径中
rec(cT, lvl + 1, nCost) # 递归调用
cT.pop() # 回溯,移除当前城市
rec([0], 0, 0) # 从起点城市开始递归
return bestP, bestT # 返回最佳路径成本和最佳路径
4.A* 算法求解最短路径问题
基本思想:
A* 是一种启发式搜索算法,结合了Dijkstra算法的路径长度度量和额外的启发式信息(估价函数),以更快地找到最短路径。
- 估价函数 𝑓(𝑛)=𝑔(𝑛)+ℎ(𝑛)f(n)=g(n)+h(n),其中 𝑔(𝑛)g(n) 是从初始节点到当前节点的实际代价,ℎ(𝑛)h(n) 是当前节点到目标节点的启发式估计代价。
求解步骤:
- 初始化:设置开放列表(待考察节点)、关闭列表(已考察节点);将起始节点加入开放列表。
- 选择开放列表中 𝑓(𝑛)f(n) 值最小的节点 𝑛n。
- 检查 𝑛n 是否为目标节点,如果是则结束搜索,构建路径。
- 否则,将 𝑛n 的所有未探索的邻居加入开放列表,并更新它们的 𝑔g 值(通过 𝑛n 到达的成本)和 𝑓f 值。
- 将 𝑛n 移入关闭列表,表示已探索过。
- 重复步骤2-5,直到找到目标或开放列表为空。
第 6 章 计算复杂性理论
1. 掌握 P、NP、NPC 和 NP Hard 问题的定义,并能举例说明。
2. 能够以图示的方式表述 P、NP、NPC 和 NP Hard 之间的关系。
1. P、NP、NPC 和 NP Hard 问题的定义及举例
P问题:
P问题是指能够在多项式时间内解决的决策问题。也就是说,对于输入规模n,算法的运行时间是n的某个多项式的函数。
- 例子:
- 排序问题:给定一个整数数组,要求输出一个从小到大排序的数组。例如,快速排序和归并排序都是O(n log n)的多项式时间算法。
- 最短路径问题:给定一个加权图和两个顶点,找到从一个顶点到另一个顶点的最短路径。例如,Dijkstra算法能够在O(V^2)或O(V log V)时间内解决该问题。
NP问题:
NP问题是指能够在多项式时间内验证其解是否正确的决策问题。也就是说,如果有一个"猜测"的解,可以在多项式时间内验证这个解是否正确。
- 例子:
- 哈密顿回路问题:给定一个图,是否存在一条经过每个顶点一次且仅一次的回路?如果给出一个具体的回路,可以在多项式时间内验证其是否是哈密顿回路。
- SAT(可满足性问题):给定一个布尔公式,是否存在一种变量赋值使得公式为真?如果给出一个变量赋值,可以在多项式时间内验证公式是否为真。
NPC问题:
NPC问题是NP问题中的最难问题。一个问题被称为NP完全问题,如果它是NP问题,且所有NP问题都可以在多项式时间内归约到该问题上。也就是说,如果能在多项式时间内解决一个NP完全问题,那么所有NP问题都能在多项式时间内解决。
- 例子:
- 旅行商问题(TSP):给定一组城市和每对城市之间的旅行成本,是否存在一个总旅行成本不超过给定值的旅行路径?这是一个NP完全问题。
- 3-SAT:给定一个布尔公式,其中每个子句包含三个变量,是否存在一种变量赋值使得公式为真?
NP Hard问题:
NP Hard问题是至少与NP问题一样难的问题。一个问题被称为NP难问题,如果所有NP问题都可以在多项式时间内归约到该问题上,但该问题本身不一定是决策问题,也不一定在NP类中。
- 例子:
- 哈密顿路径问题:找出一个图中是否存在经过每个顶点一次的路径。这是一个NP难问题,因为可以将任何NP问题归约到该问题上,但其求解不是一个简单的"是"或"否"的问题。
- 数独问题:求解一个数独谜题是一个NP难问题,因为验证一个已完成的数独是否正确可以在多项式时间内完成,但找到一个解决方案却非常困难。
2. P、NP、NPC 和 NP Hard 之间的关系
以下图示描述了P、NP、NPC和NP Hard问题之间的关系:
解释:
- P:所有可以在多项式时间内解决的问题。这些问题也都是NP问题,因为任何在多项式时间内解决的问题,其解也可以在多项式时间内验证。
- NP:所有可以在多项式时间内验证其解的问题。包含了P类问题,因为P类问题的解显然可以在多项式时间内验证。
- NPC:NP中的最难问题。所有的NP问题都可以在多项式时间内归约到这些问题上。
- NP Hard:至少与NP问题一样难的问题。包括所有的NP完全问题(NPC),但也包括一些不属于NP的问题。
第 7 章 近似算法与随机算法
1. 掌握随机算法、近似算法、启发式算法的定义,并能够进行性能分析。
2. 至少了解 3 个以上可以用随机算法求解的问题,并能够给出各自的求解策略。
3. 至少了解 3 个以上可以用近似算法求解的问题,并能够给出各自的求解策略。
4. 能够将随机算法、近似算法和启发式算法应用在具体应用场景。
1. 随机算法、近似算法、启发式算法的定义及性能分析
随机算法:
随机算法是指在算法运行过程中使用随机数生成器来引入随机性的一类算法。这些算法可能在不同的运行过程中给出不同的结果,甚至对于同一输入也会如此。随机算法有时能够显著地简化问题的求解过程或提高算法的效率。
- 例子:蒙特卡罗算法、拉斯维加斯算法。
- 性能分析:
- 期望运行时间:随机算法的运行时间通常以期望值表示。
- 成功概率:有些随机算法可能不保证每次运行都得到正确结果,因此需要分析其成功概率。
近似算法:
近似算法是指在合理时间内找到一个近似最优解的算法。通常用于求解NP难问题,这些问题在实际中很难找到精确解。近似算法提供一个可接受的解,且解的质量和计算时间之间有一个良好的平衡。
- 例子:旅行商问题的最近邻算法、背包问题的贪心算法。
- 性能分析:
- 近似比:衡量近似解与最优解之间的比值。
- 时间复杂度:算法在找到近似解时所需的时间。
启发式算法:
启发式算法使用经验法则或启发式函数来指导搜索过程,寻找问题的近似解。虽然不保证找到最优解,但通常能在合理时间内找到一个满意的解。
- 例子:模拟退火算法、遗传算法。
- 性能分析:
- 收敛性:算法是否能够逐步逼近最优解。
- 效率:算法在实践中的运行时间和解的质量。
2. 用随机算法求解的问题及求解策略
问题1:素数判定问题
- 策略:使用拉宾-米勒素数测试法。该算法通过随机选择基数来测试一个数是否为素数,利用概率方法在多次测试中提高判定的准确性。
问题2:随机图生成
- 策略:生成包含n个顶点和m条边的随机图,通过随机选择顶点对并添加边来构建图。此方法用于模拟真实网络的结构和特性。
问题3:蒙特卡罗积分
- 策略:通过在积分区域内随机生成点来估计多维积分的值。计算在区域内落在函数曲线下方的点的比例,并用该比例乘以区域面积得到积分的近似值。
3. 用近似算法求解的问题及求解策略
问题1:旅行商问题(TSP)
- 策略:使用最近邻算法。随机选择一个起点,然后每次选择最近的未访问城市,直至访问所有城市。该方法简单但近似比可能较高。
问题2:最大割问题
- 策略:使用启发式算法如贪心算法。随机划分图中的顶点,将每个顶点从当前子集中移到另一个子集以增加割的权重,直到不能再提高为止。
问题3:背包问题
- 策略:使用贪心算法。按单位重量价值排序,优先选择单位重量价值最高的物品,直到背包容量耗尽。这种方法简单有效,但不保证最优解。
4. 应用场景中的算法应用
场景1:数据中心任务调度
- 随机算法:使用模拟退火算法为任务分配资源,考虑多种可能的任务分配方案,通过随机变换和退火过程逐步优化任务调度。
场景2:图像处理中的图像分割
- 近似算法:使用K-means聚类算法对图像像素进行分类,通过迭代更新类中心点,逐步逼近最佳分割结果。
场景3:物流配送
- 启发式算法:使用遗传算法优化车辆路径规划问题。通过模拟生物进化过程,选择、交叉和变异生成新的路径方案,逐步提高配送效率。