网络流初步

最大流

最大流:对已知容量的图求其最大流量。

在图的残量网络上,从 \(s\) 到 \(t\) 的路径称为增广路。对一条增广路上所有边增加相同的流量的过程称为增广,而我们在算法实现中就取可加的最大流量即增广路上最小残量来加。

但是这样类似贪心地找增广路后有可能不是最优解,所以我们的图需要支持反悔操作。方法是建立反向边,反边的容量为 \(0\),当正边流量增加时其流量减小,这样我们就可以支持反悔。所以在建图时就可以把反边减在正边下一个便于访问。

EK 算法

每次都使用 bfs 找增广路,找到之后直接把每一条边的残量都减去路上最小的残量。时间复杂度 \(O(nm^2)\)。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define _int __int128
#define ull unsigned long long
#define pii pair<int,int>
#define fst first
#define scd second
#define pq priority_queue
#define mkp make_pair
#define popcount(x) __builtin_popcount(x)
#define endl '\n'
int n,m,s,t;
const int N = 5e3+10,inf=1e18;
int cnt=1,head[2*N],nxt[2*N],to[2*N],c[2*N],fl[N],pre[N];
bitset<N>vis;
void add(int u,int v,int w){
    nxt[++cnt]=head[u];head[u]=cnt;to[cnt]=v;c[cnt]=w;
}
int bfs(){
    vis.reset();
    queue<int>q;
    q.push(s);
    vis[s]=1;
    fl[s]=inf;
    while(!q.empty()){
        int u=q.front();q.pop();
        for(int i=head[u];i;i=nxt[i]){
            int v=to[i];
            if(!c[i]||vis[v])continue;
            vis[v]=1;
            fl[v]=min(fl[u],c[i]);
            pre[v]=i;
            q.push(v);
            if(v==t)return 1;
        }
    }
    return 0;
}
int EK(){
    int mf=0;
    while(bfs()){
        int u=t;
        while(u!=s){
            int i=pre[u];
            c[i]-=fl[t];
            c[i^1]+=fl[t];
            u=to[i^1];
        }
        mf+=fl[t];
    }
    return mf;
}
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    cin>>n>>m>>s>>t;
    for(int i=1;i<=m;i++){
        int u,v,w;
        cin>>u>>v>>w;
        add(u,v,w);
        add(v,u,0);
    }
    cout<<EK()<<endl;
    return 0;
}

dinic 算法

每次只找一条增广路太慢,考虑一次寻找多条,先用一个 bfs 把图分层,保证在 dfs 的时候只能由该节点走向 \(dep\) 更大的节点,同时使用当前弧优化,已经看过的边就没必要再看了。

dfs 参数中的 \(mf\) 表示当前最多可以给出的流量,返回的是实际的流量。

时间复杂度 \(O(n^2m)\)。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define _int __int128
#define ull unsigned long long
#define pii pair<int,int>
#define fst first
#define scd second
#define pq priority_queue
#define mkp make_pair
#define popcount(x) __builtin_popcount(x)
#define endl '\n'
int n,m,s,t;
const int N = 5e3+10,inf=1e18;
int cnt=1,head[N],nxt[2*N],to[2*N],c[2*N],dep[N],cur[N];
bitset<N>vis;
void add(int u,int v,int w){
    nxt[++cnt]=head[u];head[u]=cnt;to[cnt]=v;c[cnt]=w;
}
bool bfs(){
    for(int i=1;i<=n;i++)dep[i]=0;
    queue<int>q;q.push(s);
    dep[s]=1;
    while(!q.empty()){
        int u=q.front();q.pop();
        for(int i=head[u];i;i=nxt[i]){
            int v=to[i];
            if(dep[v]||!c[i])continue;
            dep[v]=dep[u]+1;
            q.push(v);
            if(v==t)return 1;
        }
    }
    return 0;
}
int dfs(int u,int mf){
    if(u==t)return mf;
    int sum=0;
    for(int i=cur[u];i;i=nxt[i]){
        cur[u]=i;
        int v=to[i];
        if(dep[v]!=dep[u]+1||!c[i])continue;
        int f=dfs(v,min(mf,c[i]));
        sum+=f;mf-=f;
        c[i]-=f;c[i^1]+=f;
        if(!mf)break;
    }
    if(!sum)dep[u]=0;
    return sum;
}
int dinic(){
    int ans=0;
    while(bfs()){
        for(int i=0;i<=n;i++)cur[i]=head[i];
        ans+=dfs(s,inf);
    }
    return ans;
}
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    cin>>n>>m>>s>>t;
    for(int i=1;i<=m;i++){
        int u,v,w;
        cin>>u>>v>>w;
        add(u,v,w);
        add(v,u,0);
    }
    cout<<dinic()<<endl;
    return 0;
}

例题

二分图最大匹配

把所有边视为容量为 \(1\),源点向左部点都连容量为 \(1\) 的边,右部点都向汇点连容量为 \(1\) 的边,则二分图最大匹配数等于这个图的最大流。若使用 dinic 求二分图最大匹配的复杂度可达到 \(O(m\sqrt n)\)。

P1402 酒店之王

一个人有自己所要求的房间和食物,一种房间和食物只能提供给一个人,询问最多能使多少人满意。

由于只有人分别和房间和食物有对应关系,所以我们把人的点用来连接房间的点和食物的点,源点连向房间的点,食物的点连向汇点,注意人的点要拆成两个,自己向自己连边。

P2754 家园

