Lecture | Algorithm Review

Algrithm Review Notes

Basic

  1. 简述快速排序中划分过程的作用。
  • 快速排序划分可以将整个数组划分为两部分,使得数组右边小于左边(升序),而用于划分数组的基准元被放到了正确的位置。从而在递归排序时每次划分都满足上述性质,合并后可以完成整个数组的排序。
  1. 使用快速排序算法针对数组进行升序排序,以最右边元素作为主元,请写出针对(600, 500, 400, 300, 200, 100)这个输入实例进行排序的执行过程。
  2. 简述分治法与平衡的关系,并阐述你对快速排序算法中平衡的理解。
  • 分治与平衡密不可分,具有平衡结构的分治效率往往更高。
  1. check
  • × × √ √ ×
  1. check-1
  • × √ × √ √
  1. check-2
  • × × × × √
  1. check-3
  • ×(不含等号) 不可以(包含关系) 有些问题无法在多项式时间求精确解,但可以求近似解
  1. check-4
  2. check-5
  3. check-6

模拟算法: 模拟就是用计算机来模拟题目中要求的操作。模拟题目通常具有码量大、操作多、思路繁复的特点。由于它码量大,经常会出现难以查错的情况,如果在考试中写错是相当浪费时间的。

近似算法: 在计算复杂性理论中的某些假设下,比如最著名的 假设下,对于一些可已被证明为NP完全的优化问题,无法在多项式时间内精确求到最优解,然而在现实或理论研究中,这类问题都有广泛的应用,在精确解无法得到的情况下,转而依靠高效的近似算法求可以接受的近似解。近似算法的研究也是当今计算机科学研究的一个主要方向。(有近似精度的要求)

随机算法: 在算法的过程中引入随机数,使得算法在执行的过程中随机选择下一个计算步骤。它最后可能导致结果也是不确定的。一个结果不确定的概率算法叫做 Monte Carlo 算法,而总是得到准确解的概率算法叫做 Sherwood 算法(一个例子是引进随机因子的快速排序算法)。

Master theorem

master master-1

Amortized analysis

聚合分析(Aggregate Analysis)通过计算一系列操作的总成本,并将其平均到每次操作上,从而得出每次操作的均摊时间复杂度。

以动态数组为例,首先,可以得到插入操作的两个关键成本:

  • 如果数组未满,插入操作的成本为
  • 如果数组已满,则插入操作需要扩容,扩容后复制元素的成本为 ,其中 为当前数组的大小。

所以,为了计算 n 次插入操作的总成本,可以将其分开为两部分计算:

  1. 插入操作的成本:每次插入新元素的直接成本是常数时间 ,对于 次操作,总成本是
  2. 数组扩容的成本:每次扩容涉及到复制原数组元素到新数组。这些操作发生在数组大小为 的时刻,其中 是小于等于 的最大幂。扩容操作的成本分别是 ,总和为 ,这是一个等比数列的和,其结果为

因此,该数组总的插入成本为 ,均摊到每次操作的成本为 。即使在最坏情况下,平均每次插入操作的成本依然是常数时间。

NP problems

P类问题: 在确定型图灵机上能使用多项式时间的算法得到该问题的 (显然能在多项式时间内对问题进行验证)

NP类问题: 在非确定型图灵机上能使用多项式时间进行 验证 的问题(在现有算法下不一定能以多项式时间解决) - P类问题是NP类问题的子集 - 例子:旅行商问题TSP 即有一个推销员,要到n个城市推销商品,他要找出一个包含所有n个城市的环路,这个环路路径小于a

问题约化: 可以用问题B的算法来解决A ,我们就说问题A可以约化成问题B。(二元一次方程的解法可以用于解一元一次方程)约化具有传递性。存在最大的问题。

NPC类问题: 存在这样一个NP问题,所有的NP问题都可以约化(多项式规约)成它。换句话说,只要解决了这个问题,那么所有的NP问题都解决了。其定义要满足2个条件: 首先,它得是一个NP问题;然后,所有的NP问题都可以约化到它。

