UVA 10054 The Necklace

题意

给出若干二色珠子,问你能不能把他们重新排列以连成一条环,使得相邻的两个珠子颜色相同。

思路

转化为欧拉图问题。令结点代表不同的颜色,连接两个结点的边代表珠子。把珠子连成项链的过程转化为求出一种走法使得我们可以走完全部的边然后返回起点。这就是典型的欧拉回路问题了。

如果不会这个知识,请参考:有关欧拉图问题的一些总结

代码

 

欧拉图和哈密顿图小总结

欧拉图

概念

定义

欧拉回路:图中经过每条边一次且仅一次的环;
欧拉路径:图中经过每条边一次且仅一次的路径;
欧拉图:有至少一个欧拉回路的图;
半欧拉图:没有欧拉环,但有至少一条欧拉路径的图。

无向图判定条件

  • 图是联通的(度数为0的点不用考虑,因为是孤立的点,没影响)~先决条件
  • 所有点的度数都是偶数~存在欧拉回路,欧拉图
  • 有且仅有两个点的度数是奇数~存在欧拉路径,半欧拉图
    两个奇数度数的点一个是路径起点一个是路径终点

证明:因为任意一个点,欧拉环(或欧拉路径)从它这里进去多少次就要出来多少次,故(进去的次数+出来的次数)为偶数,又因为(进去的次数+出来的次数)=该点的度数(根据定义),所以该点的度数为偶数。

有向图判定条件

  • 把所有的边强行改为无向边之后(这个图叫原图的基图),联通~先决条件
  • 所有点出度等于入度~存在欧拉回路,欧拉图
  • 有且仅有两个点出度不等于入度,一个出度比入度大1,另一个入度比出度大1~欧拉路径,半欧拉图
    出度比入度大1的点是路径起点,入度比出度大1的点是路径终点

证明:与无向图证明类似,一个点进去多少次就要出来多少次。

模板

 

初步认识图和暴力搜索

[toc]
图(Graph)作为一种非常常见且有效的模型和抽象手段,不仅是在竞赛中,乃至整个CS的学习和应用过程中都有着举足轻重的地位,尽早掌握一些关于图的概念也能辅助我们更好地分析手边的问题。本文介绍一些关于图的最基本概念和实际应用上的一些基本技术。

图的基本知识

在这里我给出这样一个问题:你站在1上,1能走到3,1能走到2也能从2走回来,7能走到5也能从5走回来,7能走到6也能从6走回来,2和5和6互相连通,1自己能走到自己,3能走到4.那么你都能走到那些点呢?

你可能很自然的会想到把我上面说的那一坨给画成一个“路线图”或“地图”,这样只要一看这个图就可以一目了然了,而接下来要说的计算机/数学中的“图”就是这样的一种抽象方式。

来和“图”桑见一面

图的定义很简单,给你几个圆,再把圆和圆之间拿线连起来,就可以称为图了。

定义: 由若干个不同的顶点与连接其中某些顶点的边组成的图形称为图。

g01
An Example of Graph

上面这个就是一个典型的图了,这个图反映的就是我上文中说的那些关系,图里绿色的圆,称为“节点/顶点(Node/Vertex)“,为了接下来叙述的方便,我给每一个节点都挂上了一个编号,在实际应用中,节点上能够挂载任何你需要的信息。圆与圆之间连接着的线,不管长成什么样,都称为“边(Edge)”,边上面也可以挂载信息。

我们发现了,上图中边中,有的是拿直线段表示的,这种边称为“无向边”,比如上面图中的2号点和5号点的连边,这样的边表示“一端(2号点)和另外一端(5号点)可以经由此边互相到达”。‘

而3号点和4号点之间的连边上面有一个箭头,方向是3号到4号,这样的边称为“有向边”,含义是“你只能从一端(3号)经由此边到达另一端(4号),但是并不能反过来”。这样看来,其实无向边可以看成两条方向互反的有向边,这一条性质非常有用。

