最详细(也可能现在不是了)网络流建模基础
网络流建模方法一览
大锅博主终于找回来了原稿。。。。
哈,据说加上最详细这几个字会比较容易吸引人阅读。
资料来源:百度百科(笑~
还有一些dalao的博客,不过也都只是因为不想打字了就copy一些很通俗的结论过来,全文几乎都是笔者的原创。
注:此文随着笔者的不断做题持续更新。
已经很熟悉网络流的dalao可以从七开始看,或许能有些收获。
网络流的题目,大部分都是在考建模,很少有考你板子的。。。除非你非得用EK结果被卡上天。笔者从做过的所有网络流的题目里寻找了一些比较常用和经典的建模方式,一共大家学习。
那么就从简单到难一步步看吧。
一、傻逼建图法
笔者打上这两个字是绝对没有任何问题的,让我们来想想网络流的模板题的要求是什么。
(注:笔者选取了洛谷为题目来源,原因竟是视觉效果友好)
很明显, 没有比这种题目更简单的网络流了,题目给你怎么连的边,你就照着题目连上就可以了,几乎是把你当做一个啥都不会的人手把手教你建图。。但是这种题目的数据范围一般较大,建议使用较高效率的网络流算法。
二、超级源点和汇点的设置
这应该是网络流题目中最最最最常用的一种方法了。经常有题目,我们要求一些最小的利益什么的,但是明显没有可用的源点和汇点或者更多情况是题目中给出了不止一个源点和汇点,这时候就要建立超级源和超级汇,把所有的题目中的可行源点和汇点分别连接到超级源和超级汇上,而事实上,我们拿到任何一道网络流题目,除非它的源点和汇点是严格给定的,我们都可以尝试建立超级源汇从而达到减小建模难度的目的。
例题:飞行员匹配问题
题解:这个题我们先不管输出方案,只看要输出的那个最大值,很明显,如果我们把英国飞行员当成源点,外国飞行员当成汇点的话,会有很多源点和汇点,本着方便至上的原则,建立超级源汇,然后板子即可。
前面的题目这么简单笔者并不想放代码
三、最小割类问题
这其实是一个很关键的定理。事实上真要细细的说这个总结里写的肯定是不够用的。我们在做题的时候,有时会遇到这样的一类问题,就是给你一个图,然后给你边权,问你如何在割掉的边权尽量小的情况下把指定的两个点or两个集合分开。然后我们就把你求得的这个最小的边权和叫做这个图的最小割。
那么补上之前不想证明的结论。
首先最大流==最小割
为什么呢,想象一下,我们现在有这么一个有向图,然后,最大流是小于等于割的,那么。。。在最大流=某个割的时候这个割就是最小的。。。感性理解一下的话,一个最大流跑到那个图上,这个图怎么看S也没法连到T去了。
然后最小割的唯一性?
我们考虑一条边是否一定在最小割中。明显,我们跑完一遍网络流之后残量网络里满流的边都是可能在最小割里的,然后又到了跑Tarjan的时候了,如果这条边在最小割里,那么跑完Tarjan之后它所连接的两个端点一定是在不同的强连通分量里。如果是的话,用脑子想想,割了这条边这个图还是没分开啊。
那么我们也可以由此得证,假设一条边所连接的点一个在S所在的强连通分量里,一个在T所在的强连通分量里的话,那么这条边一定在最小割里。
1.平面图最小割=最大流=对偶图最短路
然后就是转换成对偶图的最短路的问题。其实这已经不应该算在网络流的范畴里了(因为它变成了最短路)。那么就不在这里详述了,具体的可以去看看[BZOJ1001]狼抓兔子,虽然可以最大流全力优化蜜汁跑过。
我们在这里还是讨论最小割的问题
2.割边
这种就是我们所说的最平常的最小割问题,我们只需要直接求出图的最大流就可以了。
然后是一些高深一些的建模:有很多题目都会让你求这样一个东西,就是怎么安排这些balabala的工作可以获得balabala的利益,然后让你去求这个最大利益。很多人都会正常的去想最大流的做法但是由于有限制条件,所以不能像那么求。然后我们不妨转换一下,如果我们所有的都可以选,我们是不是可以得到一个最大利益,然后我们考虑的是放弃最小的利益和,这样我们就把这个题转换成了一个最小割的问题,可以利用最大流求出。
然后随之而来的就是另一个模型:最大权闭合图。这类题目都可以转化为最小割来求解,具体证明还是请百度吧,这篇文章并不是介绍这类东西的,并且笔者对这些模型的证明也不如网上的神犇。现在笔者又回来证这个东西了。请向后翻阅,自然能找到。。
例题:Luogu P3410 拍照
题解:这题就是一个最大权闭合图的模型,求出总利益减去不可得的利益就可以了,连边的话比较容易就不详述了,不过再往后的题目应该就会有连边的思路了。毕竟前面这些都是些入门题目。
例题:方格取数问题
题解:我们可能会有一个比较基础的贪心思想,没错,就是隔一个取一个,但是这么做并不可行,具体反例很容易找。然后我们通过观察,发现这道题和某最大权闭合子图有些类似,如果我们全取所有点,删去最小割说不准可行。 开始考虑建图,首先所有的奇数格子连源点,偶数格子连汇点,边权为点权。他们之间的边只连奇数到偶数的,边权为inf,这么连是为了避免重复计算。然后直接DINIC最大流就可以了。
3.割点(?)
这也是最小割的一种经典问题,具体实现方式我们等下放在拆点那个栏目里讲,这里就先不说了。
四、拆点
说实话,网络流最难思考的地方就在这里了,几乎所有拿得出来的经典的题目都是神奇的拆点然后求出最大流。
1.基础拆点——入点和出点
经常有这一类问题,一个图给出了点权而不是边权,我们在连接边的时候就显得十分不好操作,这个时候我们往往就会有这样一种操作,把每个点拆成入点和出点,题目给出的连边均由每个点的出点连向入点,然后每个点的入点和出点之间连一条流量为点权的边,就可以满足点权的限制了。
然后让我把刚才的坑补一下:割点
我们在很多时候都会发现,在求最小割的时候,我们要割的不是边,而是割掉一个点。这时候我们再用刚才的直接跑的方法很明显就不可以了。因为割掉一条边最多只影响两个点之间的连边,而割掉了一个点的话,与这个点相连的所有边就都失去了作用。这个时候我们就可以先拆点,然后把这个点入点和出点之间的那条边割掉,就相当于是这个点在图中不存在了。
例题:教辅的组成
题解:本题的构图思路可以表示为:
源点->练习册->书(拆点)->答案->汇点
注意一定要拆点,因为一本书只能用一次。
由于题目难度不大,这个题就提点到这里了。
例题:奶牛的电信
为什么找这道奶牛题。。以为确实是割点的题目不是很好找,剩下的题目不大适合放在这个地方。如果你好好看了刚才的割点,这个题应该可以直接做出来了。不详述了。
2.按照时间拆点
有一部分题目是这样的,我们给出的图的同时也给出了一个天数或者时间的限制,然后对每一天做出询问,最后求总和,很明显的一点是,要把每一天都连向汇点然后求出总和。这个时候我们发现,如果其他的点仅仅只是一个点的话,无法满足求出每一天这个要求,因为会互相影响,这个时候我们就相应的把这些点拆成天数这么多个点,然后分别向向对应的天数连边。例题的话一会会有一些笔者认为比较好的题集中放在一起,这里就先介绍建图方法了。
3.根据时间段拆点
这不是和上面那个一样么???实际上不是的,注意,刚才那个是时间,而这个是时间段。不过说实话这种拆点方式主要是在费用流里出现,因为有很多这种题目,就是一个人在不同的流量是费用是不一样的,所以为了满足这个要求,就把这个人拆成一共有多少不同费用的点。举个例子:你们数学老师今天布置作业越多,你的不满值越大,如果是5张以下,一张卷子增加1点不满值,如果10张以下,一张卷子增加两点,如果10张以上,那么一张卷子增加inf点不满值。那么我们就把你拆成三个点,分别对应三种不满值。
4.蛇皮拆点
有很多题目,拆点往往十分难想或者蛇皮至极有很多典型的例子就不一一详述了,比如什么一个点拆三个点四个点之类的。直接说一些例题貌似是比较好的。
[CQOI2012]交换棋子
这题不建议刚学费用流的人做,因为确实比较恶心,容易打击到自己。首先,我们可以发现交换这个操作是很难去用流量描述和限制的。那我们应该怎么办?如果我们把格子上的所有黑棋子当棋子,而剩下的白子就当成空的格子,我们就把一个交换的操作当成了一个移动的操作。那么如何限制流量呢?我们这个题要求的是每个格子参与交换的次数,针对一次交换要使用两个格子的次数,我们把格子拆成入点,原点和出点,分别计算,至于交换次数自然让费用承担。好了,这只是大体的思路,但是实际上这题还有很多坑点和不好理解的地方。
拆点更多的是一种工具,而且是网络流里必要和强大的工具。能不能想出拆点的方法从而跑出网络流,往往是解决网络流问题的关键。
五、枚举和二分
原先这个版块叫枚举而二分最大流量,现在想想,当时的理解还是不够深刻,实际上,能够枚举和二分的远远不止流量。这里详细重新讲述一下。
1.枚举和二分流量条件
当然,这个部分依然是必不可少的。
有这样一部分题目,它给你的图不一定是完整的,往往需要你确定一个值来确定是否能够跑出期望的最大流。就比如说去计算一个最少的时间或者花费时,我们并没有一个具体的数值,这个时候我们往往预先通过连接inf先跑一遍最大流,之后二分时间每次重构图,再次跑最大流,并且通过此时的流量是否和刚才相等来调整二分的上下界。抑或跟人数有关的题目,我们二分的时候判断最大流是否等于当前人数。
然后枚举的建图方法就更是简单,每次枚举+1时重构图,然后一直跑到不能再跑了为止。
2.枚举和二分费用
费用流的题目中,有些题目会给你费用的限制,或者说是间接的控制了你的流量,比如说他要一个满足条件时可能的最小费用,你仍然可以像刚才二分流量时那么跑。但是它也有可能要一个不超过一个费用时能满足的最大条件,这个时候去二分那个条件,然后控制费用。
3.枚举和二分边和点
有的题目里,各个点的添加是有顺序的,每个点的边的添加也可能是有顺序的,这个时候就有一类问题会让你确定一个值,要这个值之前的所有点可以满足最大流量,这个时候我们就要二分加入图中的点了,边也是同理。不过总的来说,二分的方法万变不离其宗,总是要通过流量或者费用调整二分上下界。
六、点的构造
大部分题目是有迹可循的,因为他们至少有点或者有明显能够当做点的状态。或者题目给出的点就真的能够当成点使用。但是也有的题目,并没有明显的点的提示,或者给出的你明显的点完全不能够当成网络流里的点使用。对于这种题目,我们就要自己构造出来一些新的点来进行网络流的使用。
单说可能不是很好理解,我们放上几道题给大家看一下。
[HEOI2016/TJOI2016]游戏
这道题目大意就是让我们在一个矩阵里放东西,如果没有硬石头限制,就是同行同列只能放一个。然后考虑算法,n,m<=50很明显这题与什么数据结构啊什么的是没有缘分了。那么就是DP或者网络流了。。考虑DP明显的状态太多根本没法转移,那么网络流呢?这一个一个点完全没有什么用处连起来也没法限制。。。这时候我们就要对这些已经有的点进行重构,根据网络流能够描述互斥关系的原理来搞出这道题。
那么:
这张网格图可以抽象成一系列行块和列块,列块和行块就是说,这一个横行块或纵列块里至多放一个炸弹,另外显而易见的,我们发现选中一个点的时候会同时选中两个块,那么也就是说这张图满足两个限制条件
1.每个点最多被选中一次
2.某些点对之间有必选关系
那么之后连边跑网络流即可了。
BZOJ4950:[Wf2017]Mission Improbable
先说一下题目大意,就是有一个网格方阵,每个格子里都有一个权值,题目要求你在保证每行每列最大的权值不变的情况下,取走尽量多的权值。并且本来有权的格子里不能取成0。
不取成0不用考虑,留下1就可以了。那么剩下的就是保证每行每列最大值不变了。我们可以贪心的考虑每行都把那个最大的留下,如果有那么一行一列的箱子的最大值是相同的,这个时候把最大值放在交点处很明显就要比放在其它的地方要好的多。那么怎么处理这种情况呢?可以像上面的题目那样把每行每列都构造成点,然后一旦出现了刚才那种点,就连起来。之后跑一遍最大流,就可以求出来我们能够放在交点出的最大值是多少了。
七、动态思想
1.基本介绍
有一些题目是这个样子的,他们往往有着较其他网络流题目更大的数据,并且他们也有一个特点,就是在一部分边连好之后,之后是先建建图或者是跑一个点再加一个点这么建图对最终结果并没有影响。这样一来我们就可以通过动态加点或者边的方式使其满足题目要求的复杂度。
2.限制条件
2.1数据范围不能过大
虽说是减小了时间复杂度,但是也只是减少了部分,很多情况下复杂度的等级并没有变化,只是因为在大部分操作里减少了一些不必要的操作使速度加快。
2.2主要应用于费用流
大多时候动态加点的题目都是费用流题目,原因就是最短路一次基本上也就只能增广出来一条增广路,如果是网络流的话,当前弧优化往往能够取到更好的效果。费用流由于大部分人都会使用EK的朴素算法,所以动态加点可以是速度提高好几个档次。
2.3当前最优原则
刚才说过了前面点的选择不会影响后面的选择,这里仔细说一下。因为在跑网络流的时候,不可避免的后面跑的点回合前面跑的点有冲突,这个时候我们可以通过反边来使冲突化解。那么如果我们一个一个的加点,很明显就没办法满足这个条件。但是如果我们可以确定当前这个点跑了这个流,后面的点跑这个流的一定不如这个点优,那么我们就可以无视那个冲突了。
3.经典的例子
对于这个思想有一个很经典的题目,更好的是它甚至有数据弱化版来对比。
[NOI2012]美食节&&[SCOI2007]修车
这两个题的原理都是一样的,就是差在了一个数据范围,我们先说后面那个数据范围小的。
题目描述
同一时刻有N位车主带着他们的爱车来到了汽车维修中心。维修中心共有M位技术人员,不同的技术人员对不同的车进行维修所用的时间是不同的。现在需要安排这M位技术人员所维修的车及顺序,使得顾客平均等待的时间最小。
说明:顾客的等待时间是指从他把车送至维修中心到维修完毕所用的时间。
数据范围
(2<=M<=9,1<=N<=60), (1<=T<=1000)
Solution
要求平均时间最短,就等同于要求总时间最短
对于一个修车工先后用\(W_1-W_n\)的几个人,花费的总时间是
不难发现倒数第a个修就对总时间产生a*原时间的贡献
然后我们将每个工人划分成N个阶段,(i,t)表示修车工i在倒数第t个修
可以建一个二分图,左边表示要修理的东西,右边表示工人+阶段
于是可以从左边的e向右边的(i,t)连边,权值是\(Time[e][i]*t\),就是第e个用i这个修车工所用时间
最小权值完全匹配后,最小权值和除以N就是答案
因为权值是正的,所以一个修车工接到的连线一定是从(i,1)开始连续的,也符合现实情况
因为假设是断续的,那后面的(i,n)改连向(i,n-k),k<n时,答案更优,违背了前面的最优性
这样我们建立好图以后直接跑费用流就可以了。
那么我们继续看下一道题
题目描述 2.0
m个厨师都会制作全部的n种菜品,但对于同一菜品,不同厨师的制作时间未必相同。他将菜品用1, 2, ..., n依次编号,厨师用1, 2, ..., m依次编号,将第j个厨师制作第i种菜品的时间记为 ti,j 。小M认为:每个同学的等待时间为所有厨师开始做菜起,到自己那份菜品完成为止的时间总长度。换句话说,如果一个同学点的菜是某个厨师做的第k道菜,则他的等待时间就是这个厨师制作前k道菜的时间之和。而总等待时间为所有同学的等待时间之和。现在,小M找到了所有同学的点菜信息: 有 pi 个同学点了第i种菜品(i=1, 2, ..., n)。他想知道的是最小的总等待时间是多少。
数据范围 2.0
对于100%的数据,n <= 40, m <= 100, p <= 800, ti,j <= 1000 (其中p = ∑pi)
Solution 2.0
连边还是和上一个题目一样。那么数据范围变大了,我们现在的时间复杂度就是O(\(n^2m\times p \times \overline k\))嗯,T了。
那么怎么办呢?通过仔细的思考可以发现,我们观察发现,第一次spfa得出的最短路肯定是某人倒数第一个修某车某厨师倒数第一个做某菜,因为倒数第一个肯定比倒数第二个距离短
那么我们可以在一开始建图的时候,只把所有“倒数第一个做的菜”的那些边加上
一旦一条增广路被用掉了(也就是一个厨师-做菜顺序二元组(j,k) 被用掉了),那么我们就把所有代表二元组(j,k+1) 加上去(一共有n条),再跑spfa
这样我们图中的总边数不会超过$n\ast\sum_{i=1}^n p \lbrack i\rbrack $
也就是总时间在\(O\left(np^2\ast \overline k\right)\)左右,k是spfa常数,就可以通过这道题。
八、二分图
二分图又称作二部图,是图论中的一种特殊模型。 设G=(V,E)是一个无向图,如果顶点V可分割为两个互不相交的子集(A,B),并且图中的每条边(i,j)所关联的两个顶点i和j分别属于这两个不同的顶点集(i in A,j in B),则称图G为一个二分图。
好了不介绍这东西的基本定义了随便百度百度就应该能百度到了,至于增广路什么的也都去看看基础教程吧,笔者这里说的是网络流的建模。。
1.最大匹配
众所周知(?),二分图的最大匹配就是在二分图上跑出来的最大流。那么最优匹配什么的也可以网络流搞搞搞出来,而且时间复杂度一般都要比想象的好很多,大部分比匈牙利要快。。。
2.基本定义
为了方便待会理解,这里特别放上几个基本的定义。
最小覆盖:即在所有顶点中选择最少的顶点来覆盖所有的边。
最大匹配:二分图左右两个点集中,选择有边相连的两个匹配成一对(每个点只能匹配一次),所能达到的最大匹配数。
独立集:集合中的任何两个点都不直接相连。
3.最小覆盖数=最大匹配数
这个比较基础,证起来主要看自己理解吧,具体的证明我也没有特别好的思路,不过还是尽量给大家证一下吧。我们先搞一个二分图,然后最大匹配一下,如图,绿点就是匹配成功的点,红点就是匹配失败的点。那么我们可以发现,每一条边都有两种情况,一种是连了一个红点一个绿点,一种是连了两个绿点。可以发现,没有任何一条边会连接两个红点,所以我们在选择覆盖点的时候都选绿点。
然后又因为如果一个点连接了一个红点,那么与这个点相连的其他的绿点都一定不会连接红点,自己可以画一画,明显可以发现如果连接红点的话,那么刚才所求的就不是最大匹配了。那么对于这些点,我们肯定选择那个连接了红点的点作为覆盖点,因为并不会有其他的点与那个红点相连了。所以可以基本上的出最小覆盖数=最大匹配。
那么我们就把这类问题转化成了网络流了。
4. 最大独立集=总点数-最小覆盖集
这个定理非常好用,不少题目都直接间接的用到了这个东西。先上图:
是不是很熟悉,没错就是刚才那个图,笔者实在懒得画另一个了。我们用那两个绿点完成了对这个图的最小覆盖。
然后我们来反证这个结论,即证最小覆盖集+最大独立集\(\neq\)总点数。
首先我们可以看出除了我们选择的两个用来最小覆盖的点之外,剩下的点之间彼此之间都没有连边,我们可以尝试把任意两个红点之间连一条边,那么明显,我们不满足最小覆盖的要求了,或者我们尝试通过转换使最小覆盖更小。。当然不可行,因为我们已经求得就是最小覆盖了,并且易证的是剩下的所有点一定构成一个独立集。并且这个独立集的大小不能够更大了,然后我们就证出了题目所给的定理。
5.删边
题目不适合起太长,这一部分主要是说:在一个二分图中,如果删去一条边能够使这个图的最大匹配减小1的话,那么这条边一定在残量网络中满流,并且它所连接的两个点一定不在同一个强连通分量当中。
首先满流的条件一定要有,不满流那这条边就不在最大匹配里了。然后就是不在同一个强连通分量里这个条件,我们可以发现,在同一个强连通分量里的话,那他们就可以自己在这个强连通分量的内部进行增广,也就是说我们可以通过操作从而找到另一组最大匹配并且不需要使用当前这条边,所以这条边删掉也就无可厚非了。
6.最小路径覆盖
说一下大意,就是现在有一个有向无环图G,要求用尽量少的不相交的简单路径覆盖所有的节点 。
那么这个也有一个结论,就是最小路径覆盖=原图节点数-最大匹配
诶,等等,哪里来的最大匹配。。
我们对当前的图拆点,把每个节点都拆成x,y两个节点。如果\(x_1有一条指向x_2的边,那么就把x_1和y_2之间连一条边\),这样我们就能得到一个二分图了,那么为什么这么做可行呢?
一开始每个点都是独立的为一条路径,总共有n条不相交路径。我们每次在二分图里找一条匹配边就相当于把两条路径合成了一条路径,也就相当于路径数减少了1。所以找到了几条匹配边,路径数就减少了多少。所以有最小路径覆盖=原图的结点数-新图的最大匹配数。
以上就是我们常用的网络流构图的基本思想了,接下来笔者会从做过的题目里挑选出来一些觉得建模比较有代表性的题目分享给大家
九、经典的建模
其实你也可以看出来笔者这个地方写的很青涩,那是早之前写的了,可能对于刚刚入门网络流的人比较有用,但是对于大佬们肯定用处不大了,下面的题目肯定也都有自己独特的见解和理解方式,所以请dalao从6开始看。。。
1.方格取数加强版
注意,这道题和刚才那个不是一道题目。
记不记得我们NOIP曾经考过一道叫做方格取数题目。这道题就是那个题的加强版。本来是要走2次,现在变成了要走n次。。原来的DP方案好像一下子就垮掉了。现在我们来这么考虑,我们把这个题当成一个图论题而不是DP题,那么我们走过的格子就要拿走相应格子的点权,刚才说过什么,如果是点权问题的话,是不是要拆点。那么我们又怎么处理每个格子可以走无数次呢?好办,我们对于每个格子的入点和出点分别连两条边,一条是费用为点权流量为1的边,表示可以取走这个格子里的点权;一条是费用为0流量为inf的边,表示这个格子可以无数次经过。然后相应的建立超级源和超级汇,分别向左上角和右下角连接流量为k的边来限制要走k次这个条件。这样我们是不是就做出了这个题。
2.[CQOI2015]网络吞吐量
这道题就开始慢慢的使建图复杂起来了,我们根据题意可以发现这道题的网络流是严格的按照最短路跑的。等等?按照最短路跑?这和树上跑网络流有个P区别,不直接写出来那个最小的流量O(1)出解更好么?然后笔者就画了画样例模拟了一下,发现这个样例,并不只有一条最短路!!!所以我们先求出来一遍最短路之后,通过遍历一个点的所有出边的方式判定到下一个点的是不是最短路。如果$ dis[v]=dis[u]+edge[i].dis $那么我们就找到了到下一个点的一条最短路。然后连边求最短路即可。
3.数字梯形问题
这个题有三问,虽然很相似就是了。让我们一问一问解决这个题目。P.s.把这个题放在这里的意义就是让大家思考一下网络流里连接inf边的道理。
首先先从起点到最上层的每个数字连流量为1的边,表示从这些数字开始走,然后从最下层的每个数字连到汇点,流量为1。
①从梯形的顶至底的 m 条路径互不相交;
这一问就是每个点和每条边都只能用一次的意思,所以练出来流量为1的边跑费用流就可以了。
②从梯形的顶至底的 m 条路径仅在数字结点处相交;
那么我们现在就可以使用重复的点了对不对。。。所以入点和出点的连边我们就可以连inf了,表示这个点可以用无数次。然后我们再跑一遍费用流。
③从梯形的顶至底的 m 条路径允许在数字结点相交或边相交。
话说。。边怎么相交,这个题貌似相交不了。。实际上就是可以经过重复的边,然后我们每个点的出点连向下个点的入点的流量也设置为inf然后我们再跑费用流。
好了,看上去这道题A了,那么交上去试试?然后你就会有这种效果:
为什么,就是因为这一句话 “然后从最下层的每个数字连到汇点,流量为1。”,如果我们这么连边了,那么最后一层的点,就只能使用一次,然后你不就挂掉了。。。所以从第二次开始,我们就要把这条边连inf变成1,然后就可以了。
4. 餐巾计划问题
网络流24题里还是有很好的题目的,比如说这一道,建图方式妥妥的一股清流。首先我们发现这个题的状态很多,首先是两个洗毛巾的操作,这个比较好办,拆点了之后直接去洗就可以了,然后就是这个题目的精髓所在,对于这种直接购买进来的操作,我们往常的思路是向入点连边,然后限制流量,然而这个题我们仅仅只在入点限制流量,而把买餐巾这个操作连到当天的出点上,这样就可以使在洗掉的餐巾不足以支持满流的情况下是这一天满流。然后因为我们有攒着餐巾不使用这种操作,所以我们要把上一天的入点和下一天的入点之间连一条inf的边表示这种情况。
5.[CTSC1999]家园
这个题具体来说就是一个拆门的操作(雾),先让笔者来说明一下拆门这个操作是什么含义吧。。
笔者有次做了一道叫做紧急疏散的网络流题目,一开始笔者是用时间点建分层图,结果发现不可以这么做,整个题目会爆炸掉,但是这个题又没有更好的方法去支持时间的限制,然后笔者发现可以有拆门这种操作,因为门,也就是指定的汇点的个数总是有限的,哪怕你把这东西拆成500个点,也不会造成有太多边的情况。然后从每天向下一天连边,就可以在一些题目上避免使用分层图造成复杂度过高。
那么这个题我们也可以这么干。首先我们把每天的地球和月球都和超级源点和汇点连inf的边,然后把太空船上一天的位置和这一天的位置连一条容量为太空船装载人数的边,然后再从每一天的向下一天连一条inf边表示可以用人留在这里,我们就可以通过每一天的最大流跑出结果。然后是一个操作,大部分这种题目我们要二分答案,然而二分的代价就是你要有非常繁杂的预处理过程才能保证你建出来的图没有问题,然而这个题的数据范围小到让你可以直接枚举,并且每次不把图清零,而是在已有的残量图上继续跑,从而达到不错的时间效率与较低的代码难度。
从这里开始这一个专题要变得不和谐起来了,并且可能有我自己YY的神奇的模型。
6.黑白染色
怎么说呢,黑白染色这东西,很迷。
6.1适用性
黑白染色一定是在棋盘图上,嗯,这点绝对没有问题。然后就是要把棋盘的格子当做点,并且经过了染色之后,你会惊奇的发现黑点只会限制白点,不会限制黑点,白点也只会限制黑点。也就是说同色的点之间并没有直接关系。
6.2为什么使用它
当你拿到一个棋盘的题手足无措,就要被水淹没时,黑白染色很有可能救你一命。黑白染色,一共只有两种颜色,所以这样建图之后一般都会和二分图非常像,然后就是随便匹配的事了。另一种情况是你把这题转化成了一个最小割的模型,发现题目中删去某个点或者添加某个点的操作就是在删边。从而随便网络流跑出来这个题。
6.3模型建立
基本上可以确定的是你选择一个你喜欢的颜色,把它连向你不喜欢的那个颜色,然后你不喜欢的那个颜色去连汇点,你喜欢的连源点。但是问题也就来了,这个东西适用于几乎所有要用到黑白染色的题目,那么也就是说,它没屁用。实际上,真正恶心的一般都在黑白点互相连接的限制上,所以具体问题只能具体分析。
6.4Difficult
然而有的时候发现黑白染色并不能完全解决问题,或者说这个题并不能符合黑白染色的定义。。那么我们就红黄蓝染色,再不行就红黄绿蓝染色。然后重新构造模型。不得不说,这东西有的时候还是能派上大用的。
6.5时间复杂度
黑白染色的题目,我们一定要相信信仰,因为建出来的图基本上都是那种二分图,还是边比较稀疏的那种,所以我们可以几乎在\(O(n^{1.5})\)的时间复杂度就解决问题,所以不要问为什么有的题1e5的数据都跑网络流了。
7.切糕模型
事先证明,这个鬼畜的名字不是我叫出来的,是网上某个博主这么叫的,然后我一想确实是很有道理。这种模型本来应该叫距离限制模型。也就是说,每个元素选择时,有多种选择,并且相邻两个元素之间的选择会相互限制。例题就是HNOI某一年的一道叫切糕的题目。
其实这个模型挺经典的,但是很多情况下题目不仅仅只是有这样一个限制,然后就被其它的模型覆盖了。
8.最大密度子图
这个事实上都可以单独拿来当成一个题做了。先说一下要求:
给定一个无向图,要求从无向图里抽出一个子图,使得子图中的边数\(\mid E\mid\)与点数\(\mid V\mid\)的比值最大,即求最大的\(\frac{\mid E\mid}{\mid V\mid}\).
给出一种解法,由前面说过的最大权闭合子图得到的。假设答案为k ,则要求解的问题是:选出一个合适的点集 V 和边集 E,令(|E|−k∗|V|)取得最大值。所谓“合适”是指满足如下限制:若选择某条边,则必选择其两端点。
建图:以原图的边作为左侧顶点,权值为1;原图的点作为右侧顶点,权值为 −k (相当于 支出 \(k\))。 若原图中存在边 (u,v),则新图中添加两条边 ([uv]−>u), ([uv]−>v),转换为最大权闭合子图。 其中k可以二分得到。
锅,突然发现这个东西没证。。。。
然后发现第一步并不能证出来。。。
现在我又证出来了。。。
无向图的子图的密度:\(D=\frac{|E|}{|V|}\)
看成是分数规划问题,二分答案 λ ,需要求
\(g(\lambda)=max(|E|-\lambda|V|)\)
g(λ)=0时我们取到最优解。
喏,现在权当我这一步证出来了。
给出证明吧。
给出函数\(\lambda=f(x)=\frac{a(x)}{b(x)}(x\in S)\)
那么我们猜测一个最优值λ,重新构造一个函数:
\(g(\lambda)=max\lbrace a(x)-\lambda b(x)\rbrace\)
若我们设一个\(\lambda^*=f(x^*)\)为最优解,那么则有:
\(g(\lambda)<0⇔\lambda>\lambda^*\)
\(g(\lambda)=0⇔ \lambda=\lambda^*\)
\(g(\lambda)>0⇔ \lambda<\lambda^*\)
似乎貌似就是大于小于的情况要么不合法,要么不够优。当g(λ)=0时正好取到最优解。
然后我们开始思考如何搞出来,可以发现我们选择一条边就要选择与它相连的两个端点。那么,随便YY一下,搞一个如上面的建图,就可以发现上面那个建图满足我们的条件。
可是有个问题,当λ取到一定值的时候,它变大并不会影响最大权闭合图的值,统统都是0.。所以我们二分求得是在g(λ)=0时最小的λ。
9.疏散模型
这个模型就是我自己YY出来的了。前面应该提到过了,我做过一道叫紧急疏散的鬼畜题。这道题让我生生调了半天代码,从此对这题印象深刻。
那么这是一个什么样的模型呢?它利用两个特点,一个是分层图的一些概念,一个是前后缀的思想。明显的是,分层图我们可能在一些题目中看出,然后坑爹的是,这个题分层的话整个题目复杂度极高,时间空间都不支持。这个时候可以尝试只拆开汇点,也就是我之前所说的拆门。然后对于那个时间点能到汇点的点,与这个时刻的门连一条边。然后利用前缀思想,把每一天的汇点一串inf连下去,最后就可以求出。而且时空复杂度都很小啊。
10.优化建图
有这么些题,你一看就知道应该怎么建图,然后仔细看看数据范围,用常规方法不是炸你空间就是让你T掉。。。。这个时候我们可以尝试一下优化,比如说使用数据结构。虽然这东西放出来之后一般人不会愿意打。那么是什么原理呢?如果我们某个区间里的每个点都要互相连边,那么就可以建线段树,然后把对应的区间连边,然后线段树的节点之间互相连边,就可以优化边数。
然而值得注意的是,我并没有写数据结构/优化建图,因为更不正常的题目,我们或许会用到计算几何。十分鬼畜,鬼畜至极。。然而这种题目。。鬼才会去写啊。。。有兴趣的去看看[Jsoi2018]绝地反击 。话说这个优化的原理是什么呢?举个栗子:假设一个题目有很多点,但是你经过各种手玩和证明之后,发现所有不在凸包上的点不会互相连边,这个时候求一发凸包,就可以大大的减少边数了。
11.最长反链和最小链覆盖
在有向无环图中,链是一个点的集合,这个集合中任意两个元素v、u,要么v能走到u,要么u能走到v。
反链是一个点的集合,这个集合中任意两点谁也不能走到谁。
最长反链是反链中最长的那个。
那怎么求呢?
11.1 传递闭包
在开始介绍这个之前,我先简单叙述一下传递闭包是个什么东西。可以发现的是百度百科里的东西都奇怪的一批根本让人看不懂。那么我就尽量通俗的说一下。
传递闭包就是求一个图里面所有满足传递性的点。那么传递性又是什么呢?简单来说就是:假设图中对于图中的节点\(i\),若果有一个j点可以到\(i\),\(i\)点又可以到\(k\),那么\(j\)就能到\(k\)。这样的节点就可以说是有传递性。看上去非常的,简单。其实就是这样。求完传递闭包之后,我们能够知道的是图中的任意两点是否相连。
那么,我们可以用Floyd算法搞出来,没错,就是Floyd。只不过我们现在是求两点是否相连,而不是求其最短路径。
11.2最小链覆盖
这个东西就是求出最长反链的关键。因为:最小链覆盖=最长反链长度。
那么,最小链覆盖又怎么求出呢?
回想一下我们之前是不是有一个地方介绍过了最小路径覆盖。那个地方所求的是不能相交的最小路径覆盖,那么这里我们要求的就是路径可以相交的最小路径覆盖。
问题又来了,这个又怎么求解呢?
我们把原来的图做一遍传递闭包,然后如果任意两点\(x,y\),满足\(x\)可以到达\(y\),那么我们就在拆点后的二分图里面连一条\((x_1,y_2)\)的边。
这样球可以绕过一些在原图中被其他路径占用的点,构造新的路径从而求出最小路径覆盖。
那么这样我们就把可以相交的最小路径覆盖转化为了路径不能相交的最小路径覆盖了。 从而我们求出了这个图的最小链覆盖。
11.3 十分感性的证明
那么我们证明一下刚才一个结论的正确性。
我们通过观察定义可以得出,最长反链的点一定在不同的链里。所以最长反链长度\(\le\)最小链覆盖数。
那么我们接着可以通过感性理解和数学归纳证出最长反链长度\(\ge\)最小链覆盖数.
如此我们就能够得证了。
11.4最长链长度 = 最小反链覆盖数
不想证了。这个结论就是上面的反向结论,其实直接背过就可以了。。。。。
12.混合图的欧拉回路
12.1欧拉回路
(1)给定一个图G,假设我们能一笔画,如果图G中的一个路径包括每个边恰好一次,则该路径称为欧拉路径(Euler path)。如果一个回路是欧拉路径,则称为欧拉回路(Euler circuit)。
具有欧拉回路的图称为欧拉图(简称E图)。具有欧拉路径但不具有欧拉回路的图称为半欧拉图。
(2)对于一个无向图,如果每个点的度数均为偶数,那么这个图是一个欧拉图。
(3)对于一个有向图,如果每个点的出度都等于入度,那么这个图是一个欧拉图。
(4)对于无向图,如果每个点度数均为偶数,或者有且仅有两个顶点度数为奇数,那么这个图中存在欧拉路径。
(5)对于有向图,如果每个点的出度等于入度,或者有且仅有两个点不符,且这两个点一个入度比出度小1,一个出度比入度小1,则这个图存在欧拉路径。
12.2求解一般欧拉图
直接暴力枚举所有点的出度入度即可,用上面的关系进行判断,可以求出。
12.3求解混合欧拉图
明显,不能通过刚刚的方法来套用到这个题上去。
对于这种图,我们首先可以自定向无向边的方向。可以rand,也可以规定一个方向从而全部指向一个方向。不过这样无法直接得到答案,说不定脸好,但是我们可以通过这样一个操作搞出来一个完全有向的图,可以套用上面的性质来初步判断这个题是否有欧拉回路。不过这样做明显没有依据,只是一个预处理的过程,再通过调整判断是否有解。
那么如何自调整呢?引用一段话:
所谓的自调整方法就是将其中的一些边的方向调整回来,使所有的点的出度等于入度。但是有一条边的方向改变后,可能会改变一个点的出度的同时改变另一个点的入度,相当于一条边制约着两个点。同时有些点的出度大于入度,迫切希望它的某些点出边转向;而有些点的入度大于出度,迫切希望它的某些入边转向。这两条边虽然需求不同,但是他们之间往往一条边转向就能同时满足二者。
然后我们发现,网络流貌似可以满足这个自调整的性质。
12.4算法步骤
1.\(首先对于每个入度>出度的点,我们将其与源点S连接一条权值为\frac{入度-出度}{2}的边;\)
\(对于每个入度<出度的点,我们将其与汇点T连接一条权值为\frac{出度-入度}{2}的边。\)
为什么除以2,因为我们改变一条边的方向时,会造成出度和入度的改变,所以。。。
2.然后将原图中所有你定了向的无向边连接的两个点连一条边权为1,方向为你定向方向的无向边。
3.跑网络流去,如果满流自然就有解。
4.把在网络流中那些因为原图无向边而建的流量为1的边中经过流量的边反向,就形成了一个能跑出欧拉回路的有向图,如果要求方案,用有向图求欧拉回路的方法求解即可 。
13.最大权闭合子图
发现前面写最小割的时候把这个东西跳过去了,。。。现在补上。
13.1定义
一个子图(点集), 如果它的所有的出边所指向的点都在这个子图当中,那么它就是闭合子图。 点权和最大的闭合子图就是最大闭合子图。
13.2构图
设置超级源汇S,T。
然后使S和所有的正权的点连接权值为点权的边,所有点权为负的点和T连接权值为点权绝对值的边。然后如果选择了某个v点才可以选u点,那么把u向v连接一条权值为\(\infty\)的边。
然后随便跑跑网络流。
最大点权和=正点权和-最小割。
13.3并不感性的证明
首先说简单割,就是我们求最小割的时候每条割边都与S或T相连。在闭合图中,由于我们不与S或T连接的边的权值都是inf,所以我们不会割到他们。容易得证闭合图中的最小割是简单割。
接着证明简单割和闭合图的关系。
因为简单割不会含有那些正无穷的边,所以不含有连向另一个集合(除T)的点,所以其出边的终点都在简单割中,满足闭合图定义。
我们假设跑完最小割之后的图中一共有两部分点\(X,Y\),\(X\)表示与S相连的点,\(Y\)表示与T相连的点,那么我们可以设\(X_0,Y_0\)为负权点,\(X_1,Y_1\)为正权点。然后由于我们求了一遍最小割,根据上面简单割的证明我们发现不会割去中间inf的边,所以我们可以求出:
\(最小割=|X_0|+|Y_1|\)
刚才已经证明过X,Y均是闭合图。
然后我们可以发现X的权值和可以表示为\(Sum_X=X_1-|X_0|\)
我们把上面那个式子代入到这个式子里。
\(Sum_X+最小割=X_1-|X_0|+|X_0|+Y_1\)
然后我们得出:
\(Sum_X+最小割=X_1+Y_1=原图所有正权点权值之和\)
然后我们得证。
14.文理分科模型
本来不想整理这个,因为确实不是特别的突出,用的地方也不是很多。然后,笔者在写这个东西的这一天连着看见两道这样的题。所以还是整理一下。
这里表示如果点的贡献不仅仅是点权,而且还有附加的条件的话,比如同时选A,同时选B,选择不相同。。。。。
那么我们就利用最小割的性质构造一下即可。用“割掉”代表选择。
然后分情况讨论所有选择情况,使对应情况下被割掉的边流量之和等于权值 即可。
到这里这个课件的系统知识部分就已经结束了,然而还是有不小的漏洞,比如线性规划,比如有上下界的网络流,比如流量平衡等等。不过笔者自信自己的绝对是目前网上能找到的最全的一个了。
十、几道脑洞大开的题目
这些题大多比较新,都是这两年的新题,并且建模的思路极其鬼畜新颖,所以还是很值得一看的。不,是很值得一看,而且我在这里写的题解确实,十分详细。。。。
1.[SDOI2017]新生舞会
题面描述
学校组织了一次新生舞会,Cathy作为经验丰富的老学姐,负责为同学们安排舞伴。
有nn 个男生和nn 个女生参加舞会买一个男生和一个女生一起跳舞,互为舞伴。
Cathy收集了这些同学之间的关系,比如两个人之前认识没计算得出 \(a_{i,j}\)
Cathy还需要考虑两个人一起跳舞是否方便,比如身高体重差别会不会太大,计算得出 \(b_{i,j}\) ,表示第i个男生和第j个女生一起跳舞时的不协调程度。
当然,还需要考虑很多其他问题。
Cathy想先用一个程序通过\(a_{i,j}\)和\(b_{i,j}\) 求出一种方案,再手动对方案进行微调。
Cathy找到你,希望你帮她写那个程序。
一个方案中有n对舞伴,假设没对舞伴的喜悦程度分别是\(a'_1,a'_2,...,a'_n\),假设每对舞伴的不协调程度分别是\(b'_1,b'_2,...,b'_n\) 。令
C=\(\frac{a'_1+a'_2+...+a'_n}{b'_1+b'_2+...+b'_n}′\)
Cathy希望C值最大。
数据范围
对于100%的数据,\(1\le n\le 100,1\le a_{i,j},b_{i,j}<=10^4\)
Solution
这个题乍一看上去很是让人束手无策,DP明显被强大的后效性D掉了,据说有线性规划的做法,但是笔者并没有想出来,搜索的话n=100让人很是头疼。那么现在没有什么办法,我们就只能再回到题目里面去看看有没有什么没有发现的信息。
然后我们发现了这个东西:C=\(\frac{a'_1+a'_2+...+a'_n}{b'_1+b'_2+...+b'_n}′\)
我们发现这个分式一定有意义,那么分母不可能为0,又因为给的都是正整数,所以分母大于0。
那么首先我们先去分母,这个式子就变成了:\((b'_1+b'_2+\cdots+d'_n)\times C=(a'_1+a'_2+\cdots+a'_n)\)
然后我们移项,它就成了\((a'_1+a'_2+\cdots+a'_n)-(b'_1+b'_2+\cdots+d'_n)\times C=0\)
然后我们去括号,就变成了:\(a'_1+a'_2+\cdots+a'_n-b'_1\times C-b'_2\times C-\cdots-b'_n\times C=0\)
为了方便计算,我们可以合并一下同类项:\((a'_1-b'_1\times C)+(a'_2-b'_2\times C)+\cdots+(a'_n-b'_n\times C)=0\)
我们题目中要求的式子就和现在的式子一样了。
此时我们就可以二分C值,然后判断tot是否大于或小于0来缩小边界。
然后我们发现每个{a,b}数对的下标都是一一对应的,所以我们直接从第i个男生想第j个女生连接一条\(a_i-b_i\times C\)的边就可以了。
说了从这里开始就会有代码。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<queue>
#define inf 1000000000000001ll
#define maxxx 500000001
#define re register
#define ll long long
#define min(a,b) a<b?a:b
using namespace std;
const long double eps=0.00000007;
struct po{
int to,nxt,w;
ll dis;
};
po edge[800001];
int n,m,s,t,b[205],p;
int head[205],num=-1;
ll tot,dis[205],pa[501][501],pb[501][501];
inline int read()
{
int x=0,c=1;
char ch=' ';
while((ch>'9'||ch<'0')&&ch!='-')ch=getchar();
while(ch=='-')c*=-1,ch=getchar();
while(ch<='9'&&ch>='0')x=x*10+ch-'0',ch=getchar();
return x*c;
}
inline void add_edge(int from,int to,int w,ll dis)
{
edge[++num].nxt=head[from];
edge[num].to=to;
edge[num].w=w;
edge[num].dis=dis;
head[from]=num;
}
inline void add(int from,int to,int w,ll dis)
{
add_edge(from,to,w,dis);
add_edge(to,from,0,-dis);
}
inline bool spfa()
{
for(re int i=s;i<=t;i++) dis[i]=inf+1;
memset(b,0,sizeof(b));
queue<int> q;
q.push(t);
dis[t]=0;
b[t]=1;
while(!q.empty()){
int u=q.front();
q.pop();
b[u]=0;
for(re int i=head[u];i!=-1;i=edge[i].nxt){
int v=edge[i].to;
if(edge[i^1].w>0&&dis[v]>dis[u]-edge[i].dis){
dis[v]=dis[u]-edge[i].dis;
if(!b[v]){
b[v]=1;
q.push(v);
}
}
}
}
return dis[s]<inf;
}
inline int dfs(int u,int low)
{
b[u]=1;
if(u==t) return low;
int diss=0;
for(re int i=head[u];i!=-1;i=edge[i].nxt){
int v=edge[i].to;
if(edge[i].w&&!b[v]&&dis[v]==dis[u]-edge[i].dis){
int check=dfs(v,min(edge[i].w,low));
if(check){
tot+=check*edge[i].dis;
low-=check;
diss+=check;
edge[i].w-=check;
edge[i^1].w+=check;
if(low==0) break;
}
}
}
return diss;
}
inline void max_flow()
{
int ans=0;
while(spfa()){
b[t]=1;
while(b[t]){
memset(b,0,sizeof(b));
ans+=dfs(s,maxxx);
}
}
return;
}
inline void build(ll x)
{
memset(head,-1,sizeof(head));
num=-1;tot=0;
for(re int i=1;i<=n;i++)
add(s,i,1,0),add(n+i,t,1,0);
for(re int i=1;i<=n;i++)
for(re int j=1;j<=n;j++)
add(i,j+n,1,-(pa[i][j]-pb[i][j]*x));
}
inline bool check(ll x)
{
build(x);
max_flow();
if(-tot<=0) return 1;
else return 0;
}
int main()
{
n=read();
for(re int i=1;i<=n;i++)
for(re int j=1;j<=n;j++)
pa[i][j]=read(),pa[i][j]*=5000000;
for(re int i=1;i<=n;i++)
for(re int j=1;j<=n;j++)
pb[i][j]=read();
s=0,t=n+n+1;
ll l=1,r=50000000000ll;
while(r>=l){
ll mid=(l+r)/2;
if(check(mid))
r=mid-1; else
l=mid+1;
}
printf("%.6lf",l*1.0/5000000);
}
这个题要用数组写费用流,卡了结构体。。。。
2.[2018多省省队联测]劈配
这是省选考试题了,可惜当时并没有想到最后。。。
题目描述
总共n名参赛选手(编号从1至n)每人写出一份代码并介绍自己的梦想。接着由所有导师对这些选手进行排名。
为了避免后续的麻烦,规定不存在排名并列的情况。
同时,每名选手都将独立地填写一份志愿表,来对总共m位导师(编号从1至m)作出评价。
志愿表上包含了共m档志愿。
对于每一档志愿,选手被允许填写最多C位导师,每位导师最多被每位选手填写一次(放弃某些导师也是被允许的)。
在双方的工作都完成后,进行录取工作。
每位导师都有自己战队的人数上限,这意味着可能有部分选手的较高志愿、甚至是全部志愿无法得到满足。节目组对”
前i名的录取结果最优“作出如下定义:
前1名的录取结果最优,当且仅当第1名被其最高非空志愿录取(特别地,如果第1名没有填写志愿表,那么该选手出局)。
前i名的录取结果最优,当且仅当在前i-1名的录取结果最优的情况下:第i名被其理论可能的最高志愿录取
(特别地,如果第i名没有填写志愿表、或其所有志愿中的导师战队均已满员,那么该选手出局)。
如果一种方案满足‘‘前n名的录取结果最优’’,那么我们可以简称这种方案是最优的。
举例而言,2位导师T老师、F老师的战队人数上限分别都是1人;2位选手Zayid、DuckD分列第1、2名。
那么下面3种志愿表及其对应的最优录取结果如表中所示:
可以证明,对于上面的志愿表,对应的方案都是唯一的最优录取结果。
每个人都有一个自己的理想值si,表示第i位同学希望自己被第si或更高的志愿录取,如果没有,那么他就会非常沮丧。
现在,所有选手的志愿表和排名都已公示。巧合的是,每位选手的排名都恰好与它们的编号相同。
对于每一位选手,Zayid都想知道下面两个问题的答案:
在最优的录取方案中,他会被第几志愿录取。
在其他选手相对排名不变的情况下,至少上升多少名才能使得他不沮丧。
作为《中国新代码》的实力派代码手,Zayid当然轻松地解决了这个问题。
不过他还是想请你再算一遍,来检验自己计算的正确性。
数据范围
Solution
这个题刚拿到题我就基本确定网络流算法了。。你看这名字,劈配,多么的人性化,直接把算法告诉你。然后开始思考建模,最后崩溃。
那么这个题应该怎么做呢,我们一步一步的分析:
2.1面向小范围数据
首先我们可以发现有那么一部分数据的范围真是有够小的,那么我们可以搜索,暴力查找出所有可能的方案,然后选取最优的一个,就可以先把1,2,3这三个数据点的分拿到手。
2.2针对数据特点
我们可以发现这些数据总共有两个特点,一个是C的特殊性,一个是b的特殊性。
针对C的特殊性,我们发现可以有一个贪心,就是最高的开始一个一个选,如果后面有不能选的,那么他一定就没人可选,然后暴力求解出第二问就可以很稳的拿到4,5,6,7这40分。
然后针对b的特殊性,嗯,有一个方法来着,动态加边的二分图匹配,这样又可以得到第9个点的10pts。
2.3 各种伪算法
然后就是各种奇奇怪怪的算法了,比如我考场上写的费用流,回来用mhr的思路写的拆点的网络流,还有什么极其暴力的复杂度很高的匈牙利什么的,它们都能过一部分点,但是全都不行。这里介绍一下mhr思路的那个做法,实际是可行的,只是我第二问做错了,他的第二问写的太丑T了而已。
很明显c=1的可以随便过去,那么C=其他 的呢?我们考虑拆点,把每一个人拆成总共志愿个数个点,把所有导师向汇点连边,所有的志愿都和导师连边。然后对每一个人动态加边,就是这个人先和他的第一志愿连边,然后判断是否有流量增量,如果没有,就先把这个边删了,和下一个志愿连边,这样针对任何一个人,他前面是已经连好的最优的情况,并且由于边的限制,它不会影响前面的最优情况。这样跑到最后我们就会得到一个最优解。
并且由于同一个时刻我们这里面最多有nm条边,所以并不会有时间复杂度问题,这样第一问就做出来,然后考虑第二问,我们枚举所有不满意的人,使用二分,二分这个人前进到多少名可以保证他能够满足要求。但是这样我们每一次都要重新建图,时间复杂度大概是\(O(nlogn\times n^2m)\),看上去问题很严重,当初我和mhr是这么说的,你不用跑网络流,你看你那里有个memset,你光这么多遍memset就超时了。。。
但是我们在经过一些剪枝优化之后,实际上的复杂度远远没有这么多,就可以在一个合理的时间范围内得到答案。然后估计可以是最优解最后一名了。。。
因为我第二问求错了,mhr写的太丑T了,所以这个方法就不放代码了。
2.4正解算法
在上一个mhr算法中,我们可以发现每一个人实际上只有一个利用的点,也就是说最后的图里面我们一共有\((n-1)\times m\)这么多的点白白浪费掉了。于是可以考虑能不能不拆点,找到一些其他的方法来实现那个过程。然后发现刚才拆点的目的是为了在后面的人选的时候不让前面的人选到更差的。最后删边完善了这个操作。但是实际上我们可以通过重新建图来达到同样的效果。可以发现的是第一问里面我们是非常严格的按照排名选的,一旦一个人的志愿等级确定了,那么就一定不会更改。并且这个人选的志愿等级我们已经保存在了输出数组里面。每一次直接重新调用就可以了。可以发现虽然多了一个十分繁琐的重新建图的过程,但是并没有将他的复杂度提升太多。
然后对于第二问,我们依然枚举n然后二分,但是现在的是时间复杂度变成了\(O(nlogn\times nm)\)的,就可以跑过去了。
代码如下了:
#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<queue>
#define re register
#define inf 50000001
#define ll long long
#define min(a,b) a<b?a:b
#define max(a,b) a>b?a:b
using namespace std;
struct po{
int nxt,to,w;
};
po edge[6000001];
int T,C,n,m,s,t;
int head[501],dep[501],num=-1,b[205],want[202];
int a[205][205],rs[205][205],ql[202],ans,cur[501],last,tag;
int out1[201],out2[201];
inline int read()
{
int x=0,c=1;
char ch=' ';
while((ch>'9'||ch<'0')&&ch!='-')ch=getchar();
while(ch=='-')c*=-1,ch=getchar();
while(ch<='9'&&ch>='0')x=x*10+ch-'0',ch=getchar();
return x*c;
}
inline void add_edge(int from,int to,int w)
{
edge[++num].nxt=head[from];
edge[num].to=to;
edge[num].w=w;
head[from]=num;
}
inline void add(int from,int to,int w)
{
add_edge(from,to,w);
add_edge(to,from,0);
}
inline bool bfs()
{
memset(dep,0,sizeof(dep));
queue<int> q;
while(!q.empty())
q.pop();
q.push(s);
dep[s]=1;
while(!q.empty())
{
int u=q.front();
q.pop();
for(re int i=head[u];i!=-1;i=edge[i].nxt)
{
int v=edge[i].to;
if(dep[v]==0&&edge[i].w>0)
{
dep[v]=dep[u]+1;
if(v==t)
return 1;
q.push(v);
}
}
}
return 0;
}
inline int dfs(int u,int dis)
{
if(u==t) return dis;
int diss=0;
for(re int& i=cur[u];i!=-1;i=edge[i].nxt){
int v=edge[i].to;
if(edge[i].w!=0&&dep[v]==dep[u]+1){
int check=dfs(v,min(dis,edge[i].w));
if(check>0)
{
dis-=check;
diss+=check;
edge[i].w-=check;
edge[i^1].w+=check;
if(dis==0) break;
}
}
}
return diss;
}
inline void dinic()
{
while(bfs())
{
for(re int i=s;i<=t;i++)
cur[i]=head[i];
while(int d=dfs(s,inf)) ans+=d;
}
}
void init(){
memset(out1,0,sizeof(out1));
memset(out2,0,sizeof(out2));
n=read();m=read();
s=0;t=m+n+1;
for(re int i=1;i<=n;i++) out1[i]=m+1;
for(re int i=1;i<=m;i++) b[i]=read();
for(re int i=1;i<=n;i++)
for(re int j=1;j<=m;j++)
a[i][j]=read();
for(re int i=1;i<=n;i++) want[i]=read();
}
int main()
{
//freopen("date.in","r",stdin);
T=read();C=read();
while(T--){
init();
for(re int i=1;i<=n;i++){
num=-1;memset(head,-1,sizeof(head));
for(re int j=1;j<=n;j++){if(j==i) tag=num+1;add(s,j,1);}
for(re int j=1;j<=m;j++) add(n+j,t,b[j]);
for(re int j=1;j<i;j++)
for(re int l=1;l<=m;l++)
if(a[j][l]==out1[j]) add(j,n+l,1);
dinic();
for(re int l=1;l<=m;l++){
for(re int j=1;j<=m;j++)
if(a[i][j]==l) add(i,n+j,1);
dinic();
if(edge[tag].w==0){out1[i]=l;break;}
}
}
for(re int i=1;i<=n;i++){
if(out1[i]<=want[i]) continue;
int l=1,r=i-1;out2[i]=i;
while(l<=r){
num=-1;memset(head,-1,sizeof(head));
for(re int j=1;j<=n;j++){if(j==i) tag=num+1;add(s,j,1);}
for(re int j=1;j<=m;j++) add(n+j,t,b[j]);
int mid=l+r>>1;
for(re int j=1;j<mid;j++)
for(re int l=1;l<=m;l++)
if(a[j][l]==out1[j]) add(j,n+l,1);
dinic();
for(re int j=1;j<=m;j++)
if(a[i][j]>0&&a[i][j]<=want[i]) add(i,n+j,1);
dinic();
if(edge[tag].w==0) {l=mid+1;out2[i]=i-mid;}
else r=mid-1;
}
}
for(re int i=1;i<=n;i++)
cout<<out1[i]<<" ";
cout<<endl;
for(re int i=1;i<=n;i++)
cout<<out2[i]<<" ";
cout<<endl;
}
}
3.[2017国家集训队测试]无限之环
题目描述
曾经有一款流行的游戏,叫做InfinityLoop,先来简单的介绍一下这个游戏:
游戏在一个n×m的网格状棋盘上进行,其中有些小方格中会有水管,水管可能在方格某些方向的边界的中点有接口
,所有水管的粗细都相同,所以如果两个相邻方格的公共边界的中点都有接头,那么可以看作这两个接头互相连接
。水管有以下15种形状:
游戏开始时,棋盘中水管可能存在漏水的地方。
形式化地:如果存在某个接头,没有和其它接头相连接,那么它就是一个漏水的地方。
玩家可以进行一种操作:选定一个含有非直线型水管的方格,将其中的水管绕方格中心顺时针或逆时针旋转90度。
直线型水管是指左图里中间一行的两种水管。
现给出一个初始局面,请问最少进行多少次操作可以使棋盘上不存在漏水的地方。
第一行两个正整数n,m代表网格的大小。
接下来n行每行m数,每个数是[0,15]中的一个
你可以将其看作一个4位的二进制数,从低到高每一位分别代表初始局面中这个格子上、右、下、左方向上是否有水管接头。
特别地,如果这个数是000,则意味着这个位置没有水管。
比如3(0011(2))代表上和右有接头,也就是一个L型,而12(1100(2))代表下和左有接头,也就是将L型旋转180度
数据范围
\(n×m≤2000\)
Solution
说实话这个题真的不好做,我自己也是调了一个上午,代码量要好调的就很大,但是缩减代码长度的话错了又不好调。。很恶心的一个题。
3.1确定算法
有谁可以一眼看出来这题网络流我真的服他,墙都不服就服他。这个题笔者仔细揣摩了好久才确定是费用流无疑。
具体怎么看出来的,我把我的分析过程跟大家说一下:首先考虑这个题最可能的算法,看到这种状态很多的题目首先就是思考能不能搜索,然后发现复杂度太高过不去。接着就是动态规划,笔者是没想出来,但是有一个大概的动态规划的构思,就是根据水管的种类其实很少,一共就15种,可不可以搞一个状压DP试试,读者有兴趣的可以自己思考。然后这题不可能是数据结构,那排除了一切不可能的情况之后,剩下的再不可能也是可能的了。于是思考网络流。
我们可以这么考虑,只要一个格子有了一个头,那么它就必须和旁边的一个格子里的头对应起来,否则就不能形成通路。所有格子的任何一个头都满足这个道理。那么我们就需要让他们全部匹配。
嗯,匹配,似乎和网络流沾上点边了。
3.2问题简化1
可以先考虑简单的版本,如果就是给了你这些水管的状态,也没让你转动他们,你是否能够用网络流算出来它能不能形成一条通路呢?答案是可以,我们发现一个地方的水管只和它上下左右的水管有着直接的联系。这样我们就把这个简化过的问题精简成了一个经典的网络流题目的模型,没错,黑白染色模型。我们拆点之后求最大流是否是水管的接头数就可以了。
3.3问题简化2
我们把这个简单的版本升华一下,现在你可以转动这些水管了,我们能不能使它形成一个通路呢?答案仍然是可行的,我们只需要拆完点之后把可以转换到的状态之间互相连边就可以了。仍然是求最大流是否是水管接头数。
3.4回到题目中
现在我们回到原来的题目中,不难发现,我们只要在问题简化2的基础上增加一个费用就可以了。那么我们如何设置这些费用呢?
很明显这个题有三种不同样式的格子,一个是死胡同形,一个是L形,还有一个T形。剩下的两种要么不能动,要么动了和没动一样,不需要讨论。
对于死胡同形,很明显转到旁边需要1的花费,而转到对面需要2的花费。
对于L形,不大明显的是我们转动90度就相当于把一条边转到了对面,而180度就是完全旋转过去。。。
对于T形,可以发现他就是一个反过来的死胡同形,反过来连边就可以了。。
那么这个题就可以套用费用流的板子了:
#pragma comment(linker, "/STACK:1024000000,1024000000")
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<queue>
#define inf 1000000001
#define re register
#define ll long long
#define min(a,b) a<b?a:b
#define MAXN 20001
#define MAXM 40000005
#define id1 nm[i][j]
#define id2 (nm[i][j]+(n*m))
#define id3 (nm[i][j]+(n*m*2))
#define id4 (nm[i][j]+(n*m*3))
#define id5 (nm[i][j]+(n*m*4))
using namespace std;
int n,m,s,t;
int head[MAXN],num=-1,tot,dis[MAXN],b[MAXN],maxx;
int to[100005],nxt[100005],w[100005],edis[100005];
int a[2001][2001],nm[2001][2001];
inline int read()
{
int x=0,c=1;
char ch=' ';
while((ch>'9'||ch<'0')&&ch!='-')ch=getchar();
while(ch=='-')c*=-1,ch=getchar();
while(ch<='9'&&ch>='0')x=x*10+ch-'0',ch=getchar();
return x*c;
}
inline void add_edge(int from,int too,int ww,ll dis)
{
nxt[++num]=head[from];
to[num]=too;
w[num]=ww;
edis[num]=dis;
head[from]=num;
}
inline void add(int from,int too,int ww,ll dis)
{
add_edge(from,too,ww,dis);
add_edge(too,from,0,-dis);
}
inline bool spfa()
{
for(re int i=s;i<=t;i++) dis[i]=inf+1;
memset(b,0,sizeof(b));
deque<int> q;
q.push_back(t);
dis[t]=0;
b[t]=1;
while(!q.empty()){
int u=q.front();
q.pop_front();
b[u]=0;
for(re int i=head[u];i!=-1;i=nxt[i]){
int v=to[i];
if(w[i^1]>0&&dis[v]>dis[u]-edis[i]){
dis[v]=dis[u]-edis[i];
if(!b[v]){
b[v]=1;
if(!q.empty()&&dis[v]<dis[q.front()])
q.push_front(v);
else
q.push_back(v);
}
}
}
}
return dis[s]<inf;
}
inline int dfs(int u,int low)
{
b[u]=1;
if(u==t) return low;
int diss=0;
for(re int i=head[u];i!=-1;i=nxt[i]){
int v=to[i];
if(w[i]&&!b[v]&&dis[v]==dis[u]-edis[i]){
int check=dfs(v,min(w[i],low));
if(check){
tot+=check*edis[i];
low-=check;
diss+=check;
w[i]-=check;
w[i^1]+=check;
if(low==0) break;
}
}
}
return diss;
}
inline int max_flow()
{
int ans=0;
while(spfa()){
b[t]=1;
while(b[t]){
memset(b,0,sizeof(b));
ans+=dfs(s,inf);
}
}
return ans;
}
int main()
{
//freopen("date.in","r",stdin);
memset(head,-1,sizeof(head));
n=read();m=read();
for(re int i=1;i<=n;i++)
for(re int j=1;j<=m;j++)
a[i][j]=read(),nm[i][j]=(i-1)*m+j;
s=0;t=5*n*m+1;
for(re int i=1;i<=n;i++)
for(re int j=1;j<=m;j++){
if((i+j)%2==0) add(s,nm[i][j],inf,0);
else add(nm[i][j],t,inf,0);
}
for(re int i=1;i<=n;i++){
for(re int j=1;j<=m;j++)
if((i+j)%2==0){
if(a[i][j]==1){add(id1,id2,1,0);add(id2,id3,1,1);add(id2,id5,1,1);add(id2,id4,1,2);maxx++;}
if(a[i][j]==2){add(id1,id3,1,0);add(id3,id2,1,1);add(id3,id4,1,1);add(id3,id5,1,2);maxx++;}
if(a[i][j]==4){add(id1,id4,1,0);add(id4,id3,1,1);add(id4,id5,1,1);add(id4,id2,1,2);maxx++;}
if(a[i][j]==8){add(id1,id5,1,0);add(id5,id2,1,1);add(id5,id4,1,1);add(id5,id3,1,2);maxx++;}
if(a[i][j]==3){add(id1,id2,1,0);add(id1,id3,1,0);add(id2,id4,1,1);add(id3,id5,1,1);maxx+=2;}
if(a[i][j]==6){add(id1,id3,1,0);add(id1,id4,1,0);add(id3,id5,1,1);add(id4,id2,1,1);maxx+=2;}
if(a[i][j]==9){add(id1,id5,1,0);add(id1,id2,1,0);add(id5,id3,1,1);add(id2,id4,1,1);maxx+=2;}
if(a[i][j]==12){add(id1,id4,1,0);add(id1,id5,1,0);add(id4,id2,1,1);add(id5,id3,1,1);maxx+=2;}
if(a[i][j]==7){add(id1,id2,1,0);add(id1,id3,1,0);add(id1,id4,1,0);add(id2,id5,1,1);add(id3,id5,1,2);add(id4,id5,1,1);maxx+=3;}
if(a[i][j]==11){add(id1,id2,1,0);add(id1,id3,1,0);add(id1,id5,1,0);add(id2,id4,1,2);add(id3,id4,1,1);add(id5,id4,1,1);maxx+=3;}
if(a[i][j]==13){add(id1,id2,1,0);add(id1,id4,1,0);add(id1,id5,1,0);add(id2,id3,1,1);add(id4,id3,1,1);add(id5,id3,1,2);maxx+=3;}
if(a[i][j]==14){add(id1,id3,1,0);add(id1,id4,1,0);add(id1,id5,1,0);add(id3,id2,1,1);add(id4,id2,1,2);add(id5,id2,1,1);maxx+=3;}
if(a[i][j]==5){add(id1,id2,1,0);add(id1,id4,1,0);maxx+=2;}
if(a[i][j]==10){add(id1,id3,1,0);add(id1,id5,1,0);maxx+=2;}
if(a[i][j]==15){add(id1,id2,1,0);add(id1,id3,1,0);add(id1,id4,1,0);add(id1,id5,1,0);maxx+=4;}
} else {
if(a[i][j]==1){add(id2,id1,1,0);add(id3,id2,1,1);add(id5,id2,1,1);add(id4,id2,1,2);maxx++;}
if(a[i][j]==2){add(id3,id1,1,0);add(id2,id3,1,1);add(id4,id3,1,1);add(id5,id3,1,2);maxx++;}
if(a[i][j]==4){add(id4,id1,1,0);add(id3,id4,1,1);add(id5,id4,1,1);add(id2,id4,1,2);maxx++;}
if(a[i][j]==8){add(id5,id1,1,0);add(id2,id5,1,1);add(id4,id5,1,1);add(id3,id5,1,2);maxx++;}
if(a[i][j]==3){add(id2,id1,1,0);add(id3,id1,1,0);add(id4,id2,1,1);add(id5,id3,1,1);maxx+=2;}
if(a[i][j]==6){add(id3,id1,1,0);add(id4,id1,1,0);add(id5,id3,1,1);add(id2,id4,1,1);maxx+=2;}
if(a[i][j]==9){add(id5,id1,1,0);add(id2,id1,1,0);add(id3,id5,1,1);add(id4,id2,1,1);maxx+=2;}
if(a[i][j]==12){add(id4,id1,1,0);add(id5,id1,1,0);add(id2,id4,1,1);add(id3,id5,1,1);maxx+=2;}
if(a[i][j]==7){add(id2,id1,1,0);add(id3,id1,1,0);add(id4,id1,1,0);add(id5,id2,1,1);add(id5,id3,1,2);add(id5,id4,1,1);maxx+=3;}
if(a[i][j]==11){add(id2,id1,1,0);add(id3,id1,1,0);add(id5,id1,1,0);add(id4,id2,1,2);add(id4,id3,1,1);add(id4,id5,1,1);maxx+=3;}
if(a[i][j]==13){add(id2,id1,1,0);add(id4,id1,1,0);add(id5,id1,1,0);add(id3,id2,1,1);add(id3,id4,1,1);add(id3,id5,1,2);maxx+=3;}
if(a[i][j]==14){add(id3,id1,1,0);add(id4,id1,1,0);add(id5,id1,1,0);add(id2,id3,1,1);add(id2,id4,1,2);add(id2,id5,1,1);maxx+=3;}
if(a[i][j]==5){add(id2,id1,1,0);add(id4,id1,1,0);maxx+=2;}
if(a[i][j]==10){add(id3,id1,1,0);add(id5,id1,1,0);maxx+=2;}
if(a[i][j]==15){add(id2,id1,1,0);add(id3,id1,1,0);add(id4,id1,1,0);add(id5,id1,1,0);maxx+=4;}
}
}
for(re int i=1;i<=n;i++)
for(re int j=1;j<=m;j++){
if((i+j)%2==0){
if(i>1) add(id2,id4-m,1,0);
if(j>1) add(id5,id3-1,1,0);
if(i<n) add(id4,id2+m,1,0);
if(j<m) add(id3,id5+1,1,0);
}
}
int d=max_flow();
cout<<((maxx==d<<1)?tot:-1);
return 0;
}
4.[HAOI2017]新型城市化
题目描述
Anihc国有n座城市.城市之间存在若一些贸易合作关系.如果城市x与城市y之间存在贸易协定.那么城市文和城市y则是一对贸易伙伴(注意:(x,y)和(y,x))是同一对城市)。
为了实现新型城市化.实现统筹城乡一体化以及发挥城市群辐射与带动作用.国 决定规划新型城市关系。一些城市能够被称为城市群的条件是:这些城市两两都是贸易伙伴。 由于Anihc国之前也一直很重视城市关系建设.所以可以保证在目前已存在的贸易合作关系的情况下Anihc的n座城市可以恰好被划分为不超过两个城市群。
为了建设新型城市关系Anihc国想要选出两个之前并不是贸易伙伴的城市.使这两个城市成为贸易伙伴.并且要求在这两个城市成为贸易伙伴之后.最大城市群的大小至少比他们成为贸易伙伴之前的最大城市群的大小增加1。
Anihc国需要在下一次会议上讨论扩大建设新型城市关系的问题.所以要请你求出在哪些城市之间建立贸易伙伴关系可以使得这个条件成立.即建立此关系前后的最大城市群的 大小至少相差1。
输入输出格式
输入格式:
第一行2个整数n,m.表示城市的个数,目前还没有建立贸易伙伴关系的城市的对数。
接下来m行,每行2个整数x,y表示城市x,y之间目前还没有建立贸易伙伴关系。
输出格式:
第一行yi个整数ans,表示符合条件的城市的对数.注意(x,y)与(y,x)算同一对城市。
接下来Ans行,每行两个整数,表示一对可以选择来建立贸易伙伴关系的城市。对于 一对城市x,y请先输出编号更小的那一个。最后城市对与城市对之间顺序请按照字典序从小到大输出。
输入输出样例
输入样例#1:
5 3
1 5
2 4
2 5
输出样例#1:
2
1 5
2 4
说明
数据规模与约定
数据点1: n≤16
数据点2: n≤16
数据点3~5: n≤100
数据点6: n≤500
数据点7~10: n≤10000
对于所有的数据保证:n <= 10000,0 <= m <= min (150000,n(n-1)/2).保证输入的城市关系中不会出现(x,x)这样的关系.同一对城市也不会出现两次(无重边.无自环)。
Solution
既然把题放在这里了,那么自然就不会像题解里写的solution一样了。
其实这个题的主体思路在前面的几个分块中没有来的及体现出来,涉及到二分图的问题。这也是笔者一个失误,二分图类的问题一定近期补到前面去。
那么我们继续来看这道题。题目给出的是一些城市之间的关系。然后看那个城市群的定义,这些城市两两之间能够互相到达。加上给出的城市之间的关系是之间没有路径能够互相到达,那么就比较明显了,如果我们根据题目中要求把城市连起来,那么我们一定可以得到一个二分图。(为什么忽略了只能是一个城市群的情况??因为就一个城市群你也没有可以建立的关系了,这么出题没有意义。)
然后我们仔细考虑这个题让我们干什么,在两个本来没有关系的城市之间建立关系,就是从我们刚刚建立的二分图里面删去一条边,最大城市群的大小增加1,就是让我们这个二分图里的最大独立集增加1。又因为定理:最大独立集=总点数-最小覆盖集=最大匹配。所以我们要求的就是删去一条边可以使二分图的最大匹配减小1的总边数。然后我们可以用网络流来实现。
那么我们现在就有了一个初步的做法了,大体思路就是枚举所有的边,然后不停的网络流,之后慢慢的计算有那些边可以删掉。不过这样过5个点也就差不多快超时了,网络上有大佬说退流或许能够达到更好的时间效率,不过我看上去并没有从根本上优化算法,所以过掉第6个点还是悬。那么我们有没有更优化的方法呢?
有的,根据定理:若一条边一定在最大匹配中,则在最终的残量网络中,这条边一定满流,且这条边的两个顶点一定不在同一个强连通分量中。 可得,我们只需要通过残量网络跑一遍Tarjan就可以了。那么这个定理又是怎么来的呢。。(话说为什么有这么多的定理。)
笔者浅显的证明一下:首先要满流,然后不在同一个强连通分量里是指,如果在同一个强连通分量里,那么我们在这个强连通分量内部增广一下,整个分量里的残量都会变化,但是网络的最大匹配并不会变化,也就是说我们又能得到一个新的最大匹配,也就是说这条边的存在是无可厚非的。
那么可以放上代码了。
Code
#pragma comment(linker, "/STACK:1024000000,1024000000")
#include <iostream>
#include <cstdlib>
#include <cmath>
#include <string>
#include <cstring>
#include <algorithm>
#include <cstdio>
#include <queue>
#include <set>
#include <map>
#define MAXN 200000
#define re register
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
#define ms(arr) memset(arr, 0, sizeof(arr))
const int inf = 0x3f3f3f3f;
struct po
{
int nxt,to,w,from;
}edge[250001];
struct ANS
{
int x,y;
}ans[MAXN];
int n,m,cur[MAXN],head[20002],num=-1,dep[20002],s,t,c[MAXN],vis[20002];
inline int read()
{
int x=0,c=1;
char ch=' ';
while((ch>'9'||ch<'0')&&ch!='-')ch=getchar();
while(ch=='-') c*=-1,ch=getchar();
while(ch<='9'&&ch>='0')x=x*10+ch-'0',ch=getchar();
return x*c;
}
inline void add_edge(int from,int to,int w)
{
edge[++num].nxt=head[from];
edge[num].from=from;
edge[num].to=to;
edge[num].w=w;
head[from]=num;
}
inline void add(int from,int to,int w)
{
add_edge(from,to,w);
add_edge(to,from,0);
}
inline bool bfs()
{
memset(dep,0,sizeof(dep));
queue<int> q;
while(!q.empty())
q.pop();
q.push(s);
dep[s]=1;
while(!q.empty())
{
int u=q.front();
q.pop();
for(re int i=head[u];i!=-1;i=edge[i].nxt)
{
int v=edge[i].to;
if(dep[v]==0&&edge[i].w>0)
{
dep[v]=dep[u]+1;
if(v==t)
return 1;
q.push(v);
}
}
}
return 0;
}
inline int dfs(int u,int dis)
{
if(u==t)
return dis;
int diss=0;
for(re int& i=cur[u];i!=-1;i=edge[i].nxt)
{
int v=edge[i].to;
if(edge[i].w!=0&&dep[v]==dep[u]+1)
{
int check=dfs(v,min(dis,edge[i].w));
if(check!=0)
{
dis-=check;
diss+=check;
edge[i].w-=check;
edge[i^1].w+=check;
if(dis==0) break;
}
}
}
return diss;
}
inline void dinic()
{
while(bfs())
{
for(re int i=s;i<=t;i++)
cur[i]=head[i];
while(int d=dfs(s,inf));
}
}
void put_color(int u,int col)
{
c[u]=col;
vis[u]=1;
for(re int i=head[u];i!=-1;i=edge[i].nxt){
int v=edge[i].to;
if(!vis[v]) put_color(v,col^1);
}
}
int dfn[MAXN],low[MAXN],stack[MAXN],color_num,color[MAXN],cnt,top;
inline void Tarjan(int u)
{
dfn[u]=low[u]=++cnt;
vis[u]=1;
stack[++top]=u;
for(re int i=head[u];i!=-1;i=edge[i].nxt){
if(!edge[i].w){
int v=edge[i].to;
if(!dfn[v]){
Tarjan(v);
low[u]=min(low[u],low[v]);
} else if(vis[v]) low[u]=min(low[u],dfn[v]);
}
}
if(dfn[u]==low[u]){
color[u]=++color_num;
vis[u]=0;
while(stack[top]!=u){
color[stack[top]]=color_num;
vis[stack[top--]]=0;
}
top--;
}
}
int x[MAXN],y[MAXN],tot;
inline bool cmp(ANS a,ANS b){
return a.x==b.x?a.y<b.y:a.x<b.x;
}
int main()
{
memset(head,-1,sizeof(head));
n=read();m=read();
for(re int i=1;i<=m;i++){
x[i]=read();y[i]=read();
add_edge(x[i],y[i],0);
add_edge(y[i],x[i],1);
}
for(re int i=1;i<=n;i++)
if(!vis[i]) put_color(i,2);
memset(head,-1,sizeof(head));
s=0,t=n+1;num=-1;
for(re int i=1;i<=n;i++){
if(c[i]==2)
add(s,i,1);
else add(i,t,1);
}
for(re int i=1;i<=m;i++){
if(c[x[i]]==2)
add(x[i],y[i],1);
else add(y[i],x[i],1);
}
dinic();
memset(vis,0,sizeof(0));
for(re int i=1;i<=n;i++)
if(!dfn[i]) Tarjan(i);
for(re int i=0;i<=num;i+=2){
int u=edge[i].from,v=edge[i].to;
if(!edge[i].w&&color[u]!=color[v]&&u!=s&&v!=t&&u!=t&&v!=s){
if(u>v) swap(u,v);
ans[++tot].x=u;ans[tot].y=v;
}
}
sort(ans+1,ans+tot+1,cmp);
cout<<tot<<endl;
for(re int i=1;i<=tot;i++)
printf("%d %d\n", ans[i].x,ans[i].y);
return 0;
}
/*6 5 3 7 2 4 1*/
待续。。。