ACM算法总结 网络流




基础知识

在图论中,网络流是指在带权有向图 G = ( V , E , C ) G=(V,E,C) 上为每条边分配流量,从而最优化某一属性。网络流图中有且仅有一个源点,有且仅有一个汇点,并且必须满足以下条件:

  • 容量限制:一条弧上的流量不能超过其容量限制 f ( u , v ) c ( u , v ) f(u,v)\leq c(u,v)
  • 流守恒:对于任意不是源点或汇点的结点 x x ,其入流量应当等于出流量 u E f ( u , x ) = v E f ( x , v ) \sum_{u\in E}f(u,x)=\sum_{v\in E}f(x,v)

最大流:从源点 s s 到汇点 t t 的最大流量。

最小割:边权值之和最小的割;其中定义为某个分割对应的某一个边集 C i C_i ,使得网络删去 C i C_i 中所有边之后 s s t t 无通路。

  • 任意流都小于等于任意割(很形象,流是真实流量,肯定小于等于弧的容量)
  • 最大流等于最小割(最大流最小割定理

最大流算法(Dinic):首先建图,建立反向边,然后基本思想就是先BFS对图进行分层(依据离源点的距离),然后结合弧优化(标记已经增广过的弧)进行DFS多路增广(其中增广路是最短的,因为按照图的层来进行增广,多路的意思就是一直增广,直到当前残余流量为0),并不断重复以上过程直至不存在增广路(也即BFS后汇点的层数为-1)。代码如下:

const int maxn=1e4+5,inf=1e9;
struct edge {int to,cap,rev,is_rev;};
vector<edge> G[maxn];
int dis[maxn],book[maxn];

void add_edge(int from,int to,int cap)
{
    G[from].push_back((edge){to,cap,(int)G[to].size(),0});
    G[to].push_back((edge){from,0,(int)G[from].size()-1,1});
}

void BFS(int s)
{
    mem(dis,-1); dis[s]=0;
    queue<int> que; que.push(s);
    while(!que.empty())
    {
        int v=que.front();que.pop();
        REP(i,0,G[v].size()-1)
        {
            edge e=G[v][i];
            if(dis[e.to]<0 && e.cap) dis[e.to]=dis[v]+1,que.push(e.to);
        }
    }
}

int dfs(int s,int t,int flow)
{
    if(s==t) return flow;
    for(int &i=book[s];i<(int)G[s].size();i++)
    {
        edge &e=G[s][i];
        if(e.cap && dis[s]<dis[e.to])
        {
            int flow2=dfs(e.to,t,min(flow,e.cap));
            if(!flow2) continue;
            e.cap-=flow2;
            G[e.to][e.rev].cap+=flow2;
            return flow2;
        }
    }
    return 0;
}

int max_flow(int s,int t)
{
    int flow=0,flow2;
    while(1)
    {
        BFS(s);
        if(dis[t]<0) return flow;
        mem(book,0);
        while((flow2=dfs(s,t,inf))>0) flow+=flow2;
    }
}

个人认为,算法的精髓之处在于反向边的建立,有了反向边这一迷之设定,就不用考虑其它的,类似贪心的思路不停地增广就行了。


一些常用套路:

  • 跑完一遍Dinic之后,可以获取当前每一条边的真实流量:在最大流的情况下,网络流中每一条弧的真实流量,等于其反向边的流量(注意代码中的流量操作都是直接在cap上操作的,所以原来的边的信息已经被破坏,但是由于每条弧的流量等于其反向弧的相反数,而反向弧初始流量为0,故可以根据反向弧的流量来推断出原来的弧的流量)。这也是为什么每条边都要加一个is_rev的标记。
  • 最小割的边集中的边一定是满流的,但是满流的不一定是其中的边。跑完Dinic后可以将所有点按照能否由源点到达分成两个集合,如果某一条边同时在两个集合中,那么这条边就属于最小割的边集。
  • 注意,上面那种方法只是求出了某一个可行解,不一定是边数最少的最优解。若要求最小割的最少边数,跑完一次Dinic后可以令满流的边容量为1,其它为inf,再跑一次Dinic即可。
  • 拆点,比如说如果有顶点流量限制,可以拆成两个点相连,边容量为其限制 等等。这个技巧性比较高,需要多练习。



上下界可行流

上下界可行流用于解决图中边的流量有上下界的限制的问题,通常分为以下几类:

扫描二维码关注公众号,回复: 8699759 查看本文章

无源汇上下界可行流(循环流):我们以下界作为初始流量(实际过程中是0,旨在转化为最大流),上界减下界作为边的容量,新增源汇点维护初始每个点的流量平衡。具体做法是对于每个点计算初始流经总流量M(流入为负,流出为正),如果M=0则初始流量平衡;如果M<0说明流入太多,则从源点添加指向当前点容量为-M的边;如果M>0说明流出太多,则添加指向汇点容量为M的边。最后如果源点到汇点的最大流等于源点(汇点)总出(入)边权(或者源点的出边全部满流),则存在稳定的可行流,通过残余网络可以计算出每条边的具体流量。

  • 这里的源点和汇点实际上起到了中和流量的作用。

有源汇上下界可行流:考虑添加一条从汇点到源点,上界为inf,下界为0的边,即转换为无源汇上下界可行流问题。最后的可行流为汇点到源点的流量。

有源汇上下界最大流:处理完有源汇上下界可行流之后,去掉源汇点(注意不是超级源汇点)之间连接的边,然后从源点到汇点的最大流加上之前的可行流就是答案。

有源汇上下界最小流:处理完有源汇上下界可行流之后,去掉源汇点(注意不是超级源汇点)之间连接的边,然后之前的可行流减去汇点到源点的最大流就是答案。



最小费用流

在每条边有流量限制,并且每条边给出单位流量的费用 c o s t cost 的情况下,用于求解从源点到汇点在给定目标流量 f l o w flow 下的最小费用。

基本思想类似于Dinic求最大流的思想,不过Dinic是按照距离BFS分层,然后选择最短路径进行增广,而最小费用流的算法通过SPFA按照费用每次求出 s s t t 的最短路,然后在这条最短路上进行增广。代码如下(当传入流量限制 f l o w flow 等于 i n f inf 时,可以同时求出最大流(单次增广累加)和最小费用):

const int maxn=2e5+5,inf=1e9;
struct edge {int to,cap,rev,cost;};
vector<edge> G[maxn];
int dis[maxn],book[maxn],prevv[maxn],preve[maxn];

void add_edge(int from,int to,int cap,int cost)
{
    G[from].push_back((edge){to,cap,(int)G[to].size(),cost});
    G[to].push_back((edge){from,0,(int)G[from].size()-1,-cost});
}

int min_cost_flow(int n,int s,int t,int flow)    //flow为流量限制
{
    int ans=0;
    while(flow>0)
    {
        fill(dis,dis+n+1,inf); mem(book,0);
        queue<int> que; que.push(s);
        dis[s]=0; book[s]=1;
        while(!que.empty())
        {
            int v=que.front(); que.pop();
            book[v]=0;
            REP(i,0,G[v].size()-1)
            {
                edge &e=G[v][i];
                if(e.cap>0 && dis[e.to]>dis[v]+e.cost)
                {
                    dis[e.to]=dis[v]+e.cost;
                    prevv[e.to]=v;
                    preve[e.to]=i;
                    if(!book[e.to]) book[e.to]=1,que.push(e.to);
                }
            }
        }
        if(dis[t]==inf) return -1;
        //if(dis[t]==inf) return ans;
        int d=flow;     // 单次增广流量
        for(int v=t;v!=s;v=prevv[v]) d=min(d,G[prevv[v]][preve[v]].cap);
        flow-=d;
        ans+=d*dis[t];
        for(int v=t;v!=s;v=prevv[v])
        {
            edge &e=G[prevv[v]][preve[v]];
            e.cap-=d;
            G[v][e.rev].cap+=d;
        }
    }
    return ans;
}



二分图

可以把图中的顶点分为两个集合,使得这两个集合内部都没有边,这样的图叫做二分图。换句话说,图中每一条边的两个端点必然分别在两个集合中。

  • 二分图不存在长度为奇数的环,所以可以以此判断某个图是否可以二分

最大匹配:找到一个边集,使得这个边集中的边所覆盖的所有顶点都只被一条边覆盖(即二分图两个点集中的点两两配对),其中覆盖顶点最多的方案中的边数就是最大匹配数。求解方法是两个点集中的点分别与新建的源点和汇点相连,边的容量为1,原来的边转化为容量为1的单向有向边(配合 s s t t 的方向),然后其最大流就是最大匹配数。容易从残量网络中推知具体的匹配方案(某一个匹配的流量必然是一条这样的路径: s s A A B B t t

最小顶点覆盖:找到一个最小的点集,使得点集覆盖了二分图中所有的边。二分图最小顶点覆盖的顶点数等于其最大匹配数。Dinic跑完最大匹配后,图中满足 (dis[i]==-1)==is_connect(i,s)​ 的顶点 i i 构成最小顶点覆盖的解。

最大独立集:找到一个最大的点集,使得点集中的点两两之间没有边。最大独立集=全集\最小顶点覆盖(这句话的意思包括了数目和方案)。详细地来说,最大独立集的顶点数目等于总顶点数目减去最大匹配,而Dinic跑完最大匹配后,图中满足 (dis[i]==-1)^is_connect(i,s) 的顶点 i i 构成最大独立集的解。

最小点权覆盖:在一个带权二分图中总权值最小的顶点覆盖。具体做法是:两个点集分别与新建的 s s t t 相连,容量为对应点的点权,原来的边转化为容量为 i n f inf 的有向边,最小点权覆盖的点权和等于最大流。

最大点权独立集:在一个带权二分图中总权值最大的独立集。最大点权独立集=总点权-最小点权覆盖。


注意,这里的匹配、独立集等等算法都是建立在二分图的基础上的!



闭合图

如果一个有向图中任意顶点的出边的终点都在这个图中,那么这个图是一个闭合图。可以形象地理解为这个图没有“支出去”的边。


最大权闭合子图:有向(点)带权图中最大点权和的闭合子图。具体解法:权值为正的点与源点相连,权值为负的点与汇点相连,边的容量都为对应点权的绝对值,原来图中的边的容量为inf,则最大权闭合子图的权值=正权值顶点权值和-最小割(最大流)。

  • 由于原来的边容量都是inf,故最小割只能割去与 s s t t 相连的边(与前面的最小割不同,这里满流的边一定属于最小割的边)。
  • 如果割掉 s s i i 相连的边,表示不选择 i i 作为解当中的点;如果割掉 t t i i 相连的边,表示选择 i i 作为解当中的点。



发布了12 篇原创文章 · 获赞 5 · 访问量 528

猜你喜欢

转载自blog.csdn.net/dragonylee/article/details/102935936
今日推荐