有的时候,在两个节点之间,可能有着多条无向边或者是相同方向的有向边,这种情况,称为“重边”。

然后再看1号点,我们发现他自己居然和自己连了一条边,这样的边称为“自环”。

而看一下2号、5号、6号和7号,我们发现这四个节点,和他们之间的边共同组成了一个“环”,如果我们以一个方向沿着环行走,恐怕是一辈子也出不去了-_-#。组成环的边可以是无向的,也可以是有向的,沿着一系列的有向边行走,最后走回到了起点,这些有向边就组成了“有向环”。

给图挂载信息

我们先假设这样一种问题情境:你自驾去了一个旅游区,现在正在景区门口盘算着旅游的路线,你知道景区里面没办法加油,所以你已经加满油了,你的油箱容量是K个单位,现在给了你景区的平面图,上面记载了两个景点之间连接着的最近道路的距离(已经被折算成了通过一次要消耗的油量)和你来景区之前调查好的每一个景点的美丽程度。你需要设计一种路线,使得可以用有限的K个单位的燃料,游玩的景点的美丽程度之和最大(你不用考虑游玩结束没油怎么回景区的问题,因为你的保险公司提供免费的拖车服务)。

这个问题就是图论派上用场的时候了,我在这里随便举了一组实例:

g02

 

景区大门和每一个景点,都可以抽象为图上的一个顶点,而每一条道路都可以抽象为图上的一条边。

我们知道每一个景点都有一个“美丽程度”,这个美丽程度就体现在上面这个图里面每一个节点里填着的数字,我们把这种称为“点权”。

每一条道路,都有一个“耗油量”,体现在图上就是边上的数字就是“边权”。

所谓“权(Weight)”,其实就是一种定量的评价标准,怎么样理解附挂在图上的权,是我们自己决定的。

而图上附挂的信息又不仅仅是权值,理论上我们可以给图的任何部分挂载任何我们想要的信息,试想一下,如果我们给每一个节点挂载一个列表,上边记载了从这个可以到达的全部节点的编号,会怎么样?如果我们给每一条边挂载一个数对,上面记载了边的两端所连接的节点的序号,会怎么样?

而接下来要讲的“图的存储”,其实就是几种“挂载信息”的方式。

图的常见存储方式

计算机是不长眼睛的,你也不可能指望它看到上面我们画出来的圆和线就能解决问题。因此我们要把手中的图通过一种计算机可以理解的方式进行转化并存储才行。

常见的存储方式有四种:“邻接矩阵”、“邻接表”、“前向星”、“链式前向星”。他们各有各的特点,其中“前向星”和“链式前向星”其实是邻接表的实现方式,这两种里面我们只需要学习“链式前向星”就可以了。

邻接矩阵

首先,为了方便起见,我们要把图中每一个节点都编号,在这里我们编号1~n。接下来创建一个n*n的全是0的矩阵(或者其他你设定的用来表示无法联通的值),然后我们挨个处理图中的每一条边,对于一条有向边,从a号点指向b号点,那么我们就把矩阵的a行b列修改为1,表示从a到b可以通过。对于一条无向边,连通着a号点和b号点,那么我们就把矩阵的a行b列、b行a列都修改为1,因为无向边可以看成两条方向互反的有向边。

如果你想给边挂边权,怎么办?很简单,矩阵的数值就可以当成边权来用,不过如果你的权值和矩阵中你设定的标记冲突,你就要单独开一个数组来记录了。

如果你想给点挂点权,也需要单独开一个数组来记录。

邻接矩阵的特点是查询一个点到另外一个点之间是否有边和这条边的信息,O(1)就可以查到。但是如果我们要询问一个点连出/连入的所有边(也就是所谓的“遍历”),就必须要检查邻接矩阵的一整行/一整列,这样复杂度就是O(n)。同时,因为是使用数组保存,如果图里有重边,你最终只能保存最后保存的那一条边,也就是会丢失信息,因此一旦有无法忽略的重边,就一定不可以使用邻接矩阵。