NP-Hard问题: NP-Hard问题是这样一种问题,它满足NPC问题定义的第二条但不一定要满足第一条(就是说,NP-Hard问题要比 NPC问题的范围广,NP-Hard问题没有限定属于NP),即所有的NP问题都能约化到它,但是它不一定是一个NP问题。

np

Divide and Conque

Prob-1 设X[0:n-1]和Y[0:n-1]为两个数组,每个数组中含有n个已排好序的数。试设计一个O(logn)时间的分治算法,找出X和Y的2n个数的中位数,并证明算法的时间复杂性为O(logn)

问题同上,只是X[0:m-1]和Y[0:n-1],两有序数组长度不同,试设计一个O(log(m+n))时间的分治算法

  1. 两个数组长度相等时:
  • 算法:考虑 数组中间位置的值 ,若 ,则说明 位于整个 个值的后半部,因此中位数在 之中;若 ,则说明 位于所有 个数的前半部,因此中位数在 之中;否则 就是中位数。若上一步没有找到中位数,则从 数组开始查找中间值,将中间值与 数组对应位置的值比较。递归上述步骤直到找到中位数。
  • 复杂度:最好情况是一次寻找就能找到中位数 ,最坏情况是每次只能去掉 的规模的数据,复杂度公式为 ,根据 Master Theorem,
  1. 两个数组长度不等时:

Prob-2 skyline

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def merge(self, left, right):
# 记录目前左右建筑物的高度
lheight = rheight = 0
# 位置
l = r = 0
# 输出结果
res = []
while l < len(left) and r < len(right):
if left[l][0] < right[r][0]:
# current point
cp = [left[l][0], max(left[l][1], rheight)]
lheight = left[l][1]
l += 1
elif left[l][0] > right[r][0]:
cp = [right[r][0], max(right[r][1], lheight)]
rheight = right[r][1]
r += 1
# 相等情况
else:
cp = [left[l][0], max(left[l][1], right[r][1])]
lheight = left[l][1]
rheight = right[r][1]
l += 1
r += 1
# 和前面高度比较,不一样才加入
if len(res) == 0 or res[-1][1] != cp[1]:
res.append(cp)
# 剩余部分添加进去
res.extend(left[l:] or right[r:])
return res

Prob-3 最大子数组问题。一个包含n个整数(有正有负)的数组A,设计一O(nlogn)算法找出和最大的非空连续子数组。对于此问题你还能设计出O(n)的算法吗? - 例如:[0, -2, 3, 5, -1, 2]应返回9,[-9, -2, -3, -5, -3]应返回-2

见DP

Prob-4 循环移位问题。给定一个数组,数组中元素按从小到大排好序,现将数组中元素循环右移若干位,请设计一算法,计算出循环右移了多少位。

分治,合并时检查是否存在>的数,若存在直接返回,位数即栈中的偏移量加上合并发现>的位置

Prob-5 两元素和为 X。给定一个由 n 个实数构成的集合 S 和另一个实数x,判断 S 中是否有两个元素的和为 x。试设计一个分治算法求解上述问题,并分析算法的时间复杂度。

遍历加二分查找:先排序,复杂度 遍历每一个实数复杂度为 ,在剩余未遍历的集合中查找 ,复杂度 。总复杂度

Prob-6 有一实数序列𝑎_1,𝑎_2,…,𝑎_𝑁,若𝑖<𝑗 且 𝑎_𝑖>𝑎_𝑗,则(𝑎_𝑖,𝑎_𝑗)构成了一个逆序对,请使用分治方法求整个序列中逆序对个数,并分析算法的时间复杂性。 - 例如:序列(4,3,2)逆序对有(4,3),(4,2),(3,2)共3个

归并排序统计逆序对。在合并时注意:若左半部分大于右半某个值,则左半部分后续的值都大于它。

merge

Prob-7 给定 个区间,求它们的最大交区间。

Dynamic Programming

  • 可以在多项式时间内解决问题

