考研复试 算法设计与分析 笔试笔记
第一~五章 算法基础、时间复杂度、分治法
1.x 算法概述
算法五个特征 详见名词解释
2.1 渐进表示法
大O记号
时, 不会快于
如果存在正数和,使得当时,有,则称是的。
和怎么找?系数加一。
简单地说,如果最高项为一次项(例如),那么就把常数项放掉,看等于几时的值开始小于。
如果既有一次项又有二次项(例如),那么首先把常数项放掉,看等于几时的值开始小于;再把一次项放掉,看等于几时的值开始小于。
p19 例2-2 证明
解:
时,有
时,有
取,,则当时,有,所以.
大Omega记号
时, 不会慢于
如果存在正数和,使得当时,有,则称是的。
和怎么找?最高次系数不动,小的全放掉。
p20 例2-6 证明
解:
对所有,有
取,,则当时,有,所以.
综合比较两个函数的时间复杂度
基本思路: 如果是的,同时又是的,那么就是的。
p29 习题2-11(2) ,
解:
- 取,对于所有时,有
因此 ,所以. - 取,对于所有时,有
因此 ,所以. - 由(1)和(2)可得。
2.2 迭代法求解时间复杂度
2.3 主定理求解时间复杂度*
若:
则:
即,谁大要谁(n的指数部分),相等时再乘一个logn
2.4 给一段代码,求时间复杂度
类似408选择题第一题,中间过程写的言之有理即可 25考研?那408第一题你已经拿下了。408时间复杂度的几个经典考法
2.4.1 单循环
int i=0;
while (i*i*i <= n)
i++;
int i=1;
while (i<=n)
i = i * 2;
x=0;
while (n >= (x+1)*(x+1))
x++;
i = n*n;
while (i != 1)
i = i / 2;
2.4.2 两重循环
int m=0, i, j;
for (i=1; i<=n; i++)
for (j=1; j<=2*i; j++)
m++;
for (i=0; i<n; i++)
for (j=0; j<m; j++)
A[i][j] = 0;
count = 0;
for (k=1; k<=n; k*=2)
for (j=1; j<=n; j++)
count++;
for (i=n-1; i>=1; i--)
for (j=1; j<i; j++)
if (A[i] > A[j+1])
swap(A[i], A[j]);
5.1 基本思想
什么是分治法 详见名词解释
5.2 ~ 5.4 分治法实例
会按照2.1的方式分析时间复杂度,能解释什么是二分搜索、快速排序、合并排序即可。
第六章 贪心
6.2 可分割物品的背包问题
描述:略
按性价比从大到小排序:
6.3 带时限的作业排序问题[贪心版]
描述:作业 必须在期限 之前完成才能获取收益 ,且完成每个作业只需1个单位时间(贪心法的前提)
分支限界版 => 9.3
6.3.1 朴素算法
思路:优先安排收益最高的作业,尽可能将其安排在最晚的可用时间点,从而为其他作业预留更多的早期时间段。
- 按收益 从大到小排序
- 按顺序遍历所有作业
- 从其截止时间向前查找可用时间 分配给该作业,若无空闲时间则跳过该作业。
作业j | 期限d | 收益p |
---|---|---|
1 | 2 | 100 |
2 | 1 | 10 |
3 | 2 | 15 |
4 | 1 | 27 |
首先按收益排序:
- 作业1:(2, 100)
- 作业4:(1, 27)
- 作业3:(2, 15)
- 作业2:(1, 10)
依次处理:
- 作业1(100):可以安排在时间2
- 作业4(27):可以安排在时间1
- 作业3(15):没有可用时间,跳过
- 作业2(10):没有可用时间,跳过
- 最终获得的最大收益为:100 + 27 = 127
习题6-3
最优解 5 6 3 2
最大收益 74
6.3.2* 改进算法
待补充
6.4 最佳合并模式
6.4.1 最佳两路合并模式(哈夫曼编码)
就是哈夫曼树求最小 WPL(带权外路径长度)
(20,30,30,10,5)
WPL = (5+10)*3 + (20+30+30)*2 = 205
6.4.2* 三(k)路合并 书p105/习题6-8
https://www.bilibili.com/video/BV1tucQeQECb
补:虚节点,数量为 个
(3, 7, 8, 9, 15, 16, 18, 20, 23, 25, 28) => n=11,k=3, (n-1)%(k-1)=0,不用补;
(1, 2, 3, 3, 5, 6, 7 ,9) => n=8,k=3,(n-1)%(k-1)=1,补1个虚节点;WPL=66
6.5 最小代价生成树(最小生成树)
对于图
6.5.1 Prim
适用于稠密图 每次从已有点集合里扩展出一条最短的边
- 使用邻接矩阵时,时间复杂度
- 使用邻接表
和优先队列时,时间复杂度
6.5.2 Kruskal
适用于稀疏图 对边从小到大排序,依次加入边,每次加入都需要判环路
- 时间复杂度
习题6-9
明显是稠密图,选 Prim
6.6 单源最短路径
Dijkstra算法 边权非负
手写步骤: 又快又准做对考研真题,从考试的角度出发【Dijkstra】【单源最短路径】https://www.bilibili.com/video/BV1nh411Y7vj/
6.7 磁带最优存储
6.7.1 单带最优存储
说人话:
把 个程序(线段)按某种顺序排列在一个磁带(数轴)上,程序不能重叠,保证磁带能放得下所有程序,要求平均检索时间(MRT)最小。
MRT = 所有程序的检索时间之和 程序个数
由于程序个数固定,所以 MRT 最小等价于检索时间之和最小。
检索时间 = 程序起点到磁带起点距离 程序长度
每次检索都从磁带起点开始,所以离起点越远的程序检索时间越长。
各种排列方式参考:书p117 表6-5
贪心策略:按程序长度从小到大排序,依次放入磁带。
6.7.2 多带最优存储
现在磁带变成了 个,其实也很简单:将程序放在第一个磁带上,程序放在第二个磁带上,程序放在第三个磁带上…当 个磁带都排完一遍,回到第一个磁带,继续顺次放程序。
#include <iostream.h>
void Store(int n, int m)
{
int j = 0;
for (int i=0; i<n; i++) {
cout << "将程序"<<i<<" 加到磁带" <<j<<endl;
j=(j+1)% m;
}
cout<<endl;
}
EX1: 最佳装载问题(习题6-17)
个集装箱和一艘载重为 的船。每个集装箱的重量为。
求装载方案,使得装载的集装箱数量最大化,同时集装箱的总重量不超过船的载重限制 。
贪心策略:每次都选择最轻的集装箱
(5) 最优方案:装 [2,3,4,5,6]
EX2: 活动安排问题(线段覆盖)
一个数轴上有n条线段,现要选取其中k条线段,使得这些线段两两没有重合部分,问最大的k为多少。线段的起点为活动的开始时间,终点为活动的结束时间。
贪心策略:每次选择结束时间最早的活动,以留下最多的时间给后续活动。
例:设待安排的11个活动的开始时间和结束时间按结束时间的非递减序排列如下:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | |
---|---|---|---|---|---|---|---|---|---|---|---|
1 | 3 | 0 | 5 | 3 | 5 | 6 | 8 | 8 | 2 | 12 | |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
贪心算法步骤
- 首先将活动按结束时间从小到大排列。
- 选择第一个活动。
- 之后遍历剩下活动,若[活动B]的 开始时间 晚于 当前选择的[活动A]的结束时间,则选择[活动B]。
答案
EX3: 多机调度问题
注意不要与动态规划的流水作业调度问题混淆。
作业数 ,机器数 ,如何分配作业让完成时间最短。
贪心策略:按 当前最短完成时间 分配作业到 最空闲的机器;优先分配处理时间较长的任务 以填满机器。
- 当 时,直接为每个作业分配一台机器完成作业
- 当 时,首先将作业按其执行时间从大到小排序,然后依次将作业分配给当前最空闲的机器
例:7个独立作业 由3台机器 加工处理。
作业编号 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
处理时间 | 2 | 14 | 4 | 16 | 6 | 5 | 3 |
答案 最优完成时间:17
第七章 动态规划
参考资料:动态规划法 AOE网络(最长路径) 弗洛伊德算法 最长公共子序列 01背包问题 https://www.bilibili.com/video/BV1L54y1U7zP
7.2 Floyd
7.2.1 递推关系
设 表示从节点 到节点 且中间节点编号不超过 的最短路径长度。
递推关系:对于每个中间节点 ,有两种情况:
- 不经过 :路径保持原值,即
- 经过 :路径拆分为 和 ,即 实际实现中,我们可以将 从 0 到 n 枚举,覆盖更新,就不再需要 k 这一维了。
简化版的状态转移:
伪代码: 枚举顺序
for k: 0->n
for i: 0->n
for j: 0->n
if(i~k~j 小于 i~j)
d[i][j] = d[i][k] + d[k][j]
7.2.2 记录路径
引入一个额外的二维数组 next
,其中 next[i][j]
表示从节点 到节点 的最短路径中, 的下一个节点。
-
初始化:
- 如果 和 之间有边,则
next[i][j] = j
- 否则,
next[i][j] = -1
- 如果 和 之间有边,则
-
更新规则:
- 当发现更短的路径时,更新
dist[i][j]
和next[i][j]
next[i][j]
被设置为next[i][k]
,因为路径从 经过 到
- 当发现更短的路径时,更新
-
路径打印:
- 使用
next
数组回溯路径,从 开始,沿着next
数组直到
- 使用
假设图的邻接矩阵如下:
调用 floyd
算法后,dist
和 next
数组如下:
代码演示(gpt-4o-mini生成,可以丢给AI解释)
#include <iostream>
#include <vector>
#include <limits>
using namespace std;
const int INF = numeric_limits<int>::max();
void floydWarshall(int n, vector<vector<int>>& graph, vector<vector<int>>& dist, vector<vector<int>>& next) {
// Initialize distance and next matrices
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
dist[i][j] = graph[i][j];
if (graph[i][j] != INF && i != j) {
next[i][j] = j;
} else {
next[i][j] = -1; // No path
}
}
}
// Floyd-Warshall algorithm
for (int k = 1; k <= n; ++k) {
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
if (dist[i][k] != INF && dist[k][j] != INF && dist[i][j] > dist[i][k] + dist[k][j]) {
dist[i][j] = dist[i][k] + dist[k][j];
next[i][j] = next[i][k];
}
}
}
}
}
void printPath(int u, int v, const vector<vector<int>>& next) {
if (next[u][v] == -1) {
cout << "No path from " << u << " to " << v << endl;
return;
}
cout << "Path from " << u << " to " << v << ": ";
while (u != v) {
cout << u << " ";
u = next[u][v];
}
cout << v << endl;
}
int main() {
int n = 4; // Number of vertices
vector<vector<int>> graph(n + 1, vector<int>(n + 1, INF));
vector<vector<int>> dist(n + 1, vector<int>(n + 1));
vector<vector<int>> next(n + 1, vector<int>(n + 1));
// Example graph initialization (0: no edge, INF: no path)
graph[1][1] = 0;
graph[1][2] = 3;
graph[1][3] = INF;
graph[1][4] = 7;
graph[2][1] = 8;
graph[2][2] = 0;
graph[2][3] = 2;
graph[2][4] = INF;
graph[3][1] = 5;
graph[3][2] = INF;
graph[3][3] = 0;
graph[3][4] = 1;
graph[4][1] = 2;
graph[4][2] = INF;
graph[4][3] = INF;
graph[4][4] = 0;
floydWarshall(n, graph, dist, next);
// Print final path and distance matrices
cout << "Distance Matrix:" << endl;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
if (dist[i][j] == INF) {
cout << "INF ";
} else {
cout << dist[i][j] << " ";
}
}
cout << endl;
}
cout << "Path Matrix:" << endl;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
cout << next[i][j] << " ";
}
cout << endl;
}
return 0;
}
7.3 矩阵连乘
矩阵 行数 ,列数
后一个矩阵的行数必等于前一个矩阵的列数,矩阵是这样用一维数组存储的:{p1, q1(p2), q2(p3), ...}
例如, 表示 (ps. 本例答案为4500)
7.3.1 普通动态规划
自底向上:通过 迭代 预先计算所有可能的子问题,按子链长度从小到大的顺序填充表格。
记 为从 i 到 j 最少乘法次数,那么:
- 对 中任意一个数 , 也都是最少的 [最优子结构性质]
- [一个矩阵不用做乘法]
- 当 时,,其中
(一次乘法操作)= 根据本节开头提到的性质,(一次乘法操作)可以改写为 。 这样改写的好处是,矩阵可以简化为用一维数组保存。
7.3.2 备忘录方法
自顶向下:通过 递归 分解问题,遇到子问题时先查表,若未计算则递归求解并保存结果,按需填充表格。
记 为从 i 到 j 最少乘法次数, 从 i 到 j 最少乘法次数的递归函数,那么:
- [一个矩阵不用做乘法]
- 其他的 均初始化为 -1
- 如果 说明子问题已经计算过,直接返回
- 当 时,
上述两种方法时间复杂度都是 ,空间复杂度都是
7.4 最长公共子序列LCS
记 为 S1 前 i 个字符、S2 前 j 个字符的 LCS,则例题所要求的就是 .
状态如何转移?
- ,长度为0哪来的子序列
- 匹配上了,皆大欢喜,
- 没匹配上,只能选前面最大的,
时间复杂度:
7.5** 最优二叉搜索树
p143 例7.7
p159 习题7.13
Optimal Binary Search Tree = OBST 书上讲的啥玩意,看不懂
如何做题:https://www.bilibili.com/video/BV1oTcUexE8T
建议看视频时对照看下面的内容:
矩阵是查找成功的概率, 矩阵是查找失败的概率
矩阵:我们目的是,让 最小
: Weight, 表示 到 的总概率,既包括成功也包括失败
例: 为什么等于 请看下图,星号表示查找失败
a₂(p2)
/ \
a₁(p1) *(q2)
/ \
*(q0) *(q1)
对于其他的树形,仍然是这几个概率相加不变
: Cost, 表示 到 内构造OBST的最小平均搜索代价
由最优子结构,我们把从 i 到 j 挨个作为根,看谁作为根的时候,(左子树代价+右子树代价) 最小,此时 就是最小。
联想一下矩阵连乘,递推式几乎就是同一个意思,只是把(一次乘法操作)改成了w(i,j)即 到 的总概率。
: Root, 当 到 代价最小时,把谁作为根?
ps. 《算法导论》(原书第三版) 15.5 最优二叉搜索树 注意算法导论解此问题,i是从1开始的,j是从0开始的。而陈的《算法设计与分析》中i和j都是从0开始。算法导论p230的推导,答案为 。如果想按照陈的书来理解,把i下标整体减1就能看懂了。
7.6 01背包
7.6.1 一般方法
设背包容量6,物品如下:
物品 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
重量 | 3 | 2 | 1 | 4 | 5 |
价值 | 25 | 20 | 15 | 40 | 50 |
记 表示仅能在前 i 件物品里面选、放入大小为 j 的背包中,得到的最大价值。例题要求的就是 .
状态如何转移?
- ,没有物品/背包容量为0,肯定没有价值
- ,背包的容量装不下第 i 件物品,选不了第 i 件物品。
- ,背包的容量能装下第 i 件物品,此时考虑选不选第 i 件。
- 若选第 i 件,价值为
(第 i 件物品的价值) + (前 i-1 件物品、放入去掉第 i 件物品重量的背包、得到的最大价值)
即 - 若不选第 i 件,价值就还是
时间复杂度:,其中n表示物品的数量,m表示背包的容量
7.6.2* 阶跃点法
https://www.bilibili.com/video/BV1ZNcUeoEuk
阶跃点(Step Points) 表示在不同背包容量下,能够达到的最优(重量,价值)组合。
每个阶跃点集合 表示处理前 个物品后的状态。
剔除被支配点:两个阶跃点A(W1, V1)和B(W2, V2),如果W1 ≤ W2且V1 ≥ V2,那么B点被支配,应该剔除。
即,A点花费了更少的容量,得到了更大的价值,必定比B点更优。就不要B点了。
设背包容量6,物品如下:
物品 | 0 | 1 | 2 |
---|---|---|---|
重量 | 1.8 | 3.2 | 2.5 |
价值 | 2 | 3 | 5 |
(1) 初始状态 (未放入任何物品)
- 初始状态为空背包,仅包含原点:
(2) 处理第0个物品(重量1.8,价值2) → 生成
- 不放入物品0:保留 的所有点 。
- 放入物品0(记为 ):生成新点 。
- 合并并剔除被支配的点:
- 比较 和 ,均未被支配。
- 最终集合:
(3) 处理第1个物品(重量3.2,价值3) → 生成
- 不放入物品1:保留 的所有点 。
- 放入物品1(记为 ):对 中的每个点 ,生成新点 :
- 合并并剔除被支配的点:
- 原集合 与新集合 合并。
- 剔除被支配的点(例如, 未被任何点支配)。
- 最终集合:
(4) 处理第2个物品(重量2.5,价值5) → 生成
- 不放入物品2:保留 的所有点。
- 放入物品2(记为 ):对 中的每个点 ,生成新点 :
- (超出容量6,舍去)
- 合并并剔除被支配的点:
- 原集合 与新集合 合并。
- 剔除被支配的点: 比 更优。
- 是最大价值点。
- 最终集合:
(5) 逆序求解选了哪些物品
如果上面的步骤看懂了,直接看视频 5:09
本例答案:
7.7 (双机)流水作业调度
这和动态规划有什么关系?
个作业 要在由两台机器 和 组成的流水线上完成加工。
每个作业加工的顺序都是先在 上加工,然后在 上加工。
和 加工作业 所需的时间分别为 和 。
求最优加工顺序,使得用时最少。
Johnson 算法:
- 所有 的作业,划归为集合 ;所有 的作业,划归为集合 .
- 将 中的作业按照 递增排序, 中的作业按照 递减排序.
- 中作业接 中作业,构成满足 Johnson 法则的最优调度。
- 最优加工顺序: 中的作业、 中作业
作业 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
5 | 1 | 8 | 5 | 3 | 4 | |
7 | 2 | 2 | 4 | 7 | 4 |
排序后 {2, 5, 1} 排序后 {6, 4, 3} 最优加工顺序: {2, 5, 1, 6, 4, 3}