看到这里你可能觉得邻接矩阵有点傻,那你可就错了。求多源最短路的Floyd-Warshall算法,求传递闭包的Floyd-Warshall算法,有限状态自动机的状态转移方法数等等都是要使用邻接矩阵的。

邻接表(STL vector实现)

还记得上文的“如果我们给每一个节点挂载一个列表,上边记载了从这个可以到达的全部节点的编号,会怎么样”吗?其实这就是邻接表的一种实现方式。

我们依然要给每一个节点编号,不过这时更多的是为了方便了(如果不编号,就可能要借助指针)。对于每一个点,我们给它挂上一个vector,在这里我给他起名叫to。然后我们还是挨个处理原图中的每一条边。对于一条从a到b的有向边,我们只要对a中的to.push_back(b)就可以了。对于无向边,因为无向边可以看成两条方向互反的有向边,所以就是a.to.push_back(b)和b.to.push_back(a)两步。

在这里我用了个结构体,这是为了方便我们挂载信息的。如果我们想挂点权,直接在Node里面多来一个int什么的就好,如果想挂边权,就再来一个vector,与to同步push_back对应边的权值。

邻接表非常常用,首先他是以点为主体,边是作为点的信息存在的,这一点很符合我们的思考规律。其次因为边采用vector保存,所以不用担心重边丢失的问题。邻接表逊色于邻接矩阵的地方恐怕就只有查询两个点之间是否有边了,在邻接表里面你必须遍历整个当前点上的to才能判断是不是能和另外一个点相连。但相对的,如果图中的点连接的边不多的话(这时候的图可以称为“疏松图”,反之,“稠密图”),邻接矩阵下的O(n)的遍历在邻接表下就快多了。

链式前向星(静态建表的邻接表实现)

上文中我们还提到了“如果我们给每一条边挂载一个数对,上面记载了边的两端所连接的节点的序号,会怎么样?”,其实链式前向星就是这种思路。

在这里假定你已经很明白链表的工作方式了,链式前向星其实就是图上所有边按照某种特殊的方式组成的链表。因为比较难理解,我先上代码。

使用上,还是先编号,然后挨个边处理,直接调用addEdge()方法就可以,查询一个节点所连出的边是,使用上面代码里iteration()里的方法,利用一个略微奇怪的for循环就可以办到。

我们不妨直接从代码的实现来分析链式前向星的工作原理,结构体里面的head是每一个节点在容器(数组)中所对应的第一条边的位置,next是每一条边在容器中所对应的同一起点的下一条边的位置,to则是真正的存储某一条边是指向哪一点的。

再看加边操作,注意到方法里的q是静态变量,这一点非常重要,q的作用是指示当前存储边的容器的末端(注意“末端”指的是并不是最后一个元素,而是其后的空地)的位置,相当于STL迭代器的end(),每一次加边的时候,要在to的末端写入新加边的信息,然后通过head[_from]提取出起点最近加的一条边的位置,然后我们要把新加的边的next指向那一条边,最后修改head,使得最近添加的边变更为新边,同时末端向后移动以供下一次添加之用。

理解了这样的加边方法,查询一个点连出的边的方法也就很好理解,要查询一个点的连出边,我们要先查head,知道这个点最近添加的那条边在哪里(查询结果在这里是j),然后比这条边早一些添加的就是next[j],再早一点的就是next[next[j]],更早一点的是next[next[next[j]]],再早一点的是……,就这样我们一直往时间添加时间更早的边查,直到查到空节点(用来标记链表结束)。