Prob-1 给出N个1-9的数字 (v1,v2,…,vN),不改变它们的相对位置,在中间加入K个乘号和N-K-1个加号,(括号随便加)使最终结果尽量大。因为乘号和加号一共就是N-1个了,所以恰好每两个相邻数字之间都有一个符号。说明其具有优化子结构性质及子问题重叠性质。 - 例如: N=5, K=2,5个数字分别为1、2、3、4、5,可以加成: - 1 * 2 * (3 + 4 + 5) = 24 - 1 * (2 + 3) * (4 + 5) = 45 - (1 * 2 + 3) * (4 + 5) = 45

优化子结构性质 优化子结构性质意味着问题的最优解包含其子问题的最优解。对于这个问题,我们可以将整个序列分为两部分,分别求解这两部分的最大值,然后将这两部分的最大值通过加号或乘号连接起来,以获得整个序列的最大值。

子问题重叠性质 子问题重叠性质意味着在递归求解过程中,相同的子问题会被多次计算。在这个问题中,当我们计算不同分割点的最大值时,很多子序列会被重复计算,因此我们可以将这些子序列的结果存储起来,避免重复计算。

动态规划解决方案 我们可以定义一个二维数组 dp[i][j] 表示从第 i 个数字到第 j 个数字之间插入乘号和加号能得到的最大值。我们的目标是计算 dp[0][N-1]。

状态转移方程 对于任意的 i 和 j(i < j),我们有两种选择:

在 i 和 j 之间插入一个加号,那么问题就变成了求解 dp[i][k] 和 dp[k+1][j] 的最大值,其中 k 是 i 和 j 之间的某个分割点,然后我们将这两个结果相加:dp[i][j] = max(dp[i][j], dp[i][k] + dp[k+1][j])。 在 i 和 j 之间插入一个乘号,那么问题就变成了求解 dp[i][k] 和 dp[k+1][j] 的乘积,其中 k 是 i 和 j 之间的某个分割点:dp[i][j] = max(dp[i][j], dp[i][k] * dp[k+1][j])。 我们需要枚举所有可能的 k 来更新 dp[i][j]。

初始化 dp[i][i] = v[i],因为只有一个数字,不需要任何操作。 计算顺序 我们按照 j - i 的长度从小到大计算 dp[i][j],这样可以确保在计算 dp[i][j] 时,其子问题 dp[i][k] 和 dp[k+1][j] 已经被计算过。

返回值 最终,dp[0][N-1] 将给出整个序列的最大值。

Prob-2 给定一长度为N的整数序列(a1,a2,…,aN) ,将其划分成多个子序列(此问题中子序列是连续的一段整数),满足每个子序列中整数的和不大于一个数 B,设计一种划分方法,最小化所有子序列中最大值的和。说明其具有优化子结构及子问题重叠性质 例如: 序列长度为8的整数序列(2,2,2,8,1,8,2,1),B=17,可将其划分成三个子序列(2,2,2),(8,1,8)以及(2,1),则可满足每个子序列中整数和不大于17,所有子序列中最大值的和12为最终结果。

考虑子问题: , 显然当存在 有更小值时,将该值替换问题的最优解,可以得到更优解,因此原问题最优解不成立而矛盾,所以该问题具有最优子结构。而在计算 时由于要遍历 而产生重复值计算的值,因此具有子问题重叠性质。

Prob-3 对一棵树进行着色,每个结点可着黑色或白色,相邻结点不能着相同黑色,但可着相同白色。令树的根为r,请设计一种算法对树中尽量多的节点着黑色。

只有两种情况:(1)根节点为黑色 0,子节点只能为白色 1,(2)根节点为白色,子节点可黑可白, 维护DP表。递推公式是

当节点是叶子节点时,

Prob-4 在自然语言处理中一个重要的问题是分词,例如句子“他说的确实在理”中“的确”“确实”“实在”“在理”都是常见的词汇,但是计算机必须为给定的句子准确判断出正确分词方法。一个简化的分词问题如下:给定一个长字符串y=y1y2…yn,分词是把y切分成若干连续部分,每部分都单独成为词汇。我们用函数quality(x)判断切分后的某词汇x=x1x2…xk的质量,函数值越高表示该词汇的正确性越高。分词的好坏用所有词汇的质量的和来表示。例如对句子“确实在理”分词,quality(确实) + quality(在理) > quality(确)+quality(实在)+quality(理)。请设计一个动态规划算法对字符串y分词,要求最大化所有词汇的质量和。(假定你可以调用quality(x)函数在一步内得到任何长度的词汇的质量)

