二分图、网络流模版总结
这个星期重新回去理解二分图和网络流的算法,觉得当初真的是有点傻萌,都不太了解就直接套模版……
关于二分图
关于二分图的概念不想吐槽了,太多而且好多都好乱,看了很多个什么概念总结结果发现有的人把最大匹配叫最佳匹配。反正是看的我一脸蛋疼,决定还是抛开概念吧,看着像啥用啥得了。
匈牙利算法:
其实就是不断寻找增广路。那么什么是增广路呢?
既然是二分图,我们先定义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;
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.
更多:
二分图最大匹配的三个定理
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
最小费用最大流
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.
关于更多
有上下界的网络流,费用流:这个看书上说的balabala说是要见好多个附加网络神马的,然后看hzw大神的代码发现根本不需要那么麻烦,似乎只有多加个超汇点和超源点判断是否可行,建边要拆开,上界-下界的结果直接建,然后下界起点连超汇点,超源点连边终点,然后再用原源点和原汇点跑一边,两次答案相加就是最大流答案了?
以前做了好多还是用几天时候回顾下看看当时是不是比较傻就直接过了?