可以发现,链式前向星相对于邻接表的vector实现,常数更小。同时如果想直接遍历每一条边,而不考虑顺序,可以直接遍历to数组(当然,为了知道每条边的起点,你可能还需要建一个from数组),这就使得我们可以从边入手解决问题。因为是邻接表,我们也可以以点为中心来考虑问题。基本可以说链式前向星在各方面都优于邻接表的vector实现。

如果你不用数组,而是用指针来实现,那就是“前向星”了。

暴力搜索

在讲搜索之前,我们还要再来一点关于图的话题,那就是“树”,当然不是种出来的树,而是一种特别一点的图,不过就像你在大街上看到的各种树一样,能被称为一棵“树”的图一定要满足以下几种特征:

  • 每两个点之间有且仅有一条路径
  • 没有环,而且只要再添加一条边,就一定会出现环
  • 只要去掉一条边,就无法保证第一条性质
  • 只要增加一个点,为了保证第一条性质就必须还要增加一条边
An Example of Tree
An Example of Tree

如果只有光秃秃的一个节点,其实也可以称为树,树上的边的方向性也没有要求,可以有向也可以无向,当有多棵“树”同时存在的时候,这些互相没有相交的树共同组成的图也被称作“森林”。

g04
An Example of Forest

自然界的树都是有根的,同样的,有时候为了分析问题的便利,我们也会选取树上的某个节点作为“根节点”。选取了根节点的树这时候也称为“有根树”。

选取了根之后,伴随之而来的还有节点与节点之间的“辈分关系”,我们把根视为辈分最大的祖先,他连接的所有点都是它的“孩子”,而对于他连接着的所有点来说他就是这些点的“父亲”。这样的关系对于所有的节点都是一样的,我们注意到了,在一棵树上,每个节点可以有好多孩子,但是只能有一个父亲,而对于没有孩子的节点,我们对他还有一个称呼叫“叶子节点”,对于有同一个父亲的若干个节点,他们互称“兄弟节点”。

Relation on a Tree
Relation on a Tree

细心的你也发现了,上面这幅图从上面到下面颜色是越来越深的,没错,我想借此来表达有根树的“深度”概念。对于根部,他的“辈分”最大,但是他的“深度”是最小的,父亲的深度始终是比孩子节点的深度大1的。

理解了上面的这些概念,我们就可以开始说“暴力搜索”了。

我们说的“搜索”是什么

我们对搜索的认识,恐怕都是来源于“百度”、“谷歌”,也就是“从所有的信息中找出我们需要的信息的这一过程”,而我们现在接下来要说的“搜索”虽然感觉上还是和你键入几个关键字然后敲回车有一定区别的,但是实质上其实是一样的,不过现在这个过程要交给你亲自去实现。

试想这么几个问题,给你一个表示各个景点之间连通关系的图,现在让你找13号点在哪里,你如何“系统地”完成这个寻找过程?给你一个迷宫,里面有好多墙壁,现在要你找出脱出迷宫的最短路线,你又该如何“系统地”完成这一过程?更直接地,给你一棵树,制定一个根部,你能“系统地”把这棵树的亲子关系和深度都算出来吗?

这些问题,对于你来说可能就是瞟一眼那样简单,不过你知道计算机可是没有眼睛的,也没法扫视全局,因此“系统性”的算法就显得非常重要,也就是说你要有一种“套路”,而不是随便去瞟一眼这么简单。

接下来我们要介绍两种最为常见的搜索“套路”,深度优先搜索和广度优先搜索。在学习这些之前,我已经假定你理解了我上面所讲的的概念,以及递归(调用栈)队列的知识。

深度优先搜索(Depth First Search)

理解深度优先搜索(DFS)的最佳途径就是从“树的先序遍历(DLR)”开始,所谓“遍历”就是把树的每一个节点都走一遍。树的先序遍历,是指对于给定的一棵有根树,从这棵树的根部开始,不断尝试进入儿子节点(下潜),直到进入了叶子节点,然后才会返回到上一层,继续查找下一个儿子节点进入,通过不断重复这一过程,直到所有的节点都被进入并最终返回根部。简单的说就是:不断尝试探底,不走重复的路。下面举个例子来解说:

