网络流

之前写的都什么玩意,重写!

定义

网络是一种特殊的有向图,特殊之处在于其存在两个特殊的点 \(s,t\),叫做源点汇点;同时每条边有权值 \(c\),表示这条边的容量;每个边还需要记录一个值 \(f\) 表示流量
残量网络:对于一条边,将其容量与流量之差称为剩余流量,网络中的所有点和剩余流量大于 \(0\) 的所有边的集合即为残量网络。
增广路:在残量网络上的一条 \(s\)\(t\) 的路径。

最大流问题

这里仅介绍最常用的 Dinic 算法。
求解最大流的方式是不断地寻找增广路。
对于每一条边,建立一条容量为 \(0\) 的反边。
在残量网络上进行 BFS,确定每个节点到源点的最短距离 \(dis\),按照 \(dis\) 划分层次。若 BFS 无法到达 \(t\),说明不存在增广路,算法结束。
在构建好的分层图中 DFS,尽可能多地寻找增广路,将流量推送到汇点。
记录当前弧,每次增广后记录尝试边,下一次直接从这条边开始,跳过之前已经处理过的边。
记录可推送流量限制。
若到达 \(t\),说明找到了一条增广路,返回当前路径还能推送的流量上限。
DFS 过程从当前弧开始,遍历出边尝试继续增广。将可推送流量限制与当前边剩余流量取较小值。
若成功增广了流量,在正向边的 \(f\) 加上流量,反向边的 \(f\) 减去流量方便退流(等价于令反向边剩余流量增加,这也是反向边容量为 \(0\) 的意义)。同时更新可推送流量限制和答案。
你也可以直接令 \(c\) 为剩余流量,这样就不用记录 \(f\) 了。记录 \(f\) 可以使得在一些需要输出方案的题目中非常好写。
时间复杂度 \(O(n^2m)\)。但是绝大多数情况下跑不满。

#include<iostream>
#include<queue>
#include<cstring>
using namespace std;
typedef long long ll;
constexpr int N=210,M=5010;
constexpr ll inf=1e18;
int n,m,s,t,head[N],tot;
ll ans;
struct edge{
    int to,nxt;
    ll c,f;
}e[M*2];
inline void add_edge(int u,int v,int c){
    e[++tot].nxt=head[u];
    e[tot].to=v;
    e[tot].c=c;
    head[u]=tot;
}
int cur[N],dis[N];
bool bfs(){
    memcpy(cur,head,sizeof(int)*(n+10));
    memset(dis,0x7f,sizeof(int)*(n+10));
    queue<int> q;
    dis[s]=0;
    q.push(s);
    while(!q.empty()){
        int u=q.front();
        q.pop();
        for(int i=head[u];i;i=e[i].nxt){
            int v=e[i].to;
            ll c=e[i].c,f=e[i].f;
            if(dis[v]<2e9||c-f==0) continue;
            dis[v]=dis[u]+1;
            q.push(v);
            if(v==t) return 1;
        }
    }
    return 0;
}
ll dfs(int u,ll lim){
    if(u==t) return lim;
    ll res=0;
    for(int i=cur[u];i;i=e[i].nxt){
        cur[u]=i;
        int v=e[i].to;
        ll c=e[i].c,f=e[i].f;
        if(dis[v]!=dis[u]+1||c-f==0) continue;
        ll temp=dfs(v,min(lim,c-f));
        if(!temp) dis[v]=2e9;
        else{
            e[i].f+=temp;
            e[i^1].f-=temp;
            lim-=temp;
            res+=temp;
        }
        if(!lim) break;
    }
    return res;
}
int main(){
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    tot=1;
    cin>>n>>m>>s>>t;
    for(int i=1,u,v,w;i<=m;i++){
        cin>>u>>v>>w;
        add_edge(u,v,w);
        add_edge(v,u,0);
    }
    while(bfs()) ans+=dfs(s,inf);
    cout<<ans<<'\n';
    return 0;
}

习题:

最小割问题

定义 \(s\)-\(t\) 割为在网络上选取若干条边,将它们断开后可以将网络分为包含 \(s\) 和包含 \(t\) 的两部分。定义 \(s\)-\(t\) 割的容量为这些边的容量之和。我们下文中提到的最小割都指最小 \(s\)-\(t\) 割。可以证明,最大流等于最小割。这就是最大流最小割定理

