图论

1. 建图

分为邻接表 和 邻接矩阵两种 邻接表用的比较多

2. 拓扑排序

拓扑排序就是统计每个点的入度,入度为0的一定在前面, 遍历每个入度为0的点,把他的邻接点的入度减1, 重复这个操作就得到拓扑排序的序列了;

此外,只有有向无环图才有拓扑排序 所以也是判断有没有环的一种方式;

3. 最短路问题

最短路问题要写的可就多了 也是趁这机会好好复习一下吧

最短路问题主要记的有以下几个方法:

堆优化dijkstra, Bellman-Ford, SPFA, Floyd; 其实还有双端队列BFS(0 - 1 边权的最短路问题), 等会也写上吧

1. 堆优化版dijkstra: 适用于正(其实是非负)边权

记住dijkstra是出栈(出队)时确定最小距离就好了

2. Bellman-Ford

现在看来, Bellman-Ford 算法最适合解决的问题是有边数限制的最短路问题 剩下比如有负权边的最短路问题,负环的判断等等都可以用SPFA来解决

3. SPFA:(正负都可以)

spfa 的代码其实和堆优化dijkstra很像 因为都是用堆去优化那个找当前最小距离的过程;

这两个的判断条件不一样 要记清:

  • dijkstra 是出队就保证最短距离

  • 但是spfa是出队都没有办法保证是最短距离,只要更新就要入栈,st数组的意义在于如果栈中有这个元素就不必再次加入了(因为肯定是要重算)

1. SPFA 求最短路

2. SPFA 判断是否存在负环

相较于求最短路 额外围护一个cnt数组,这个数组记录最短路径的节点数量 只要超过 n + 1 则证明必有负环存在

注意这里要把所有点都加入队列,dist清空or not 都无所谓 避免起点与负环不连通-> 找不到

找正环 : 把求最短路变成求最长路即可!

常用于0-1分数规划问题

除了这个之外还有个额外的trick,如果超时了的话 可以按照spfa出栈元素的次数判断是否存在负环 如果出栈元素的次数大于一个经验值(可以试一试 2*n 3*n)就可以当成有负环了

4. floyd: 多源汇最短路

基于动态规划 记住那三重循环的顺序 先遍历的是中间点k

同样的 可以将floyd退化为bool值 这样直接判断是否连通

5. 双端队列BFS: (只有0-1边权的最短路问题)

对于只有0 - 1 两种边权的最短路问题,可以用双端队列广搜来实现 不必维护一个heap;

4.最小生成树问题

最小生成树就记一个Kruskal算法就好;

Kruskal 算法

Kruskal算法基于的是并查集,整体思路很简单, 将所有边按权重从小到大排序,枚举所有边(a, b, w) 如果 a b尚未连通,就将这条边加入集合中 并统计权重就好了;

对于Kruskal算法而言 可以只求前一半 - 最小生成森林 / 后一半 - 缩点的最小生成树 都是可以的;

有这样两个性质: - 最小生成树的边数一定是n - 1 - 得到的解不仅是边权和最小,最大边权也最小

5. 二分图

二分图这一块有判断是否是二分图(染色法) 和 二分图的最大匹配(匈牙利算法)两块;

具体细分而言

  • 匈牙利算法: 匹配 最大匹配 匹配点 增广路径

  • 最小点覆盖 最大独立集 最小路径点覆盖 最小路径重复点覆盖问题

    • 公式:最大匹配数 = 最小点覆盖 = 总点数 - 最大独立集 = 总点数 - 最小路径覆盖

1. 染色法判断二分图

一个图是二分图 等价于 图中没有奇数环

染色法判断二分图的实质就是DFS;一个边连接的两个点不会是同一个颜色

0 - 未染色 1 - 颜色1 2 - 颜色2

2. 二分图的最大匹配-匈牙利算法

这个要好好背一下;

3. 最小路径重复点覆盖问题

方法:

    1. 求原图的传递闭包

    1. 原图的最小路径重复点覆盖 = 新图的最小路径覆盖 = 新图的 n - 最大匹配

6. 差分优化

...

7. 最近公共祖先(LCA)

LCA问题:在一个有根树中,最近的公共祖先(自己也算)

1. 倍增法

方法如下:

  • 预处理:fa[i, j]表示从i开始 向上走2^j步所到达的节点

  • 预处理:depth[i]表示i节点所在的深度

  • 查询: 求lca(a, b)

    • 如果a的深度比b大 -> 先跳到同一层

    • 保证在同一层下,共同向上跳,直到跳到LCA的下面一层

大概的框架就是这样,此外在代码实现上还有许多细节(比如哨兵节点0的应用等等)

depth显然用dfs bfs都可以,而fa[i,j] 可以用递推求解 fa[i, j] = fa[fa[i, j - 1], j - 1] (爸爸的爸爸是爷爷)