在这里我给出了一棵树,并且设定好了根部,同时因为我们的起点在根部,我把根部标记为1.

g06
Example Tree to Show DLR

根节点(1号节点),连接了两个儿子节点,按照上文所述的思想,我们优先访问最左面的那个,我们把它标记为2,并进入这个节点。

g07

 

进入2号节点之后,我们顺着刚才的思路,不断下潜,最后我们到达了叶子节点4号。

g08

 

现在我们发现没有可以用的孩子节点了,那么我们就要回到当前节点的父亲那里去,也就是3号节点。在3号节点,我们发现了除了已访问的4号节点之外的另外一个儿子,我们把它标记为5号节点,并进入。

g10

我们在5号节点又遇到了4号一样的情况,没有儿子,那样的话我们就要返回父亲节点3号。当我们返回3号,我们发现儿子已经走了个遍了,这个时候我们又要返回父亲节点2号,在2号同样没有可以没走过的儿子了,我们返回到2号的父亲节点1号。

g11

等我们回到了一号节点,我们又可以找到新的没有走过的儿子了,于是我们按照刚才的调子,进入这个儿子,这个节点已经是我们走过的第6个新节点了,给他标号6,然后我们又找到了他的儿子,进入,标号7。

g12

7号节点又是叶子节点,于是我们返回到他的父亲6号,然后发现还有一个没走过的儿子,于是进入,标号8。

g13

8是叶子节点,返回6,6没有没走过的儿子,返回1,1没有没走过的儿子。好了,任务完成了!

g14

以上就是“树的先序遍历”,是不是很简单?而DFS其实就可以理解成先序遍历在各种其他情境的推广,比如各种“图的遍历”、迷宫搜索(可以转化为图)、找出联通块(水洼问题)等等。通过思考我们也发现了,DFS实现起来的最佳方式就是递归,递归可以完美的还原我们的“下潜(发动递归)”和“返回(函数返回)”动作。下面我给出使用DFS遍历图的实现,使用了vector邻接表,所有访问到的节点,均会在标记数组vis中被标true。

如果你还是一头雾水,不妨试着将上面我做演示的那棵树建立成图,然后手动模拟一边这份实现的执行过程,就能理解了(前提是你必须知道递归的原理)。

一旦你发动了dfs(a),那么在搜索执行完毕之后,所有与a号节点直接或间接连通的点都会被在vis中标记,所以只要对这份实现稍加改动,我们就可以解决“水洼问题”了。而要想解决“迷宫出路”这种问题,我们还要稍微动下脑筋,思考一下vis这个标记数组应该怎样活用,怎样利用vis让dfs达到我们想要的效果(在这份实现中,一旦一条路被走过,就无法再次走过,而在迷宫中,我们很有可能要走之前走过的道路,这样我们就要适时地复原vis标记)。

广度优先搜索(Breadth First Search)

相对于DFS,广度优先搜索(BFS)也许更加直观一点,我们还是从“树的遍历”说起。对于一棵给定的有根树,我们依然从根开始入手,不过这次我们建立了一个队列,队列存储的是“状态”,最开始队列是空的,要开始搜索,我们要给他加入“当前在根节点”这一状态,之后我们不断拉取队伍的队首,然后分别把我们进入了各个儿子的状态逐一加入队列,直到队列清空,这个时候我们一定把每一个“在XX节点”的状态都经历过了,所以我们是走遍了整棵树。

我在这里依然用在DFS里的那棵树来举例子,图下面的方括号,表示队列的变化,左半方括号表示队列头,右半方括号表示队列尾。

最开始,我们的队列里只有1号状态,我们给它拿出来,然后发现他有两个儿子,于是分别创建进入2号儿子的2号状态和进入3号儿子的3号状态,并将他们加入队列尾端。

