图论大杂汇
图论涉及的内容广泛,复杂,综合性较强。我在学习《算法竞赛进阶指南》图论部分后,为了方便日后复习让自己感觉学了东西,写下这篇大杂汇,内容主要源自我自己对《算法竞赛进阶指南》图论部分概括以及学习做题的经验。
一、最短路
Dijkstra基于贪心,所以不适用于有负权边的图,复杂度n^2,可以用STL的二叉堆优化到nlogn。
SPFA在稀疏图上运行效率较高,为O(km)级别,其中k为一个较小的常数,m为边数,但在稠密图或特殊构造的网格图上,可能退化为O(nm)。SPFA可以对应于基于普通队列的BFS,通过不断收敛使所有边最终满足三角不等式,因此可以适用于有负权边的图,但显然不适用于有负环的图(因为不存在最短路)。也可以优化为基于优先队列的BFS,这样就与堆优化的Dijkstra殊途同归了(代码都是一样的),当然也不适用于有负权边的图了。
代码实现:比较简单就不贴了。主要是注意SPFA每个点出队时记得标记为false;堆优化Dijkstra每个点第一次出队时就得到了它到源点的最短路,为了保证效率,可以标记一下,以后就不能再入队了。
BZOJ2200道路与航线(思维题)
这是一道单源最短路问题,只是图中有双向边和单向边,且单向边权可能为负数。
因为有负权边,不能直接上Dijkstra,用SPFA又会超时(话说当初我硬是用SLF优化的SPFA通过了,详情请移步这篇blog)。
题目中双向边一定非负,只有单向边可能为负,且单向边不构成环。可以先只加入双向边,然后划分连通块,各个连通块就由单向边连成DAG(可能不止一个),按照拓扑序依次对每个连通块内部执行堆优化Dijkstra即可。
Floyed三层循环,注意k在最外层。
Floyed可以解决传递闭包:
for(int i=1;i<=n;++i) d[i][i]=1;//注意这是必要的 for(int i=1;i<=m;++i){ 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];
Floyed的过程中还可以顺便求解无向图最小环:
for(int k=1;k<=n;++k){ for(int i=1;i<k;++i) //因为Floyed外层枚举的k表示“经过若干个编号不超过k的节点”,所以这里枚举的i,j上界是小于k,以保证i到j是一条不经过k的路径 for(int j=i+1;j<k;++j) ans=max(ans,d[i][j]+a[i][k]+a[j][k]); for(int i=1;i<=n;++i) for(int j=1;j<=n;++j) d[i][j]=min(d[i][j],d[i][k]+d[k][j]); }
对于有向图的最小环问题,可枚举起点s=1~n,执行堆优化Dijkstra求解单源最短路,s一定是第一个被取出堆的节点,我们扫面s的所有出边,当扩展、更新完成后,令d[s]=+∞,然后继续求解。当s第二次被从堆中取出时,d[s]就是经过s的最小环长度。
POJ3613Cow Relays
给定一张无向图,求从起点S到重点E恰好经过N条边(可重复经过)的最短路。
结论:一般地,若矩阵Am保存任意两点之间恰好经过m条边的的最短路,则:
看起来是不是像一个关于min与加法运算的“矩阵乘法”?显然这个“矩阵乘法”也满足结合律,因此我们可以在一般的矩阵快速幂的基础上,把原来的加法用min替代,把原来的乘法用加法替代。
这个实现很有意思。你要考虑在新的矩阵乘法规则下,如何表示矩阵中的”0“,如何构造单位矩阵,代码:
#include<bits/stdc++.h> using namespace std; #define rg register typedef long long LL; const int N=201; inline int read(){ int x=0,w=0;char ch=0; while(!isdigit(ch)) w|=ch=='-',ch=getchar(); while(isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48),ch=getchar(); return w?-x:x; } int k,m,s,t,n; int vis[1005]; struct Matrix{ int a[N][N]; Matrix operator *(const Matrix &tmp)const{ Matrix res; memset(res.a,0x3f,sizeof res.a); for(rg int i=1;i<=n;++i) for(rg int j=1;j<=n;++j)if(res.a[i][j]) for(rg int k=1;k<=n;++k) res.a[i][j]=min(res.a[i][j],a[i][k]+tmp.a[k][j]); return res; } Matrix operator ^(int k)const{ Matrix res,cur; memset(res.a,0x3f,sizeof res.a); for(rg int i=1;i<=n;++i) res.a[i][i]=0;//单位矩阵res cur=*this; for(;k;k>>=1){ if(k&1) res=res*cur; cur=cur*cur; } return res; } }mat; int main(){ k=read(),m=read(),s=read(),t=read(); memset(mat.a,0x3f,sizeof mat.a);//INF表示”0“ for(rg int i=1;i<=m;++i){ int w=read(),u=read(),v=read(); if(vis[u]) u=vis[u];else u=vis[u]=++n; if(vis[v]) v=vis[v];else v=vis[v]=++n; mat.a[u][v]=mat.a[v][u]=min(mat.a[u][v],w); } cout<<(mat^k).a[vis[s]][vis[t]]<<endl; return 0; }
二、最小生成树(MST)
定理:任意一棵MST一定包含无向图中权值最小的边。
证明:反证法。假设不包含权值最小的边e=(u,v,w),那么我们可以把e加入MST,这样就形成了一个u到v的环,再删去环上任意一条边(当然不能删e)都可以得到一棵新的生成树,且由于w的最小性,这棵树比原MST的权值和还要小,与原MST是MST矛盾。故假设成立。
注意,这种在树上连边构成环,再删去换上一条边得到一棵新树的思想,在有关MST的题目中会经常用到。
由上述得到推论:(有点长我简写了)如果在无向图G中已经选出k<n-1条边构成G的一个生成森林,要再从剩下的边选出n-1-k条边添加到生成森林构成G的生成树且使选出的边权值最小,则该生成树一定包含剩下的边中连接生成森林的两个不连通节点的最小的边。
Kruscal算法和Prim算法均是基于这一推论,不同之处在于Kruscal总是维护无向图的最小生成森林,而Prim总是维护MST的一部分。
Kruscal的实现是先对m条边从小到大排序,然后依次遍历每条边e=(u,v,w),若u,v在同一个集合中则跳过这条边;否则e就是MST的一条边,ans+=w,并把u,v所在集合合并。直到选出n-1条边为止。集合用并查集实现。
Prim就不说了,直接上代码:
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||d[j]<d[x])) x=j; v[x]=true; for(int j=1;j<=n;++j) if(!v[j]) d[j]=min(d[j],a[x][j]);//若x与j无边a[x][j]的值是INF } for(int i=2;i<=n;++i) ans+=d[i]; }
Kruscal时间复杂度为O(mlogm),与边数挂钩,主要适用于稀疏图;Prim时间复杂度为O(n^2),与点数挂钩,主要适用于稠密图。
Prim似乎可以堆优化,但我在一道题里亲测普通PrimAC,而堆优化Prim超时,因此之后就不敢用堆优化Prim了。如果您知道原因欢迎评论区告知。
TYVJ1391/CH6201走廊泼水节。
题意:给定一棵n个节点的树,要求增加若干条边,把这棵树扩充为完全图,并满足图的唯一MST仍然是这棵树,求增加边的权值总和的最小值。保证原有的边权均为非负整数。
对原有的n-1条边执行一个类似于Kruscal的过程,当遍历到一条边e=(u,v,w),设u所在集合大小为Su,v所在集合大小为Sv,为了扩充为完全图,我们需要在两个集合之间添加Su*Sv-1条边,而为了保持e在MST中,添加的边的权值必须大于w,所以我们爱添加权值为w+1的边(题设要求最小),也就是说ans+=(w+1)*(Su*Sv-1)。
POJ2728最优比率生成树:0/1分数规划的扩展,区别在于二分check时要求原图的最大生成树or最小生成树。
(未完)