C++博客-Mato is No.1-随笔分类-网络流http://www.cppblog.com/MatoNo1/category/16324.htmlMato是一只超级大沙茶……比赛结果从后往前排列,Mato总是No.1……zh-cnSat, 12 May 2012 21:25:17 GMTSat, 12 May 2012 21:25:17 GMT60关于网络流建模的方法(一)http://www.cppblog.com/MatoNo1/archive/2012/05/11/174440.htmlMato_No1Mato_No1Fri, 11 May 2012 13:32:00 GMThttp://www.cppblog.com/MatoNo1/archive/2012/05/11/174440.htmlhttp://www.cppblog.com/MatoNo1/comments/174440.htmlhttp://www.cppblog.com/MatoNo1/archive/2012/05/11/174440.html#Feedback0http://www.cppblog.com/MatoNo1/comments/commentRss/174440.htmlhttp://www.cppblog.com/MatoNo1/services/trackbacks/174440.html
网络流建模主要分为两类:直接用最大流建模、用最大流—最小割定理转化为最小割来建模。这里主要总结的是前一种。

(1)增广路思想:
应用范围较小,但是确实有一些模型用增广路思想很容易解释,用流量平衡思想却很难解释(比如下面举的例子)。
增广路思想可以概括为:原题的方案的得出可以很明显地分为一些阶段,每一阶段都会对一些变量(这些变量可能是实的也可能是虚设的)产生同样的效果值累加,而这些变量恰好有各自的限制,且互不关联。这刚好相当于网络中的一条从源点到汇点的一条增广路,对路上所有边的流量都会增加,且流量有各自限制(容量),且互不关联。并且,该模型满足下面(3)中的两条原则(可行性原则和最优性原则)。在比较多的时候,用增广路思想能够解释的模型往往是一个很明显的“物质路径”模型,某一种物质(可以是实的也可以是虚的)从源点往汇点“走”,边上的流量代表物质经过的量。
例1:[NOIP2011]观光公交
首先,由于来出发地的时间已知且一定,所以“旅行时间总和最小”其实就是所有人下车的时间总和尽可能小,因此,先求出在不用任何加速器(初始)情况下,到达每一站的时间,设为S[i],又设M[i]为在第i站上车的来的最晚的人来的时间,则很显然可以得到初始的递推式:S[i]=max{S[i-1], M[i-1]}+D[i-1](初始的D值),边界S[0]=0。
下面来看一下D[i]的减少是如何影响S值的。看下面这个例子:
N=5
i                  :  0   1   2   3   4
D[i](初始):  3   4   3   2   \
M[i]              : 1   2   6  14   \
S[i](初始):  0  4   8  11  16
现在将D[0]的值减小1之后:
i    :  0   1   2   3   4
D[i]:  2   4   3   2   \
M[i]: 1   2   6  14   \
S[i]:  0  3   7  10  16
可以发现,D[0]值减小1之后,S[1..3]的值都减小了1,而S[4]的值不变。这是因为在D[0]减小1之前,对于1<=i<3均有S[i]>M[i],D[0]若减小1,显然S[1]会减小1,而由于S[1]>M[1],S[1]=max{S[1], M[1]},所以S[1]的值减小1会使得max{S[1], M[1]}减小1,从而S[2]的值减小1,然后由于初始的S[2]>M[2],同样会使得S[3]减小1,而初始的S[3]<=M[3],故S[3]减小1不会使得max{S[3], M[3]}发生变化,所以S[4]的值不会受到影响。
所以,可以得到:D[i]减小1,会使得S[i+1..j+1]均减小1,其中j是使任意i+1<=k<=j0均满足S[k](减小前)>M[k]的最大的j0值。
从这个当中可以发现,对于原题的每一个可行方案,必然都是分为若干个阶段,其中每一阶段是将某个D[i]值减小1(当然,要满足D[i]在减小前>0),每一阶段进行后都会将从S[i+1]开始的连续的一段S值都减小1,恰好可以抽象成一条连续的路径,又因为当S[i]减小到<=M[i]的时候就必须停止了(准确来说是不能再往后延伸了),所以每个S[i]的能够继续延伸的减小的量都是有限的,为初始的S[i]-M[i](如果这个值<0,则取0),刚好是一个上限。这很明显是增广路思想。
所以,经过整理,可以建立一个网络流模型:
<1>设立两个源点s和s'(其中s是真正的源点)及汇点t,连边<s, s'>,容量为K,费用为0,表示最多只能有K个阶段;
<2>将每一站i拆成两个点i'和i'',连边<i', i''>,容量为max(S[i]-M[i], 0),费用为0,表示该点最多只能接受max(S[i]-M[i], 0)次加速器作用;
<3>对于所有的i满足1<=i<N,连边<(i-1)'', i'>,容量为INF,费用为第i站下车的人数(这是因为即使S[i]<=M[i],加速器对于本站仍然有效,只是不能继续延伸,所以表示加速器起的效果的边应该在本站的限制之前);
<4>对于所有的i满足0<=i<N-1,连边<s', i''>,容量为初始D[i],费用为0,表示使用加速器的地方,从下一站开始对S[i]起效果;
<5>对于所有的i满足1<=i<N,连边<i', t>,容量为INF,费用为0,表示加速器作用的结束。
(其实,0'和(N-1)''这两个点是木有任何意义的,可以从图中删掉)
这样,每一阶段加速器的作用都可以表示为一条从s到t的增广路,该网络流模型中的各种限制也反应了题目中的限制。对该网络求最大费用最大流,得到的总的最大费用从初始的总旅行时间中减去(注意总旅行时间是long long的),即为答案。可以证明,这个模型符合“两条原则”,所以是正确的。

(2)流量平衡思想:
这个思想的应用非常广,可以解释绝大多数网络流模型。
所谓流量平衡,就是指在一个可行流里,除了源点和汇点外,其余每个点的入边流量总和都等于出边流量总和。可以证明,一个流是可行流当且仅当其:(1)每条边的流量都不超过容量限制;(2)符合流量平衡。
流量平衡思想的主要用处是:可以把图中的每条边的流量(当然必须是非负的)都想像为一个变量的值,对于每个点,满足流量平衡,也就是一些变量的和值满足某种等量关系,如果这些等量关系刚好能够反映题目中的所有信息,边的容量限制也反映题目中的条件,且这个模型符合“两条原则”,则该模型就是正确的了。在建模的时候,应先单独考虑各个点,找到它们的所有入边和出边代表的变量是什么,然后再将这些边合并,构成图。
在用流量平衡建模时有一些技巧:
<1>要注意每条边都同时作为一个点的出边和一个点的入边,因此,每个变量必然同时关联两个等量关系,且分别出现在这两个等量关系的等号的左边和右边(或者是以一对相反数形式出现);
<2>如果题目中给出的变量和值关系不是等量关系,而是不等关系,那么可以将剩余的流量通过从源点或往汇点连边的办法,使其平衡。比如,若题目中有y1+y2>=x1+x2>=y1+y2-5这样的关系,则可以这样做:设置一个点,将y1、y2代表的边作为该点的入边,将x1、x2代表的边作为该点的出边,然后从该点往汇点连一条容量为5的边;
<3>如果点内部有限制(比如某个点自身的权值不能超过X等等),那么该点内部也“暗含”一个变量,此时就需要拆点(不一定拆成两个点,可能拆成更多的点),然后在拆出的点当中再连边,附加一些限制,然后再考虑流量平衡;
<4>如果一条边有上下界,且上下界相等(也就是该边的流量已经定死了),则可以改装成费用流,将这条边的费用设为一个绝对值很大的负数,这样就肯定能保证该边满流了。
例2:餐巾计划问题(经典问题)
这个的模型用增广路思想根本就不能解释。其实,可以用增广路思想建立一个模型,但是是错误的,可以用下面的“两条原则”检查出来。
<1>对于每天,要处理的餐巾总数=当天买的餐巾总数+当天洗好的餐巾总数+上一天保留下来的未处理的餐巾总数,这三个当作入边;
<2>对于每天,要处理的餐巾总数=送快洗部的餐巾总数+送慢洗部的餐巾总数+保存起来留到下一天处理的餐巾总数,这三个都当作出边;
<3>每天的内部有限制:要用的餐巾总数>=当天的需求量,其实,总可以构造出要用的餐巾总数=当天的需求量的最优方案,所以这些限制其实是上下界相等的。
而<1>和<2>刚好描述了每天这个整体的流量平衡,<3>是一个内部限制,用拆点解决。仔细观察所有的边可以发现,“当天洗好的餐巾总数”与“送快洗部的餐巾总数”和“送慢洗部的餐巾总数”可以合并,“上一天保留下来的未处理的餐巾总数”与“保存起来留到下一天处理的餐巾总数”也可以合并。
这样,可以构造出两种模型:
1):第i天拆成两个点i'和i'',连边<i', i''>,容量为第i天需求量,费用为0;对于任意0<=i<N-1,连边<i'', (i+1)''>,容量INF,费用0;对于任意0<=i<N,连边<S, i'>,容量INF,费用p,连边<i'', T>,容量INF,费用0;对于任意0<=i<N-m,连边<i'', (i+m)'>,容量INF,费用f;对于任意0<=i<N-n,连边<i'', (i+n)'>,容量INF,费用s;求最小费用最大流,最小的总费用就是结果;
2):第i天拆成两个点i'和i'',连边<S, i''>和<i', T>,容量均为第i天需求量,费用均为0;对于任意0<=i<N-1,连边<i'', (i+1)''>,容量INF,费用0;对于任意0<=i<N,连边<S, i'>,容量INF,费用p;对于任意0<=i<N-m,连边<i'', (i+m)'>,容量INF,费用f;对于任意0<=i<N-n,连边<i'', (i+n)'>,容量INF,费用s;求最小费用最大流,最小的总费用就是结果。
以上两种模型,看上去都符合题目中的限制,也符合流量平衡,但是,模型1)是错误的,模型2)是正确的,这是为什么呢?

