图论专题笔记
目录
题目完成进度
0/10
一、最短路
单源最短路径
以下皆默认1号节点为起点
$dijkstra$算法
算法流程如下:
1.初始化$dist[1]=0$,其余节点的$dist$值为无穷大
2.找出一个未被标记的,$dist[x]$最小的节点$x$,然后标记节点$x$
3.扫描节点$x$的所有出边$(x,y,z)$,$dist[y]=min(dist[y],dist[x]+z)$
4.重复2、3两步,直到所有节点都被标记
$dijkstra$算法基于贪心思想,只适用于所有边的长度都是非负数的边。
算法复杂度为$O(n^2)$,如果用二叉堆对$dist$数组进行维护,用$O(log\ n)$的时间获取最小值并从堆中删除,用$O(log\ n)$的时间执行一条边的扩展和更新,最终可在$O(mlog\ n)$的时间内实现$dijkstra$算法。
$Bellman-Ford$算法和$SPFA$算法
若对于图中的某一条边$(x,y,z)$,有$dist[y]\le dist[x]+z$,则称该边满足三角形不等式。若所有边都满足三角形不等式,则$dist$数组就是所求最短路。
首先说一下基于迭代思想的$Bellman-Ford$算法,算法流程如下:
1.扫描所有边$(x,y,z)$,若$dist[y]\le dist[x]+z$,则用$dist[x]+z$更新$dist[y]$
2.重复上述步骤,知道没有更新操作发生。
时间复杂度为$O(nm)$
$SPFA$算法实际上是“队列优化的$Bellman-Ford$算法”,算法流程如下:
1.建立一个队列,最初队列中只含有起点1
2.取出队头节点$x$,扫描所有出边$(x,y,z)$,若可以更新则更新,同时如果$y$不在队列中,则把$y$入队
3.重复第2步,直到队列为空
在任意时刻,队列中都保存了待扩展的节点,每次入队相当于完成了一次$dist$数组的更新操作,使其满足三角形不等式。一个节点可能会入队、出队多次。最终,图中节点收敛到全部满足三角形不等式的状态。这个队列避免了$Bellman-Ford$算法中对不需要扩展的节点的冗余扫描,在稀疏图上运行效率较高,为$O(km)$级别,其中$k$是一个小常数。但在稠密图或特殊构造的网格图上,该算法仍可能退化为$O(nm)$。(所以$SPFA$容易被卡)
$Bellman-Ford$算法和$SPFA$算法在有长度为负数的边的图中也能正常工作,只不过时间复杂度会进一步增加。有一个$SLF$优化策略,基于双端队列的思想,在每次更新$dist[y]$之后,把$dist[y]$与当前队头节点(是$x$出队后,队头的那个节点)的$dist$值进行比较。若$dist[y]$更小,则从队头把$y$入队,否则仍从队尾入队。
如果图中不存在长度为负数的边,那么类似于优先队列$bfs$,我们也可以用二叉堆对$SPFA$算法进行优化,堆代替了一般的队列,用于保存待扩展的节点,每次取出“当前距离最小”的节点(堆顶)进行扩展,节点第一次从堆中被取出时,就得到了该点的最短路。与堆优化$dijkstra$算法的流程一致,这两种做法的思想殊途同归,都是非负权图上$O(mlog\ n)$的单源最短路径算法。
例题——
poj3662 Telephone Lines
Luogu P1073 最优贸易
bzoj2200 道路与航线
任意两点间的最短路
我们可以把每个点作为起点,求解$n$次单源最短路问题。不过在任意两点间最短路问题中,图一般比较稠密。这里讲一讲$O(N^3)$的$Floyd$算法。
$Floyd$算法
咕咕咕咕
传递闭包
在交际网络中,给定若干个元素和若干对二元关系,且关系具有传递性。“通过传递性推导出尽量多的元素之间的关系”的问题被称为传递闭包。
建立临接矩阵$d$,$d[i][j]=1$表示$i$与$j$有关系,$d[i][j]=0$表示$i$与$j$没有关系,特别的,$d[i][i]$始终为1。使用$Floyd$算法可以解决传递闭包问题
bool d[N][N]; int n,m; int main(){ cin>>n>>m; for(int i=1;i<=n;i++) d[i][i]=1; for(int i=1;i<=m;i++){ int x,y; cin>>x>>y; d[x][y]=d[y][x]=1; } for(int k=1;k<=n;k++) for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) d[i][j]|=d[i][k]&d[k][j]; return 0; }
例题——
poj1094 Sorting It All Out
poj1734 Sightseeing trip
poj3613 Cow Relays
二、最小生成树
昂讲一下$Kruskal$算法和$Prim$算法
$Kruskal$算法
在任意时刻,$Kruskal$算法从剩余的边中选出一条权值最小的,并且这条边的两个端点不连通,图中节点的连通情况可以用并查集维护。算法流程如下:
1.建立并查集,每个点各自构成一个集合
2.把所有边按权值从小到大排序,依次扫描每条边$(x,y,z)$
3.若$x,y$属于同一集合(连通),则忽略这条边,继续扫描下一条
4.否则,合并$x,y$所在的集合,并把$z$累加到答案中
5.所有边扫描完成后,第4步中处理过的边就构成最小生成树
时间复杂度为$O(mlog\ m)$
$Prim$算法
最初,$Prim$算法仅确定1号节点属于最小生成树。在任意时刻,设已经确定属于最小生成树的节点集合$T$,剩余节点集合为$S$。$Prim$算法找到$z$最小的边$(x,y,z)$满足$x\in T,y\in S$,然后把$y$从集合$S$中删除,加入到集合$T$,并把$z$累加到答案中。
具体来说,可以维护数组$d$:若$x\in S$,则$d[x]$表示节点$x$与集合$T$中的接地那之间权值最小边的权值。若$x\in T$,则$d[x]$表示就等于$x$被加入$T$时选出的最小边的权值。
类比$dijkstra$算法,同一个数组标记节点是否属于$T$。每次从未标记的节点中选出$d$值最小的,将其标记(新加入$T$),同时扫描所有出边,更新另一个端点的$d$值。最后,最小生成树的权值总和就是$\sum_{i=2}^{n}d[i]$
算法的时间复杂度为$O(n^2)$,可以用二叉堆优化到$O(mlog\ n)$,$Prim$算法主要用于稠密图,尤其是完全图的最小生成树的求解。
int a[N][N],d[N],n,m,ans; bool v[N]; void prim(){ memset(d,0x3f,sizeof(d)); memset(v,0,sizeof(v)); d[1]=0; for(int i=1;i<n;i++){ int x=0; for(int j=1;j<=n;j++) if(!v[j]&&(x==0||d[j]<d[x])) x=j; v[x]=1; for(int j=1;j<=n;j++) if(!v[j]) d[j]=min(d[j],a[x][j]); } } int main(){ cin>>n>>m; memset(a,0x3f,sizeof(a)); for(int i=1;i<=n;i++) a[i][i]=0; for(int i=1;i<=m;i++){ int x,y,z; cin>>x>>y>>z; a[y][x]=a[x][y]=min(a[x][y],z); } prim(); for(int i=2;i<=b;i++) ans+=d[i]; cout<<ans<<endl; return 0; }
例题——
TUVJ1391 走廊泼水节
poj1639 Picnic Planning
poj2728 最优比率生成树
黑暗城堡
三、树的直径与最近公共祖先
四、基环树
五、负环与差分约束
六、Tarjan算法与无向图连通性
无向图的割点与桥
给定无向连通图$G=(V,E)$
七、Tarjan算法与有向图连通性
八、二分图的匹配
九、二分图的覆盖与独立集
十、网络流初步