算法导论 2023年 的回忆版真题,来源于计算机速通之家 | QQ 群号:468081841。
本文连载于算法导论-2023-回忆版| HeZzz.
🙇♂️🙇♂️🙇♂️时间仓促,有不足之处烦请及时告知。邮箱hez2z@foxmail.com 或者在 速通之家 群里 @9¾。
复杂度
给了两段程序代码,问复杂度。
- 第一小问是最长公共子序列的代码
- 第二小问是二分搜索代码
第一小问:最长公共子序列:
参考 OJ 题目:最长公共子序列
def lcs_length(s1, s2):
m, n = len(s1), len(s2)
# 创建二维DP数组
dp = [[0] * (n + 1) for _ in range(m + 1)]
# 填充DP表
for i in range(1, m + 1):
for j in range(1, n + 1):
if s1[i-1] == s2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
return dp[m][n]
复杂度分析: 两层嵌套循环,时间复杂度为 ,其中 、 分别为两个字符串的长度。
第二小问:二分搜索:
def binary_search(arr, target):
left, right = 0, len(arr) - 1
while left <= right:
mid = (left + right) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
复杂度分析: 每次将搜索范围缩小一半,时间复杂度为 。
中位数
求一个有 (奇数)个元素数组的中位数,设计一个高效的算法,求出最坏情况下的时间复杂度。
参考 OJ 题目:求第 小
算法思路(线性时间选择):
采用快速选择算法(Quick Select),这是分治算法的一个应用:
- 选择一个基准元素
- 将数组分为小于基准和大于基准的两部分
- 根据基准位置判断中位数在哪一部分,递归处理
- 最坏情况时间复杂度:
代码实现:
import random
def quick_select(arr, k):
"""
找出数组中第 k 小的元素(k 从 1 开始)
"""
if len(arr) == 1:
return arr[0]
# 随机选择基准
pivot = random.choice(arr)
# 分区
left = [x for x in arr if x < pivot]
mid = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]
if k <= len(left):
return quick_select(left, k)
elif k <= len(left) + len(mid):
return mid[0]
else:
return quick_select(right, k - len(left) - len(mid))
def find_median(arr):
"""
找出奇数长度数组的中位数
"""
n = len(arr)
return quick_select(arr, (n + 1) // 2)
其他方法:
- 快速排序:最坏情况 ,平均
- 归并排序:
- 线性时间选择:最坏情况 (最优解)
矩阵连乘
参考 OJ 题目:矩阵连乘规则
题目: 给定 个矩阵 ,每个矩阵的维度已知,求最优的矩阵连乘顺序,使得总的乘法次数最少。
算法思路(动态规划):
根据动态规划思想,采用自顶向下分析,自底向上求解:
- 状态定义: 表示计算矩阵 的最小乘法次数
- 初始状态: (单个矩阵不需要乘法)
- 状态转移方程:
其中 是维度数组, 和 分别是矩阵 的行数和列数。
代码实现:
def matrix_chain_multiplication(p):
"""
p: 维度数组,p[i-1]和p[i]是第i个矩阵的行数和列数
返回最小乘法次数
"""
n = len(p) - 1 # 矩阵个数
if n <= 1:
return 0
# dp[i][j] 表示第i到第j个矩阵的最小乘法次数
dp = [[float('inf')] * n for _ in range(n)]
# 单个矩阵乘法次数为0
for i in range(n):
dp[i][i] = 0
# 按照子链长度递推
for length in range(2, n + 1): # 子链长度
for i in range(n - length + 1):
j = i + length - 1
for k in range(i, j):
cost = dp[i][k] + dp[k+1][j] + p[i] * p[k+1] * p[j+1]
dp[i][j] = min(dp[i][j], cost)
return dp[0][n-1]
# 示例:三个矩阵 A1(30×35), A2(35×15), A3(15×5)
p = [30, 35, 15, 5]
result = matrix_chain_multiplication(p)
print(f"最小乘法次数: {result}") # 输出: 15750
注意: 动态规划保证得到全局最优解。
图像压缩
题目: 将一个 像素的图像分成若干段进行压缩存储,每段有固定的段开销,求使得总存储空间最小的分段方案。
问题建模:
- 设图像有 个像素
- 将图像分为若干段,每段的存储代价 = 段内像素数 + 段开销
- 假设每段最多 个像素,段开销为
- 目标:找到最优分段方案,使总存储空间最小
算法思路(动态规划):
- 状态定义: 表示前 个像素的最小存储空间
- 初始状态:
- 状态转移方程:
其中 表示当前段的像素数, 是段开销。
算法伪代码:
算法 ImageCompression(n)
输入: 像素总数 n
输出: 最小存储空间
1. 初始化 dp[0] = 0
2. for i = 1 to n do
3. dp[i] = ∞
4. for j = 1 to min(i, 256) do
5. cost = dp[i-j] + j + 11 // 前i-j个的最优解 + 当前段代价
6. dp[i] = min(dp[i], cost)
7. end for
8. end for
9. return dp[n]
Python 实现:
def image_compression(n, max_segment=256, overhead=11):
"""
计算图像压缩的最小存储空间
n: 像素总数
max_segment: 每段最大像素数(默认256)
overhead: 段开销(默认11)
"""
dp = [float('inf')] * (n + 1)
dp[0] = 0
for i in range(1, n + 1):
# 尝试不同的段长度
for j in range(1, min(i, max_segment) + 1):
cost = dp[i - j] + j + overhead
dp[i] = min(dp[i], cost)
return dp[n]
# 示例
n = 1000 # 1000个像素
min_space = image_compression(n)
print(f"最小存储空间: {min_space}")
注意: 题目如未明确说明,可按教材假设每段最多 个像素,段开销 。
贪心
题目: 个人排队完成任务,每个人完成任务都有一个时间 ,求使得 个人的平均等待时间最小的顺序,并计算总等待时间。
算法思路(贪心算法):
根据贪心策略,完成任务时间短者优先:
- 贪心选择性质: 时间越短的任务越早完成,可使后续等待的人数减少
- 策略: 按照完成时间从小到大排序
- 证明: 设有两个人 ,完成时间分别为 ()
- 若 先完成:总等待时间 =
- 若 先完成:总等待时间 =
- 因为 ,所以 ,故时间短者先完成更优
代码实现:
def min_average_waiting_time(times):
"""
计算最小平均等待时间
times: 每个人完成任务的时间列表
返回: (最优顺序, 总等待时间, 平均等待时间)
"""
n = len(times)
# 按时间从小到大排序(贪心策略)
sorted_times = sorted(times)
total_waiting = 0
current_time = 0
for i, t in enumerate(sorted_times):
current_time += t # 当前人完成任务的时刻
if i < n - 1: # 最后一个人不需要等待
total_waiting += current_time
avg_waiting = total_waiting / n
return sorted_times, total_waiting, avg_waiting
# 示例
times = [3, 1, 4, 2, 5]
order, total, avg = min_average_waiting_time(times)
print(f"最优顺序: {order}") # [1, 2, 3, 4, 5]
print(f"总等待时间: {total}") # 1 + (1+2) + (1+2+3) + (1+2+3+4) = 20
print(f"平均等待时间: {avg:.2f}") # 4.00
结论: 完成任务时间短者优先,这是贪心算法的典型应用。
智力大冲浪
小伟报名参加电视台的智力大冲浪节目。本次挑战赛吸引了众多参赛者,主持人为了表彰大家的勇气,先奖励每个参赛者 元。先不要太高兴,因为这些钱还不一定都是你的。接下来,主持人宣布了比赛规则:
- 首先,比赛时间分为 个时段,它又给出了很多小游戏,每个小游戏都必须在规定期限 前完成()
- 如果一个游戏没能在规定期限前完成,则要从奖励费 元中扣去一部分钱 , 为自然数。不同的游戏扣去的钱数是不一样的
- 当然,每个游戏本身都很简单,保证每个参赛者都能在一个时段内完成,而且都必须从整时段开始。主持人只是想考考每个参赛者如何安排组织自己做游戏的顺序
作为参赛者,小伟如何赢取最多的钱?
示例数据:
- 初始奖金:
- 游戏个数:
- 结束时段:
- 罚款金额:
算法思路(贪心算法):
- 贪心策略: 按罚款金额从大到小排序,优先完成罚款高的游戏
- 调度规则: 对于每个游戏,尽可能安排在其截止期限前的最晚时刻
- 时间复杂度: 或使用并查集优化到
代码实现:
def max_reward(initial_money, deadlines, penalties):
"""
计算最大奖金
initial_money: 初始奖金
deadlines: 每个游戏的截止时段列表
penalties: 每个游戏的罚款列表
"""
n = len(deadlines)
# 创建游戏列表 (截止时间, 罚款, 索引)
games = [(deadlines[i], penalties[i], i) for i in range(n)]
# 按罚款从大到小排序(贪心策略)
games.sort(key=lambda x: x[1], reverse=True)
# 时间槽,初始都为空
max_time = max(deadlines)
time_slots = [False] * (max_time + 1)
total_penalty = 0
completed = []
# 按罚款从大到小遍历游戏
for deadline, penalty, idx in games:
# 尝试在截止期限前找到最晚的空闲时段
for t in range(deadline, 0, -1):
if not time_slots[t]:
time_slots[t] = True
completed.append((idx, t))
break
else:
# 无法安排,需要支付罚款
total_penalty += penalty
final_money = initial_money - total_penalty
return final_money, completed, total_penalty
# 示例
initial = 10000
deadlines = [4, 2, 4, 3, 1, 4, 6]
penalties = [70, 60, 50, 40, 30, 20, 10]
final, completed, penalty = max_reward(initial, deadlines, penalties)
print(f"最终奖金: {final}")
print(f"被罚款: {penalty}")
print(f"完成的游戏安排: {sorted(completed, key=lambda x: x[1])}")
解题步骤:
- 按罚款从大到小排序:
- 依次安排每个游戏到其截止期限前的最晚空闲时段
- 无法安排的游戏计入罚款
- 最终奖金 = 初始奖金 - 总罚款
注意: 这是 xjk 老师 PPT 原题,贪心策略的典型应用。
整数方程求解(回溯法)
题目: 求解下列不等式的所有整数解: 其中 为大于等于 的整数。
要求:
- 确定解空间类型,设计剪枝函数
- 用回溯法给出所有动态解空间树
- 给出所有的整数解
算法分析(回溯法):
-
解空间类型: 子集树(每个变量可以取多个值)
- 的可能取值:(因为 )
- 的可能取值:(因为 )
- 的可能取值:(因为 )
-
剪枝函数:
- 约束函数(控制左子树): 如果当前计算结果 ,则剪枝
- 限界函数(控制右子树): 如果当前计算结果 + 剩余变量取最大值后的结果 ,则剪枝
-
搜索策略: 深度优先搜索(DFS),第 层结点搜索第 个变量的值
代码实现:
def solve_equation_backtrack():
"""
回溯法求解 4x1 + 3x2 + 2x3 = 12 的所有整数解
"""
solutions = []
coefficients = [4, 3, 2]
target = 12
def backtrack(index, current_sum, path):
# 终止条件
if index == 3:
if current_sum == target:
solutions.append(path.copy())
return
# 计算当前变量的取值范围
coef = coefficients[index]
max_val = (target - current_sum) // coef
# 尝试当前变量的每个可能取值
for value in range(max_val + 1):
new_sum = current_sum + coef * value
# 剪枝1:约束函数 - 当前和不能超过目标
if new_sum > target:
break
# 剪枝2:限界函数 - 检查剩余部分能否达到目标
remaining = target - new_sum
remaining_coefs = coefficients[index + 1:]
if remaining_coefs:
min_possible = 0 # 全取0
max_possible = sum(remaining * (remaining // c)
for c, remaining in zip(remaining_coefs, [remaining] * len(remaining_coefs)))
# 简化:检查是否有可能达到目标
if index == 2: # 最后一个变量
if remaining % 2 != 0:
continue
path.append(value)
backtrack(index + 1, new_sum, path)
path.pop()
backtrack(0, 0, [])
return solutions
# 求解
solutions = solve_equation_backtrack()
print(f"共有 {len(solutions)} 个整数解:")
for i, sol in enumerate(solutions, 1):
x1, x2, x3 = sol
print(f"解{i}: x1={x1}, x2={x2}, x3={x3}, 验证: 4×{x1}+3×{x2}+2×{x3}={4*x1+3*x2+2*x3}")
所有整数解:
解空间树: 共有 种整数解,遍历过程中约有 个结点(不包括剪枝结点)。
回溯法要点:
- 解空间组织:子集树或排列树
- 搜索策略:深度优先搜索(DFS)
- 剪枝函数:约束函数(控制左子树)+ 限界函数(控制右子树)
- 适用场景:寻找所有最优解