1. 算法概述与复杂度分析
算法是计算机科学的核心基础,简单来说就是解决问题的明确步骤。就像做菜需要菜谱一样,算法就是计算机的"菜谱"。一个合格的算法必须满足五个基本特性:输入(明确需要处理的数据)、输出(必须有结果)、有限性(步骤不能无限循环)、确定性(每个步骤必须明确无歧义)和可行性(能用基本操作实现)。
在实际考试中,复杂度分析绝对是高频考点。我当年备考时就因为没吃透这个概念,在时间复杂度计算上栽过跟头。复杂度分为空间复杂度和时间复杂度,前者关注算法运行需要多少内存,后者关注算法执行需要多少时间。更具体地说,时间复杂度又分为:
- 最好情况时间复杂度(最理想的情况)
- 最坏情况时间复杂度(最差的情况)
- 平均情况时间复杂度(所有可能情况的期望值)
举个生活中的例子:在一堆杂乱的文件中找特定文件。最好情况是第一个就是(O(1)),最坏情况是最后一个才找到(O(n)),平均情况则是需要检查一半文件(O(n/2))。在实际分析中,我们通常用大O表示法来描述最坏情况,因为它给出了性能的下界保证。
关于P和NP问题,这是算法领域最著名的未解之谜之一。简单理解:
- P问题:能在多项式时间内被确定性图灵机解决的问题
- NP问题:能在多项式时间内被非确定性图灵机验证解的问题
- NPC问题(NP完全问题):NP中最难的问题,所有NP问题都能在多项式时间内归约到它
考试中常考的易错点是:P⊆NP是正确的,但P=NP与否尚未被证明,所以任何断言P=NP或P≠NP的说法都是错误的。渐进符号的证明也是重点,特别是用比值法证明o和ω关系,教材上的6个O的性质一定要掌握。
2. 递归与分治策略
2.1 递归基础与分治框架
递归就像俄罗斯套娃,一个函数不断调用自身直到满足终止条件。它的优点是代码简洁,能自然表达许多问题的结构;缺点是效率低,频繁的函数调用会消耗大量栈空间。我记得初学递归时,最头疼的就是忘记写终止条件,结果程序直接栈溢出崩溃。
分治策略是递归的典型应用,它的代码框架必须包含三个关键部分:
- 基准情况:问题规模小到可以直接解决
- 分解步骤:将问题分解为若干个相同结构的子问题
- 合并步骤:将子问题的解合并为原问题的解
def divide_conquer(problem): # 基准情况 if problem is small_enough: return solve_directly(problem) # 分解步骤 subproblems = divide(problem) # 递归求解子问题 subresults = [divide_conquer(sub) for sub in subproblems] # 合并步骤 return merge(subresults)2.2 递归式求解方法
递归式求解绝对是考试重点中的重点!主方法和递归树法必须熟练掌握。主方法适用于形如T(n)=aT(n/b)+f(n)的递归式,分为三种情况:
- 若f(n)=O(n^(log_b a-ε)),则T(n)=Θ(n^(log_b a))
- 若f(n)=Θ(n^(log_b a)),则T(n)=Θ(n^(log_b a)lgn)
- 若f(n)=Ω(n^(log_b a+ε))且af(n/b)≤cf(n),则T(n)=Θ(f(n))
递归树法则更直观,通过画出递归树计算各层代价和叶节点代价。例如求解T(n)=3T(n/4)+Θ(n²):
- 每层分支数为3,深度为log₄n
- 第i层有3^i个节点,每个节点代价为(n/4^i)²
- 总代价为各层代价之和
2.3 典型分治算法
快速排序是分治的经典案例,我强烈建议手写实现一遍。它的核心是partition操作:
def quicksort(arr, low, high): if low < high: pi = partition(arr, low, high) quicksort(arr, low, pi-1) # 左子数组 quicksort(arr, pi+1, high) # 右子数组 def partition(arr, low, high): pivot = arr[high] i = low - 1 for j in range(low, high): if arr[j] < pivot: i += 1 arr[i], arr[j] = arr[j], arr[i] arr[i+1], arr[high] = arr[high], arr[i+1] return i+1线性时间选择算法能在O(n)时间内找到第k小元素,它巧妙结合了快速排序的partition和递归策略。考试常考根据伪码写递推式,比如最接近点对问题的伪码:
1. 按x坐标排序所有点 2. 中线划分左右区域 3. 递归求左右区域的最小距离d 4. 检查中线附近距离小于d的点对对应的递推式就是T(n)=2T(n/2)+O(n),解得T(n)=O(nlogn)。
3. 动态规划精要
3.1 基本思想与解题步骤
动态规划(DP)和分治都用到递归思想,但关键区别在于DP有重叠子问题,通过记忆化存储避免重复计算。解题通常分三步:
- 定义子问题:找出最优解的结构特征
- 建立递推式:用子问题表示原问题
- 确定边界条件:最小子问题的解
最优子结构性质是DP的核心,常用反证法证明。比如矩阵连乘问题,假设存在更优的括号方案,必然导致矛盾。
3.2 典型DP问题
矩阵连乘是理解DP的绝佳案例。给定矩阵链A₁A₂...Aₙ,找到最小乘法次数的括号化方案。定义m[i,j]为计算A[i:j]的最小代价,递推式为:
m[i,j] = min{m[i,k]+m[k+1,j]+p_{i-1}p_kp_j} for i≤k<j边界条件是m[i,i]=0。实际编程时采用自底向上填表法:
def matrix_chain(p): n = len(p) - 1 m = [[0]*n for _ in range(n)] for l in range(2, n+1): # 链长度 for i in range(n-l+1): j = i + l - 1 m[i][j] = float('inf') for k in range(i, j): cost = m[i][k] + m[k+1][j] + p[i]*p[k+1]*p[j+1] if cost < m[i][j]: m[i][j] = cost return m[0][n-1]背包问题是另一类经典DP。0/1背包的状态转移方程:
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]]+v[i])其中dp[i][j]表示前i件物品放入容量j背包的最大价值。考试可能要求手工推导dp表,比如物品重量w=[2,3,4],价值v=[3,4,5],背包容量5时的最优解。
4. 回溯法与分支限界法
4.1 回溯法框架
回溯法采用深度优先搜索策略,系统地遍历解空间。它的核心框架必须掌握:
def backtrack(path, choices): if meet_condition(path): results.append(path.copy()) return for choice in choices: if not is_valid(choice): continue # 剪枝 path.append(choice) backtrack(path, new_choices) path.pop() # 回溯考试常考子集树和排列树框架。子集树用于组合问题(如装载问题),排列树用于排列问题(如TSP)。重排原理是优化关键:让分支少的方向靠前,尽早剪枝。
4.2 典型回溯问题
n皇后问题要求在国际象棋棋盘上放置n个皇后,使其互不攻击。解空间是排列树,约束函数是:
def is_valid(board, row, col): for i in range(row): if board[i] == col or \ abs(board[i]-col) == abs(i-row): return False return True图的m着色问题为图的顶点着色,使相邻顶点颜色不同。解空间是子集树,约束函数检查当前顶点颜色是否与邻接顶点冲突。
4.3 分支限界法
与回溯法不同,分支限界法采用广度优先搜索,使用活结点表管理待扩展节点。根据活结点表的实现方式分为:
- 队列式(FIFO):先进先出,类似BFS
- 优先队列式:按优先级取出,常用代价函数估计
TSP问题的分支限界解法中,活结点保存当前路径和已访问城市,代价函数用最小生成树估计剩余路径下界。
5. 算法实战技巧
5.1 应试策略
根据历年真题分析,考试通常包含:
- 选择题:概念辨析(如P/NP问题)、复杂度分析
- 简答题:递推式求解、算法框架默写
- 大题:解空间树绘制、DP问题求解
建议重点准备:
- 根据伪码写递推式(如快速排序、线性时间选择)
- 主方法和递归树法求解复杂度
- 动态规划问题的状态转移方程
- 回溯法的剪枝条件设计
5.2 常见错误规避
在算法考试中,我见过太多同学犯这些错误:
- 复杂度分析:混淆最好/最坏情况,忽略递归的隐藏成本
- DP问题:边界条件处理不当,递推式缺少初始值
- 回溯法:忘记恢复状态(回溯步骤),剪枝条件不充分
一个实用的检查方法是:写完算法后,用简单测试用例手工模拟执行过程。比如对快速排序,用长度为3的数组验证partition是否正确。