倍增法求LCA的复杂度:预处理o(nlogn) 查询一次:o(logn)

2. Tarjan 离线求LCA

这一定要上图了 avatar

8. 连通分量

1. 有向图的强连通分量

tarjan缩点

此外 还有些额外的结论

  • id的倒序 就是DAG的拓扑排序

  • 设DAG的入度为0点数量为p 出度为0点的数量为q 则使得整个图为强连通量 最少需要添加的边数为max(p, q)

2. 无向图的双连通分量

无向图的双连通分量分为两种:

  • 边的双连通分量 e_dcc: 不含桥的连通分量

  • 点的双连通分量 v_dcc: 不含割点的连通块

1. 边的双连通分量问题

和有向图的强连通分量求法类似,也是引入一个时间戳的概念 维护dfnlow两个时间戳数组

如何找到桥? x -> y dfn[x] < low[y]

如何找到所有边的双连通分量? 用一个栈帮助找到连通分量内的所有点

桥(割边) 对于一个无向连通图来说, 如果我们删除一条边之后, 图变得不再连通了, 那么我们九讲这条边称为桥.

定义 一个极大的(图中不存在任何一个非连通块中的点加入连通块之后, 该连通块依然是连通块)不包含桥的连通块

性质 对于一个边连通分量来说, 我们不管删除连通分量的哪一条边, 它都是连通的. 对于一个边连通分量来说, 任意两点之间至少包含两条不相交的路径(两条路径上无公共边) 这是一个充要条件

tarjan算法求边双连通分量

无向图不存在横叉边

因为双向不存在后面能到前面, 前面不能到后面的情况, 那么如果存在后面连到前面的一条边, 那么dfs序遍历的时候, 我们就已经将后面的点遍历到了, 就不可能再后面再遍历到后面那个点了.

时间戳 dfn[u]: 遍历到当前节点的编号 low[u]: 不经过走过来的那条边(父边)能走到的最小编号节点

判断桥 对于x, y之间的一条边, 如果low[y]能到x的祖先节点, 那么就不是桥, 若low[y] = y, 即是桥 dfn[x]<low[y](严格小于)

找e-dcc代码思路:

  1. 将所有的桥删掉剩下的都是边连通分量了

  2. 用栈存储搜到的所有点, 当搜到一个点的dfn[x] == low[x], 即这个点上面的一条边就为桥, 将栈中所有点标记即可

找e-dcc代码模板:

2. 点的双连通分量问题

点双连通分量

割点 每一个割点都至少属于两个连通分量 对于一个无向连通图来说, 如果我们删除某个点之后, 该连通图不连通了, 那么我们就称其为该联通图的割点.

定义 一个极大的不包含割点的连通块 tarjan算法求点双连通分量

时间戳 dfn[u]: 意义同边双连通分量 low[u]: 每个顶点在不经过父节点的情况下能到的最小编号顶点, 如果low[u]大于等于dfn[father] (就是不能到已经遍历过的点, 即father的祖先节点), father为割点.

判断(找)割点

9. 欧拉回路与欧拉路径

算法会简单一些hhh

背景:哥尼斯堡七桥问题 / 一笔画问题

欧拉路径 给定一张无向图,若存在一条从节点 S 到节点 T 的路径,恰好不重不漏地经过每条边一次(可以重复经过图中的节点),则称该路径为 S 到 T 的欧拉路。

欧拉回路 若存在一条从节点 S 出发的路径,恰好不重不漏地经过每条边一次(可以重复经过图中的节点),最终回到起点 S,则称该路径为欧拉回路。

定义一下:

  • 入度(din):进入该点的边的数量。

  • 出度(dout):走出该点的边的数量。

  • 度(deg):每个点的 入度 + 出度。

  • 奇结点(奇顶点):连接该结点的边的数量为奇数

  • 偶结点(偶顶点):连接该结点的边的数量为偶数

判断依据

  1. 对于无向图,所有边都是连通的

    • 存在欧拉路径的充分必要条件:度数为奇数的点只能有0 or 2

    • 存在欧拉回路的充分必要条件:度数为奇数的点只能有0

  2. 对于有向图,所有边都是连通的

    • 存在欧拉路径的充分必要条件:

      • 要么所有点的出度等于入度

      • 要么除了两个点之外,所有点的出度等于入度,并且满足剩下的两个点 :一个出度比入度多1(起点)一个入度比出度多1(终点)

    • 存在欧拉回路的充分必要条件:所有点的出度均等于入度

求欧拉回路 注意这里的删边优化

  1. dfs搜索

  2. 在递归搜索结束后加入答案序列

  3. 最后的欧拉回路是答案序列的倒序

输出字典序最小的路径 : 按照字典序来搜

Last updated