建模时可以将容量设为 \(\inf\) 表示不可割。

最小割的必须边可行边

  • 一条边是最小割的必须边,当且仅当图中所有最小割都包含这条边。
  • 一条边是最小割的可行边,当且仅当存在至少一个最小割包含这条边。

以下是求解方法:
首先,可行边和必须边必须满流。

  • 必须边:残量网络中不存在 \(u\to v\) 的路径。在算法中的体现是 \(u,v\) 不属于同一个强连通分量。

  • 可行边:\(s\) 可到达 \(u\)\(v\) 可到达 \(t\)。在算法中的体现是 \(s,u\) 在同一个强连通分量中且 \(v,t\) 在同一个强连通分量中。

模板题:[AHOI2009] 最小割

最小费用最大流问题

费用流:对于一个网络,每条边再加上一个额外的权值 \(w\),表示单位流量的费用。当流量为 \(f\) 时,需要花费 \(f\times w\) 的费用。最小费用最大流问题就是在求得最大流的前提下最小化费用。
实现方法只需令反边的费用为 \(-w\),将 Dinic 算法中 BFS 求增广路的过程改为最短路算法(SPFA),寻找费用最小的增广路即可。
DFS 时记录遍历过的点防止走环路。
最坏时间复杂度 \(O(nmf)\)\(f\) 为最大流。

#include<iostream>
#include<cstring>
#include<queue>
#include<climits>
using namespace std;
constexpr int N=5e3+10,M=5e4+10,inf=1e9;
int n,m,s,t,tot,head[N],cur[N],dis[N],ans1,ans2;
struct edge{int to,nxt,c,w;}e[M*2];
inline void add_edge(int u,int v,int c,int w){
    e[++tot].nxt=head[u];
    e[tot].to=v;
    e[tot].c=c;
    e[tot].w=w;
    head[u]=tot;
}
bool vis[N];
queue<int> q;
bool spfa(){
    memset(dis,0x3f,sizeof(int)*(n+10));
    memcpy(cur,head,sizeof(int)*(n+10));
    dis[s]=0;
    vis[s]=1;
    q.push(s);
    while(!q.empty()){
        int u=q.front();
        q.pop();
        vis[u]=0;
        for(int i=head[u];i;i=e[i].nxt){
            int v=e[i].to,c=e[i].c,w=e[i].w;
            if(dis[v]<=dis[u]+w||!c) continue;
            dis[v]=dis[u]+w;
            if(vis[v]) continue;
            q.push(v);
            vis[v]=1;
        }
    }
    return dis[t]<inf;
}
int dfs(int u,int lim){
    if(u==t) return lim;
    vis[u]=1;
    int res=0;
    for(int i=cur[u];i;i=e[i].nxt){
        cur[u]=i;
        int v=e[i].to,c=e[i].c,w=e[i].w;
        if(dis[v]!=dis[u]+w||!c||vis[v]) continue;
        int temp=dfs(v,min(lim,c));
        if(!temp) dis[v]=inf;
        else{
            e[i].c-=temp;
            e[i^1].c+=temp;
            res+=temp;
            lim-=temp;
            ans2+=temp*w;
        }
        if(!lim) break;
    }
    vis[u]=0;
    return res;
}
int main(){
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    tot=1;
    cin>>n>>m>>s>>t;
    for(int i=1,u,v,c,w;i<=m;i++){
        cin>>u>>v>>c>>w;
        add_edge(u,v,c,w);
        add_edge(v,u,0,-w);
    }
    while(spfa()) ans1+=dfs(s,INT_MAX);
    cout<<ans1<<' '<<ans2<<'\n';
    return 0;
}

习题:

二分图最大匹配问题

网络流可以求解二分图最大匹配问题。建图方法:

  • \(s\) 向所有左部点连容量为 \(1\) 的边;
  • 所有右部点向 \(t\) 连容量为 \(1\) 的边;
  • 二分图上的原始边仍然连接,容量为 \(1\)