递推公式:

Prob-5 给定 𝑛 个活动,活动𝑎_𝑖表示为一个三元组(𝑠_𝑖,𝑓_𝑖,𝑣_𝑖),其中𝑠_𝑖表示活动开始时间,𝑓_𝑖表示活动的结束时间,𝑣_𝑖表示活动的权重。带权活动选择问题是选择一些活动,使得任意被选择的两个活动𝑎_𝑖和𝑎_𝑗执行时间互不相交,即区间[𝑠_𝑖,𝑓_𝑖]与[𝑠_𝑗,𝑓_𝑗]互不重叠,并且被选择的活动的权重和最大。请设计一种方法求解带权活动选择问题。

  1. 先将活动按照结束时间排序,定义最早结束的活动到第 个结束的活动的最优排列方案是 ,结束时间早于 的活动集合的最优排列方案为 ,则
  2. 满足最优子结构:
  3. 重叠子问题集合:
  4. 空间复杂度 ,时间复杂度

Prob-6 受限最短路径长度问题:给定一无向图G=(V, E, A, B),A(e)表示边e的长度,B(v)表示顶点v的花费,计算小明从顶点s到顶点d的最短路径长度,满足以下限制,初始时小明随身携带M元钱,每经过一个顶点v,须交B(v)的过路费,若身上有大于B(v)的钱则可以通过,否则不可以通过。求顶点s到顶点d的最短路径

递推公式:

然后BFS从开始节点遍历子节点,注意记忆化存储

Prob-7 给定𝑛个物品,每个物品有大小𝑠_𝑖,价值𝑣_𝑖。背包容量为𝐶。要求找到一组物品,这些物品整包完全占满背包容量𝐶,且总体价值最大。请写出动态规划迭代公式。

表示装入前 个物品时,容量为 的背包的最大总价值。

递推公式: 或者

每个物品只能放一次,因此是0-1背包,注意内层循环需要反向,最后需要检查背包是否占满(==)

Prob-8 最大子数组问题:一个包含n个整数(有正有负)的数组A,设计一O(nlogn)算法找出和最大的非空连续子数组。(例如:[0, -2, 3, 5, -1, 2]应返回9,[-9, -2, -3, -5, -3]应返回-2。)

DP解法: 递推公式:,找到DP数组,再找DP数组的最大值

DC解法:合并跨越中线的数组时遍历即可,注意左边是 右边是

Prob-9 最长非降子序列:一个序列有N个数:A[1],A[2],…,A[N],求出最长非降子序列的长度。

对于长度为 的子序列的最后一个元素的最小值 ,如果新来的元素大于它,就插入到 数组末尾(说明长度为 时,子序列最后一个元素最小是这个新来的元素);如果小于它,则用二分查找,找第一个大于新元素的,用新元素 替换 它 (假设找到的位置是 即在不降子序列长度为 时,末尾的元素还能更小)。

对于最长 上升 子序列问题,类似地,可以令 d_i 表示所有长度为 i 的最长上升子序列的末尾元素的最小值。

需要注意的是,若 ,由于最长上升子序列中相邻元素不能相等,需要在 d 序列中找到 第一个 不小于 的元素,用 替换之。

Greedy

Prob-1 给定n个物品,物品价值分别为P1,P2,…,Pn,物品重量分别W1,W2, …, Wn,背包容量为M。每种物品可部分装入到背包中。输出X1,X2,…,Xn,0<=Xi<=1, 使得 最大,且 。试设计一个算法求解该问题,分析算法的正确性。

Prob-2 海面上有一些船需要与陆地进行通信,需要在海岸线上布置一些基站。现将问题抽象为,在x轴上方,给出N条船的坐标𝑝_1,𝑝_2,…,𝑝_𝑁,𝑝_𝑖=(𝑥_𝑖,𝑦_𝑖),𝑥_𝑖≥0, 𝑦_𝑖≤d,1≤𝑖≤𝑁,在x轴上安放的基站可以覆盖半径为d的区域内的所有点,问在x轴上至少要安放几个点才可以将x轴上方的点都覆盖起来。试设计一个算法求解该问题,并分析算法的正确性。 greedy-2