g15
[1] -> [2,3]
接下来,我们取出队列头的2号状态,我们发现它只有一个儿子,我给他标号4,于是把“进入4号节点”的4号状态加入队列。

[2,3] -> [3,4]
[2,3] -> [3,4]
接下来,取出队列头的3号状态,我们发现它有两个儿子,在这里标号为5和6,于是借此生成5号和6号状态,加入队列。

[3,4] -> [4,5,6]
[3,4] -> [4,5,6]
之后,取出队列头的四号状态,发现在这个状态所在的4号节点有两个儿子,在这里标号7和8,同时据此创建状态并加入队列。

[][]
[4,5,6] -> [5,6,7,8]
最终,队列弹出的5,6,7,8号状态均是在叶子节点的状态,无法创建新状态,队列就此清空,我们完成了遍历任务!来看一下我们整个BFS的全貌吧:

State tree of the whole BFS process
State tree of the whole BFS process

相信你已经可以基本理解BFS的工作方式了。

显然光会个遍历树是不够的,将上面所讲述的思考方式拓展一下,BFS也可以完成“图的遍历”,“迷宫最短路”等问题,下面我会给出一种用于遍历图的BFS实现,图的存储方式采用vector实现的邻接表。

 

选择适合的搜索方式

一招鲜显然并不能吃遍天,DFS,BFS中的任何一个都不能应付全部的场合。DFS使用递归实现,因此当搜索深度比较大的时候就存在巨大的栈溢出风险,而实用模拟栈实现的DFS又十分复杂,得不偿失,因此当栈空间不足以支持DFS时,必须换用BFS。BFS相对于DFS,实现相对比较繁琐,如果使用STL queue的话,会带来一些常数上的劣势(这种劣势在SPOJ等黑心OJ上十分明显),模拟队列又可能会占用大量内存。因此我们需要根据问题的实际来考虑使用哪种搜索方式。

一般来说,DFS在求连通性问题上具有十分的优势(Tarjan SCC/DCC算法均基于DFS),而BFS在求最短路问题上占上风(Dijkstra/SPFA算法均基于BFS),掌握好这两种算法是学好图论的基础。如果你够细心的话,你会发现我们在DFS的同时给树进行了编号,那个编号其实叫做“DFI(DFS序)”,这个东西有着一些神奇的应用。同时DFS和BFS又可以升级为一种考虑问题的思想,当然这些高级一些的问题就需要我们在练习的时候慢慢体会了。

 

本文系hahaschool原创,同时作为Tic ACM Club讲稿,未经许可禁转载。

 

生成树小总结

无向图最小生成树

Prim算法

这个实现做的非常不好,需要改进。对付稠密图,Prim更胜Kruskal一筹。

Kruskal算法

[这个实现等待填坑]

Borůvka算法

[这个实现等待填坑]

有向图最小生成树(最小树形图)

复杂度O(V * E),以下代码仅存储边,而且会对存储结构发生破坏,请注意。实现来源于国立台湾师范大学。我对这份代码进行了更加详细的注释。

适用于固定了生成树的根的情形

适用于生成树的根无法确定的情形

引用自GentieH,谢谢。

建立虚拟节点x,令x连所有的点,权值为所有边权的总和+1,求得最后结果减去该值即可,由于虚拟节点的存在,不会显式地出现孤立点,这时通过判断如果存在多与1个节点的pre为root,则说明无解。如果需要求出根节点,由于之前将x与图中的点连边时,我们按顺序连的话,考虑到可以用边去标识点,对于每一个终点v,如果它的pre为root,则标记这条边who,在求结果时,让who-E即可。

要求输出方案的情形

CF240E

 

次小生成树

[Pending Session]

增量最小生成树

[Pending Session]

最小瓶颈生成树

[Pending Session]

生成树问题混杂其他类型问题

