二分图、网络流模版总结

 

这个星期重新回去理解二分图和网络流的算法,觉得当初真的是有点傻萌,都不太了解就直接套模版……

 

关于二分图

关于二分图的概念不想吐槽了,太多而且好多都好乱,看了很多个什么概念总结结果发现有的人把最大匹配叫最佳匹配。反正是看的我一脸蛋疼,决定还是抛开概念吧,看着像啥用啥得了。

匈牙利算法:
其实就是不断寻找增广路。那么什么是增广路呢?
既然是二分图,我们先定义X集和Y集。然后我们对X集里面的点一个一个来,拿出一个没用过的X集的点(简记为i),找所有和它相连的Y集的点(简记为j),如果这个Y集的点没用过,那么这两个点在一起就行了,如果用过了,就dfs这个Y集中的点是被那个X集中的点用的(简记为i'),这时候还是一样,找所有和这个i'点相连的Y点……如果这样dfs很多次后终于找到一个Y集中的点是没有用到了,那么原来是j-i',j'-i''……现在就变成i(之前没用过)-j,i'-j',……,i''-j''(之前没用过),这样就多了一个匹配。

procedure addedge(j,k:longint);
begin
  inc(tot);
  edge[tot].toward:=k;
  edge[tot].next:=first[j];
  first[j]:=tot;
end;

function dfs(x:longint):boolean;
var
  i,too:longint;
begin
  i:=first[x];
  while i>0 do begin
    too:=edge[i].toward;
    if not chose[too] then begin
      chose[too]:=true;
      if (match[too]=0) or dfs(match[too]) then begin
        match[too]:=x;
        exit(true);
      end
    end;
    i:=edge[i].next;
  end;
  exit(false);
end;

procedure work;
var
  ans,i:longint;
begin
  ans:=0;
  for i:=1 to tot1 do begin
    fillchar(chose,sizeof(chose),false);
    if dfs(i) then inc(ans);
  end;
  writeln(ans);
end;
View Code

 

KM算法
很神奇。用来解决最大权匹配(雾之概念)。其实就是边加上了权,求某个匹配使权值最大。这类问题前提是必须存在一个方案使得可以让X每个点都在某个匹配里面。
我们可以搞个标号lx(),ly(),如果有边i->j,那么保证lx(i)+ly(j)>cij(边的权值)。然后这样的标号方案是不唯一的,但是这样的标号一定满足大于等于所有匹配(所以就包括了那个最大的方案),那么我们可以找到一个最小的标号方案使它等于最大的方案。
经过什么构造矩阵还是什么鬼的(搞了一个晚上搞懂了但是不想解释),我们可以得到一个流程。

如果一个x点不能加进图里面,那么调整标号,取出那些所有不再dfs中走到的Y点,找到最小slack值(在dfs顺便处理出的,slack[j]=min{slack[j],lx[i]+ly[j]-cij},i为和j有边相连的点)。然后所有dfs中走到的X点的标号-最小slack值,所有走到的y点标号+最小slack值,同时slack数组也变了,也就是所有dfs中走到的Y点的slack值-最小slack值。再跑匈牙利,不行再改,直到可以。

type
  arr=record
    toward,next,from,cost:longint;
  end;

const
  maxm=1000000;
  maxn=50000;

var
  edge:array[0..maxm]of arr;
  l,slack,first,match:array[0..maxn]of longint;//实际中我为了偷懒就直接开一个l,然后y集合的点编号统一加上n
  chose:array[0..maxn]of boolean;
  tot,n,m:longint;

function min(x,y:longint):longint;
begin
  if x<y then exit(x);
  exit(y);
end;

procedure addedge(j,k,l:longint);
begin
  inc(tot);
  edge[tot].from:=j;
  edge[tot].toward:=k;
  edge[tot].cost:=l;
  edge[tot].next:=first[j];
  first[j]:=tot;
end;

function dfs(x:longint):boolean;
var
  i,too,value:longint;
begin
  chose[x]:=true;
  i:=first[x];
  while i>0 do begin
    too:=edge[i].toward;
    value:=edge[i].cost;
    if not chose[too] then
      if l[x]+l[too]=value then begin
        chose[too]:=true;
        if (match[too]=0) or dfs(edge[match[too]].from) then begin
          match[too]:=i;
          exit(true);
        end;
      end
      else
        slack[too]:=min(slack[too],l[x]+l[too]-value);
    i:=edge[i].next;
  end;
  exit(false);
end;

procedure relable;
var
  i,delta:longint;
begin
  delta:=maxlongint;
  for i:=n+1 to n+m do
    if not chose[i] then delta:=min(delta,slack[i]);
  for i:=1 to n do
    if chose[i] then dec(l[i],delta);
  for i:=n+1 to n+m do
    if chose[i] then inc(l[i],delta)
      else dec(slack[i],delta);
end;

function km:longint;
var
  i,ans:longint;
begin
  for i:=1 to n do begin
    fillchar(slack,sizeof(slack),$7f);
    while true do begin
      fillchar(chose,sizeof(chose),false);
      if dfs(i) then break;
      relable;
    end;
  end;
  ans:=0;
  for i:=n+1 to n+m do
    inc(ans,edge[match[i]].cost);
  exit(ans);
end;

procedure into;
var
  i,j,k,h:longint;
begin
  readln(n,m);
  for i:=1 to n do
    for j:=1 to m do begin
      read(k);
      addedge(i,j+n,k);
      if k>l[i] then l[i]:=k;
    end;
end;

begin
  into;
  writeln(km);
  readln;
  readln;
end.
View Code

 

更多:
二分图最大匹配的三个定理
   1:最大匹配数 + 最大独立集 = n + m
   2:二分图的最小覆盖数 = 最大匹配数
   3:最小路径覆盖 = 最大独立集
    最大独立集是指求一个二分图中最大的一个点集,该点集内的点互不相连。
    最小顶点覆盖是指 在二分图中,用最少的点,让所有的边至少和一个点有关联。
     最小路径覆盖是指一个不含圈的有向图G 中,G的一个路径覆盖是一个其结点不相交的路径集合P,图中的每一个结点仅包含于P 中的某一条路径。路径可以从任意结点开始和结束,且长度也为任意值,包括0.

几种模型

   http://wenku.baidu.com/link?url=TiCZ5ILAyn7F5fHSKvwOxnr8J1TgCBOdHUtipi0E78fgVmXszj-8OMlgX8i4z-oPDFsPGbyM-3pvDYNweXwA5As7Lrn0CA7SVs71yZM6AAu

   文章后面总结的五个基本模型。

 

 

网络流
很多的问题都可以转为网络流求解,所以这是个万能的骗分算法(雾)。

最大流
学的是sap,觉得这个东西很好懂。现在再看的时候发现以前自己总结的那些资料非常好,一下子就回忆起来的。所以不想讲

//蔡大神当初的模版
const maxn=1000000;
type edgetype=record
 toward,cap,next:longint;   //邻接表 toward是指向哪个点,cap是容量,next是下一条边。
end;

var
 cur,first,dist,num:array[0..maxn] of longint;  //cur为当前弧优化 first为该点指向的最后一条边(邻接表用) dist、num是用于层次数记录
 edge:array[0..maxn] of edgetype;               //邻接表
 n,m,s,t,tot,i,j,a,b,c:longint;             //tot是记边数

function min(x,y:longint):longint;
begin
 if x<y then exit(x) else exit(y);
end;

procedure add(i,j,k:longint);
begin
 edge[tot].toward:=j;
 edge[tot].cap:=k;
 edge[tot].next:=first[i];
 first[i]:=tot;
 inc(tot);
end;

procedure addedge(i,j,k:longint);
begin
 add(i,j,k);
 add(j,i,0);
end;

function sap(v,flow:longint):longint;
var i,j,tmp,more,now:longint;
begin
 if v=t then exit(flow);
 now:=0;
 i:=cur[v];                      //当前弧优化
 while i<>-1 do
  begin
   tmp:=edge[i].toward;
   if (edge[i].cap>0) and (dist[v]=dist[tmp]+1) then
    begin
     more:=sap(tmp,min(edge[i].cap,flow-now));
     dec(edge[i].cap,more);
     inc(edge[i xor 1].cap,more);
     inc(now,more);
     cur[v]:=i;
     if now=flow then exit(flow);
    end;
   i:=edge[i].next;
  end;
 dec(num[dist[v]]);
 if num[dist[v]]=0 then dist[s]:=t;   
//这是个优化,成为gap优化。当该图的层次数中发现不存在某一层次,说明这个图已经断成两半了,根本无法连通,所以可以直接退出
 inc(dist[v]);
 inc(num[dist[v]]);
 cur[v]:=first[v];
 exit(now);
end;

function maxflow:longint;
var i,flow:longint;
begin
 fillchar(dist,sizeof(dist),0);
 fillchar(num,sizeof(num),0);
 flow:=0;
 num[0]:=t;
 for i:= 0 to n do cur[i]:=first[i];  //当前弧初始化
 while dist[s]<t do inc(flow,sap(s,maxlongint));
 exit(flow);
end;

procedure init;

begin
 readln(n,m,s,t);    //n个点,m条边,s、t分别为起点终点
 for i:= 1 to n do first[i]:=-1;
 tot:=0;
 for i:=1 to m do
  begin
   readln(a,b,c);
   addedge(a,b,c);
  end;
end;

Begin
init;
writeln(maxflow);
End.

附测试用数据:
Data:
6 10 1 6
1 3 3
1 4 2
1 2 3
4 2 1
3 4 1
2 5 4
5 4 1
3 6 2
4 6 4
5 6 1

Ans:7
View Code

 

最小费用最大流
spfa大法和zkw大法当时都学了。
而且当时蔡大神总结的也非常好(点赞)。所以也不想讲了。
zkw大法的话其实还是用标号思想啦,不行就调整在找,找到不行有调整再搞……然后关于跑有负边的zkw费用流,当时并不是很理解。现在再看重新想了下终于懂了……其实还是一样,同样是满足一个标号,Di≥Dj+cij(i->j),一般情况是当边是正的时D是从s到t递减,不断增大Di以求更多的边。现在边有负,所以是从s到t递增,解决方案很简单,先算出t到s每个点D是正数最小是多少,初始化一下标号,还是可以套用以前的式子。

扔个有负边的

//vijos1525 生命之泉
type
  arr=record
    toward,cap,cost,next:longint;
  end;

const
  maxn=50000;

var
  edge:array[0..10000]of arr;
  first,dist,slack:array[0..5000]of longint;
  q:array[0..maxn]of longint;
  chose:array[0..5000]of boolean;
  i,j,k,l,s,t,n,m,tot,maxcost,maxflow,kk:longint;

procedure add(i,j,k,l:longint);
begin
  edge[tot].toward:=j;
  edge[tot].cap:=k;
  edge[tot].cost:=l;
  edge[tot].next:=first[i];
  first[i]:=tot;
  inc(tot);
end;

procedure addedge(i,j,k,l:longint);
begin
  add(i,j,k,l);
  add(j,i,0,-l);
end;

function min(x,y:longint):longint;
begin
  if x>y then exit(y);
  exit(x);
end;

procedure spfa;
var
  head,tail,i,too,value:longint;
begin
  fillchar(chose,sizeof(chose),false);
  for i:=0 to t do dist[i]:=maxlongint;
  head:=1;
  tail:=1;
  q[1]:=s;
  chose[s]:=true;
  dist[s]:=0;
  while head<=tail do begin
    j:=q[head];
    i:=first[j];
    while i<>-1 do begin
      too:=edge[i].toward;
      value:=edge[i].cost;
      if (edge[i].cap>0) and (dist[too]>dist[j]+value) then begin
        dist[too]:=dist[j]+value;
        if not chose[too] then begin
          inc(tail);
          if tail=maxn then tail:=1;
          q[tail]:=too;
          chose[too]:=true;
        end;
      end;
      i:=edge[i].next;
    end;
    inc(head);
    if head=maxn then head:=1;
    chose[j]:=false;
  end;
end;

function aug(v,flow:longint):longint;
var
  rec,ret,too,value,i:longint;
begin
  if v=t then begin
    inc(maxcost,(dist[t]-dist[s])*flow);
    inc(maxflow,flow);
    exit(flow);
  end;
  i:=first[v];
  chose[v]:=true;
  rec:=0;
  while i<>-1 do begin
    too:=edge[i].toward;
    value:=edge[i].cost;
    if (edge[i].cap>0) and (not chose[too]) then
      if dist[v]=dist[too]+value then begin
        ret:=aug(too,min(flow-rec,edge[i].cap));
        dec(edge[i].cap,ret);
        inc(edge[i xor 1].cap,ret);
        inc(rec,ret);
        if rec=flow then exit(flow);
      end else
        slack[too]:=min(slack[too],dist[too]+value-dist[v]);
    i:=edge[i].next;
  end;
  exit(rec);
end;

function rel:boolean;
var
  spent,i:longint;
begin
  spent:=maxlongint;
  for i:=0 to t do
    if not chose[i] then spent:=min(spent,slack[i]);
  if spent=maxlongint then exit(false);
  for i:=0 to t do
    if chose[i] then inc(dist[i],spent);
  exit(true);
end;

procedure costflow;
begin
  spfa;
  for i:=0 to t do
    dist[i]:=-dist[i];
  repeat
    for i:=0 to t do slack[i]:=maxlongint;
    repeat
      fillchar(chose,sizeof(chose),false);
    until aug(s,maxlongint)<=0;
  until not rel;
end;

procedure into;
begin
  readln(n,m,kk);
  s:=0;
  t:=n+2;
  tot:=0;
  for i:=0 to t do first[i]:=-1;
  addedge(1,t,kk,0);
  addedge(s,n+1,kk,0);
  for i:=1 to m do begin
    readln(j,k,l);
    addedge(k+1,j,1,-l);
  end;
  for i:=1 to n do
    addedge(i+1,i,maxlongint,0);
end;

Begin
  into;
  costflow;
  writeln(maxcost);
  readln;
  readln;
End.
View Code

 

关于更多

有上下界的网络流,费用流:这个看书上说的balabala说是要见好多个附加网络神马的,然后看hzw大神的代码发现根本不需要那么麻烦,似乎只有多加个超汇点和超源点判断是否可行,建边要拆开,上界-下界的结果直接建,然后下界起点连超汇点,超源点连边终点,然后再用原源点和原汇点跑一边,两次答案相加就是最大流答案了?

以前做了好多还是用几天时候回顾下看看当时是不是比较傻就直接过了?

posted @ 2015-01-04 21:55  Macaulish  阅读(619)  评论(0编辑  收藏  举报