Prob-3 某公司有个工厂和仓库。由于原材料等价格波动,工厂每个月的生产成本也会波动,令第𝑖个月产品的单位生产成本为𝑐_𝑖(该月生产一个产品的成本为𝑐_𝑖)。仓库储存产品的也有成本,假设每个月产品的单位储存成本为固定值1(存储一个产品一个月的成本为1)。令第𝑖个月需要供应给客户的产品数量为𝑦_𝑖,仓库里的和生产的产品均可供应给客户。假设仓库的容量无限大,供应给客户剩余的产品可储存在仓库中。若已知𝑛个月中各月的单位生产成本𝑐_𝑖、以及产品供应量𝑦_𝑖,设计一算法决策每个月的产品生产数量𝑥_𝑖,使得𝑛个月的总成本最低。例如:𝑛=3,𝑐_𝑖:2,5,3,𝑦_𝑖:2,4,5,则𝑥_𝑖:6,0,5,即第1个月生产6个供应2个(代价2×2=4),储存4个供应给第2个月(代价(2+1)×4=12),第3个月生产5个供应5个(代价3×5=15),使总成本4+12+15=31最小

Prob-4 给定直线上 2n个点的序列P[1,2,… ,2n],每个点 P[i]要么是白点要么是黑点,其中共有n个白点和 n个黑点,相邻两个点之间距离均为1,请设计一个算法将每个白点与一黑点相连,使得连线的总长度最小。例如,图中有4个白点和4个黑点,以图中方式相连,连线总长度为1+1+1+5=8。 greedy-4

Prob-5 有n个作业需要在一台机器上执行,一个时刻机器上只能执行一个作业,每个作业可在单位时间内完成,作业i有截止时间di,当作业i在截止时间被执行完,则可获得pi的收益,请设计算法获得最大收益,并分析算法的正确性

Prob-6 假设有数目不限的面值为25美分,10美分,5美分,1美分的硬币,请使用最少个数的硬币凑出3.33美元。

Searching

Prob-1 searching-1-1 searching-1-2 请从给定的点数网格𝒂[𝟏,𝟐,…,𝟕][𝟏,𝟐,…,𝟖]使用搜索算法求出对应 的骨牌号图𝒃[𝟏,𝟐,…,𝟕][𝟏,𝟐,…,𝟖],有可能的话,给出相关剪枝策略。

Network flow/Bipartition matching

Ford-Fulkerson alg

适用于稠密图,可以用DFS找增广路,时间复杂度为 是最大流量:

  • 搜索出一条增广路;
  • 在这条路径中所有的边容量减去这条增广路的流量,并建立流量为增广路增加流量相反数的反向边;
  • 返回操作一,如果没有增广路则得到答案
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
* 查找增广路
* @param c 当前节点
* @param t 汇点
* @param f 当前路径中的容量最小值
* @return
*/
int dfs(int c, int t, int f) {
// 如果当前节点是汇点 t,直接返回容量最小值,即增广路增加的流量
if (c == t) {
return f;
}
// 记忆化搜索染色
used[c] = true;
// 遍历 c 节点下一个节点
for (int i = 0; i < G[c].size(); ++ i) {
Edge &e = G[c][i];
// 如果这个节点未被访问到,并且其当前容量大于 0
if (!used[e.to] && e.cap > 0) {
// 访问到最深层节点
int d = dfs(e.to, t, min(f, e.cap));
if (d > 0) {
// 当前边容器减少
e.cap -= d;
// 反向边容量增加
G[e.to][e.rev].cap += d;
return d;
}
}
}
return 0;
}

int max_flow(int s, int t) {
int flow = 0;
int cnt = 0;
for (;;) {
memset(used, 0, sizeof(used));
int f = dfs(s, t, INF);
cnt += 1;
if (f == 0) {
cout << cnt << endl;
return flow;
}
flow += f;
}
}

Edmond-Karp alg