二分图小总结

知识

二分图大讲堂——彻底搞定最大匹配数(最小覆盖数)、最大独立数、最小路径覆盖、带权最优匹配

这一篇已经足够了。

模板

匈牙利算法,复杂度O(V*E),使用“vector邻接表”存储图。MAXN为单边最大节点数量,需要按照题目要求调整,INF是理论最大值不够的话按照题目要求调整。左手边和右手边的节点序号从0开始,建图前,执行init,并正确配置nx(左手边节点数量)和ny(右手边节点数量),BGM_H()返回最大匹配数。

Hopcroft-Karp算法,复杂度O(sqrt(V) * E),适配方法同匈牙利算法模板,调用是BGM_HK(),不适合活用。

配套练习

二分图・ア

POJ 1274 The Perfect Stall(教学题)
POJ 2446 Chessboard(骨牌模型转化)
POJ 2594 Treasure Exploration(允许相交的最小路径覆盖)
POJ 1325 Machine Schedule(适当建图,最小点覆盖问题)
POJ 1469 COURSES(教学题)
HDU 3225 Flowers Placement(匈牙利算法在DFS中的剪枝作用)

 

HDU 3225 Flowers Placement

题意

求第k小的二分图最大匹配方案。

思路

这个题需要我们活用匈牙利算法了。首先正常地建图,运行一次匈牙利算法,这样初步得到了一种最大匹配样态。

接下来,我们使用DFS,从第一束花开始,从小到大挨个枚举这一位放什么,对于每一次枚举,首先要检查在当前已经有的匹配里面是不是这种花已经被匹配过了,如果是的话要解除这一匹配,接下来我们要把这一位在二分图中的匹配指向被枚举的那种花,如果被枚举的那种花原来是指向当前位置的,我们可以直接继续搜索,因为二分图可以继续有机会保持匹配,否则的话我们要以被枚举的那种花原来的东家为起点,进行一次增广路搜索(即匈牙利算法中的一环),如果没有搜索到增广路,说明这样下去也不可能出现最大匹配,可以剪枝了。

注意,如果增广路搜索失败,要把启动搜索之前的那些临时性修改复原,如果增广路搜索成功,那些修改就可以保留了。在增广路搜索过程中,注意不可以让增广路搜索修改当前DFS深度之前的已经固定了的节点。

当深度到达n时,就可以知道当前是一种合法的最大匹配样态,我们可以给计数器+1,当计数器到达k时,中断DFS过程,纪录回溯路径就是答案了。

这种方法可能有一些难以理解,其实只要把增广路过程给当成一种把当前的图给变成匹配了的图的一种“尝试修复”的过程就好了,如果尝试成功,说明以当前图的形态,还有机会出现最大匹配,如果尝试适失败,说明当前图的形态没有前途,不用再深入考虑了。

代码

 

POJ 1469 COURSES

题意

判定给出的二分图是否存在完备匹配。

思路

按照题目要求直接建立二分路,之后运行最大匹配算法,出答案。

代码

使用了匈牙利算法。

 

POJ 1325 Machine Schedule

题意

给你两台机器和k个工件,要你设计最优调度方案。对于每个机器,一共有0 ~ n-1(或者是m-1)这n(或者是m)个状态,初始状态都是0。对于每个工件,要么在x号状态下加工,要么在y号状态下加工。你要给出两台机器一共最少要切换多少次状态,才可以加工完全部工件。

思路

这个题对于建图是很讲究的,第一眼看上去很有二分图的意思。但是如果你这么建图:一边是0~max(m,n)这些状态(因为无论是两台还是一台,其实都是一样的,因为机器运转的时间是无限的,理论上讲就算只给一个机器,也能完成任务,而且和两台机器所需要的切换次数一样),另一边是0~k这些工件,每一个工件都和他可以被加工的状态连上一条边。

