Dijkstra算法中不允许边的权是负权,如果遇到负权,则可以采用Bellman-Ford算法。 Bellman-Ford算法能在更普遍的情况下(存在负权边)解决单源点最短路径问题。对于给定的带权(有向或无向)图 G=(V,E),其源点为s,加权函数 w是 边集 E 的映射。对图G运行Bellman-Ford算法的结果是一个布尔值,表明图中是否存在着一个从源点s可达的负权回路。若不存在这样的回路,算法将给出从源点s到 图G的任意顶点v的最短路径d[v]。
适用条件&范围 1.单源最短路径(从源点s到其它所有顶点v); 2.有向图&无向图(无向图可以看作(u,v),(v,u)同属于边集E的有向图); 3.边权可正可负(如有负权回路输出错误提示); 4.差分约束系统; Bellman-Ford算法描述:
1,.初始化:将除源点外的所有顶点的最短距离估计值 d[v] ←+∞, d[s] ←0;
2.迭代求解:反复对边集E中的每条边进行松弛操作,使得顶点集V中的每个顶点v的最短距离估计值逐步逼近其最短距离;(运行|v|-1次)
3.检验负权回路:判断边集E中的每一条边的两个端点是否收敛。如果存在未收敛的顶点,则算法返回false,表明问题无解;否则算法返回true,并且从源点可达的顶点v的最短距离保存在 d[v]中。
描述性证明:
首先指出,图的任意一条最短路径既不能包含负权回路,也不会包含正权回路,因此它最多包含|v|-1条边。
其次,从源点s可达的所有顶点如果 存在最短路径,则这些最短路径构成一个以s为根的最短路径树。Bellman-Ford算法的迭代松弛操作,实际上就是按顶点距离s的层次,逐层生成这棵最短路径树的过程。
在对每条边进行1遍松弛的时候,生成了从s出发,层次至多为1的那些树枝。也就是说,找到了与s至多有1条边相联的那些顶点的最短路径;对每条边进行第2遍松弛的时候,生成了第2层次的树枝,就是说找到了经过2条边相连的那些顶点的最短路径……。因为最短路径最多只包含|v|-1 条边,所以,只需要循环|v|-1 次。
每实施一次松弛操作,最短路径树上就会有一层顶点达到其最短距离,此后这层顶点的最短距离值就会一直保持不变,不再受后续松弛操作的影响。(但是,每次还要判断松弛,这里浪费了大量的时间,怎么优化?单纯的优化是否可行?)
如果没有负权回路,由于最短路径树的高度最多只能是|v|-1,所以最多经过|v|-1遍松弛操作后,所有从s可达的顶点必将求出最短距离。如果 d[v]仍保持 +∞,则表明从s到v不可达。
如果有负权回路,那么第 |v|-1 遍松弛操作仍然会成功,这时,负权回路上的顶点不会收敛。
出自Bupt_wiki
跳转到: 导航, 搜索
[ 编辑] 一. 知识讲解
1.最小生成树是数据结构中图的一种重要应用,它的要求是从一个含有n个结点的带权无向完全图中选择n-1条边并使这个图仍然连通(也即得到了一棵生成树),同时还要考虑使树的权最小。 为了得到最小生成树,人们设计了很多算法,最著名的有prim算法和kruskal算法。
2.最小生成树的性质:设G=(V,E)是一个连通网络,U是结点集V的一个真子集。若(u,v)是G中一条“一个端点在U中(例如:u∈U),另一个端点不在U中的边(例如:v∈V-U),且(u,v)具有最小权值,则一定存在G的一棵最小生成树包括此边(u,v)。
3.Prim算法:复杂度为O(N^2)
该算法的代码与Dijkstra很像,可以想一想有什么不同~
假设V是图中顶点的集合,E是图中边的集合,TE为最小生成树中的边的集合,则prim算法通过以下步骤可以得到最小生成树:
1) 初始化:U={u 0},TE={f}。此步骤设立一个只有结点u 0的结点集U和一个空的边集TE作为最小生成树的初始形态,在随后的算法执行中,这个形态会不断的发生变化,直到得到最小生成树为止。 2) 在所有u∈U,v∈V-U的边(u,v)∈E中,找一条权最小的边(u 0,v 0),将此边加进集合TE中,并将此边的非U中顶点加入U中。此步骤的功能是在边集E中找一条边,要求这条边满足以下条件:首先边的两个顶点要分别在顶点集合U和V-U中,其次边的权要最小。找到这条边以后,把这条边放到边集TE中,并把这条边上不在U中的那个顶点加入到U中。这一步骤在算法中应执行多次,每执行一次,集合TE和U都将发生变化,分别增加一条边和一个顶点,因此,TE和U是两个动态的集合,这一点在理解算法时要密切注意。 3) 如果U=V,则算法结束;否则重复步骤2。可以把本步骤看成循环终止条件。我们可以算出当U=V时,步骤2共执行了n-1次(设n为图中顶点的数目),TE中也增加了n-1条边,这n-1条边就是需要求出的最小生成树的边。
4.Kruskal:复杂度为O(NlogN + E)
K r u s k a l算法每次选择n- 1条边,所使用的贪婪准则是:从剩下的边中选择一条不会产生环路的具有最小耗费的边加入已选择的边的集合中。注意到所选取的边若产生环路则不可能形成一棵生成树。K r u s k a l算法分e 步,其中e 是网络中边的数目。按耗费递增的顺序来考虑这e 条边,每次考虑一条边。当考虑某条边时,若将其加入到已选边的集合中会出现环路,则将其抛弃,否则,将它选入。
[ 编辑] 二. 例题分析
http://blog.163.com/rennyzh@yeah/blog/static/10273671920088994824119/
3.4 网络流基础
出自Bupt_wiki
跳转到: 导航, 搜索
[ 编辑] ●知识精讲
流网络G=(V,E)是一个有向图,其中每条弧(u,v)∈E均有一个非负容量c(u,v)>=0。如果(u,v)不属于E,则假定c(u,v)=0。同时每条弧(u,v)上定义一非负数f(u,v),称为流量,流量的集合称为网络上的一个流。流网络中有两个特别的顶点:源点s和汇点t。对一个流网络G=(V,E),其容量函数为c,G的可行流f满足下列三个性质:
容量限制:对所有的u,v∈V,要求f(u,v)<=c(u,v)。 反对称性:对所有的u,v∈V,要求f(u,v)=-f(v,u)。 流守恒性:对所有u∈V-{s,t},要求∑f(u,v)=0 (v∈V)。
下面给出残留网络,增广路,割的定义:
在给定的流网络G=(V,E)中,残留容量的定义为:cf(u,v)=c(u,v)-f(u,v)。而由所有属于G的边的残留容量所构成的带权有向图就是G的残留网络。定理:若f是G中的一个流,Gf是由G导出的残留网络,f'是Gf中的一个流,则f+f'是G中一个流, 且其值|f+f'|=|f|+|f'|。
在可行流上任意一顶点之处,凡离开vi的有向弧称为vi的前向弧,凡进入vi的有向弧称为vi的后向弧。如果在残留网络中存在一条从s到t到的路径P满足:
(1) 所有前向弧上 f(u,v)<c(u,v) (2) 所有后向弧上f(u,v)>0
则称P为增广路径。
流网络G(V,E)的割(S,T)将V划分成为S和T=V-S两部分,使得s∈S,t∈T。如果f是一个流,则穿过割(S,T)的净流被定义为f(S,T)=∑f(x,y) (x∈S,y∈T),割(S,T)的容量为c(S,T)。一个网络的最小割就是网络中所有割中具有最小容量的割。
最大流最小割定理: 如果f是具有源s和汇点t的流网络G=(V,E)中的一个流,则下列条件是等价的: 1) f是G中一个最大流。 2) 残留网络Gf不包含增广路径。 3) 对G的某个割(S,T),有|f|=c(S,T)。
Ford-Fulkerson方法求解最大流: Ford-Fulkerson方法是一种迭代的方法。开始时,对所有的u,v∈V有f(u,v)=0,即初始状态时流的值为0。在每次迭代中,可通过寻找一条“增广路径”来增加流值。增广路径可以看成是从源点s到汇点t之间的一条路径,沿该路径可以压入更多的流,从而增加流的值。反复进行这一过程,直至增广路径都被找出来,根据最大流最小割定理,当不包含增广路径时,f是G中的一个最大流。在算法导论中给出的Ford-Fulkerson实现代码如下:
FORD_FULKERSON(G,s,t) for each edge(u,v)∈E[G] do f[u,v] <— 0 f[v,u] <— 0 while there exists a path p from s to t in the residual network Gf do cf(p) <— min{ cf(u,v) : (u,v) is in p } for each edge(u,v) in p do f[u,v] <— f[u,v]+cf(p) f[v,u] <— -f[u,v] //对于反向的边,根据反对称性求
在这个方法中最关键的问题就是寻找增广路径,算法运行的复杂度也主要取决于寻找增广路径的方法,不同的方法也就得到了不同的求解最大流的算法,这也是Ford-Fulkerson被称为一种方法而不是具体算法的原因。
[ 编辑] ●例题分析
EK算法:
Edmonds-Karp(EK)算法实际上就是采用广度优先搜索来实现对增广路径的p的计算。
bool Edmonds_Karp(int src,int des,int n){ int v,i; for(i=0;i<n;i++)visit[i]=false; front=rear=0; //初始化 que[rear++]=src; visit[src]=true; while(front!=rear){ //将源点进队后开始广搜的操作 v=que[front++]; //这里如果采用邻接表的链表实现会有更好的效率,但是要注意(u,v)或(v,u)有任何一条 //边存在于原网络流中,那么邻接表中既要包含(u,v)也要包含(v,u) for(i=0;i<n;i++){ if(!visit[i]&&c[v][i]){ //只有残留容量大于0时才存在边 que[rear++]=i; visit[i]=true; pre[i]=v; if(i==des)return true; //如果已经到达汇点,说明存在增广路径返回true } } } return false; }
3.5 匹配
出自Bupt_wiki
跳转到: 导航, 搜索
[ 编辑] ●知识精讲
1.二分图:
二分图又称作二部图,是图论中的一种特殊模型。 设G=(V,E)是一个无向图,如果顶点V可分割为两个互不相交的子集(A,B),并且图中的每条边(i,j)所关联的两个顶点i和j分别属于这两个不同的顶点集(i in A,j in B),则称图G为一个二分图。简单来说,二分图其所有的顶点分成两个集合A和B,其中A或B中任意两个在同一集合中的点都不相连。
2.二分图的最大匹配:
给定一个二分图G,在G的一个子图M中,M的边集中的任意两条边都不依附于同一个顶点,则称M是一个匹配。选择这样的边数最大的子集称为图的最大匹配问题。 如果一个匹配中,图中的每个顶点都和图中某条边相关联,则称此匹配为完全匹配,也称作完备匹配。
二分图最大匹配可以用网络流或者匈牙利算法求得,这里介绍匈牙利算法。
求最大匹配的一种显而易见的算法是:先找出全部匹配,然后保留匹配数最多的。但是这个算法的复杂度为边数的指数级函数,不可取。匈牙利算法是求解最大匹配的有效算法,该算法用到了增广路的定义(也称增广轨或交错轨)
增广路径的定义:
若P是图G中一条连通两个未匹配顶点的路径,并且属M的边和不属M的边(即已匹配和待匹配的边)在P上交替出现,则称P为相对于M的一条增广路径。
实线为M中的线,3-4-1-5-2-6为一条增广路径。
由增广路径的定义可以推出下述三个结论:
1. P的路径长度必定为奇数,第一条边和最后一条边都不属于M。 2. P经过取反操作(即非M中的边变为M中的边,原来M中的边去掉)可以得到一个更大的匹配M’。 3. M为G的最大匹配当且仅当不存在相对于M的增广路径。
匈牙利算法的实现:
(1)置M为空 (2)找出一条增广路径P,通过取反操作获得更大的匹配M’代替M。 (3)重复(2)操作直到找不出增广路径为止。
程序实现见例题。
用匈牙利算法解决的问题(结论):
1.图的最小点覆盖数 = 图的最大匹配数; 2.图的最大点独立集 = 图顶点数 - 图的最大匹配数; 3.图的最小路径覆盖数 = 原图的顶点数 - 原图拆点后形成的二部图的最大匹配数;
3.二分图的最佳匹配(KM算法):
带权二分图:带权二分图即在原二分图的边上加权。
给定一个带权二分图(x,y),找到一种匹配数最大的方案,记做最大匹配。|x|=|y|=匹配数时,我们称该匹配方案为完备匹配,要求一种完备匹配方案,使得所有匹配边的权和最大,记做最优完备匹配。特殊的,当所有边的权为1时,就是最大完备匹配问题。
KM算法是通过给每个顶点一个标号(叫做顶标)来把求最大权匹配的问题转化为求完备匹配的问题的。设顶点Xi的顶标为A[i],顶点Yi的顶标为B [i],顶点Xi与Yj之间的边权为w[i,j]。在算法执行过程中的任一时刻,对于任一条边(i,j),A[i]+B[j]>=w[i,j]始终 成立。 KM算法的正确性基于以下定理:
若由二分图中所有满足A[i]+B[j]=w[i,j]的边(i,j)构成的子图(称做相等子图)有完备匹配,那么这个完备匹配就是二分图的最大权匹配。
算法理解:
1. 令A[i]为所有与顶点Xi关联的边的最大权,B[j]=0。如果当前的相等子图没有完备匹配,就按下面的方法修改顶标以使扩大相等子图,直到相等子图具有完备匹配为止。
2. 我们求当前相等子图的完备匹配失败了,是因为对于某个X顶点,我们找不到一条从它出发的交错路。这时我们获得了一棵交错树,它的叶子结点全部是X顶点。现在我们把交错树中X顶点的顶标全都减小某个值d,Y顶点的顶标全都增加同一个值d,那么我们会发现:
1)两端都在交错树中的边(i,j),A[ i ]+B[j]的值没有变化。也就是说,它原来属于相等子图,现在仍属于相等子图。 2)两端都不在交错树中的边(i,j),A[ i ]和B[j]都没有变化。也就是说,它原来属于(或不属于)相等子图,现在仍属于(或不属于)相等子图。 3)X端不在交错树中,Y端在交错树中的边(i,j),它的A[ i ]+B[j]的值有所增大。它原来不属于相等子图,现在仍不属于相等子图。 4)X端在交错树中,Y端不在交错树中的边(i,j),它的A[ i ]+B[j]的值有所减小。也就说,它原来不属于相等子图,现在可能进入了相等子图,因而使相等子图得到了扩大。
现在的问题就是求d值了。为了使A[ i ]+B[j]>=w[i,j]始终成立,且至少有一条边进入相等子图,d应该等于: Min{A[ i ]+B[j]-w[i,j] | Xi在交错树中,Yi不在交错树中}。
KM算法流程:
(1)初始化可行顶标的值; (2)用匈牙利算法寻找完备匹配; (3)若未找到完备匹配则修改可行顶标的值; (4)重复(2)(3)直到找到相等子图的完备匹配为止。
程序实现见例题。
4.婚姻匹配问题:
背景: 某社团中有n位女士和m位男士。假定每位女士按照其对每位男士作为配偶的偏爱程度排名次,无并列。也就是说,这种排列是纯顺序的,每位女士对这些男士的排列可以看成一个与自然数对应的序,1,2,3,.....,n.类似的,男士也会对女士进行这种排序。
我们知道,在这个社团里配对成完备婚姻的方式有n!种。假定某种婚姻匹配中存在女士A和B,男士a和b,并且满足如下条件:
i) A和a结婚 ii) B和b结婚 iii)A更偏爱b而非a(名次优先) iv) b更偏爱A而非B
那么,我们认为该完备婚姻是不稳定的。因为在这种假设下,A和b可能会背着别人相伴逃跑,他们都认为,与当前配偶相比每个都更偏爱自己的新伴侣。
如果完备婚姻不是不稳定的,我们则称其为稳定的完备婚姻。对于每一个优先秩评定矩阵,都存在稳定的完备婚姻策略。
延迟认可算法(Gale-Shapley算法,伪代码表示):
while 存在男人m是自由的且还没对每个女人都求过婚 选择这个男人m 令w是m的优先表中还没求过婚的最高排名的女人 if w是自由的 (m,w)变成约会状态 else w当前与m1约会 if w更偏爱m1而不爱m m保持自由 else w更偏爱m而不爱m1 (m,w)变成约会状态 m1变成自由 endif endif endwhile
5.多重匹配问题: 所谓多重匹配,就是Y集合某点可以和X集合多点同时匹配,也就是说Y集合点度不再是一,问X集合的最大匹配是多少。解决方法是匈牙利算法的改进,对于X集合点u要和Y集合点v匹配,如果v点度未满,则直接uv匹配,否则,对v点的每一个父节点增广。
[ 编辑] ●例题分析
1.最大匹配问题: 题意描述:Poj 1325 Machine Schedule(求二分图的最小覆盖数)
有A,B两台机器,和K个需要运行的任务;A,B分别有N,M中模式,每个任务都既可以在A中以x模式,也可以在B中以y模式完成,每台机器可以按照任意顺序执行,但是每台机器每转化一次模式就需重启一次,安排一种顺序,使机器重启次数最少,(开始时A,B都处于模式0)。 题目分析:: 我们需要最少的模式数,将所有的任务覆盖;应该把每个任务看成一条边,把A机器的每个模式看成一个X结点,B机器的每个模式看成一个Y结点,这样就建立了二分图;我们要求的是用最少的点将所有的边覆盖。最小点覆盖数=最大匹配数,因此这道题用匈牙利算法求一次最大匹配即可。源代码:
#include<cstdio> #include<cstring> bool Map[110][110],visit[110]; int link[110],n,m; bool dfs(int t) { for (int i=0; i<m; i++) if (!visit[i] && Map[t][i]){ visit[i] = true; if (link[i]==-1 || dfs(link[i])){ link[i] = t; return true; } } return false; } int main() { int k,a,b,c; while (scanf("%d",&n),n){ memset(Map,false,sizeof(Map)); scanf("%d%d",&m,&k); while (k--){ scanf("%d%d%d",&a,&b,&c); if (b && c) Map[b][c] = true; } memset(link,-1,sizeof(link)); int ans = 0; for (int i=0; i<n; i++){ memset(visit,false,sizeof(visit)); if (dfs(i)) ans++; } printf("%d\n",ans); } }
2.KM算法核心代码(参考):
//km算法:求二分图最佳匹配 n^3复杂度 int dfs(int u)//匈牙利求增广路 { int v; sx[u]=1; for ( v=1; v<=n; v++) if (!sy[v] && lx[u]+ly[v]==map[u][v]){ sy[v]=1; if (match[v]==-1 || dfs(match[v])){ match[v]=u; return 1; } } return 0; } int bestmatch(void)//km求最佳匹配 { int i,j,u; for (i=1; i<=n; i++){//初始化顶标 lx[i]=-1; ly[i]=0; for (j=1; j<=n; j++) if (lx[i]<map[i][j]) lx[i]=map[i][j]; } memset(match, -1, sizeof(match)); for (u=1; u<=n; u++){ while (1){ memset(sx,0,sizeof(sx)); memset(sy,0,sizeof(sy)); if (dfs(u)) break; int dx=Inf;//若找不到增广路,则修改顶标~~ for (i=1; i<=n; i++){ if (sx[i]) for (j=1; j<=n; j++) if(!sy[j] && dx>lx[i]+ly[j]-map[i][j]) dx=lx[i]+ly[j]-map[i][j]; } for (i=1; i<=n; i++){ if (sx[i]) lx[i]-=dx; if (sy[i]) ly[i]+=dx; } } } int sum=0; for (i=1; i<=n; i++) sum+=map[match[i]][i]; return sum; }
3.6 强连通分量
出自Bupt_wiki
跳转到: 导航, 搜索
[ 编辑] ●知识精讲
有向图强连通分量: 在有向图G中,如果两个顶点vi,vj间(vi != vj)有一条从vi到vj的有向路径,同时还有一条从vj到vi的有向路径,则称两个顶点强连通。如果有向图G的每两个顶点都强连通,称G是一个强连通图。非强连通图有向图的极大强连通子图,称为强连通分量。
求强连通分量的算法主要有三种:
1. Kosaraju算法(双DFS)。 2. Tarjan算法。 3. Gabow算法。
Kosaraju算法:
1:对原图G进行深度优先遍历,记录每个节点的离开时间。 2:选择具有最晚离开时间的顶点,对反图GT进行遍历,删除能够遍历到的顶点,这些顶点构成一个强连通分量。 3:如果还有顶点没有删除,继续2,否则算法结束。
简要说明:假设以上算法从u访问到了v,那么说明反向图有一条从u到v的边,也就说明了原图中有一条从v到u的边,又因为u的标号是大于v的,那么,u一定在v之前访问到(否则v的标号将大于u),并且是从u访问到v了的(v到u也有一条路径,否则就会从v访问到u)。
如果我们把求出来的每个强连通分量收缩成一个点,并且用求出每个强连通分量的顺序来标记收缩后的节点,那么这个顺序其 实就是强连通分量收缩成点后形成的有向无环图的拓扑序列。
Tarjan算法:
Tarjan算法是基于对图深度优先搜索的算法,每个强连通分量为搜索树中的一棵子树。搜索时,把当前搜索树中未处理的节点加入一个堆栈,回溯时可以判断栈顶到栈中的节点是否为一个强连通分量。
定义DFN(u)为节点u搜索的次序编号(时间戳),Low(u)为u或u的子树能够追溯到的最早的栈中节点的次序号。由定义可以得出:
Low(u)=Min{ DFN(u), Low(v),(u,v)为树枝边,u为v的父节点 DFN(v),(u,v)为指向栈中节点的后向边(非横叉边)}, 当DFN(u)=Low(u)时,以u为根的搜索子树上所有节点是一个强连通分量。
tarjan(u) { DFN[u]=Low[u]=++Index // 为节点u设定次序编号和Low初值 Stack.push(u) // 将节点u压入栈中 for each (u, v) in E // 枚举每一条边 if (v is not visted) // 如果节点v未被访问过 tarjan(v) // 继续向下找 Low[u] = min(Low[u], Low[v]) else if (v in S) // 如果节点v还在栈内 Low[u] = min(Low[u], DFN[v]) if (DFN[u] == Low[u]) // 如果节点u是强连通分量的根 repeat v = S.pop // 将v退栈,为该强连通分量中一个顶点 print v until (u== v) }
注:Kosaraju算法和 Tarjan算法的时间复杂度都为O(M+N),但由于Tarjan算法仅需要一次DFS,在实际应用中比Kosaraju算法要高效一些,学好Tarjan对学习双连通分量等算法都有很大的帮助。
[ 编辑] ●例题分析
题目描述:PKU 2186.Popular Cows 有N(N<=10000)头牛,每头牛都想成为最受欢迎的牛,给出M(M<=50000)个关系,如(1,2)代表1欢迎2,关系可以传递,但 1欢迎2不代表2欢迎1。给出N,M和M个欢迎关系,求被所有牛都欢迎的牛的数量。
题目分析::首先求出连通分量的个数,然后依次求各个连通分量的出度,如果仅有一个连通分量出度为0则这个连通分量内的点的个数就是答案;如果有多于一个的连通分量的出度为0,则说明此有向图肯定不连通。因此直接输出0。
核心代码(双DFS过程):
void dfsa(int u) { visited[u] = true; int len = adj[u].size(); for (int i=0; i<len; i++){ if (!visited[adj[u][i]]) dfsa(adj[u][i]); } num[++cnt] = u; } void dfsb(int u) { visited[u] = true; scc[u] = cnt; int len = nadj[u].size(); for (int i=0; i<len; i++){ if (!visited[nadj[u][i]]) dfsb(nadj[u][i]); } } void Kosaraju() { memset(visited,false,sizeof(visited)); cnt = 0; for (int i=1; i<=n; i++) if (!visited[i]) dfsa(i); memset(visited,false,sizeof(visited)); cnt = 0; for (int i=n; i>=1; i--){ if (!visited[num[i]]){ cnt++; dfsb(num[i]); } } }