(3)判定网络流模型是否正确的两个原则:
<1>可行性原则:原题中的每一种可行方案,在建立的网络流模型中都对应着一个“能求出的”流(一般是满足一定的条件的流,比如某些边必须满流等),注意这里的对应必须是“一一对应”,就是,既不能有可行方案丢失,也不能出现不可行方案;
<2>最优性原则:原题中的最优方案(准确来说是最优方案的结果),在建立的网络流模型中都对应着一个“能求出的”量(最大流量或者满足最大流量的前提下的最小费用),也就是,最优结果必须是可以通过这个模型求出的。
一个网络流模型正确,当且仅当其符合以上两条原则。
这两个原则可以检查所建立的网络流模型是否正确。比如,对于例2中的两个模型,模型1)由于最大流对应的是“买的餐巾总数尽可能多”的方案,不是最优方案,因此原题中的最优结果无法求出,显然不符合最优性原则,因此它是错误的。模型2)中,由于可行方案必然能使所有<S, i''>中的边满流,且能够求出,符合可行性原则;最优方案由于<i', T>这条边的限制,必然是最大流,且是费用最小的最大流,其最小费用为最优结果,符合最优性原则,因此它是正确的。


Mato_No1 2012-05-11 21:32 发表评论
]]>
网络流图边表的新表示法:Dancing Link边表(解决需要删边或删点或改容量的多次求最大流问题)http://www.cppblog.com/MatoNo1/archive/2011/05/07/145885.htmlMato_No1Mato_No1Sat, 07 May 2011 06:12:00 GMThttp://www.cppblog.com/MatoNo1/archive/2011/05/07/145885.htmlhttp://www.cppblog.com/MatoNo1/comments/145885.htmlhttp://www.cppblog.com/MatoNo1/archive/2011/05/07/145885.html#Feedback0http://www.cppblog.com/MatoNo1/comments/commentRss/145885.htmlhttp://www.cppblog.com/MatoNo1/services/trackbacks/145885.html
struct edge {
        
int a, b, f, next;
} ed[MAXM 
+ MAXM];
表示这条边起点为a,终点为b,容量为f,邻接边(就是下一条起点为a的边)的编号为next。
如果要求多次最大流,那么在每次求最大流之前就要把所有边的容量恢复到初始值,解决方法是引入一个“初始容量”域fs,在这条边刚刚被加入的时候将它的fs值和f值都设为初始给它的容量,在恢复时,只要将所有边的f值恢复到fs即可。若要改变边容量,则将该边的fs值和f值都设为改变后的容量即可。