适用于稀疏图,用BFS找增广路,算法复杂度为

  • 使用 BFS 找到一条增广路(对应下面的步骤 1);
  • 计算这条路的最小容量边,为汇点加流量,并建立反向边,其容量为增加的流量(对应下面的步骤 2);
  • 重复第一步,如果不能找到一条增广路则得到最大流;

但是在实现上,由于我们采用了 BFS 方法,则无法对这条增广路进行回溯处理。所以在代码实现的时候,我们需要通过一个数组或者一个 Map 来记录下对应点在增广路上的入度边

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// 用来记录当前路径上的最小容量,用于加流量操作
int a[MAX_V];
// 记录下标点的边编号,pair 对应 G[x1][x2],x1 是描述哪个入度点,x2 是描述 x1 点的第 x2 条边
unordered_map<int, pair<int, int>> pre;
void bfs(int s, int t) {
// a 初始化成 0,也可以判断是否已经被染色,从而剪枝情况
memset(a, 0, sizeof(a));
// 使用队列,保存处理节点
queue<int> que;
que.push(s);
// 每个节点所流过的流量设置为 INF 无穷大
// 这样可以起到求最小的作用
a[s] = INF;
while (!que.empty()) {
int x = que.front();
que.pop();
// 遍历当前节点的所有边
for (int i = 0; i < G[x].size(); ++ i) {
Edge& e = G[x][i];
// 如果相连的点没有访问,并且这条边的容量大于 0
if (!a[e.to] && e.cap > 0) {
// 记录下一个点的入度边
pre[e.to] = make_pair(x, i);
// 计算当前路径的最小容量
a[e.to] = min(a[x], e.cap);
que.push(e.to);
}
}
if (a[t]) break;
}
}

int max_flow(int s, int t) {
// 最大流结果
int ret = 0;
while (1) {
// 从 S -> T 使用 bfs 查询一条增广路
bfs(s, t);
// 如果发现容量最小是 0 ,说明查不到了
if (a[t] == 0) break;
int u = t;
while (u != s) {
// 使用 pre 来获取当前增广路中汇点 T 的入度边下标信息
int p = pre[u].first, edge_index = pre[u].second;
// 获取正向边和反向边
Edge& forward_edge = G[p][edge_index];
Edge& reverse_edge = G[forward_edge.to][forward_edge.rev];
// 更新流量
forward_edge.cap -= a[t];
reverse_edge.cap += a[t];
// 逆增广路方向移动游标继续更新
u = reverse_edge.to;
}
ret += a[t];
}
return ret;
}

Prob-1 nf-1-1 nf-1-2

Graph

Topological sorting

Activity on Vertex Network (AOV) 顶点表示活动。当活动的所有前驱节点都完成,该节点才能执行。

拓扑序列 构造如下

  • 从图中选择入度为0的点
  • 输出该顶点,然后从图中删除此顶点的所有出边

重复上述步骤直到所有顶点都输出,拓扑排序完成;否则图中不存在入度为0的点,是有环图,死锁。

Activity on Edge Network (AOE) 边表示活动/权值/时间,顶点表示事件。

  • 事件(顶点)的最早发生时间 即需要所有前驱节点完成,是源点到该节点的最大路径
  • 事件(顶点)的最迟发生时间 事件的所有后继活动的最迟开始时间的最小值
  • 活动(边)的最早发生时间
  • 活动(边)的最迟发生时间 后继事件的最迟发生时间 - 该事件的持续时间(权值)
  • 关键路径:AOE 网中从源点到汇点的最长路径的长度。
  • 关键活动:即关键路径上的活动,它的最早开始时间和最迟开始时间相等。

递推求最早和最迟发生时间: 按拓扑顺序求,最早发生时间从前往后递推,最迟发生时间从后往前递推

Kaha算法 初始状态下,集合 S 装着所有入度为 0 的点,L 是一个空列表。 每次从 S 中取出一个点 u(可以随便取)放入 L, 然后将 u 的所有边 删除。对于边 ,若将该边删除后点 v 的入度变为 0,则将 v 放入 S 中。不断重复以上过程,直到集合 S 为空。检查图中是否存在任何边,如果有,那么这个图一定有环路,否则返回 L,L 中顶点的顺序就是构造拓扑序列的结果。

  • 时间复杂度:

