第五弹图论入门的主要内容有以下几部分:图的搜索、连通性、最短路径、生成树、二分图匹配、最大流等内容。
一、图的搜索
有两种常用的方法可用来搜索图:即深度优先搜索和广度优先搜索。它们最终都会到达所有连通的顶点。深度优先搜索通过栈来实现,而广度优先搜索通过队列来实现。
广度优先搜索:
Part1:
常常我们有这样一个问题,从一个起点开始要到一个终点,我们要找寻一条最短的路径,从图2-1举例,如果我们要求V0到V6的一条最短路(假设走一个节点按一步来算)【注意:此处你可以选择不看这段文字直接看图3-1】,我们明显看出这条路径就是V0->V2->V6,而不是V0->V3->V5->V6。先想想你自己刚刚是怎么找到这条路径的:首先看跟V0直接连接的节点V1、V2、V3,发现没有V6,进而再看刚刚V1、V2、V3的直接连接节点分别是:{V0、V4}、{V0、V1、V6}、{V0、V1、V5}(这里画删除线的意思是那些顶点在我们刚刚的搜索过程中已经找过了,我们不需要重新回头再看他们了)。这时候我们从V2的连通节点集中找到了V6,那说明我们找到了这条V0到V6的最短路径:V0->V2->V6,虽然你再进一步搜索V5的连接节点集合后会找到另一条路径V0->V3->V5->V6,但显然他不是最短路径。
你会看到这里有点像辐射形状的搜索方式,从一个节点,向其旁边节点传递病毒,就这样一层一层的传递辐射下去,知道目标节点被辐射中了,此时就已经找到了从起点到终点的路径。
我们采用示例图来说明这个过程,在搜索的过程中,初始所有节点是白色(代表了所有点都还没开始搜索),把起点V0标志成灰色(表示即将辐射V0),下一步搜索的时候,我们把所有的灰色节点访问一次,然后将其变成黑色(表示已经被辐射过了),进而再将他们所能到达的节点标志成灰色(因为那些节点是下一步搜索的目标点了),但是这里有个判断,就像刚刚的例子,当访问到V1节点的时候,它的下一个节点应该是V0和V4,但是V0已经在前面被染成黑色了,所以不会将它染灰色。这样持续下去,直到目标节点V6被染灰色,说明了下一步就到终点了,没必要再搜索(染色)其他节点了,此时可以结束搜索了,整个搜索就结束了。然后根据搜索过程,反过来把最短路径找出来,图3-1中把最终路径上的节点标志成绿色。
整个过程的实例图如图3-1所示。
初始全部都是白色(未访问)
即将搜索起点V0(灰色)
已搜索V0,即将搜索V1、V2、V3
……终点V6被染灰色,终止
找到最短路径
图3-1 寻找V0到V6的过程
图3-2 广度优先搜索的流程图
part2:
宽度优先搜索,顾名思义,就是将一棵树一层一层往下搜。算法首先搜索和s距离为k的所有顶点,然后再去搜索和S距离为k+l的其他顶点。BFS是一种完备策略,即只要问题有解,它就一定可以找到解。并且,广度优先搜索找到的解,还一定是路径最短的解。但是它盲目性较大,尤其是当目标节点距初始节点较远时,将产生许多无用的节点,因此其搜索效率较低。需要保存所有扩展出的状态,占用的空间大。
一般需求最优解的时候用广搜。
搜索过程是:从初始节点S0开始逐层向下扩展,在第n层节点还没有全部搜索完之前,不进入第n+1层节点的搜索。假设有两个表:Open表存放待处理节点,Closed表存放处理完节点Open表中的节点总是按进入的先后排序,先进入Open表的节点排在前面,后进入Open表的节点排在后面。
广度优先搜索算法如下:(用 QUEUE)
(1) 把初始节点S0放入Open表中;
(2) 如果Open表为空,则问题无解,失败退出;
(3) 把Open表的第一个节点取出放入Closed表,并记该节点为n;
(4) 考察节点n是否为目标节点。若是,则得到问题的解,成功退出;
(5) 若节点n不可扩展,则转第(2)步;
(6) 扩展节点n,将其子节点放入Open表的尾部,并为每一个子节点设置指向父节点的指针,然后转第(2)步。
代码框架:
BFS()
{
初始化队列
while(队列不为空且未找到目标节点)
{
取队首节点扩展,并将扩展出的节点放入队尾;
必要时要记住每个节点的父节点;
}
}
part3:
对于无向连通图,广度优先搜索是从图的某个顶点v0出发,在访问v0之后,依次搜索访问v0的各个未被访问过的邻接点w1,w2,…。然后顺序搜索访问w1的各未被访问过的邻接点,w2的各未被访问过的邻接点,…。即从v0开始,由近至远,按层次依次访问与v0有路径相通且路径长度分别为1,2,…的顶点,直至连通图中所有顶点都被访问一次。
广度优先搜索的顺序不是唯一的,具体描述如下:
设图G的初态是所有顶点均未访问,在G 中任选一顶点i作为初始点,则广度优先搜索的基本思想是:
(1)从图中的某个顶点V出发,访问之;并将其访问标志置为已被访问,即visited[i]=1;
(2)依次访问顶点V的各个未被访问过的邻接 点,将V的全部邻接点都访问到;
(3)分别从这些邻接点出发,依次访问它们的未被访问过的邻接点,并使“先被访问的顶 点的邻接点”先于“后被访问的顶点的邻接点”被访问,直到图中所有已被访问过的顶 点的邻接点都被访问到。
依此类推,直到图中所有顶点都被访问完为止 。
广度优先搜索在搜索访问一层时,需要记住已被访问的顶点,以便在访问下层顶点时,从已被访问的顶点出发搜索访问其邻接点。所以在广度优先搜索中需要设置一个队列Queue,使已被访问的顶点顺序由队尾进入队列。在搜索访问下层顶点时,先从队首取出一个已被访问的上层顶点,再从该顶点出发搜索访问它的各个邻接点。
如下图(c)中为对图(a)的遍历:
按照广度优先算法,其遍历顺序为:
广度优先搜索算法的C语言描述:
#include <cstdio>
#define MAX_VERTEX_NUM 20
#include <conio.h>
#include <cstdlib>
typedef char VertexType;
//我们依然用邻接表来作图的存储结构
typedef struct ArcNode
{
int adjvex;
struct ArcNode *nextarc;
int info;
}ArcNode; //表结点类型
typedef struct VNode
{
VertexType data;
ArcNode *firstarc;
}VNode,AdjList[MAX_VERTEX_NUM]; //头结点
typedef struct
{
AdjList vertices; //邻接表
int vexnum,arcnum;
}ALGraph;
typedef struct Qnode
{ //链队结点的类型
int data;
struct Qnode *next;
}Qnode,*QueuePtr;
typedef struct
{ //链队指针类型
QueuePtr front;
QueuePtr rear;
}LinkQueue;
int visited[MAX_VERTEX_NUM];
int LocateVex(ALGraph G,char u)
{
int i;
for (i=0;i<G.vexnum;i++)
{ if(u==G.vertices[i].data) return i; }
if (i==G.vexnum) {printf("Error u!\n");exit(1);}
return 0;
}
void InitQueue(LinkQueue &Q)
{
Q.front=Q.rear=(QueuePtr)malloc(sizeof(Qnode));
if(!Q.front) exit(1); //存储分配失败
Q.front->next=NULL;
}
void EnQueue(LinkQueue &Q,int e)
{
QueuePtr p;
p=(QueuePtr)malloc(sizeof(Qnode));
p->data=e;
p->next=NULL;
Q.rear->next=p;
Q.rear=p;
}
int QueueEmpty(LinkQueue &Q)
{
return(Q.front==Q.rear? 1:0);
}
void DeQueue(LinkQueue &Q,int &e)
{
QueuePtr p;
if(QueueEmpty(Q))
{
printf("\n Queue is free!");
exit(1);
}//if
p=Q.front->next;
e=p->data;
Q.front->next=p->next;
if(Q.front->next==NULL) Q.rear=Q.front;
free(p);
}
void CreateALGraph_adjlist(ALGraph &G)
{
int i,j,k,w;
char v1,v2,enter;
ArcNode *p;
printf("Input vexnum & arcnum:\n");
scanf("%d",&G.vexnum);
scanf("%d",&G.arcnum);
printf("Input Vertices(以回车隔开各个数据):\n");
for (i=0;i<G.vexnum;i++)
{ scanf("%c%c",&enter,&G.vertices[i].data);//注意点,解说
G.vertices[i].firstarc=NULL;
}//for
printf("Input Arcs(v1,v2,w)以回车分开各个数据:\n");
for (k=0;k<G.arcnum;k++)
{
scanf("%c%c",&enter,&v1);
scanf("%c%c",&enter,&v2);
//scanf("%d",&w);
i=LocateVex(G,v1);
j=LocateVex(G,v2);
p=(ArcNode*)malloc(sizeof(ArcNode));
p->adjvex=j;
//p->info = w;
p->nextarc=G.vertices[i].firstarc; //前插法,即每次都插入到头结点的后面
G.vertices[i].firstarc=p;
printf("Next\n");
}//for
return;
}//CreateALGraph_adjlist
void BFSTraverse(ALGraph &G)
{
LinkQueue Q;
for(int v=0;v<G.vexnum;++v) visited[v]=false;
InitQueue(Q);
for(int v=0;v<G.vexnum;++v)
if(!visited[v])
{
EnQueue(Q,v);
while(!QueueEmpty(Q))
{
int u;
DeQueue(Q,u);
visited[u]=true;
printf("->%c",G.vertices[u].data);//visit一下
for(ArcNode *w=G.vertices[u].firstarc;w;w=w->nextarc)
if(!visited[w->adjvex]) EnQueue(Q,w->adjvex);
}//while
}//if
}//BFSTraverse
int main()
{
ALGraph G;
CreateALGraph_adjlist(G);
BFSTraverse(G);
}
深度优先搜索:
part1:
深度优先搜索是一种用来遍历或者搜索树(TREE)或图(GRAPH)结构的算法。搜索开始于某个根节点(从图中选取某个节点),然后在开始回溯前尽可能远地探索到这一支的终点。它的思想:假设初始状态是图中所有顶点均未被访问,首先访问图中某一起始顶点v,然后由v出发,访问与v邻接且未被访问的任一顶点w1,再访问与w1邻接且未被访问的任一顶点w2,……重复上述过程。当不能再继续向下访问时,依次退回到最近被访问的顶点。 若此时尚有其他顶点未被访问到,则另选一个未被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。显然,深度优先搜索是一个递归的过程。
对于DFS的实际应用程序来说,DFS常常因为要搜索的图的某一条搜索路径太长(甚至是无限的)而陷入性能瓶颈,所以我们经常制定DFS只能搜索到某个深度,
举例:
给出如图3-1所示的图,求图中的V0出发,是否存在一条路径长度为4的搜索路径。
图3-1
显然,我们知道是有这样一个解的:V0->V3->V5->V6。下面我们来看这个的处理过程:
part2:
深度优先搜索遍历类似于树的先序遍历。假定给定图G的初态是所有顶点均未被访问过,在G中任选一个顶点i作为遍历的初始点,则深度优先搜索递归调用包含以下操作:
(1)访问搜索到的未被访问的邻接点;
(2)将此顶点的visited数组元素值置1;
(3)搜索该顶点的未被访问的邻接点,若该邻接点存在,则从此邻接点开始进行同样的访问和搜索。
深度优先搜索DFS可描述为:
(1)访问v0顶点;
(2)置 visited[v0]=1;
(3)搜索v0未被访问的邻接点w,若存在邻接点w,则DFS(w)。
遍历过程:
DFS 在访问图中某一起始顶点 v 后,由 v 出发,访问它的任一邻接顶点w1;再从 w1 出发,访问与 w1邻 接但还没有访问过的顶点 w2;然后再从 w2 出发,进行类似的访问,… 如此进行下去,直至到达所有的邻接顶点都被访问过的顶点 u 为止。接着,退回一步,退到前一次刚访问过的顶点,看是否还有其它没有被访问的邻接顶点。如果有,则访问此顶点,之后再从此顶点出发,进行与前述类似的访问;如果没有,就再退回一步进行搜索。重复上述过程,直到连通图中所有顶点都被访问过为止。如下图所示:
深度优先算法C语言描述
深度优先算法C语言实现:
#include <cstdio>
#define MAX_VERTEX_NUM 10
#include <conio.h>
#include <cstdlib>
typedef char VertexType;
typedef struct ArcNode
{
int adjvex;
struct ArcNode *nextarc;
int info;
}ArcNode; //表结点类型
typedef struct VNode
{
VertexType data;
ArcNode *firstarc;
}VNode,AdjList[MAX_VERTEX_NUM]; //头结点
typedef struct
{
AdjList vertices; //邻接表
int vexnum,arcnum;
}ALGraph;
int visited[MAX_VERTEX_NUM];
int LocateVex(ALGraph G,char u)
{
int i;
for (i=0;i<G.vexnum;i++)
{ if(u==G.vertices[i].data) return i; }
if (i==G.vexnum) {printf("Error u!\n");exit(1);}
return 0;
}
void CreateALGraph_adjlist(ALGraph &G)
{
int i,j,k,w;
char v1,v2,enter;
ArcNode *p;
printf("Input vexnum & arcnum:\n");
scanf("%d",&G.vexnum);
scanf("%d",&G.arcnum);
printf("Input Vertices:\n");
for (i=0;i<G.vexnum;i++)
{
scanf("%c%c",&enter,&G.vertices[i].data);//注意点,解说
G.vertices[i].firstarc=NULL;
}//for
printf("Input Arcs(v1,v2,w)以回车分开各个数据:\n");
for (k=0;k<G.arcnum;k++)
{
scanf("%c%c",&enter,&v1);
scanf("%c%c",&enter,&v2);
scanf("%d",&w);
i=LocateVex(G,v1);
j=LocateVex(G,v2);
p=(ArcNode*)malloc(sizeof(ArcNode));
p->adjvex=j;
p->info = w;
p->nextarc=G.vertices[i].firstarc;
G.vertices[i].firstarc=p;
}//for
return;
}//CreateALGraph_adjlist
void DFS(ALGraph &G, int v)
{
ArcNode *p;
printf("%c",G.vertices[v].data);
visited[v]=1;
p=G.vertices[v].firstarc;
while (p)
{
if (!visited[p->adjvex])
DFS(G,p->adjvex);
p=p->nextarc;
}
} //从第v个顶点出发DFS
void DFSTraverse(ALGraph &G)
{
for (int v=0;v<G.vexnum;++v)
visited[v]=0;
for (int v=0;v<G.vexnum;++v)
if (!visited[v]) DFS(G,v);
}//DFSTraverse
int main()
{
ALGraph G;
CreateALGraph_adjlist(G);
DFSTraverse(G);
}
二、图的连通性
一,图的定义
什么是图呢?
图是一种复杂的非线性结构。
在线性结构中,数据元素之间满足唯一的线性关系,每个数据元素(除第一个和最后一个外)只有一个直接前趋和一个直接后继;
在树形结构中,数据元素之间有着明显的层次关系,并且每个数据元素只与上一层中的一个元素(双亲节点)及下一层的多个元素(孩子节点)相关;
而在图形结构中,节点之间的关系是任意的,图中任意两个数据元素之间都有可能相关。
图G由两个集合V(顶点Vertex)和E(边Edge)组成,定义为G=(V,E)
二,图相关的概念和术语
1,无向图和有向图
对于一个图,若每条边都是没有方向的,则称该图为无向图。图示如下:
因此,(Vi,Vj)和(Vj,Vi)表示的是同一条边。注意,无向图是用小括号,而下面介绍的有向图是用尖括号。
无向图的顶点集和边集分别表示为:
V(G)={V1,V2,V3,V4,V5}
E(G)={(V1,V2),(V1,V4),(V2,V3),(V2,V5),(V3,V4),(V3,V5),(V4,V5)}
对于一个图G,若每条边都是有方向的,则称该图为有向图。图示如下。
因此,<Vi,Vj>和<Vj,Vi>是两条不同的有向边。注意,有向边又称为弧。
有向图的顶点集和边集分别表示为:
V(G)={V1,V2,V3}
E(G)={<V1,V2>,<V2,V3>,<V3,V1>,<V1,V3>}
2,无向完全图和有向完全图
我们将具有n(n-1)/2条边的无向图称为无向完全图。同理,将具有n(n-1)条边的有向图称为有向完全图。
3,顶点的度
对于无向图,顶点的度表示以该顶点作为一个端点的边的数目。比如,图(a)无向图中顶点V3的度D(V3)=3
对于有向图,顶点的度分为入度和出度。入度表示以该顶点为终点的入边数目,出度是以该顶点为起点的出边数目,该顶点的度等于其入度和出度之和。比如,顶点V1的入度ID(V1)=1,出度OD(V1)=2,所以D(V1)=ID(V1)+OD(V1)=1+2=3
记住,不管是无向图还是有向图,顶点数n,边数e和顶点的度数有如下关系:
因此,就拿有向图(b)来举例,由公式可以得到图G的边数e=(D(V1)+D(V2)+D(V3))/2=(3+2+3)/2=4
4,子图
故名思义,这个就不解释了。
5,路径,路径长度和回路
路径,比如在无向图G中,存在一个顶点序列Vp,Vi1,Vi2,Vi3…,Vim,Vq,使得(Vp,Vi1),(Vi1,Vi2),…,(Vim,Vq)均属于边集E(G),则称顶点Vp到Vq存在一条路径。
路径长度,是指一条路径上经过的边的数量。
回路,指一条路径的起点和终点为同一个顶点。
6,连通图(无向图)
连通图是指图G中任意两个顶点Vi和Vj都连通,则称为连通图。比如图(b)就是连通图。下面是一个非连通图的例子。
上图中,因为V5和V6是单独的,所以是非连通图。
7,强连通图(有向图)
强连通图是对于有向图而言的,与无向图的连通图类似。
8,网
带”权值”的连通图称为网。如图所示。
三,图的创建和遍历
1,图的两种存储结构
1) 邻接矩阵,原理就是用两个数组,一个数组保存顶点集,一个数组保存边集。下面的算法实现里边我们也是采用这种存储结构。如下图所示:
2) 邻接表,邻接表是图的一种链式存储结构。这种存储结构类似于树的孩子链表。对于图G中每个顶点Vi,把所有邻接于Vi的顶点Vj链成一个单链表,这个单链表称为顶点Vi的邻接表。
2,图的两种遍历方法
1) 深度优先搜索遍历 2)广度优先搜索遍历
三、最小生成树和最短路径
1,最小生成树
什么是最小生成树呢?在弄清什么是最小生成树之前,我们需要弄清什么是生成树?
用一句语简单概括生成树就是:生成树是将图中所有顶点以最少的边连通的子图。
比如图(g)可以同时得到两个生成树图(h)和图(i)
知道了什么是生成树之后,我们就很容易理解什么是最小生成树了。所谓最小生成树,用一句话总结就是:权值和最小的生成树就是最小生成树。
比如上图中的两个生成树,生成树1和生成树2,生成树1的权值和为:12,生成树2的权值为:14,我们可以证明图(h)生成树1就是图(g)的最小生成树。
那么如何构造最小生成树呢?可以使用普里姆算法。
2,最短路径
求最短路径也就是求最短路径长度。下面是一个带权值的有向图,表格中分别列出了顶点V1其它各顶点的最短路径长度。
从图中可以看出,顶点V1到V4的路径有3条(V1,V2,V4),(V1,V4),(V1,V3,V2,V4),其路径长度分别为15,20和10,因此,V1到V4的最短路径为(V1,V3,V2,V4)。
那么如何求带权有向图的最短路径长度呢?可以使用迪杰斯特拉(Dijkstra)算法。
最小生成树:克鲁斯卡尔算法、普里姆算法。---->传送门
最短路径:Dijkstra算法、Bellman-Ford算法和Floyd算法---->传送门
四、二分图匹配
给定一个二分图G,在G的一个子图M中,M的边集{E}中的任意两条边都不依附于同一个顶点,则称M是一个匹配。
二分图:简单来说,如果图中点可以被分为两组,并且使得所有边都跨越组的边界,则这就是一个二分图。准确地说:把一个图的顶点划分为两个不相交集 UU 和VV ,使得每一条边都分别连接UU、VV中的顶点。如果存在这样的划分,则此图为一个二分图。二分图的一个等价定义是:不含有「含奇数条边的环」的图。图 1 是一个二分图。为了清晰,我们以后都把它画成图 2 的形式。
匹配:在图论中,一个「匹配」(matching)是一个边的集合,其中任意两条边都没有公共顶点。例如,图 3、图 4 中红色的边就是图 2 的匹配。
我们定义匹配点、匹配边、未匹配点、非匹配边,它们的含义非常显然。例如图 3 中 1、4、5、7 为匹配点,其他顶点为未匹配点;1-5、4-7为匹配边,其他边为非匹配边。
最大匹配:一个图所有匹配中,所含匹配边数最多的匹配,称为这个图的最大匹配。图 4 是一个最大匹配,它包含 4 条匹配边。
完美匹配:如果一个图的某个匹配中,所有的顶点都是匹配点,那么它就是一个完美匹配。图 4 是一个完美匹配。显然,完美匹配一定是最大匹配(完美匹配的任何一个点都已经匹配,添加一条新的匹配边一定会与已有的匹配边冲突)。但并非每个图都存在完美匹配。
举例来说:如下图所示,如果在某一对男孩和女孩之间存在相连的边,就意味着他们彼此喜欢。是否可能让所有男孩和女孩两两配对,使得每对儿都互相喜欢呢?图论中,这就是完美匹配问题。如果换一个说法:最多有多少互相喜欢的男孩/女孩可以配对儿?这就是最大匹配问题。
基本概念讲完了。求解最大匹配问题的一个算法是匈牙利算法,下面讲的概念都为这个算法服务。
交替路:从一个未匹配点出发,依次经过非匹配边、匹配边、非匹配边…形成的路径叫交替路。
增广路:从一个未匹配点出发,走交替路,如果途径另一个未匹配点(出发的点不算),则这条交替路称为增广路(agumenting path)。例如,图 5 中的一条增广路如图 6 所示(图中的匹配点均用红色标出):
增广路有一个重要特点:非匹配边比匹配边多一条。因此,研究增广路的意义是改进匹配。只要把增广路中的匹配边和非匹配边的身份交换即可。由于中间的匹配节点不存在其他相连的匹配边,所以这样做不会破坏匹配的性质。交换后,图中的匹配边数目比原来多了 1 条。
我们可以通过不停地找增广路来增加匹配中的匹配边和匹配点。找不到增广路时,达到最大匹配(这是增广路定理)。匈牙利算法正是这么做的。在给出匈牙利算法 DFS 和 BFS 版本的代码之前,先讲一下匈牙利树。
匈牙利树一般由 BFS 构造(类似于 BFS 树)。从一个未匹配点出发运行 BFS(唯一的限制是,必须走交替路),直到不能再扩展为止。例如,由图 7,可以得到如图 8 的一棵 BFS 树:
这棵树存在一个叶子节点为非匹配点(7 号),但是匈牙利树要求所有叶子节点均为匹配点,因此这不是一棵匈牙利树。如果原图中根本不含 7 号节点,那么从 2 号节点出发就会得到一棵匈牙利树。这种情况如图 9 所示(顺便说一句,图 8 中根节点 2 到非匹配叶子节点 7 显然是一条增广路,沿这条增广路扩充后将得到一个完美匹配)。
下面给出匈牙利算法的 DFS 和 BFS 版本的代码:
// 顶点、边的编号均从 0 开始
// 邻接表储存
struct Edge
{
int from;
int to;
int weight;
Edge(int f, int t, int w):from(f), to(t), weight(w) {}
};
vector<int> G[__maxNodes]; /* G[i] 存储顶点 i 出发的边的编号 */
vector<Edge> edges;
typedef vector<int>::iterator iterator_t;
int num_nodes;
int num_left;
int num_right;
int num_edges;
int matching[__maxNodes]; /* 存储求解结果 */
int check[__maxNodes];
bool dfs(int u)
{
for (iterator_t i = G[u].begin(); i != G[u].end(); ++i) { // 对 u 的每个邻接点
int v = edges[*i].to;
if (!check[v]) { // 要求不在交替路中
check[v] = true; // 放入交替路
if (matching[v] == -1 || dfs(matching[v])) {
// 如果是未盖点,说明交替路为增广路,则交换路径,并返回成功
matching[v] = u;
matching[u] = v;
return true;
}
}
}
return false; // 不存在增广路,返回失败
}
int hungarian()
{
int ans = 0;
memset(matching, -1, sizeof(matching));
for (int u=0; u < num_left; ++u) {
if (matching[u] == -1) {
memset(check, 0, sizeof(check));
if (dfs(u))
++ans;
}
}
return ans;
}
queue<int> Q;
int prev[__maxNodes];
int Hungarian()
{
int ans = 0;
memset(matching, -1, sizeof(matching));
memset(check, -1, sizeof(check));
for (int i=0; i<num_left; ++i) {
if (matching[i] == -1) {
while (!Q.empty()) Q.pop();
Q.push(i);
prev[i] = -1; // 设 i 为路径起点
bool flag = false; // 尚未找到增广路
while (!Q.empty() && !flag) {
int u = Q.front();
for (iterator_t ix = G[u].begin(); ix != G[u].end() && !flag; ++ix) {
int v = edges[*ix].to;
if (check[v] != i) {
check[v] = i;
Q.push(matching[v]);
if (matching[v] >= 0) { // 此点为匹配点
prev[matching[v]] = u;
} else { // 找到未匹配点,交替路变为增广路
flag = true;
int d=u, e=v;
while (d != -1) {
int t = matching[d];
matching[d] = e;
matching[e] = d;
d = prev[d];
e = t;
}
}
}
}
Q.pop();
}
if (matching[i] != -1) ++ans;
}
}
return ans;
}
匈牙利算法的要点如下
-
从左边第 1 个顶点开始,挑选未匹配点进行搜索,寻找增广路。
- 如果经过一个未匹配点,说明寻找成功。更新路径信息,匹配边数 +1,停止搜索。
- 如果一直没有找到增广路,则不再从这个点开始搜索。事实上,此时搜索后会形成一棵匈牙利树。我们可以永久性地把它从图中删去,而不影响结果。
由于找到增广路之后需要沿着路径更新匹配,所以我们需要一个结构来记录路径上的点。DFS 版本通过函数调用隐式地使用一个栈,而 BFS 版本使用
prev
数组。
性能比较
两个版本的时间复杂度均为O(V⋅E)。DFS 的优点是思路清晰、代码量少,但是性能不如 BFS。我测试了两种算法的性能。对于稀疏图,BFS 版本明显快于 DFS 版本;而对于稠密图两者则不相上下。在完全随机数据 9000 个顶点 4,0000 条边时前者领先后者大约 97.6%,9000 个顶点 100,0000 条边时前者领先后者 8.6%, 而达到 500,0000 条边时 BFS 仅领先 0.85%。
补充定义和定理:
最大匹配数:最大匹配的匹配边的数目
最小点覆盖数:选取最少的点,使任意一条边至少有一个端点被选择
最大独立数:选取最多的点,使任意所选两点均不相连
最小路径覆盖数:对于一个 DAG(有向无环图),选取最少条路径,使得每个顶点属于且仅属于一条路径。路径长可以为 0(即单个点)。
定理1:最大匹配数 = 最小点覆盖数(这是 Konig 定理)
定理2:最大匹配数 = 最大独立数
定理3:最小路径覆盖数 = 顶点数 - 最大匹配数
匈牙利算法详解---->传送门
五、最大流
最大流问题(maximum flow problem)一种组 合最优化问题.网络流理论研究的一个基本问题是 求网络中一个可行流f,使其流量V<f)达到最大, 这种流f称为最大流(参见“网络流”),这个问题称 为(网络)最大流问题.最大流问题是一个特殊的线 性规划问题,一般用标号法寻求最大流比用求线性 规划问题的一般方法要方便得多。
它其实就是不停的找增广路直到找不到为止。此时通过的所有流量就是最大流量。推荐一篇文章---> Ford-Fulkerson 最大流算法
一、网络流
G=<V,E>为有向图
1. 容量为非负: 如果有向边(u,v) 存在,c(u,v)≥0; 如果有向边(u,v)不存在,c(u,v)=0
2. 网络流:
(1)容量限制:f(u,v)≤c(u,v) ==> 单向 流速受限
(2)反对称性:f(u,v)=-f(v,u) ==> 在管道中向不同方向看,水流一面迎面而来 ,一面向前推我
(3)流守恒性:f(V-s-t,V)=0 ==> A. 中间顶点不存储流 (流进=流出 )
B. 进入中间顶点的正网络流=离开该点的正网络流
注意:
流守恒性也可以写作f(V,V-s-t)=0,即流入一个顶点的总流为0;
在f(V-s-t,V)或f(V,V-s-t)的中,其中每一元素可以为正可以为负,不要以为不能为负(这是“残留网络”中的限制,网络流不受限)
引理26.1: G=<V,E>是流网络,f是G的一个流。
定义f(X,Y)=∑f(x,y),x∈X, y∈Y.
(1)任意X⊆V, 则 f(X,X)=0 ==>反对称性: f(u,v)= - f(v,u)
(2)任意X,Y⊆V, 则f(X,Y)=-f(Y,X) ==>(1)的推广
(3)X,Y,Z⊆ V, X∩Y=∅ 则f(X∪Y,Z)=f(X,Z)+f(Y,Z)
二、流值 和 最大流
1. 流f的值|f|=f(s,V)=f(V,t): 源s送出多少水 /汇t喝了多少水
2. 最大流问题: 给出流网络G(源点s,汇点t),希望从中找出流值|f*|最大的流f*, f*称为最大流
三、最大流问题求解
1. 三个理论基础 和 “最大流最小割定理”:
理论一:“残留网络 residual network”
网络流图G ==决定唯一==> 有向边的 残留容量 cf(u,v)=c(u,v)-f(u,v)
网络流图G ==决定唯一==> G导出 的残留网络 Gf=<V,Ef>, Ef={(u,v)∈V×V: cf(u,v)>0}
(1)后者(残留网络)由满足条件的前者(残留容量严格>0的有向边)组成
(2)残留网络中的所有有向边上的残留容量均>0. 如果认为残留容量cf(u,v)是残留网络的(有向)边的权值,则所有权值严格>0,∵不满足介个条件的有向边根本就通不过海选!
理论二:“增广路径 augmenting path”
增广路径 p: 残留网络Gf中从源点s到汇点t的一条简单路径。
增广路径p的残留容量 : cf(p)=min{cf(u,v): (u,v)在增广路径p上}
理论三:“割 cut”
网络流G的割(S,T),源点s∈S, 汇点t∈T。从S到T的边称为割边。
最大流最小割定理 :
(1)f是G的一个最大流 ==>达到流值|f|=f(s,V)最大
(2)残留网络Gf不包含增广路径 ==>不能再压入正网络流
(3)对G的某个割(S,T) ,存在|f|=c(S,T) ==>对最大流的限制来自最小容量的那个割
2. 求解最大流问题
2.1 枚举算法
时间复杂度: O(2^|V|·|E|)
思路:枚举所有割(S,T),找到容量c(S,T)最小的那个割的容量,即为最大流的流值
评价:算法复杂度高,因此仅当顶点个数|V|较少时适用(否则整数越界);另外,它还不能给出得到最大流的具体的网络流,只是返回了最大流值
2.2 增广路算法 FORD-FULKERSON方法:
其中,f是流值|f|不断增加的网络流,初始时f=0,之后不断沿着增广路径压入正网络流;
p是每次找到的增广路径。
最大流知识点转自 最大流问题详解
推荐kuangbin大神的博文-->传送门