但是这么建图并不能解决问题,首先,只要工件可以在0状态加工,就可以看成不存在了,因为机器无需切换状态就可以加工好,这样,我们在图中所有与0状态相连的点,和0状态本身都要去掉。其次,现在我们规约好的问题已经变成了:在一边的图中选出尽可能少的点,使得这些点的所连接的点可以覆盖对面全部的点。这是典型的“最小支配集”问题。最小支配集问题目前还是NP困难问题。所以此路已经不通了。

我们只好换一种思路,我们发现,如果把一个工件可以加工的两个状态连接起来,建立二分图之后。在这个图上,图的两边分别是A机器和B机器,图上的每一条边代表一个工件,我们要求的东西就变成了选出最少的点,使得所引出的边的集合中包含了全部的边,也就是“最小点覆盖问题”。二分图的最大匹配数等于最小覆盖数。问题可做了。

代码

使用了匈牙利算法。

 

POJ 2594 Treasure Exploration

题意

给出一个DAG(有向,没有环的图),现在要你选出尽可能少的几条路径,使得只要把你选出来的路径都走了一遍,就可以保证你已经走过的图中的每一个节点。注意,路径与路径之间,可以有公共点(即允许路径相交)。

思路

典型的“最小路径覆盖”问题。在这里直接给出此类问题的解法:

有向无环图最小不相交路径覆盖

       定义:用最少的不相交路径覆盖所有顶点。

       定理:把原图中的每个点V拆成Vx和Vy,如果有一条有向边A->B,那么就加边Ax-By。这样就得到了一个二分图,最小路径覆盖=原图的节点数-新图最大匹配。

       简单证明:一开始每个点都独立的为一条路径,总共有n条不相交路径。我们每次在二分图里加一条边就相当于把两条路径合成了一条路径,因为路径之间不能有公共点,所以加的边之间也不能有公共点,这就是匹配的定义。所以有:最小路径覆盖=原图的节点数-新图最大匹配。

 

有向无环图最小可相交路径覆盖

       定义:用最小的可相交路径覆盖所有顶点。

       算法:先用floyd求出原图的传递闭包,即如果a到b有路,那么就加边a->b。然后就转化成了最小不相交路径覆盖问题。

 

所谓传递闭包(Transitive Closure)就是指把原图里面所有能直接间接联通的点之间都给连上边(如果有联通的方向限制,这个方向也要保留)之后形成的图,也就是说只要在传递闭包中存在A->B,就一定可以在原图中找到一条从A到B的路径。求传递闭包使用Warshall算法,最简单的实现就是借助邻接矩阵了(下图直接截自离散数学课件):

QQ20150829-1@2x

代码

以下实现使用了Hopcraft-Karp算法和Warshall算法。

 

POJ 2446 Chessboard

题意

给出一个M*N大小的棋盘(M,N都非常小),棋盘上戳了几个洞,然后问你有没有可能用若干张1*2的骨牌,横着或者竖着填满整个棋盘,有洞的地方是不可以放置骨牌的。

思路

因为骨牌是1*2的,如果我们把整个棋盘看成一个图,每个没洞的地方都是一个节点,在棋盘上只要相邻(上下左右四联通)就可以看成两个节点有一条边相连,这样我们就可以发现,每一块骨牌正好就是图上的一条边。所以我们就把问题转化为了:给出一个图,要你用选出尽可能少的边,使得图上的每一个顶点都是选出的边中的某一条的端末,也就是“最小边覆盖”问题。

最小边覆盖=图中点的个数-最大匹配数=最大独立集

所以接下来的事情就很简单,按着上面的说法建图,然后运行一次最大匹配算法,之后作个差就搞定了。

还要注意一点,本题数据给出的形式可能和预想的不一样,挖洞的点是横坐标在前,纵坐标在后,如果WA,不妨检查一下是不是输入的时候做反了。

代码

本实现使用了匈牙利算法。

 

Scroll to top