Minimum spanning tree (MST)

(m是边数,n是点数)

Kruskal:维护一个森林,查询两个结点是否在同一棵树中,连接两棵树。时间复杂度

kruskal

Prim:每次要选择距离最小的一个结点,以及用新的边更新其他结点的距离。时间复杂度 (暴力查找), (二叉堆)

prim

Minimum path

性质

  • 对于边权为正的图,任意两个结点之间的最短路,不会经过重复的结点。
  • 对于边权为正的图,任意两个结点之间的最短路,不会经过重复的边。
  • 对于边权为正的图,任意两个结点之间的最短路,任意一条的结点数不会超过 n,边数不会超过 n-1。

Floyd算法: 可以求任意两个结点之间的最短路。适用于任何图,不管有向无向,边权正负,但是最短路必须存在。(不能有个负环)

  • f[k,x,y] 表示只允许经过节点 1 到 k 的 x 到 y 的最短路。则最终要求的是 f[n,x,y]f[0,x,y]=0 or inf
  • f[k,x,y] = min(f[k-1,x,y], f[k-1,x,k] + f[k-1,k,y]):分成是否经过点 k 来考虑
  • 第一维对于结果无影响
  • 时间复杂度 ,空间复杂度
    1
    2
    3
    4
    for (k = 1; k <= n; k++)
    for (x = 1; x <= n; x++)
    for (y = 1; y <= n; y++)
    f[x][y] = min(f[x][y], f[x][k] + f[k][y]);

Bellman-Ford算法:可以处理负权图,可以判断最短路是否存在。复杂度

[bellman-ford]bellman-ford.png)

  • 需要注意的是,以 S 点为源点跑 Bellman–Ford 算法时,如果没有给出存在负环的结果,只能说明从 S 点出发不能抵达一个负环,而不能说明图上不存在负环。
  • 因此如果需要判断整个图上是否存在负环,最严谨的做法是建立一个超级源点,向图上每个节点连一条权值为 0 的边,然后以超级源点为起点执行 Bellman–Ford 算法。

Dijkstra算法:求解非负权图单源最短路。

将结点分成两个集合:已确定最短路长度的点集(记为 S 集合)的和未确定最短路长度的点集(记为 T 集合)。一开始所有的点都属于 T 集合。初始化 dis(s)=0,其他点的 dis 均为 。然后重复这些操作:

  • 从 T 集合中,选取一个最短路长度最小的结点,移到 S 集合中。
  • 对那些刚刚被加入 S 集合的结点的所有出边执行松弛操作。
  • 直到 T 集合为空,算法结束。

时间复杂度:稀疏图 二叉堆实现更优;稠密图 暴搜优于二叉堆。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct edge {
int v, w;
};

vector<edge> e[MAXN];
int dis[MAXN], vis[MAXN];

void dijkstra(int n, int s) {
memset(dis, 0x3f, (n + 1) * sizeof(int));
dis[s] = 0;
for (int i = 1; i <= n; i++) {
int u = 0, mind = 0x3f3f3f3f;
for (int j = 1; j <= n; j++)
if (!vis[j] && dis[j] < mind) u = j, mind = dis[j];
vis[u] = true;
for (auto ed : e[u]) {
int v = ed.v, w = ed.w;
if (dis[v] > dis[u] + w) dis[v] = dis[u] + w;
}
}
}

Bipartite graph

性质

  • 如果两个集合中的点分别染成黑色和白色,可以发现二分图中的每一条边都一定是连接一个黑色点和一个白色点。
  • 二分图不存在长度为奇数的环(可以用DFS/BFS遍历图,不存在奇数环的连通图则是二部图)

最大匹配 给定二分图,要求选定一些边,使得边之间没有公共顶点,且边的数量最大。

  • 交替路/增广路:从未匹配的点出发,依次交替经过非匹配边、匹配边,到达另一个非匹配点。然后对增广路上所有的非匹配和匹配取反,得到新的匹配图。(DFS,
  • 转为最大流:源点连接左边集合,汇点连右集合,容量都为1

----