下面来分析需要删边或删点的情况。在这种情况下,如果采用只有next的单向链表,则删除时next域极难处理,而且,在一般的边表中,还设立了表头hd,hd[a]表示边表中起点为a的第一条边的编号。因此,若删除的边<a, b, f>是起点为a的第一条边,还会影响hd[a]的值,使情况变得更为复杂。因此,必须采用双向链表!还记得Dancing Link么?边表其实也可以搞成Dancing Link,方法如下:
设图中有N个点,M条边(注意,这M条边只包括正向边,不包括反向边。由于每条正向边<a, b, f>都对应一条反向边<b, a, 0>,因此边表中边的数目其实是M+M)。首先把边表ed的0~N这(N+1)个位置(下标)空出来,作表头(表头不是边,因此在遍历边的时候不会遍历到它们)。其中,ed[0]为总表头,用于遍历ed[1..N]中每个未被删去的点;ed[1..N]为单点表头,ed[i]用来遍历图中所有以i为起点的边(和DLX中的二维DL惊人相似)。然后,若N为偶数,则空一个位置(也就是将ed[N+1]丢弃不用),这是因为我们在增广过程中需要引用到一条边对应的逆向边(正向边对应反向边,反向边对应正向边),一般认为编号为p的边对应的逆向边是p ^ 1,这样,就要求图中所有正向边的编号都是偶数,所有反向边的编号都是奇数(否则会造成混乱)。因此当N为偶数时,(N+1)为奇数,不能放置第一条正向边,需要从ed[N+2]开始放置正向边。若N为奇数则不用空位。
接下来就是边类型了。在这里,边类型一共需要包括6个域:a, b, fs, f, pre, next,表示这条边起点为a,终点为b,初始容量为fs,当前容量为f,上一条起点为a的边编号为pre,下一条起点为a的边编号为next。注意,和DL一样,整个链表是循环的,也就是我们认为表中最后一条起点为a的边的下一条邻接边编号就是a(表头),同样,a的上一条邻接边也就是这条边。
struct edge {
    
int a, b, fs, f, pre, next;
} ed[MAXM 
+ MAXM];
接下来就是几个重要过程了。
(1)初始化表头:
void init_d()
{
    re1(i, n) {ed[i].a 
= i; ed[i].b = -1; ed[i].f = 0; ed[i].pre = ed[i].next = i;}
    
if (n % 2) m = n + 1else m = n + 2;
}
这里n是图中的点数(相当于N),m是边的编号指针(相当于DLX中的nodes)
(2)添加新边:
void add_edge(int a, int b, int f)
{
    ed[m].a 
= a; ed[m].b = b; ed[m].fs = ed[m].f = f; ed[m].pre = ed[a].pre; ed[m].next = a; ed[a].pre = m; ed[ed[m].pre].next = m++;
    ed[m].a 
= b; ed[m].b = a; ed[m].fs = ed[m].f = 0; ed[m].pre = ed[b].pre; ed[m].next = b; ed[b].pre = m; ed[ed[m].pre].next = m++;
}
这个和DLX类似,不解释了囧……

最后进入最核心的部分——到底如何处理删边或删点?有了DL型边表就爆好搞了:删去一条边,只要直接删去该边在DL中的位置即可:
void deledge(int No)
{
    ed[ed[No].pre].next 
= ed[No].next; ed[ed[No].next].pre = ed[No].pre;
}
恢复一条已删去的边:
void resuedge(int No)
{
    ed[ed[No].pre].next 
= ed[ed[No].next].pre = No;
}
需要删点的情况类似,对单点表头处理即可。

【具体题目】PKU1815
这题就是求有向图的字典序最小的最小点割集问题,方法是先求出最小点连通度(有关最小点连通度的求法见图的连通度问题的求法),然后按编号递增顺序枚举每个点,若删去该点(其实是删去建成的新图中该点i'到该点附加点i''之间的边)后图的最小点连通度减小,则应删去该点,否则不应删去该点。删去后,继续枚举下一个点,直到求出点割集为止。
注意,本题只有删边,没有删点,因此总表头可以不需要,直接从ed[0]开始作单点表头。此时,关于是否空位就刚好反过来了:如果N是奇数就要空位,N是偶数不空位(不过这题里由于建出的网络流图中有2*N0个结点,总是偶数,可以不管,不过本沙茶还是管这个了)。

代码(神犇不要鄙视):
#include <iostream>
#include 
<stdio.h>
using namespace std;
#define re(i, n) for (int i=0; i<n; i++)
const int MAXN = 501, MAXM = 100000, INF = ~0U >> 2;
struct edge {
    
int a, b, fs, f, pre, next;
} ed[MAXM 
+ MAXM];
int n0, n, m = 0, s, t, start[MAXN], pr[MAXN], hs[MAXN], lev[MAXN], q[MAXN], now, flow, reslen = 0, res[MAXN];
bool vst[MAXN];
void init_d()
{
    re(i, n) {ed[i].a 
= i; ed[i].b = -1; ed[i].f = 0; ed[i].pre = ed[i].next = i;}
    
if (n % 2) m = n + 1else m = n;
}
void add_edge(int a, int b, int f)
{
    ed[m].a 
= a; ed[m].b = b; ed[m].fs = ed[m].f = f; ed[m].pre = ed[a].pre; ed[m].next = a; ed[a].pre = m; ed[ed[m].pre].next = m++;
    ed[m].a 
= b; ed[m].b = a; ed[m].fs = ed[m].f = 0; ed[m].pre = ed[b].pre; ed[m].next = b; ed[b].pre = m; ed[ed[m].pre].next = m++;
}
void init()
{
    
int x;
    scanf(
"%d%d%d"&n0, &s, &t);
    n 
= n0 << 1; s--; t += n0 - 1; init_d();
    re(i, n0) re(j, n0) {
        scanf(
"%d"&x);
        
if (i == j) {
            
if (i == s || i == t - n0) add_edge(i, i + n0, INF); else add_edge(i, i + n0, 1);
        } 
else if (x) add_edge(i + n0, j, INF);
    }
}
void aug()
{
    
int z = hs[t], i = t, p; flow += z;
    
while (i != s) {
        hs[i] 
-= z; p = pr[i]; ed[p].f -= z; ed[p ^ 1].f += z; i = ed[p].a;
        
if (!ed[p].f) now = i;
    }
}
bool dfs()
{
    re(i, n) vst[i] 
= 0; vst[s] = 1; q[0= s; lev[s] = 0;
    
int i, j, f0;
    
for (int front=0, rear=0; front<=rear; front++) {
        i 
= q[front];
        
for (int p=ed[i].next; p != i; p=ed[p].next) if (ed[p].f) {
            j 
= ed[p].b;
            
if (!vst[j]) {vst[j] = 1; q[++rear] = j; lev[j] = lev[i] + 1;}
        }
    }
    
if (!vst[t]) return 0;
    now 
= s; re(i, n) {start[i] = ed[i].next; vst[i] = 0;} hs[now] = INF;
    
bool fd;
    
while (!vst[s]) {
        
if (now == t) aug();
        fd 
= 0;
        
for (int p=start[now]; p != now; p=ed[p].next) {
            j 
= ed[p].b; f0 = ed[p].f;
            
if (lev[now] + 1 == lev[j] && !vst[j] && f0) {
                fd 
= 1; start[now] = pr[j] = p; hs[j] = hs[now] <= f0 ? hs[now] : f0; now = j; break;
            }
        }
        
if (!fd) {
            vst[now] 
= 1;
            
if (now != s) now = ed[pr[now]].a;
        }
    }
    
return 1;
}
void deledge(int No)
{
    ed[ed[No].pre].next 
= ed[No].next; ed[ed[No].next].pre = ed[No].pre;
}
void resuedge(int No)
{
    ed[ed[No].pre].next 
= ed[ed[No].next].pre = No;
}
void resu_all()
{
    re(i, n) 
for (int p=ed[i].next; p != i; p=ed[p].next) ed[p].f = ed[p].fs;
}
void solve()
{
    flow 
= 0while (dfs()) ; int f_ = flow;
    
if (!flow) {reslen = -1return;}
    re(i, m) 
if (ed[i].a + n0 == ed[i].b && ed[i].a != s && ed[i].b != t) {
        deledge(i); deledge(i 
^ 1); resu_all();
        flow 
= 0while (dfs()) ;
        
if (flow < f_) {res[reslen++= ed[i].a + 1; f_--;} else {resuedge(i ^ 1); resuedge(i);}
    }
}
void pri()
{
    
if (reslen == -1) puts("0"); else if (reslen) {
        printf(
"%d\n", reslen);
        re(i, reslen 
- 1) printf("%d ", res[i]); printf("%d\n", res[reslen - 1]);
    } 
else puts("NO ANSWER!");
}
int main()
{
    init();
    solve();
    pri();
    
return 0;
}



Mato_No1 2011-05-07 14:12 发表评论
]]>
图的连通度问题的求法http://www.cppblog.com/MatoNo1/archive/2011/04/05/143449.htmlMato_No1Mato_No1Tue, 05 Apr 2011 08:23:00 GMThttp://www.cppblog.com/MatoNo1/archive/2011/04/05/143449.htmlhttp://www.cppblog.com/MatoNo1/comments/143449.htmlhttp://www.cppblog.com/MatoNo1/archive/2011/04/05/143449.html#Feedback0http://www.cppblog.com/MatoNo1/comments/commentRss/143449.htmlhttp://www.cppblog.com/MatoNo1/services/trackbacks/143449.html图的连通度分为点连通度和边连通度:
(1)点连通度:只许删点,求至少要删掉几个点(当然,s和t不能删去,这里保证原图中至少有三个点);
(2)边连通度:只许删边,求至少要删掉几条边。
并且,有向图和无向图的连通度求法不同,因此还要分开考虑(对于混合图,只需将其中所有的无向边按照无向图的办法处理、有向边按照有向图的办法处理即可)。

【1】有向图的边连通度:
这个其实就是最小割问题。以s为源点,t为汇点建立网络,原图中的每条边在网络中仍存在,容量为1,求该网络的最小割(也就是最大流)的值即为原图的边连通度。

【2】有向图的点连通度:
需要拆点。建立一个网络,原图中的每个点i在网络中拆成i'与i'',有一条边<i', i''>,容量为1(<s', s''>和<t', t''>例外,容量为正无穷)。原图中的每条边<i, j>在网络中为边<i'', j'>,容量为正无穷。以s'为源点、t''为汇点求最大流,最大流的值即为原图的点连通度。

说明:最大流对应的是最小割。显然,容量为正无穷的边不可能通过最小割,也就是原图中的边和s、t两个点不能删去;若边<i, i''>通过最小割,则表示将原图中的点i删去。

【3】无向图的边连通度:
将图中的每条边(i, j)拆成<i, j>和<j, i>两条边,再按照有向图的办法(【1】)处理;

【4】无向图的点连通度:
将图中的每条边(i, j)拆成<i, j>和<j, i>两条边,再按照有向图的办法(【2】)处理。


Mato_No1 2011-04-05 16:23 发表评论
]]>
最大闭合子图的预处理(去环)问题http://www.cppblog.com/MatoNo1/archive/2011/03/27/142798.htmlMato_No1Mato_No1Sun, 27 Mar 2011 10:30:00 GMThttp://www.cppblog.com/MatoNo1/archive/2011/03/27/142798.htmlhttp://www.cppblog.com/MatoNo1/comments/142798.htmlhttp://www.cppblog.com/MatoNo1/archive/2011/03/27/142798.html#Feedback2http://www.cppblog.com/MatoNo1/comments/commentRss/142798.htmlhttp://www.cppblog.com/MatoNo1/services/trackbacks/142798.html
最大闭合子图就是其中的一种。如果要求最大闭合子图的有向图里面有环就很囧了,因为在某些题目里(比如NOI2009的pvz),取点是有先后顺序的,因此环中的点一个也取不了(有的题则不是这样,子图里的点可以一次全部取来,这时对于环就有两种方案了:要么全取,要么一个不取,此时不用管环,直接进入网络流即可),不仅如此,根据闭合子图的定义,如果一个点i可以到达一个点j(注,是“可以到达”点j,也就是从i到j有路径),而点j属于某个环,那么点i也不能取,因此在预处理中需要把点i也删掉。以下将属于某个环中的点成为“环点”,将可以到达环点的点称为“环限制点”,这两种点在预处理中都要删除。