根据最大流最小割定理,此网络的最大流等于最大匹配数。
输出方案直接遍历左部点,检查流量为 \(1\) 的边即可。
由于网络中所有边容量均为 \(1\),Dinic 算法求二分图最大匹配的时间复杂度为 \(O(m\sqrt n)\),优于匈牙利算法。

习题:

建模技巧

拆点

火星探险问题
将车的数量作为流量,采集的岩石数量作为费用,则本题就是要求出最大费用最大流。
由于点权不方便跑网络流,考虑将一个点拆位入点和出点,下面是建图方法:

  • 对于所有非障碍点,入点向出点连接容量为 \(\inf\),费用为 \(0\) 的边,表示可以无限经过但没有岩石;
  • 对于有岩石的点,入点向出点额外连接一条容量为 \(1\),费用为 \(1\) 的边,表示岩石最多被采集 \(1\) 次;
  • 对于所有非障碍点 \((x,y)\),其出点向 \((x+1,y)\)\((x,y+1)\)(如果存在)的入点连接容量为 \(\inf\),费用为 \(0\) 的边。
  • \(s\)\((1,1)\) 的入点连容量为 \(n\),费用为 \(0\) 的边,表示最多有 \(n\) 辆车;
  • \((p,q)\)\(t\) 连接容量为 \(\inf\),费用为 \(0\) 的边。

可以将费用取负然后跑最小费用最大流。
输出方案可以记录每条边的流量,DFS 检查流量大于 \(0\) 的出边,将流量减 \(1\) 然后继续遍历即可。

[CQOI2015] 网络吞吐量
首先跑一遍最短路保留可能出现在最短路上的边,然后拆点使得点权转换为边容量,跑最大流即可。

黑白染色

方格取数问题
首先可以将最大化问题转化为最小化问题,即求出最小的由于违反约束而放弃的数的和。
尝试将方格黑白染色,任意两个相邻的方格必然颜色不同。
下面是建图方法:

  • 连接 \(s\) 到所有白格的边,容量为方格中的数字;
  • 连接所有黑格到 \(t\) 的边,容量为方格中的数字,以上两种边的割都表示我们放弃选取这条边所连的方格;
  • 连接所有方格图中相邻的边,容量为 \(\inf\),表示若同时选择了这条边所连的两个点,所求情况不合法,无法被割。

这实质上是在求二分图最大权独立集。

互异决策转化为割

文理分科
\(s\) 向所有点 \((i,j)\) 连边,容量为 \(art_{i,j}\),所有点 \((i,j)\)\(t\) 连边,容量为 \(science_{i,j}\),则不考虑额外满意值的情况下最大总满意值为总满意值减去最小割,因为此时的一个 \(s\)-\(t\) 割可以视为将点划分为选文和选理两部分。
现在尝试将额外满意值加入答案。首先将所有满意值和额外满意值加入答案,则减去考虑额外满意值情况下的最小割就是答案。
建图方法:

  • 对于每个点 \((i,j)\),额外建立文科点 \(A(i,j)\) 和理科点 \(S(i,j)\)
  • \(s\) 向每个 \((i,j)\) 连边,容量为 \(art_{i,j}\),表示割这条边损失 \(art_{i,j}\) 的贡献;
  • 每个 \((i,j)\)\(t\) 连边,容量为 \(science_{i,j}\)
  • \(s\) 向每个 \(A(i,j)\) 连边,容量为 \(same\_art_{i,j}\)\(A(i,j)\)\((i,j)\) 及它的相邻点连边,容量为 \(\inf\),表示不可割,也就是说,只要选择 \(A(i,j)\) 就必须选 \((i,j)\) 及它的相邻点,否则必须割掉 \(s\)\(A(i,j)\) 损失贡献;
  • 每个 \(S(i,j)\)\(t\) 连边,容量为 \(same\_science_{i,j}\)\((i,j)\) 及它的相邻点向 \(S(i,j)\) 连边,容量为 \(\inf\)

[COCI 2020/2021 #3] Selotejp
拜谢 @Kenma
暂时拿下最优解

参考资料

https://oi-wiki.org/graph/flow/

https://zh.wikipedia.org/wiki/最大流最小割定理

posted @ 2025-10-10 19:22  headless_piston  阅读(7)  评论(0)    收藏  举报