我们对每一个时间都建立所有点,第 \(i\) 号点在时刻 \(d\) 的点为 \(d\times (n+2)+i\),每一时刻该点的人可以选择离开或留下,则所有点向下一时刻的自己连容量为无限的边,在对应的飞船开到的两点之间连接边,当 \(1\) 到当前时刻的总流量之和超过了总人数说明所有人转移完成。

P2766 最长不下降子序列问题

把每个点拆成入点和出点,要求拼出最长的不下降子序列,则把 \(f_i+1=f_j\) 的 \(i+n\) 向 \(j\) 连边,\(s\) 向所有 \(f_i=1\) 的 \(i\) 连边,所有 \(f_i=len\) 的 \(i+n\) 向 \(t\) 连边,跑最大流即可。第三问改一下容量即可。

最小割

例题:

P1344 [USACO4.4] 追查坏牛奶 Pollutant Control

求最小割以及最小割的割边数量,当容量都是整数的时候,给每条边容量变为 \(w\times c_i+1\),这样求出的最小割就是 \(f=w\times ans +k\),则对于大于 \(k\) 的 \(w\),最小割等于 \(f/w\),最小割的割边数量等于 \(f \bmod w\)。

P5039 最小生成树

一次操作可以让一条边的边权增加 \(1\),询问至少操作多少次能让要求的边 \(AB\) 一定出现在 MST 中。重点在于想到一条边什么时候必定会被选为 MST 中的边,显然在所有包含 \(AB\) 的环上,\(AB\) 的边权不能是最大的。我们以 \(A\) 为源点,\(B\) 为汇点,考虑除了 \(AB\) 的所有边,若其边权大于 \(w_{AB}\) 则忽略,否则 \(u,v\) 之间连双向边,边权为 \(w_{AB}-w_i+1\)。则此时最小割就是需要的最小操作次数。

二选一模型

有 \(A\),\(B\) 两个集合,如果把第 \(i\) 个物品不放在 \(A\) 集合有 \(a_i\) 花费,不放在 \(B\) 集合有 \(b_i\) 花费,若某两个点不在同一集合有 \(w\) 的花费,询问最小花费。则令所有点有 \(s\) 连容量为 \(a_i\) 的边,向 \(t\) 连容量为 \(b_i\) 的边,\(u,v\) 之间连容量为 \(w\) 的边,则断边说明不在同一集合中,求最小割就是最小花费。
例题 1:P2057 善意的投票
例题 2:P4313 文理分科
这题有了联合权值,即同时有多个点在同一个集合内有额外收益,新建一个联合点向所有或者被所有内部点连接容量为 inf 的边表示内部必须全选,要求最大化收益就是用总权值减去最小化的总亏损即最小割。

最大权值闭合图模型

每个点有正或负点权,点之间有依赖关系,要求选出一个点权和最大的子图使得每个被选中的点其依赖点也被选中。
把 \(s\) 向所有点权为正的点连接容量为其点权的边,所有点权为负的点向 \(t\) 连接容量为点权相反数的边,有依赖的两点连容量为 inf 的边,此时答案为正点权之和减去最小割。
例题 1:P2762 太空飞行计划问题
例题 2:P2805 [NOI 2009] 植物大战僵尸
这题的依赖性可能成环,注意环上的点不能出现在网络里面,可以用拓扑排序判断哪些点是环中的点,只要拓扑排序结束后入度依然大于 \(0\) 说明是成环的点。

二分图带权最大独立集/最小点覆盖

在有点权的二分图上做最大独立集,要求使得独立集的点权和最大。
例题:P2774 方格取数问题
把方格黑白染色,让 \(s\) 连向所有黑点,容量为点权,白点连向 \(t\),容量为点权,在相邻的黑白点之间连容量为 inf 的边,此时总点权减去最小割就是答案。因为如果要断掉 \(s \to a \to b \to t\) 的路径,其中 \(a\) 是黑点,\(b\) 是白点,就只能断掉 \(s \to a\) 或 \(b \to t\),符合题目要求的相邻点只能选择一个。注意 inf 边只能由黑点连向白点。

可行边与必须边

P4126 最小割
询问哪些边是必须被割的,哪些边是可以被割的。首先被割的边应该满流,其次不能有其他 \(u \to v\) 的路径,否则可以退流。对残量网络缩点,则可行边应该满足残量为 \(0\),且 \(u,v\) 不在同一 SCC 内。而必须边则额外要求 \(u\) 和 \(s\) 在同一个 SCC 中,\(v\) 和 \(t\) 在同一个 SCC 中。

切糕模型

例题:P3227 切糕
在一个 \(n \times m\) 的矩阵里填入小于等于 \(r\) 的数,\(v_{i,j,k}\) 是 \((i,j)\) 填 \(k\) 时的得分,要求相邻位置填数之差不能超过 \(d\),最小化得分。
把每个点拆成长为 \(r\) 的链,\(x_{i,j,k} \to x_{i,j,k+1}\),容量为 \(v_{i,j,k}\),断开这条边就相当于这个点取这个值。考虑如何处理相邻的限制,
观察到我们只要向相邻的点的 \(k-d\) 连边,那条链要断开就必须断大于那个位置的边,对于 \(k+d\) 的处理是由上面的点连下来的。
值得注意的是,当 \(v_{i,j,k}\) 可以为负数时,必须每个点向 \(x_{i,j,k-1}\) 连容量为无限的边。

posted @ 2026-09-29 15:04  farenara_sayowell  阅读(7)  评论(0)    收藏  举报