本沙茶以前用的一般方法是:先求图的传递闭包,找出所有的环点(能够到达自己的点),再从每个环点开始进行逆向遍历(将原图所有边反向,再遍历),找到所有的环限制点。该方法的时间复杂度高达O(N3),且写起来也爆麻烦。

其实,真正用于去环的最佳方法是拓扑排序!!!

首先将原图的所有边反向,然后进行拓扑排序,所有遍历到的点是保留下来的点,而没有遍历到的点就是环点或环限制点,需要删除。
【证明:环点显然是不可能被遍历到的,而在反向后的新图中,对于一个环限制点j,必然存在一个环点i能够到达它,而i不能被遍历到,故j也不能被遍历到。除了这两种点外,其它的点的所有前趋必然也都不是环点或环限制点(否则这些点就成了环限制点),因此只要入度为0(不存在前趋)的点能够遍历到,这些点也能够遍历到,而入度为0的点显然能遍历到,故这些点也能被遍历到。证毕】
由于求反向图和拓扑排序都可以在O(M)时间内完成,整个去环过程的时间复杂度就是O(M)的。

下面附上NOI2009 pvz代码:(注意,本题的第9个点是一个超级大数据,最后建出来的网络的边数将会达到300000,故MAXM取150000,另外,本题必须使用Dinic,SAP会超)
#include <iostream>
#include 
<stdio.h>
#include 
<string.h>
using namespace std;
#define re(i, n) for (int i=0; i<n; i++)
#define re1(i, n) for (int i=1; i<=n; i++)
#define re2(i, l, r) for (int i=l; i<r; i++)
const int MAXN = 602, MAXM = 150000, INF = ~0U >> 2;
struct link {
    
int kk;
    link 
*next;
*ed[MAXN], *ed2[MAXN];
struct edge {
    
int a, b, f, next;
    edge () {}
    edge (
int _a, int _b, int _f): a(_a), b(_b), f(_f), next(-1) {}
} ed_[MAXM 
+ MAXM];
int n, m = 0, s, t, sc[MAXN], hd[MAXN], tl[MAXN], st[MAXN], lev[MAXN], q[MAXN], hs[MAXN], pr[MAXN], ind[MAXN], now, res = 0;
bool vst[MAXN];
void init()
{
    freopen(
"pvz.in""r", stdin);
    
int n0, m0, A[20][30], num = 1, x, y, z;
    scanf(
"%d%d"&n0, &m0);
    re(i, n0) re(j, m0) A[i][j] 
= num++;
    n 
= n0 * m0 + 2; s = 0; t = n - 1; memset(ed, 0, n << 2); memset(ed2, 0, n << 2);
    re1(i, n 
- 2) {
        scanf(
"%d%d"&sc[i], &num);
        re(j, num) {
            scanf(
"%d%d"&x, &y); z = A[x][y];
            link 
*p1 = new link; p1->kk = i; p1->next = ed[z]; ed[z] = p1;
            link 
*p2 = new link; p2->kk = z; p2->next = ed2[i]; ed2[i] = p2; ind[z]++;
        }
    }
    re(i, n0) re2(j, 
1, m0) {
        z 
= A[i][j];
        link 
*p1 = new link; p1->kk = z; p1->next = ed[z - 1]; ed[z - 1= p1;
        link 
*p2 = new link; p2->kk = z - 1; p2->next = ed2[z]; ed2[z] = p2; ind[z - 1]++;
    }
    fclose(stdin);
}
inline 
void add_edge(int a, int b, int f)
{
    ed_[m] 
= edge(a, b, f);
    
if (hd[a] != -1) tl[a] = ed_[tl[a]].next = m++else hd[a] = tl[a] = m++;
    ed_[m] 
= edge(b, a, 0);
    
if (hd[b] != -1) tl[b] = ed_[tl[b]].next = m++else hd[b] = tl[b] = m++;
}
void prepare()
{
    
int front = 0, rear = -1;
    re1(i, n 
- 2if (!ind[i]) {q[++rear] = i; vst[i] = 1;}
    
int i, j;
    
for (; front<=rear; front++) {
        i 
= q[front];
        
for (link *p=ed2[i]; p; p=p->next) {
            j 
= p->kk; ind[j]--;
            
if (!ind[j]) {vst[j] = 1; q[++rear] = j;}
        }
    }
    memset(hd, 
-1, n << 2); memset(tl, -1, n << 2);
    re1(i, n 
- 2if (vst[i]) {
        
if (sc[i] > 0) {res += sc[i]; add_edge(s, i, sc[i]);}
        
if (sc[i] < 0) add_edge(i, t, -sc[i]);
    }
    re1(i, n 
- 2if (vst[i]) for (link *p=ed[i]; p; p=p->next) {
        j 
= p->kk;
        
if (vst[j]) add_edge(i, j, INF);
    }
}
void aug()
{
    
int z = hs[t], i = t, p;
    
while (i != s) {
        hs[i] 
-= z; p = pr[i]; ed_[p].f -= z; ed_[p ^ 1].f += z; i = ed_[p].a;
        
if (!ed_[p].f) now = i;
    }
    res 
-= z;
}
bool dfs()
{
    q[
0= s; memset(vst, 0, n); vst[s] = 1; lev[s] = 0;
    
int i, j, f0;
    
for (int front=0, rear=0; front<=rear; front++) {
        i 
= q[front];
        
for (int p=hd[i]; p != -1; p=ed_[p].next) {
            j 
= ed_[p].b; f0 = ed_[p].f;
            
if (!vst[j] && f0) {vst[j] = 1; lev[j] = lev[i] + 1; q[++rear] = j;}
        }
    }
    
if (!vst[t]) return 0;
    now 
= s; hs[s] = INF; memset(vst, 0, n);
    re(i, n) st[i] 
= hd[i];
    
bool ff;
    
while (!vst[s]) {
        
if (now == t) aug();
        ff 
= 0;
        
for (int p=st[now]; p != -1; p=ed_[p].next) {
            j 
= ed_[p].b; f0 = ed_[p].f;
            
if (lev[now] + 1 == lev[j] && !vst[j] && f0) {
                st[now] 
= pr[j] = p; hs[j] = hs[now] <= f0 ? hs[now] : f0; now = j; ff = 1break;
            }
        }
        
if (!ff) {
            vst[now] 
= 1;
            
if (now != s) now = ed_[pr[now]].a;
        }
    }
    
return 1;
}
void solve()
{
    
while (dfs()) ;
}
void pri()
{
    freopen(
"pvz.out""w", stdout);
    printf(
"%d\n", res);
    fclose(stdout);
}
int main()
{
    init();
    prepare();
    solve();
    pri();
    
return 0;
}



Mato_No1 2011-03-27 18:30 发表评论
]]>
求有向图最大闭合子图的最小容量(PKU2987、HFTSC2011 第1题 profit)http://www.cppblog.com/MatoNo1/archive/2011/03/27/142797.htmlMato_No1Mato_No1Sun, 27 Mar 2011 09:57:00 GMThttp://www.cppblog.com/MatoNo1/archive/2011/03/27/142797.htmlhttp://www.cppblog.com/MatoNo1/comments/142797.htmlhttp://www.cppblog.com/MatoNo1/archive/2011/03/27/142797.html#Feedback0http://www.cppblog.com/MatoNo1/comments/commentRss/142797.htmlhttp://www.cppblog.com/MatoNo1/services/trackbacks/142797.html最不该挂的是第一题。第一问就是个裸的最大闭合子图,关键就出在第二问上,要求最大闭合子图的最小容量。本沙茶后来才发现这竟然是PKU原题!(PKU2987),因为,最大流求出来的最大闭合子图一定是容量最小的!故第二问只要在求出最大流后来一次遍历,找到S可达的结点个数即可。

详细证明(转网上某神犇的):
———————————————————————————————————————————————————最大权不可能是负数,当是0的情况,自然是一个点不选最优,下面探讨忽略0的情况。

         1:首先我假设有两个闭合子图都拥有相同的最大权,并且没有共同点,很容易证明不会出现这种情况,因为如果我们把两个闭合子图都选择上,那么最大权会变大。

         2:仍然假设有两个闭合子图都拥有相同的最大权,但是有共同点,即重叠的意思。根据闭合图的特点,这些共同点不是随意的,可以知道,只要有一个点相同,那么这个点的能到达的所有后续点都必定是共同点。所以会得出一个结论,两个闭合子图重叠,重叠的部分必然是1个或者多个不重叠闭合图。


    然后我们考虑不重叠的部分,这部分的点权和可以证明一定是非负。我们可以假设非重叠部分的点权和是负数,那么假如我们删掉这部分,只选取重叠部分(因为重叠部分肯定是闭合图),那么最大权也会变大,矛盾。所以非重叠部分点权和一定是非负数。
    下面继续探讨非重叠部分的性质。上面的证明已经得出他们的点权和一定是非负。下面先抛开点权和等于0的情况,先讨论正数的情况。
假设两个闭合子图的非重叠部分都是正数,那么把这两个部分加起来重新构成闭合图,最大权必然会变大,与假设的两个同为最大权的闭合图矛盾。固可以证明非重叠部分的点权和肯定是0。

    探讨到这部分,我们已经可以初步得出一个结论,就是一个最大权闭合子图的点数多少问题只能受到一些0权和子图(所有点权加起来等于0的子图)的干扰。

    重点来到这些0权和子图上。下面我们又来做一个假设,就是假设我们求出了一个最大权闭合子图,并且里面有包含到一些0权和子图。而我们这时候需要做的就是找到那些可以删去的0权和子图,当我们删去了所有的这些子图,那么点数就可以达到最小。

    关键来了。到底哪些0权和子图是可以删去的,哪些0权和子图是不可以删去的呢?

    根据闭合图的性质,我们要删除一个点,那么必须把所有能到达这个点的点删去。那么很清晰的看到要删除一个子图,必须保证在这个子图外没有任何一个点指向这个子图。也就是说这个子图是封闭的,只有出边,没有入边。



    最后一步,假如我们能证明在求解最大权闭合图的过程中保证不会选上这些0权和子图,那么这个证明就可以结束了。
通过最大流求解最大权闭合子图,我们把正点权和源点建边,边权为点权值,负点权和汇点建边,边权为点权绝对值。仔细分析求解最大流的过程,会发现一些东西。

    由于那些可选可不选的0权和子图是封闭的,没有入边的,那么在求解最大流的过程中,不可能有外部流补充,所以这些0权和子图的每个点与源或者汇的边都是满流的。





    最后求最大权闭合子图的点,方法是从源点开始通过非满流边搜索一个联通图,图内的点(除了源点)就是求出来的最大权闭合子图的点。而上面证明到0权和子图的点和源汇都是满流,并且没有入边,出边也没有流,那么肯定无法从源点搜索到。那么在求解的过程中这些0权和子图的点是肯定没有选上的。

   就此证毕。
   结论:通过最大流求解最大权闭合子图的问题,求出来的闭合子图点数也是最少的。

———————————————————————————————————————————————————
代码:
#include <iostream>
#include 
<stdio.h>
#include 
<string.h>
using namespace std;
#define re(i, n) for (int i=0; i<n; i++)
#define re1(i, n) for (int i=1; i<=n; i++)
const int MAXN = 10002, MAXM = 120000, INF = ~0U >> 2;
struct edge {
    
int a, b, f, next;
    edge () {}
    edge (
int _a, int _b, int _f) : a(_a), b(_b), f(_f), next(-1) {}
} ed[MAXM 
+ MAXM];
int n, m = 0, s, t, hd[MAXN], tl[MAXN], st[MAXN], lev[MAXN], pr[MAXN], hs[MAXN], q[MAXN], now, res = 0, res_num = 0;
bool vst[MAXN];
void add_edge(int a, int b, int f)
{
    ed[m] 
= edge(a, b, f);
    
if (hd[a] != -1) tl[a] = ed[tl[a]].next = m++else hd[a] = tl[a] = m++;
    ed[m] 
= edge(b, a, 0);
    
if (hd[b] != -1) tl[b] = ed[tl[b]].next = m++else hd[b] = tl[b] = m++;
}
void init()
{
    freopen(
"profit.in""r", stdin);
    
int n0, m0, a0, b0, x;
    scanf(
"%d%d"&n0, &m0);
    n 
= n0 + 2; s = 0; t = n - 1;
    memset(hd, 
-1, n << 2); memset(tl, -1, n << 2);
    re1(i, n0) {
        cin 
>> x;
        
if (x > 0) {add_edge(s, i, x); res += x;}
        
if (x < 0) add_edge(i, t, -x);
    }
    re1(i, m0) {
        scanf(
"%d%d"&a0, &b0);
        add_edge(a0, b0, INF);
    }
    fclose(stdin);
}
void aug()
{
    
int z = hs[t], i = t, p;
    
while (i != s) {
        hs[i] 
-= z; p = pr[i]; ed[p].f -= z; ed[p ^ 1].f += z; i = ed[p].a;
        
if (!ed[p].f) now = i;
    }
    res 
-= z;
}
bool dfs()
{
    q[
0= s; memset(vst, 0, n); vst[s] = 1; lev[s] = 0;
    
int i, j, f0;
    
for (int front=0, rear=0; front<=rear; front++) {
        i 
= q[front];
        
for (int p=hd[i]; p != -1; p=ed[p].next) {
            j 
= ed[p].b; f0 = ed[p].f;
            
if (!vst[j] && f0) {vst[j] = 1; lev[j] = lev[i] + 1; q[++rear] = j;}
        }
    }
    
if (!vst[t]) return 0;
    now 
= s; memset(vst, 0, n); hs[s] = INF;
    re(i, n) st[i] 
= hd[i];
    
bool ff;
    
while (!vst[s]) {
        
if (now == t) aug();
        ff 
= 0;
        
for (int p=st[now]; p != -1; p=ed[p].next) {
            j 
= ed[p].b; f0 = ed[p].f;
            
if (lev[now] + 1 == lev[j] && !vst[j] && f0) {st[now] = pr[j] = p; hs[j] = hs[now] <= f0 ? hs[now] : f0; now = j; ff = 1break;}
        }
        
if (!ff) {
            vst[now] 
= 1;
            
if (now != s) now = ed[pr[now]].a;
        }
    }
    
return 1;
}
void solve()
{
    
while (dfs()) ;
    q[
0= s; memset(vst, 0, n); vst[s] = 1;
    
int i, j, f0;
    
for (int front=0, rear=0; front<=rear; front++) {
        i 
= q[front];
        
for (int p=hd[i]; p != -1; p=ed[p].next) {
            j 
= ed[p].b; f0 = ed[p].f;
            
if (!vst[j] && f0) {vst[j] = 1; res_num++; q[++rear] = j;}
        }
    }
}
void pri()
{
    freopen(
"profit.out""w", stdout);
    printf(
"%d\n%d\n", res, res_num);
    fclose(stdout);
}
int main()
{
    init();
    solve();
    pri();
    
return 0;
}



Mato_No1 2011-03-27 17:57 发表评论
]]>
欧拉环、欧拉路径的判定和求法http://www.cppblog.com/MatoNo1/archive/2011/03/27/142776.htmlMato_No1Mato_No1Sun, 27 Mar 2011 03:06:00 GMThttp://www.cppblog.com/MatoNo1/archive/2011/03/27/142776.htmlhttp://www.cppblog.com/MatoNo1/comments/142776.htmlhttp://www.cppblog.com/MatoNo1/archive/2011/03/27/142776.html#Feedback0http://www.cppblog.com/MatoNo1/comments/commentRss/142776.htmlhttp://www.cppblog.com/MatoNo1/services/trackbacks/142776.html欧拉路径:图中经过每条边一次且仅一次的路径;
欧拉图:有至少一个欧拉环的图;
半欧拉图:没有欧拉环,但有至少一条欧拉路径的图。

【无向图】
一个无向图是欧拉图当且仅当该图是连通的(注意,不考虑图中度为0的点,因为它们的存在对于图中是否存在欧拉环、欧拉路径没有影响)且所有点的度数都是偶数;一个无向图是半欧拉图当且仅当该图是连通的且有且只有2个点的度数是奇数(此时这两个点只能作为欧拉路径的起点和终点);

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

【有向图】
一个有向图是欧拉图当且仅当该图的基图(将所有有向边变为无向边后形成的无向图,这里同样不考虑度数为0的点)是连通的且所有点的入度等于出度;一个有向图是半欧拉图当且仅当该图的基图是连通的且有且只有一个点的入度比出度少1(作为欧拉路径的起点),有且只有一个点的入度比出度多1(作为终点),其余点的入度等于出度。

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

【无向图、有向图中欧拉环的求法】
与二分图匹配算法类似,是一个深度优先遍历的过程,时间复杂度O(M)(因为一条边最多被访问一次)。核心代码(边是用边表存储的而不是邻接链表,因为无向图中需要对其逆向的边进行处理,在有向图中,可以用邻接链表存储边):
void dfs(int x)
{
    
int y;
    
for (int p=hd[x]; p != -1; p=ed[p].next) if (!ed[p].vst) {
        y 
= ed[p].b;
        ed[p].vst 
= 1;
        ed[p 
^ 1].vst = 1;     //如果是有向图则不要这句
        dfs(y);
        res[v
--= y + 1;
    }
}
要注意的是在res中写入是逆序的,所以初始的v应设成(边数-1)。
但是有一个问题是,这是递归实现的,当点数过多时有爆栈危险,所以最好使用非递归:
void dfs()
{
    
int x = 0, y, tp = 1; stk[0= 0;
    re(i, n) now[i] 
= hd[i];
    
bool ff;
    
while (tp) {
        ff 
= 0;
        
for (int p=now[x]; p != -1; p=ed[p].next) if (!ed[p].vst) {
            y 
= ed[p].b;
            ed[p].vst 
= 1;
            ed[p 
^ 1].vst = 1;     //如果是有向图则不要这句
            now[x] = p; stk[tp++= y; x = y; ff = 1break;
        }
        
if (!ff) {
            res[v
--= x + 1;
            x 
= stk[--tp - 1];
        }
    }
}
当原图是欧拉图时,一定可以求出欧拉回路。当原图是半欧拉图时,求欧拉路径,只要找到起点i和终点j,添加边<j, i>(或(j, i)),求欧拉环,再在求出的欧拉环中删除添加的新边即可。

不过最为BT的还不是这个,而是接下来的——
【混合图】
混合图(既有有向边又有无向边的图)中欧拉环、欧拉路径的判定需要借助网络流!

(1)欧拉环的判定:
一开始当然是判断原图的基图是否连通,若不连通则一定不存在欧拉环或欧拉路径(不考虑度数为0的点)。

其实,难点在于图中的无向边,需要对所有的无向边定向(指定一个方向,使之变为有向边),使整个图变成一个有向欧拉图(或有向半欧拉图)。若存在一个定向满足此条件,则原图是欧拉图(或半欧拉图)否则不是。关键就是如何定向?

首先给原图中的每条无向边随便指定一个方向(称为初始定向),将原图改为有向图G',然后的任务就是改变G'中某些边的方向(当然是无向边转化来的,原混合图中的有向边不能动)使其满足每个点的入度等于出度。
设D[i]为G'中(点i的出度 - 点i的入度)。可以发现,在改变G'中边的方向的过程中,任何点的D值的奇偶性都不会发生改变(设将边<i, j>改为<j, i>,则i入度加1出度减1,j入度减1出度加1,两者之差加2或减2,奇偶性不变)!而最终要求的是每个点的入度等于出度,即每个点的D值都为0,是偶数,故可得:若初始定向得到的G'中任意一个点的D值是奇数,那么原图中一定不存在欧拉环!

若初始D值都是偶数,则将G'改装成网络:设立源点S和汇点T,对于每个D[i]>0的点i,连边<S, i>,容量为D[i]/2;对于每个D[j]<0的点j,连边<j, T>,容量为-D[j]/2;G'中的每条边在网络中仍保留,容量为1(表示该边最多只能被改变方向一次)。求这个网络的最大流,若S引出的所有边均满流,则原混合图是欧拉图,将网络中所有流量为1的中间边(就是不与S或T关联的边)在G'中改变方向,形成的新图G''一定是有向欧拉图;若S引出的边中有的没有满流,则原混合图不是欧拉图。

为什么能这样建图?
考虑网络中的一条增广路径S-->i-->...-->j-->T,将这条从i到j的路径在G'中全部反向,则:i的入度加1出度减1,j的入度减1出度加1,路径中其它点的入度出度均不变。而i是和S相连的,因此初始D[i]>0,即i的出度大于入度,故这样反向之后D[i]减少2;同理,j是和T相连的,这样反向之后D[j]增加2。因此,若最大流中边<S, i>满流(流量为初始D[i]/2),此时D[i]值就变成了0,也就是i的入度等于出度。因此只要使所有S引出的边全部满流,所有初始D值>0的点的D值将等于0,又因为将边变向后所有点的D值之和不变,所有初始D值小于0的点的D值也将等于0,而初始D值等于0的D点既不与S相连也不与T相连,所以它们是网络中的中间点,而中间点的流入量等于流出量,故它们的入度和出度一直不变,即D值一直为0。因此,整个图G'成为欧拉图。

(2)欧拉路径的判定:
首先可以想到的是枚举欧拉路径的起点i和终点j,然后在图中添加边<j, i>,再求图中是否有欧拉回路即可。但是,该算法的时间复杂度达到了O(M * 最大流的时间),需要优化。
前面已经说过,在将边变向的过程中任何点的D值的奇偶性都不会改变,而一个有向图有欧拉路径的充要条件是基图连通且有且只有一个点的入度比出度少1(作为欧拉路径的起点),有且只有一个点的入度比出度多1(作为终点),其余点的入度等于出度。这就说明,先把图中的无向边随便定向,然后求每个点的D值,若有且只有两个点的初始D值为奇数,其余的点初始D值都为偶数,则有可能存在欧拉路径(否则不可能存在)。进一步,检查这两个初始D值为奇数的点,设为点i和点j,若有D[i]>0且D[j]<0,则i作起点j作终点(否则若D[i]与D[j]同号则不存在欧拉路径),连边<j, i>,求是否存在欧拉环即可(将求出的欧拉环中删去边<j, i>即可)。这样只需求一次最大流。

【典型例题】Sightseeing tour(PKU1637,ZJU1992)
本题就是求混合图的欧拉环问题,题目中已经说明图是连通的(Input的最后一句话),故不需判连通。
(本沙茶一开始把DFS中的l0 = aug中的"= aug"写漏了,TLE了N次)
#include <iostream>
#include 
<stdio.h>
#include 
<string.h>
using namespace std;
#define re(i, n) for (int i=0; i<n; i++)
const int MAXN = 2002, MAXM = 10000, INF = ~0U >> 2;
struct edge {
    
int a, b, f, next;
    edge () {}
    edge (
int _a, int _b, int _f): a(_a), b(_b), f(_f), next(-1) {}
} ed[MAXM 
+ MAXM];
int n, m, s, t, D[MAXN], hd[MAXN], tl[MAXN], lb[MAXN], vl[MAXN], flow, lmt;
bool res;
int dfs(int i, int aug)
{
    
if (i == t) {flow += aug; return aug;}
    
int l0 = aug, l1, j, f0;
    
for (int p=hd[i]; p != -1; p=ed[p].next) {
        j 
= ed[p].b; f0 = ed[p].f;
        
if (lb[i] == lb[j] + 1 && f0) {
            l1 
= dfs(j, l0 <= f0 ? l0 : f0);
            l0 
-= l1; ed[p].f -= l1; ed[p ^ 1].f += l1;
            
if (lb[s] == n || !l0) return aug;
        }
    }
    
int minlb = n - 1;
    
for (int p=hd[i]; p != -1; p=ed[p].next) if (ed[p].f) {
        j 
= ed[p].b;
        
if (lb[j] < minlb) minlb = lb[j];
    }
    
if (--vl[lb[i]]) vl[lb[i] = minlb + 1]++else lb[s] = n;
    
return aug - l0;
}
inline 
void add_edge(int a, int b, int f)
{
    ed[m] 
= edge(a, b, f);
    
if (hd[a] != -1) tl[a] = ed[tl[a]].next = m++else hd[a] = tl[a] = m++;
    ed[m] 
= edge(b, a, 0);
    
if (hd[b] != -1) tl[b] = ed[tl[b]].next = m++else hd[b] = tl[b] = m++;
}
void solve()
{
    
int tests;
    scanf(
"%d"&tests);
    
int n0, m0, a0, b0, f;
    re(testno, tests) {
        scanf(
"%d%d"&n0, &m0);
        n 
= n0 + 2; m = 0; s = 0; t = n - 1;
        memset(D, 
0, n0 << 2); memset(hd, -1, n << 2); memset(tl, -1, n << 2);
        re(i, m0) {
            scanf(
"%d%d%d"&a0, &b0, &f);
            D[a0 
- 1]++; D[b0 - 1]--;
            
if (!f) add_edge(a0, b0, 1);
        }
        res 
= 1; lmt = 0; flow = 0;
        re(i, n0) {
            
if (D[i] % 2) {res = 0break;}
            
if (D[i] > 0) {add_edge(s, i + 1, D[i] >> 1); lmt += D[i] >> 1;}
            
if (D[i] < 0) add_edge(i + 1, t, -D[i] >> 1);
        }
        
if (res) {
            memset(lb, 
0, n << 2); vl[0= n; memset(vl + 10, n << 2);
            
while (lb[s] < n) dfs(s, INF);
            
if (flow < lmt) res = 0;
        }
        puts(res 
? "possible" : "impossible");
    }
}
int main()
{
    solve();
    
return 0;
}



Mato_No1 2011-03-27 11:06 发表评论
]]>
二分图多重匹配问题http://www.cppblog.com/MatoNo1/archive/2011/03/26/142766.htmlMato_No1Mato_No1Sat, 26 Mar 2011 13:53:00 GMThttp://www.cppblog.com/MatoNo1/archive/2011/03/26/142766.htmlhttp://www.cppblog.com/MatoNo1/comments/142766.htmlhttp://www.cppblog.com/MatoNo1/archive/2011/03/26/142766.html#Feedback0http://www.cppblog.com/MatoNo1/comments/commentRss/142766.htmlhttp://www.cppblog.com/MatoNo1/services/trackbacks/142766.html在二分图最大匹配中,每个点(不管是X方点还是Y方点)最多只能和一条匹配边相关联,然而,我们经常遇到这种问题,即二分图匹配中一个点可以和多条匹配边相关联,但有上限,或者说,Li表示点i最多可以和多少条匹配边相关联。

二分图多重匹配分为二分图多重最大匹配与二分图多重最优匹配两种,分别可以用最大流与最大费用最大流解决。

(1)二分图多重最大匹配:
在原图上建立源点S和汇点T,S向每个X方点连一条容量为该X方点L值的边,每个Y方点向T连一条容量为该Y方点L值的边,原来二分图中各边在新的网络中仍存在,容量为1(若该边可以使用多次则容量大于1),求该网络的最大流,就是该二分图多重最大匹配的值。

(2)二分图多重最优匹配:
在原图上建立源点S和汇点T,S向每个X方点连一条容量为该X方点L值、费用为0的边,每个Y方点向T连一条容量为该Y方点L值、费用为0的边,原来二分图中各边在新的网络中仍存在,容量为1(若该边可以使用多次则容量大于1),费用为该边的权值。求该网络的最大费用最大流,就是该二分图多重最优匹配的值。

例题:
【1】POJ1698 Alice's Chance
将电影作为X方点,每一天作为Y方点(最多50周,每周7天,所以共设350个Y方点),若第i个电影可以在第j天搞就连边(i, j)。每个X方点的L值为该电影总共要搞多少天,每个Y方点的L值为1(每天最多只能搞一个电影),然后求二分图多重最大匹配,若能使所有从源点连向X方点的边都满流,则输出Yes,否则输出No。
【2】POJ2112 Optimal Milking
先预处理求出每两个点(包括挤奶点和牛)间的最短距离,然后将所有挤奶点作为X方点(L值为该挤奶点最多可以容纳多少牛),所有牛作为Y方点(L值为1),Xi和Yj间边的权值为这两个点之间的最短距离(若这两点间不存在路径则此处无边),然后问题就变成了求一个多重匹配,使得每个Y方点都有匹配点且匹配边中权值的最大值最小。
可以枚举最大边权值S,然后,原图中所有权值大于S的边都要删去。若此时图中存在符合要求的多重匹配,则S合法否则S不合法。由于S的合法性是单调的,所以可以二分枚举S。


Mato_No1 2011-03-26 21:53 发表评论
]]>
profit是怎样被SAP的多路增广虐爆的……http://www.cppblog.com/MatoNo1/archive/2011/03/19/142225.htmlMato_No1Mato_No1Sat, 19 Mar 2011 09:55:00 GMThttp://www.cppblog.com/MatoNo1/archive/2011/03/19/142225.htmlhttp://www.cppblog.com/MatoNo1/comments/142225.htmlhttp://www.cppblog.com/MatoNo1/archive/2011/03/19/142225.html#Feedback4http://www.cppblog.com/MatoNo1/comments/commentRss/142225.htmlhttp://www.cppblog.com/MatoNo1/services/trackbacks/142225.html代码1:SAP单路增广(非递归);

代码2:SAP多路增广(递归);

代码3:Dinic单路增广(非递归);

代码4:Dinic多路增广(递归);

结果:

代码1:

代码2:

代码3:

 代码4:

结果:
SAP加了多路增广后,直接秒掉后2个点;
Dinic加了多路增广后效率差不多,还更低了一点……

(另外发现,SAP的多路增广不支持当前弧优化……这点和zkw费用流有点像囧……不过效率影响不大……)


Mato_No1 2011-03-19 17:55 发表评论
]]>