一:程序阅读分析题(共40分)
1.(8分)阅读“算法1”,分析算法1的功能、时间复杂度。
答案:经典的汉诺塔问题,其目标是将 n 个不同大小的盘子从柱子 A 移动到柱子 C,借助柱子 B 作为辅助。在移动的过程中,必须满足以下条件:
每次只能移动一个盘子。
任何时候,在较小的盘子必须位于较大的盘子上面。
使用柱子 B 作为中介,将顶部的 n-1 个盘子从柱子 A 移动到柱子 B。
将最大的盘子直接从 A 移动到 C。
再次使用柱子 A 作为中介,将 B 上的 n-1 个盘子移动到柱子 C。
汉诺塔问题的时间复杂度是 O(2^n)
2.(8分)阅读“算法2”,分析算法2的功能,设计算法的一个输入,并给出对应的算法输出结果。
答案:蒙特卡罗方法,用于估计圆周率(π)。蒙特卡罗方法是一种统计模拟方法,利用随机数来解决计算问题。这里的具体方法是通过估计单位圆内的点占单位正方形的比例来计算π。
1初始化点的总数 n 和落在单位圆内的点数 m。
2循环 n 次,每次生成一个点的坐标 (x, y),其中 x 和 y 是 [0,1] 范围内的随机数。
3检查每个点是否位于单位圆内,即判断 x*x + y*y <= 1 是否成立。如果成立,增加 m 的值。
4估算π的值为 4.0 * m / n。这是因为单位圆面积与包围它的正方形面积的比是 π/4。
3.(8分)“算法3”为回溯法求无向不带权图G的最大团问题的递归函数backtrack(),请您依据递归函数,写出该问题解的形式、解空间的组织结构和搜索条件。假设用邻接矩阵g[][]存储无向不带权图G。
答案:解的形式
在最大团问题中,解是指无向不带权图中的一个顶点子集,其中任何两个顶点都相邻。这个问题的解可以用一个数组 bestx[] 来表示,其中 bestx[i] = 1 表示顶点 i 属于最大团,而 bestx[i] = 0 则表示顶点 i 不属于最大团。
解空间的组织结构
解空间可以组织为一个决策树,其中每个节点代表一个顶点的决策过程(是否加入团中)。树的每一层对应图中的一个顶点,每个节点有两个可能的分支:将当前顶点加入团中(即 x[t] = 1)和不加入(即 x[t] = 0)。通过遍历这棵决策树,可以探索所有可能的顶点组合。
搜索条件
基本终止条件:当考察完所有顶点后(即 t > n),检查当前团是否是到目前为止找到的最大团。如果是,则更新 bestx[] 和 bestn。
可行性函数(Place(t)):这个函数判断将顶点 t 加入当前团中是否可行。可行性的基本条件是顶点 t 必须与当前团中的所有顶点相邻。这可以通过检查邻接矩阵 g[][] 来实现,确保对于任意已在团中的顶点 i(即 x[i] = 1),都有 g[t][i] = 1。
剪枝条件:如果当前已经选入团中的顶点数 cn 加上剩余可考察的顶点数(n - t)小于已知的最大团大小 bestn,则不可能通过继续添加剩余的顶点得到更大的团,此时应剪枝。
4.(8分)阅读“算法4”,算法4中的time为结构体数据类型,定义了开始时间start_time和结束时间end_time两个数据项,请您设计一个n=6的实例,并给出算法4得到的x的值。
答案:假设我们有一个由 6 个会议组成的数组 SE,每个会议有一个开始时间 start_time 和一个结束时间 end_time。我们需要选择尽可能多的会议,而这些会议之间不会相互冲突。
假设会议时间如下:
会议 1:开始时间 = 9:00, 结束时间 = 10:00
会议 2:开始时间 = 10:15, 结束时间 = 11:15
会议 3:开始时间 = 10:30, 结束时间 = 11:30
会议 4:开始时间 = 11:20, 结束时间 = 12:20
会议 5:开始时间 = 12:30, 结束时间 = 13:30
会议 6:开始时间 = 13:00, 结束时间 = 14:00
这些会议已根据结束时间排列。
贪心算法过程
该贪心算法从第一个会议开始,每次选择下一个与当前会议不冲突的最早结束的会议。这是一个典型的“最早完成时间优先”策略,用于解决活动选择问题。
初始化
x[] = {false, false, false, false, false, false} 表示选择状态的数组,初始状态没有会议被选择。
选择第一个会议,因此 x[1] = true。
j = 1 表示当前选择的最后一个会议是第 1 个。
算法执行过程
i = 2:会议 2 的开始时间是 10:15,会议 1 的结束时间是 10:00。因为 10:15 >= 10:00,所以 x[2] = true,更新 j = 2。
i = 3:会议 3 的开始时间是 10:30,现在 j = 2,会议 2 的结束时间是 11:15。因为 10:30 < 11:15,所以 x[3] = false。
i = 4:会议 4 的开始时间是 11:20,会议 2 的结束时间是 11:15。因为 11:20 >= 11:15,所以 x[4] = true,更新 j = 4。
i = 5:会议 5 的开始时间是 12:30,会议 4 的结束时间是 12:20。因为 12:30 >= 12:20,所以 x[5] = true,更新 j = 5。
i = 6:会议 6 的开始时间是 13:00,会议 5 的结束时间是 13:30。因为 13:00 < 13:30,所以 x[6] = false。
结果
最终,x[] = {true, true, false, true, true, false}。所选会议为 1, 2, 4, 5。这些会议的选择遵循了最大化参加会议数量的目标,同时确保会议时间不冲突
5.(8分)阅读“算法5”,分析算法的功能,并给出一个例子,展示算法的运行过程。
算法5: MergeSort (int A[],int low,int high)
{
if (low<high)
{
Int middle=(low+high)/2; //分解
MergeSort(A,low,middle); //对左半部分A[low:middle]递归
MergeSort(A,middle+1,high); //对右半部分A[middle+1:high]递归
Merge(A,low,middle,high); //将两个有序子序列A[low:middle]、
A[middle+1:high]合并为一个有序序列
}
}
答案:算法功能
归并排序(MergeSort)是一种高效的排序算法,采用分治法(Divide and Conquer)的一个典型应用。它将一个大数组分成两个小数组去解决。然后将这些数据排序后再将排序好的数组合并在一起。这个方法首先将数组分解最小可能的单元,然后将它们排序后再合并起来,整个数组最终排序完成。
步骤解析
分解:将当前区间一分为二,即找到中间点 middle。
递归:递归地对左半部分 A[low:middle] 和右半部分 A[middle+1:high] 进行排序。
合并:将两个已排序的子序列合并成一个完整的已排序序列。
示例输入与过程演示
假设有一个数组 A = [38, 27, 43, 3, 9, 82, 10],我们希望对其进行排序。
初始调用
MergeSort(A, 0, 6)
分解的过程
low = 0, high = 6, middle = 3
对左半部分进行排序:MergeSort(A, 0, 3)
对右半部分进行排序:MergeSort(A, 4, 6)
左半部分继续分解
low = 0, high = 3, middle = 1
对左半部分进行排序:MergeSort(A, 0, 1)
对右半部分进行排序:MergeSort(A, 2, 3)
最左边的部分继续分解
最后分解到单个元素,开始合并:
MergeSort(A, 0, 1) 分解到 MergeSort(A, 0, 0) 和 MergeSort(A, 1, 1) 然后合并 [38] 和 [27] 得到 [27, 38]
同理,MergeSort(A, 2, 3) 会合并 [43] 和 [3] 得到 [3, 43]
再将 [27, 38] 和 [3, 43] 合并得到 [3, 27, 38, 43]
右半部分的处理
同样地,MergeSort(A, 4, 6) 会逐步分解并合并得到 [9, 10, 82]
最终合并
将两部分 [3, 27, 38, 43] 和 [9, 10, 82] 合并得到最终排序数组 [3, 9, 10, 27, 38, 43, 82]
结论
归并排序是一种稳定的排序方法,其时间复杂度为O(nlogn),不依赖于输入数据的初始排列状态。虽然需要额外的存储空间进行合并操作,但其分治策略使得它非常适合处理大规模数据集。
二:计算题(共20分)
1.(8分)给定5种字符a,b,c,d,e及它们出现的频率0.45,0.25,0.15,0.10,0.05,选择一种编码方法,求这5个字符的编码方案。
- 描述所用的编码方法(4分)
- 给出5个字符的编码(2分)
- 求该编码方案的平均码长(2分)
答案:1. 描述所用的编码方法
为了有效编码这五个字符,我们选择使用霍夫曼编码(Huffman Coding)。这种方法是一种广泛使用的最优前缀编码方法,其目的是通过使用不等长的编码减少编码后的总体长度。字符频率越高,其对应的编码长度越短,这样可以最大限度地减少平均编码长度。
霍夫曼编码的步骤:
将所有字符按照它们的频率排列,并视为一个森林,每个字符是一个节点。
每次从森林中选出两个频率最低的树合并,这两个树的根节点将成为一个新节点的两个子节点,新节点的频率是两个子节点频率之和。
重复上述过程直到森林中只剩下一个树,这棵树就是霍夫曼树。
从霍夫曼树的根节点到每个字符节点的路径定义了字符的编码。向左是0,向右是1。
2. 给出5个字符的编码
设定初始频率如下:e (0.05), d (0.10), c (0.15), b (0.25), a (0.45)。
构建霍夫曼树的过程如下:
合并 e (0.05) 和 d (0.10) 得到 0.15
合并上述生成的 0.15 与 c (0.15) 得到 0.30
合并 b (0.25) 和最后得到的 0.30 得到 0.55
最后合并 a (0.45) 和上一步的 0.55 得到 1.00(整棵树)
根据树,我们可以得到编码:
a: 0
b: 10
c: 110
d: 1110
e: 1111
3. 求该编码方案的平均码长
计算平均码长的公式为:
Average Code Length=∑所有字符(字符频率×字符编码长度)Average Code Length=∑所有字符(字符频率×字符编码长度)
具体计算:
0.45×1+0.25×2+0.15×3+0.10×4+0.05×4=0.45+0.50+0.45+0.40+0.20=2.000.45×1+0.25×2+0.15×3+0.10×4+0.05×4=0.45+0.50+0.45+0.40+0.20=2.00
因此,这种编码方案的平均码长为 2.00 位。
2.(12分)给定序列X={E, F, G, A, B, C, B}和Y={E,B, D, F, A, G, B, A},求它们的最长公共子序列。
- 写出所用的求解方法(2分)
- 描述该方法求解的步骤(4分)
- 给出求解过程(4分)
- 给出求解结果(2分)
1. 所用的求解方法
这里使用动态规划方法来求解最长公共子序列问题(Longest Common Subsequence, LCS)。这种方法通过构建一个二维表来记录两个序列中所有位置的最长公共子序列的长度,并逐步扩展到整个序列。
2. 描述该方法求解的步骤
初始化表格:创建一个二维数组 LCS,其维度为 (len(X)+1) × (len(Y)+1)。这里,len(X) 和 len(Y) 分别是序列 X 和 Y 的长度。LCS[i][j] 表示序列 X 的前 i 个元素和序列 Y 的前 j 个元素的最长公共子序列的长度。
填充边界:将 LCS[0][j] 和 LCS[i][0] 的所有值初始化为 0,表示空序列与任何序列的最长公共子序列长度为 0。
填充表格:对于每一对 (i, j),如果 X[i-1] == Y[j-1],则 LCS[i][j] = LCS[i-1][j-1] + 1;否则,LCS[i][j] = max(LCS[i-1][j], LCS[i][j-1])。
回溯构建子序列:从 LCS[len(X)][len(Y)] 开始回溯,如果 X[i-1] == Y[j-1],则这个字符是公共子序列的一部分,并继续对 LCS[i-1][j-1] 回溯;否则,根据 LCS[i-1][j] 和 LCS[i][j-1] 的值决定向上或向左回溯。
3. 给出求解过程
假设序列 X = {E, F, G, A, B, C, B},Y = {E, B, D, F, A, G, B, A}。
我们构建一个表格,填充过程如下(略过初始化步骤):
4. 给出求解结果
从表格的右下角 LCS[7][8] = 5 开始回溯,可得到最长公共子序列为 {E, F, A, B, B}。
三:分析编程题(共30分)
1、(15分)0-1背包问题:给定n个物品,1个背包,背包容量为W,n个物品的重量和价值分别为:(wi,vi)i=1,2,3,...,n。物品不能分割,请设计一算法,求解在不超过背包容量的前提下,怎么装能够使得装入的物品总价值最大。
- 给出选用的算法策略(2分)
- 写出该算法策略的思想(4分)
- 写出存储0-1背包问题的输入、输出所用的数据结构(2分)
- 给出求解问题的算法步骤(可以选择自然语言、伪码、流程图、程序设计语言中的任何一种形式描述)(7分)
1. 选用的算法策略
对于0-1背包问题,推荐使用动态规划算法。这是因为它能有效地处理每个物品只能被选用一次的约束,同时寻找最大价值的组合。
2. 该算法策略的思想
动态规划方法解决0-1背包问题的核心思想是使用一个表格来保存每个子问题的最优解。表格的每一行代表考虑到某个物品,每一列则代表不同的背包容量。通过逐步增加考虑的物品和背包的容量,我们可以构建出一个解的表格,其中包含了在不同背包容量和不同物品选择情况下的最大价值。
3. 输入输出的数据结构
输入数据结构:
int n: 物品的数量。
int W: 背包的最大容量。
int w[]: 一个数组,存储每个物品的重量。
int v[]: 一个数组,存储每个物品的价值。
输出数据结构:
int max_value: 能装入背包的最大价值。
4. 求解问题的算法步骤(伪码形式)
function optimalLoading(n, C, w):
sort w in ascending order // 将箱子按重量升序排序
int currentWeight = 0 // 当前装载的总重量
int count = 0 // 已装载的箱子数量
for i from 1 to n:
if currentWeight + w[i] <= C: // 如果加上这个箱子不超过载重限制
selected[i] = true // 标记这个箱子为已装载
currentWeight += w[i] // 更新当前的总重量
count += 1 // 增加已装载的箱子数量
else:
selected[i] = false // 标记这个箱子为未装载
return (count, selected) // 返回已装载的箱子数量和装载状态
这个算法首先将所有箱子按照重量从轻到重排序,然后依次尝试装载每个箱子。一旦当前箱子的重量加上已装载的总重量超过船的载重限制,该箱子就不被装载。最终算法返回能装载的最大箱子数量以及每个箱子的装载状态。
2.(15分)最优装载问题:给定n个箱子,其重量为wi(i=1,2,3,...,n)),某艘船的载重量为C,船的体积不受限制,在不超过船的载重量的前提下,设计一算法,将尽量多的箱子装到船上。
- 给出选用的算法策略(2分)
- 写出该算法策略的思想(4分)
- 写出存储最优装载问题的输入、输出所用的数据结构(2分)
- 给出求解问题的算法步骤(可以选择自然语言、伪码、流程图、程序设计语言中的任何一种形式描述)(7分)
1. 选用的算法策略
对于最优装载问题,推荐使用贪心算法。这种策略在处理载重限制下尽可能多地装载物品的问题时表现出色。
2. 该算法策略的思想
贪心算法的核心思想是在每一步选择中都采取当前条件下最优的选择,即每次选择最轻的箱子来装载。这样做可以保证在限定的载重量下装入最大数量的箱子。由于船的体积不受限制,我们只需关注总重量不超过船的载重量。
3. 存储最优装载问题的输入、输出所用的数据结构
输入数据结构:
int n: 箱子的总数。
int C: 船的最大载重量。
int[] w: 存储每个箱子的重量的数组。
输出数据结构:
int max_count: 最大可以装载的箱子数量。
bool[] selected: 一个布尔数组,标记哪些箱子被选中进行装载。
4. 算法步骤(Python代码形式描述)
def optimalLoading(w, C):
# 对箱子重量进行排序
w.sort()
currentWeight = 0 # 当前船上载重
count = 0 # 已装载的箱子数
selected = [] # 装载的箱子索引
# 遍历排序后的箱子重量
for weight in w:
if currentWeight + weight <= C: # 检查加上当前箱子是否超载
currentWeight += weight # 更新当前载重
selected.append(weight) # 添加此箱子为已装载
count += 1 # 装载箱子数增加
else:
break # 超过载重后停止装载
return count, selected # 返回总装载箱子数和装载的箱子重量列表
# 示例调用
n = 5 # 箱子数量
C = 100 # 船的载重量
w = [20, 50, 30, 10, 40] # 箱子重量
max_count, loaded_weights = optimalLoading(w, C)
print("可装载箱子数量:", max_count)
print("装载的箱子重量:", loadedWeights)
这段代码首先将箱子按重量排序,然后逐一检查每个箱子是否可以加入到船上而不超过载重限制。它记录了已装载的箱子的数量和它们的重量,以实现尽可能多地装载箱子。