浅谈网络流

本文章同步发表在洛谷博客。

前言

该文为笔者学习网络流后写下的文章,内容较为丰富,涵盖了最大流、费用流、二分图(最小割)及上下界网络流知识,末尾还有各类网络流的配套习题。另外推荐大家去做包含【网络流与线性规划 24 题】标签的网络流题目。

关于本文代码:也许你已经注意到了,某些代码使用的是 vector 存图,而其他代码用的是链式前向星存图。这是因为笔者原是用 vector 存图的,后来发现在网络流算法中,用链式前向星会更方便,逐改变了自己的习惯。可能看起来有些不适,烦请各位多多谅解。不过逻辑都是一样的啦,只是实现方式不同,所以影响应该也不大呀。

如对本文内容有疑惑,欢迎私信询问,我将尽量在三个工作日内答复。文章可能有些瑕疵,如果你发现了,也欢迎在下面评论区中提出,我会尽力修改。

网络模型

想象一些有向水管构成的图,每条水管都有固定的流量上限,有源点可以出水,有汇点可以收水。

形式化描述这个东西。设 \(f_{u \to v}\) 表示 \(u\) 向 \(v\) 流的流量,\(c_{u \to v}\) 表示 \(u \to v\) 这条路上的容量(流量)限制。\(S\) 为源点(起点),\(T\) 为汇点(终点)。

对于边 \(u \to v\) 有 \(w\) 的流量,则 \(f_{u \to v} = w\),并且 \(f_{v \to u} = -w\) 而非什么 \(f_{v \to u} = 0\)。也许你已经看出来了,这个东西和牛顿第三定律有些类似,负的流量表示反向流动。


问题来了,如何判断一个流是否合法?换句话说,合法流的充要条件是什么?

首先有反对称性,也就是所谓的 \(f_{u \to v} = -f_{v \to u}\)。

接着,流量满足限制,即 \(f_{u \to v} \le c_{u \to v}\)。

以及要满足内部流量守恒。即,对于 \(u \not= S,T\),有 \(\sum_{x} f_{u \to x} = 0\)。
推论:外部流量守恒 \(\sum_{x} f_{S \to x} = \sum_{y} f_{y \to T}\),即源点流出量等于汇点流入量。


要求找出一个合法的流所对应的 \(f\),并最大化 \(\sum_{x} f_{S \to x}\)(源点流出的流量),等价于最大化 \(\sum_{x} f_{x \to T}\)(汇点流入的流量)。

这,称为最大流问题。

这个模型和现实情况还是略有区别的,它允许凭空出现的“环流”。不过在最大流问题中,“环流”不仅不能贡献流量,还浪费了容量,所以并不会对最优解造成任何影响。

残量网络上的调整

记 \(r_{u \to v} = c_{u \to v} - f_{u \to v}\),即“剩余流量”。称 \(r\) 为残量网络。

残量调整定理:

对于网络 \(c\),记可能的所有合法流的集合为 \(F_c\)。对于任意一个 \(f_0 \in F_c\),记 \(r = c - f_0\)(即残量网络),则 \(F_c = f_0 + F_r\)。也就是说,源网络的所有合法流可以在残量网络上调整而获得。

可以直观理解,残量网络上的任意流相当于“不超出容量限制的调整”,相当于在原图上增加流量,也可以把已经流过去的流撤回来。这自然可以得到所有合法的流。

增广路与增广

根据残量调整定理,可以得到一种最大流算法的思路:对于当前网络 \(c\),求一个流方案 \(f\) 满足 \(\text{flow}(f) > 0\),然后将 \(c\) 减去 \(f\) 并将最大流答案加上 \(\text{flow}(f)\)。重复这一过程,最大流在不断变大,由于最大流必然是有限的,这个算法必然能结束。

即不断在残量网络上找出一种正的流(调整方法),直到找不到为止。
最终 \(c\) 上不能存在任何正的流,即有容量的边不能联通 \(S,T\)。

我们不妨先考虑最简单的流 \(f\)——一条路径。

于是这里就要引入两个定义:增广路与增广。

  • 增广路:找出残量网络 \(r\) 中从 \(S\) 到 \(T\) 的一条路径,其中路径上的 \(r\) 都 \(>0\),称这条路径为一条增广路。
  • 增广:算出增广路中 \(r\) 的最小值 \(x\),让增广路上的每条边都多流 \(x\) 的流量,这一过程叫做增广。

我们只要不断找出增广路,然后对其进行增广,直到找不出为止,即可得到最大流。

最大流的算法实现

Ford-Fulkerson 算法

简称 FF 算法。

它的做法是,直接用 DFS 找增广路,找到一条就增广。

时间复杂度上界 \(O(m \times \max x)\),\(\max x\) 表示最大流量。

Edmonds-Karp 算法

简称 EK 算法。

它在 FF 算法的基础上进行了一些改进,只考虑容量为正的边,并改用 BFS 找增广路。

这里有一个最短路单调定理:在 EK 算法不断增广的过程中,源点到各个点的最短路单调不降。根据这个最短路单调定理,可证明 EK 算法的时间复杂度为 \(O(n \times m^2)\)。

Dinic 算法

在 EK 算法的基础上改进,考虑用一次“多路增广”完成原先的“增广路长度保持为 \(L\) 的一系列连续增广”。

首先跑一次 BFS 求出 \(dis_{1 \sim n}\)。有一个性质,那就是只需要考虑在最短路上的边。即对于边 \(u \to v\),当且仅当 \(dis_v = dis_u + 1\) 时才需要考虑这条边。

用 DFS 进行多路增广。具体地,到达 \(u\) 点,流入流量为 \(f\),枚举各个出边搜索看看能流出多少。如果能流,将 \(f\) 减去流走的流量,并调整残量网络,尝试下一条边。最后返回总的流出去的流量。

这里有个优化方法叫做当前弧优化。具体地,其为点 \(u\) 记录了 \(p_u\),表示第 \(1 \sim p_u\) 条出边都已经流不出流量,下次访问该点,枚举从 \(p_u + 1\) 开始。这样,每条边都只有一次尝试失败。

总时间复杂度为 \(O(n^2 \times m)\),但是如果不加当前弧优化的话,复杂度会退化为 \(O(n^2 \times m^2)\)。

例题选讲

P3163 危桥

首先根据题目中桥的连边进行建图,正常的桥建成的边的流量为 \(+ \infty\),而危桥的流量限制为 \(2\),因为题目要求只能经过最多两次。

接着建立源点 \(S\) 和汇点 \(T\),然后让它们分别对应地去连边。具体地,源点向 \(a_1,b_1\) 分别连流量为 \(a_n,b_n\) 的边,\(a_2,b_2\) 分别向 \(T\) 连 \(a_n,b_n\) 的边。

直接对这个图跑最大流,然后判断最终的流量是否为 \(2 \times (a_n + b_n)\) 就行了……吗?

不!因为你可能往返,危桥不一定只经过了 \(2\) 次。然后这里有一个很厉害的解决办法,我们对这个图反着再跑一次最大流!看它的流量是不是也是 \(2 \times (a_n + b_n)\)。如果它也是,那么往返的问题就不存在了;如果它不是,就说明确实存在往返,危桥发生状况。

反着跑最大流到底是什么东西呢。简单来说,就是把 \(b_1\) 和 \(b_2\) 颠倒了过去,再连边跑最大流。

这里需要跑两次最大流,因此边的信息要维护好,别漏了或重了。

#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
using namespace std;
const int N = 100;
const int INF = 0x3f3f3f3f;
struct line{int to,wei,Db;};
int n,sa,ta,an,sb,tb,bn,St,Ed,dis[N],Ans;
vector<line> g[N],e[N];
queue<int> q;bool flag;
int read(){
    int su=0,pp=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
    return su*pp;
}
void Add_Edge(int x,int y,int z){
    int idx=e[x].size(),idy=e[y].size();
    e[x].pb({y,z,idy}),e[y].pb({x,0,idx});return;
}
void Add_Gdge(int x,int y,int z){
    int idx=g[x].size(),idy=g[y].size();
    g[x].pb({y,z,idy}),g[y].pb({x,0,idx});return;
}
bool BFS_Canit(){
    for(int i=1;i<=n+2;i++)dis[i]=0;
    while(!q.empty())q.pop();
    dis[St]=1,q.push(St);
    while(!q.empty()){
        int u=q.front();q.pop();
        for(auto [v,w,id]:g[u])if(w&&!dis[v])
            {dis[v]=dis[u]+1;if(v==Ed)return 1;q.push(v);}
    }return 0;
}
int DFS_Cntflow(int u,int flow){
    if(u==Ed)return flow;int now=flow;
    for(auto &[v,w,id]:g[u])
        if(w&&dis[v]==dis[u]+1){
            int tmp=DFS_Cntflow(v,min(now,w));
            if(!tmp)dis[v]=0;else w-=tmp,g[v][id].wei+=tmp,now-=tmp;
        }
    return flow-now;
}
int Sol_Dinic(){
    int res=0,flow=0;
    while(BFS_Canit())
        while(flow=DFS_Cntflow(St,INF))res+=flow;
    return res;
}
int main(){
    while(scanf("%d %d %d %d %d %d %d",&n,&sa,&ta,&an,&sb,&tb,&bn)!=EOF){
        sa++,ta++,sb++,tb++;
        for(int i=1;i<=n;i++)
            for(int j=1;j<=n;j++){
                char h;cin>>h;
                if(h=='O')Add_Edge(i,j,2);
                if(h=='N')Add_Edge(i,j,INF);
            }
        for(int i=1;i<=n;i++)g[i]=e[i];
        St=n+1,Ed=n+2,flag=1;
        Add_Gdge(St,sa,2*an);Add_Gdge(St,sb,2*bn);
        Add_Gdge(ta,Ed,2*an);Add_Gdge(tb,Ed,2*bn);
        Ans=Sol_Dinic();if(Ans!=2*(an+bn))flag=0;
        for(int i=1;i<=n;i++)g[i]=e[i];
        Add_Gdge(St,sa,2*an);Add_Gdge(St,tb,2*bn);
        Add_Gdge(ta,Ed,2*an);Add_Gdge(sb,Ed,2*bn);
        Ans=Sol_Dinic();if(Ans!=2*(an+bn))flag=0;
        if(flag)cout<<"Yes\n";else cout<<"No\n";
        for(int i=1;i<=n+2;i++)
            g[i].clear(),e[i].clear();
    }
    return 0;
}

P1231 教辅的组成

考虑对其建网络流的一张图。但是这张图该怎么建呢?

首先考虑让匹配的书与练习册,或者书与答案,进行一个连边。容量限制当然就是 \(1\)。

此时再建立源点 \(S\) 和汇点 \(T\),让 \(S\) 连练习册,\(T\) 连答案,接着从 \(S\) 出发向 \(T\) 跑最大流。

这样就结束了吗?不!这样跑,你会发现一个很严重的事情,那就是在这里,一本书你可能对应上了多本练习册或者答案。这样的事情是不允许的,虽然题目说这个人大致知道对应关系,但这也仅仅是可能的对应关系而已。一本书不可能对应两本练习册或者两本答案,但是题目中可能给定的一本书连向了多本练习册或者答案。

那这个东西该怎么改进呢?有一个很绝的法子——拆点。怎么说?就是把书给拆成两个点。本来我们的书是只有一个点的,现在我们给拆成两个,一个连练习册们,一个连答案们,然后再把它俩连起来。容量?限制为 \(1\)!这就是核心——你限制了容量是 \(1\),那么不管你左右连了多少分叉,你都只能抓出一个匹配上,不然这里就过不去,那这个流方案就不合法了。

于是这样你就可以解决该问题了,拆点的时候有些小细节要注意一下。

#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
using namespace std;
const int N = 5e4+5;
const int INF = 0x3f3f3f3f;
struct line{int to,wei,Db;};
int n1,n2,n3,mm,St,Ed,dis[N];
vector<line> g[N];queue<int> q;
int read(){
    int su=0,pp=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
    return su*pp;
}
int ID(int p,int x){
    if(p==1)return x;
    if(p==2)return n2+x;
    if(p==3)return n1+n2+x;
    if(p==4)return n1+n1+n2+x;
}
void Add_edge(int x,int y,int z){
    int idx=g[x].size(),idy=g[y].size();
    g[x].pb({y,z,idy}),g[y].pb({x,0,idx});return;
}
bool BFS_Canit(){
    for(int i=1;i<=Ed;i++)dis[i]=0;
    while(!q.empty())q.pop();
    dis[St]=1,q.push(St);
    while(!q.empty()){
        int u=q.front();q.pop();
        for(auto [v,w,id]:g[u])if(w&&!dis[v])
            {dis[v]=dis[u]+1;if(v==Ed)return 1;q.push(v);}
    }return 0;
}
int DFS_Cntflow(int u,int flow){
    if(u==Ed)return flow;int now=flow;
    for(auto &[v,w,id]:g[u]){
        if(now<=0)break;
        if(w&&dis[v]==dis[u]+1){
            int tmp=DFS_Cntflow(v,min(now,w));
            if(!tmp)dis[v]=0;else w-=tmp,g[v][id].wei+=tmp,now-=tmp;
        }
    }return flow-now;
}
int Sol_Dinic(){
    int res=0,flow=0;
    while(BFS_Canit())
        while(flow=DFS_Cntflow(St,INF))res+=flow;
    return res;
}
int main(){
    n1=read(),n2=read(),n3=read();
    mm=read();
    while(mm--){
        int x=read(),y=read();
        Add_edge(ID(1,y),ID(2,x),1);
    }mm=read();
    while(mm--){
        int x=read(),y=read();
        Add_edge(ID(3,x),ID(4,y),1);
    }for(int i=1;i<=n1;i++)
        Add_edge(ID(2,i),ID(3,i),1);
    St=n1+n1+n2+n3+1,Ed=n1+n1+n2+n3+2;
    for(int i=1;i<=n2;i++)
        Add_edge(St,ID(1,i),1);
    for(int i=1;i<=n3;i++)
        Add_edge(ID(4,i),Ed,1);
    cout<<Sol_Dinic()<<"\n";
    return 0;
}

P2754 家园 / 星际转移问题

题外话:地球上怎么只剩 \(k \le 50\) 的人类了。

首先判断是否存在解。只需用并查集将每艘公共交通太空船能抵达的所有太空站(和地球、月球)相互连接,判断地球与月球是否连通即可。

确定了题目情况必然存在解后,我们就可以开始思考如何求解了。

考虑枚举时间 \(t\) 即最终答案。由于该题在网络流建模时,点的状态均为“第 \(i\) 个太空站在第 \(t\) 秒”型,故 \(t\) 每增加 \(1\),就需要新建“一套”地球和太空站的点。

建图!建立超级源点并让月球当汇点。先让源点向时间 \(t\) 下的地球连边,容量为 \(\infty\)。以及每个空间站要从自己在时间 \(t-1\) 下的点向自己在时间 \(t\) 下的点连边,容量也是 \(\infty\)。接着考虑飞船的移动,枚举每个飞船,可利用余数和周期性知其在时间 \(t-1\) 下所位于的点以及其在时间 \(t\) 下所位于的点,连上容量为该飞船载人数的点。

此时我们只需要跑最大流 Dinic 然后判断答案与地球上的人数 \(k\) 的大小关系即可知晓时间 \(t\) 是否合法。

这个时候你就要问了,诶上面建图的时候只考虑了转移到时间 \(t\) 的情况啊,前面的转移不用管吗?嗯嗯,但是我会告诉你,对啊确实不用管,因为之前连过了呢!你又要问了,可是之前不也跑过 Dinic 了吗,剩下的网络不完整啊,这咋整,难不成要在之前跑剩下的残量网络上跑啊???

对呢!就是要在之前跑剩下的残量网络上面跑,保留原先的答案只考虑新增部分,结果不变,效率增倍!这法子挺巧的,对吧?

于是这题就做完咯,详细的看代码吧 > <

#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
using namespace std;
const int N = 5e5+5;
const int INF = 0x3f3f3f3f;
int n,m,k,ren[105],tk[105],g[105][105],fa[N];
int St,Ed,dis[N],Tim,res,flow;queue<int> q;
int tot=1,head[N],to[N],wei[N],nxt[N];
int read(){
    int su=0,pp=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
    return su*pp;
}
int FF(int u){return (fa[u]==u?u:fa[u]=FF(fa[u]));}
void Merge(int u,int v){u=FF(u),v=FF(v);if(u!=v)fa[u]=v;return;}
void Add_edge(int x,int y,int z){
    ++tot,to[tot]=y,wei[tot]=z;
    nxt[tot]=head[x],head[x]=tot;
    ++tot,to[tot]=x,wei[tot]=0;
    nxt[tot]=head[y],head[y]=tot;return;
}
bool BFS_Canit(){
    for(int i=1;i<=Ed;i++)dis[i]=0;
    while(!q.empty())q.pop();
    dis[St]=1,q.push(St);
    while(!q.empty()){
        int u=q.front();q.pop();
        for(int i=head[u];i;i=nxt[i]){
            int v=to[i],w=wei[i];
            if(!w||dis[v])continue;
            dis[v]=dis[u]+1;
            if(v==Ed)return 1;q.push(v);
        }
    }return 0;
}
int DFS_Cntflow(int u,int flow){
    if(u==Ed)return flow;int now=flow;
    for(int i=head[u];i;i=nxt[i]){
        if(now<=0)break;
        int v=to[i],w=wei[i];
        if(!w||dis[v]!=dis[u]+1)continue;
        int tmp=DFS_Cntflow(v,min(now,w));
        if(!tmp)dis[v]=0;
        else wei[i]-=tmp,wei[i^1]+=tmp,now-=tmp;
    }return flow-now;
}
int Sol_Dinic(){
    int res=0,flow=0;
    while(BFS_Canit())
        while(flow=DFS_Cntflow(St,INF))res+=flow;
    return res;
}
int main(){
    n=read(),m=read(),k=read();
    for(int i=1;i<=n+2;i++)fa[i]=i;
    for(int i=1;i<=m;i++){
        ren[i]=read(),tk[i]=read();
        for(int j=0;j<tk[i];j++){
            g[i][j]=read();
            if(g[i][j]==0)g[i][j]=n+1;
            if(g[i][j]==-1)g[i][j]=n+2;
            if(j)Merge(g[i][j-1],g[i][j]);
        }
    }if(FF(n+1)!=FF(n+2)){cout<<"0\n";return 0;}
    St=N-10,Ed=St+1;
    for(Tim=1;;Tim++){
        Add_edge(St,(Tim-1)*(n+1)+n+1,INF);
        for(int i=1;i<=m;i++){
            int x=(Tim-1)%tk[i],y=Tim%tk[i];
            if(g[i][x]==n+2)x=Ed;
            else x=(Tim-1)*(n+1)+g[i][x];
            if(g[i][y]==n+2)y=Ed;
            else y=Tim*(n+1)+g[i][y];
            Add_edge(x,y,ren[i]);
        }while(BFS_Canit())
            while(flow=DFS_Cntflow(St,INF))res+=flow;
        if(res>=k){cout<<Tim<<"\n";return 0;}
        for(int i=1;i<=n+1;i++)Add_edge((Tim-1)*(n+1)+i,Tim*(n+1)+i,INF);
    }
    return 0;
}

费用流模型

现在自来水公司要为水管收费了。对于一条 \(u \to v\) 的水管,其费用记为 \(l_{u \to v}\),表示每通过该水管运送 \(1\) 单位流量,就要收费 \(l_{u \to v}\)。在最大化流量的前提下,还要使得总费用最小,这就是费用流的模型。

费用具有反对称性,即 \(l_{u \to v} = - l_{v \to u}\)。对于一条边上的流量,撤回的费用是相反数。

有一个定理:求得同流量中最小费用的流,当且仅当残量网络中不存在负环(费用和为负数的环)。

简单证明一下。首先有充分性,若存在负环,在这个负环上流一圈,流量不变费用减小。以及有必要性,记残缺网络为 \(r\),据残量调整定理,若当前流不是最小费用流,一定存在 \(f \in F_r\) 满足 \(\text{flow}(f) = 0\),而 \(f\) 没有外来流量,只是由多个环流叠加而成。每次从 \(f\) 中找出一个环流,如果是非负环,就把它从 \(f\) 中删掉,这样 \(\text{cost}(f)\) 不会变大。重复这个过程,最终一定能找到负环。

费用流的算法实现

SSP 算法

每次找出残量网络上 \(S \to T\) 的最短路进行增广。

这里又有一个定理:若初始图中无负环,SSP 算法的增广过程中,残量网络上不会产生负环。

证明过于复杂,就不写了。

好吧证明还是要写的喵!

假设在某次增广后第一次出现负环,则负环必然与增广路有交。记交集为 \(a \leadsto b\),增广路为 \(S \leadsto a \leadsto b \leadsto T\),负环为 \(b \leadsto a \leadsto_2 b\)。根据负环有 $\text{cost}(b \leadsto a) + \text{cost}(a \leadsto_2 b) <0 $,又有 \(\text{cost}(b \leadsto a) = -\text{cost}(a \leadsto b)\),故 \(\text{cost}(a \leadsto b) > \text{cost}(a \leadsto_2 b)\)。可以构造出更短增广路 \(S \leadsto a \leadsto_2 b \leadsto T\),矛盾。

推论:SSP 算法最终求得的是最小费用最大流。

SSP 算法是求解费用流的主干思想,其中的关键步骤是最短路的求解。根据求最短路的方法不同,产生了多个子算法。

Edmonds-Karp 算法

EK 算法是每次增广用 SPFA 求解最短路。

EK 算法是伪多项式复杂度的(没有关于 \(n,m\) 的多项式上界),受到费用的引导,在构造数据中的表现不会比 FF 算法高明。

但它是竞赛实践中最常用的费用流算法。和 Dinic 算法跑不满的情况类似,大多数题目中,使用 EK 算法就已经足够了。

例题选讲

P2045 方格取数加强版

考虑如何建模。

首先将一个方格 \((i,j)\) 转化为两个点,一个入点,一个出点。从每个入点向对应的出点连两条边,一条容量为 \(1\),费用为格子里的数值,还有一条容量为 \(k-1\),费用为 \(0\)。

还有,从 \((i,j)\) 的出点向 \((i+1,j)\) 和 \((i,j+1)\) 的入点各连一条边,容量为 \(k\),费用为 \(0\)。

最后让 \((1,1)\) 的入点为源点,\((n,n)\) 的出点为汇点,跑最大费用最大流即可。

#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
using namespace std;
const int N = 2e5+5;
const int INF = 0x3f3f3f3f;
int n,k,St,Ed,dis[N],f[N],ps[N];
int tot=1,head[N],to[N],wei[N],cst[N],nxt[N];
bool vis[N];queue<int> q;
int read(){
    int su=0,pp=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
    return su*pp;
}
void Add_edge(int x,int y,int z,int w){
    to[++tot]=y,wei[tot]=z,cst[tot]=w;
    nxt[tot]=head[x],head[x]=tot;
    to[++tot]=x,wei[tot]=0,cst[tot]=-w;
    nxt[tot]=head[y],head[y]=tot;return;
}
int Num(int x,int y,int k){return (x-1)*n+y+k*n*n;}
bool SPFA_Canit(){
    while(!q.empty())q.pop();
    memset(dis,-0x3f,sizeof(dis));
    memset(vis,0,sizeof(vis));
    q.push(St),dis[St]=0,vis[St]=1,f[St]=INF;
    while(!q.empty()){
        int u=q.front();vis[u]=0;q.pop();
        for(int i=head[u];i;i=nxt[i]){
            if(!wei[i])continue;int v=to[i];
            if(dis[v]<dis[u]+cst[i]){
                dis[v]=dis[u]+cst[i];
                f[v]=min(f[u],wei[i]),ps[v]=i;
                if(!vis[v])vis[v]=1,q.push(v);
            }
        }
    }return dis[Ed]!=dis[0];
}
int Sol_EK(){
    int res=0;
    while(SPFA_Canit()){
        int now=Ed;
        while(now!=St){
            int id=ps[now];
            wei[id]-=f[Ed],wei[id^1]+=f[Ed];
            now=to[id^1];
        }res+=dis[Ed]*f[Ed];
    }return res;
}
int main(){
    n=read(),k=read();
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++){
            int x=read();
            Add_edge(Num(i,j,0),Num(i,j,1),1,x);
            Add_edge(Num(i,j,0),Num(i,j,1),k-1,0);
            if(i<n)Add_edge(Num(i,j,1),Num(i+1,j,0),k,0);
            if(j<n)Add_edge(Num(i,j,1),Num(i,j+1,0),k,0);
        }
    St=1,Ed=2*n*n;
    cout<<Sol_EK()<<"\n";
    return 0;
}

P4014 分配问题

这道题用费用流做是非常简单的,只需要建个模。

首先有源点汇点,然后从源点向每个人连一条容量为 \(1\)、费用为 \(0\) 的边。同样,从每一个工作向汇点连一条容量为 \(1\)、费用为 \(0\) 的边。

然后对于第 \(i\) 个人和第 \(j\) 个工作,连一条容量为 \(1\)、费用为 \(c_{i,j}\) 的边。

最后跑一个最大费用最大流和一个最小费用最大流就解决咯。

#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
using namespace std;
const int N = 2e5+5;
const LL INF = 0x3f3f3f3f3f3f3f3f;
LL n,St,Ed,a[55][55],dis[N],f[N],ps[N];
bool vis[N];queue<int> q;
LL tot=1,head[N],to[N],wei[N],cst[N],nxt[N];
LL read(){
    LL su=0,pp=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
    return su*pp;
}
void Add_edge(int x,int y,LL z,LL w){
    to[++tot]=y,wei[tot]=z,cst[tot]=w;
    nxt[tot]=head[x],head[x]=tot;
    to[++tot]=x,wei[tot]=0,cst[tot]=-w;
    nxt[tot]=head[y],head[y]=tot;return;
}
bool SPFA_small(){
    while(!q.empty())q.pop();
    memset(dis,0x3f,sizeof(dis));
    memset(vis,0,sizeof(vis));
    q.push(St),dis[St]=0,vis[St]=1,f[St]=INF;
    while(!q.empty()){
        int u=q.front();vis[u]=0;q.pop();
        for(int i=head[u];i;i=nxt[i]){
            if(!wei[i])continue;int v=to[i];
            if(dis[v]>dis[u]+cst[i]){
                dis[v]=dis[u]+cst[i];
                f[v]=min(f[u],wei[i]),ps[v]=i;
                if(!vis[v])vis[v]=1,q.push(v);
            }
        }
    }return dis[Ed]!=dis[0];
}
bool SPFA_big(){
    while(!q.empty())q.pop();
    memset(dis,-0x3f,sizeof(dis));
    memset(vis,0,sizeof(vis));
    q.push(St),dis[St]=0,vis[St]=1,f[St]=INF;
    while(!q.empty()){
        int u=q.front();vis[u]=0;q.pop();
        for(int i=head[u];i;i=nxt[i]){
            if(!wei[i])continue;int v=to[i];
            if(dis[v]<dis[u]+cst[i]){
                dis[v]=dis[u]+cst[i];
                f[v]=min(f[u],wei[i]),ps[v]=i;
                if(!vis[v])vis[v]=1,q.push(v);
            }
        }
    }return dis[Ed]!=dis[0];
}
LL EK_small(){
    LL res=0;
    while(SPFA_small()){
        int now=Ed;
        while(now!=St){
            int id=ps[now];
            wei[id]-=f[Ed],wei[id^1]+=f[Ed];
            now=to[id^1];
        }res+=dis[Ed]*f[Ed];
    }return res;
}
LL EK_big(){
    LL res=0;
    while(SPFA_big()){
        int now=Ed;
        while(now!=St){
            int id=ps[now];
            wei[id]-=f[Ed],wei[id^1]+=f[Ed];
            now=to[id^1];
        }res+=dis[Ed]*f[Ed];
    }return res;
}
int main(){
    n=read(),St=2*n+1,Ed=St+1;
    for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)
        a[i][j]=read(),Add_edge(i,j+n,1,a[i][j]);
    for(int i=1;i<=n;i++)Add_edge(St,i,1,0);
    for(int i=1;i<=n;i++)Add_edge(i+n,Ed,1,0);
    cout<<EK_small()<<"\n";
    for(int i=1;i<=tot;i++)
        to[i]=0,wei[i]=0,cst[i]=0,nxt[i]=0;
    for(int i=1;i<=Ed;i++)head[i]=0;tot=1;
    for(int i=1;i<=Ed;i++)
        vis[i]=0,dis[i]=0,f[i]=0,ps[i]=0;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)Add_edge(i,j+n,1,a[i][j]);
    for(int i=1;i<=n;i++)Add_edge(St,i,1,0);
    for(int i=1;i<=n;i++)Add_edge(i+n,Ed,1,0);
    cout<<EK_big()<<"\n";
    return 0;
}

P1251 餐巾计划问题

题意不难,重点是图论建模。废话。

首先拆点,把每天拆成早上和晚上。每天早上会得到新餐巾,每天晚上会得到脏餐巾。这些餐巾就是图中的流量,而花费就是每条边的费用,也就是我们最后要最小化的东西。

接下来具体考虑每一种情况下的建图:

  1. 从源点向第 \(i\) 天晚上连一条流量为当日所用餐巾数量 \(r_i\)、费用为 \(0\) 的边,表示当日使用餐巾并获得了脏餐巾。
  2. 从第 \(i\) 天早上向汇点连一条流量为当日所用餐巾数量 \(r_i\)、费用为 \(0\) 的边,表示向汇点提供 \(x\) 条新餐巾。连这个边的作用是为了跑最大流确认当日流满。
  3. 从第 \(i\) 天晚上向第二天晚上连一条流量为 \(\infty\)、费用为 \(0\) 的边,表示每天晚上可以把脏餐巾留到第二天晚上。注意不是早上,因为早上要获得的是新餐巾。
  4. 从第 \(i\) 天晚上向第 \(i+m\) 天早上连一条流量为 \(\infty\)、费用为 \(f\) 的边,表示每天晚上可以以单价 \(f\) 元的价格把脏餐巾送到快洗部并在第 \(i+m\) 天早上收到新餐巾。注意连边时要判断是否满足 \(i+m \le N\)。
  5. 从第 \(i\) 天晚上向第 \(i+n\) 天早上连一条流量为 \(\infty\)、费用为 \(s\) 的边,表示每天晚上可以以单价 \(s\) 元的价格把脏餐巾送到慢洗部并在第 \(in\) 天早上收到新餐巾。同样注意连边时判断是否满足 \(i+n \le N\)。
  6. 从源点向每一天早上连一条流量为 \(\infty\)、费用为 \(p\) 的边,表示每天早上可以以单价 \(p\) 元的价格购买新餐巾。

这玩意儿怎么这么复杂啊!

建完图直接跑板子最大流最小费用就可以啦。

```plaintext
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
using namespace std;
const int M = 1e5+5;
const LL INF = 0x3f3f3f3f3f3f3f3f;
LL n,St,Ed,m,t1,m1,t2,m2;
LL dis[M],f[M],ps[M];queue<int> q;bool vis[M];
LL tot=1,head[M],to[M],wei[M],cst[M],nxt[M];
LL read(){
    LL su=0,pp=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
    return su*pp;
}
void Add_edge(int x,int y,LL z,LL w){
    to[++tot]=y,wei[tot]=z,cst[tot]=w;
    nxt[tot]=head[x],head[x]=tot;
    to[++tot]=x,wei[tot]=0,cst[tot]=-w;
    nxt[tot]=head[y],head[y]=tot;return;
}
bool SPFA_Canit(){
    while(!q.empty())q.pop();
    memset(dis,0x3f,sizeof(dis));
    memset(vis,0,sizeof(vis));
    q.push(St),dis[St]=0,vis[St]=1,f[St]=INF;
    while(!q.empty()){
        int u=q.front();vis[u]=0;q.pop();
        for(int i=head[u];i;i=nxt[i]){
            if(!wei[i])continue;int v=to[i];
            if(dis[v]>dis[u]+cst[i]){
                dis[v]=dis[u]+cst[i];
                f[v]=min(f[u],wei[i]),ps[v]=i;
                if(!vis[v])vis[v]=1,q.push(v);
            }
        }
    }return dis[Ed]!=dis[0];
}
LL Sol_EK(){
    LL res=0;
    while(SPFA_Canit()){
        int now=Ed;
        while(now!=St){
            int id=ps[now];
            wei[id]-=f[Ed],wei[id^1]+=f[Ed];
            now=to[id^1];
        }res+=dis[Ed]*f[Ed];
    }return res;
}
int main(){
    n=read(),St=2*n+1,Ed=St+1;
    for(int i=1;i<=n;i++){
        LL x=read();
        Add_edge(St,i,x,0);
        Add_edge(i+n,Ed,x,0);
    }m=read(),t1=read(),m1=read();
    t2=read(),m2=read();
    for(int i=1;i<=n;i++){
        Add_edge(St,i+n,INF,m);
        if(i<n)Add_edge(i,i+1,INF,0);
        if(i+t1<=n)Add_edge(i,i+n+t1,INF,m1);
        if(i+t2<=n)Add_edge(i,i+n+t2,INF,m2);
    }cout<<Sol_EK()<<"\n";
    return 0;
}

最小割及其相关定理

对于一张网络,将点集 \(V\) 划分为两个集合 \((V_S,V_T)\)(要求 \(V_S \cap V_T = \varnothing\) 且 \(V_S \cup V_T = V\)),这样的划分被称为割。

对于指定源点 \(S\) 和汇点 \(T\) 要求 \(S \in V_S\) 且 \(T \in V_T\) 的割被称为有源汇割。

割 \((V_S,V_T)\) 的容量 \(\text{cap}(V_S,V_T)\) 定义为 \(\sum_{a \in V_S , b \in V_T} c_{a \to b}\),即从 \(V_S\) 到 \(V_T\) 的边的容量之和。

指定源汇时容量最小的有源汇割 \((V_S,V_T)\) 被称为有源汇最小割,记作 \(\text{mincut}(S,T)\),或简记为 \(\text{cut}(S,T)\)。未指定源汇的容量最小的割称为全局最小割。

最小割最大流定理:最小割 \(=\) 最大流,即 \(\text{mincut}(S,T) = \text{maxflow}(S,T)\)。

证明:
对于任意一种割 \((V_S,V_T)\),任意一个流的流量可以写成流出 \(V_S\) 的流量减去流入 \(V_S\) 的流量。而割是 \(V_S\) 的出边边权和,显然不小于前者。
对于任意一个最大流方案,在残量网络中从 \(S\) 开始搜索,只能走容量 \(> 0\) 的边,得到点集 \(V_S\) 满足出边容量都为 \(0\)(增广路不存在才能 \(T \notin V_S\)),那么 \((V_S,V-V_S)\) 构成了一种合法的割,且割的容量,即出边的原始容量和,恰好与最大流相等。
任意的割大于等于任意的流,且能构造一种割等于最大流,那么这就是最小割。定理得证。

二分图的定义与判定定理

若一张图中,存在一种对于点集 \(V\) 的划分 \((L,R)\),使得 \(L,R\) 各自内部没有边,则这张图被称为二分图。

二分图判定定理:无向图为二分图的充要条件为图中不存在奇环。以下为该定理的证明。

注意到一个奇环显然不可能划分为两个点集使得内部无边。

若不存在奇环,二染色必然可以构造出一种划分方案。考虑一个连通分量,只需随便选择某个点染成黑色,将其相邻点染成白色,再将新染的白点的相邻点染成黑色,以此类推即可。可以用 DFS 完成这个过程。

二分图最大匹配

什么是匹配?
对于一张无向图,选出边集 \(E\),若每个点至多选出一条相连的边,则称 \(E\) 为一种匹配。

给出一张二分图,求边数最多的匹配,这个匹配被称为二分图最大匹配。

考虑将匹配转化为多源多汇网络流,左部点设为源,右部点设为汇。在匹配中,由于每个点只能连一条边,所以各个小源汇的容量都得是 \(1\)。左部点到右部点有一条有向边,容量限制为 \(1\),也可以设成 \(\infty\)。

如何处理多源多汇?我们可以建立超级源点和超级汇点,超级源点向左部点连边,右部点向超级汇点连边,容量限制都是 \(1\)。

在该网络上求解最大流,即可得最大匹配。使用 Dinic 算法的时间复杂度为 \(O(m \sqrt{n})\)。

例题选讲

P2763 试题库问题

这道题目显然是匹配,难点在于如何建这张二分图。

首先建立源点和汇点,源点与试题相连,汇点与类型相连,对应试题与对应类型相连。

接下来考虑边的容量:因为一道题只能有一个,所以源点和试题连的边容量为 \(1\);同理一道题只能满足一种类型,所以试题和类型连的边的容量也为 \(1\);而需要满足的类型是有多个的,所以类型与汇点连的边的容量为所需类型的数量。

由最小割最大流定理,我们建完图直接跑最大流就可以了,统计方案只需找到没有被割掉的边输出即可。

#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
using namespace std;
const int N = 2005;
const int Inf = 0x3f3f3f3f;
struct line{int to,wei,Db;};
int n,k,sum,St,Ed,dis[N];
vector<line> g[N];queue<int> q;
int read(){
    int su=0,pp=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
    return su*pp;
}
void Add_edge(int x,int y,int z){
    int idx=g[x].size(),idy=g[y].size();
    g[x].pb({y,z,idy}),g[y].pb({x,0,idx}); return;
}
bool BFS_Canit(){
    for(int i=1;i<=n+k+2;i++)dis[i]=0;
    while(!q.empty())q.pop();
    dis[St]=1,q.push(St);
    while(!q.empty()){
        int u=q.front();q.pop()
        for(auto [v,w,id]:g[u])
            if(w&&!dis[v])
                dis[v]=dis[u]+1;
                if(v==Ed)return 1;
                q.push(v);
            }
    }return 0;
}
int DFS_Cntflow(int u,int flow){
    if(u==Ed)return flow;
    int now=flow;
    for(auto &[v,w,id]:g[u])
        if(w&&dis[v]==dis[u]+1){
            int tmp=DFS_Cntflow(v,min(now,w));
            if(!tmp)dis[v]=0;
            else w-=tmp,g[v][id].wei+=tmp,now-=tmp;
        }
    return flow-now;
}
int Sol_Dinic(){
    int res=0,flow=0;
    while(BFS_Canit())
        while(flow=DFS_Cntflow(St,Inf))res+=flow;
    return res;
}
int main(){
    k=read(),n=read();
    St=n+k+1,Ed=St+1;
    for(int i=1;i<=k;i++){int w=read();sum+=w;Add_edge(n+i,Ed,w);}
    for(int i=1;i<=n;i++){
        Add_edge(St,i,1);int cnt=read();
        while(cnt--){int x=read();Add_edge(i,x+n,1);}
    }if(Sol_Dinic()==sum){
        for(int i=1;i<=k;i++){
            cout<<i<<":";
            for(auto [j,w,id]:g[n+i])
                if(w&&j<=n)cout<<" "<<j;
            cout<<"\n";
        }
    }else cout<<"No Solution!\n";
    return 0;
}

P3355 骑士共存问题

二分图。

先考虑构造二分图的左右部点。将棋盘格按照横纵坐标之和(即 \(x+y\))的奇偶性将其分成两类格子,令奇数格为 \(\text{A}\) 类格,偶数格为 \(\text{B}\) 类格。

观察骑士的攻击方位,不难发现同类格子是不可能相互攻击的。

分好类了就开始建模。从源点向 \(\text{A}\) 类格连边,\(\text{B}\) 类格向着汇点连边,容量均为 \(1\)。若某个 \(\text{A}\) 类格子可以攻击另一个 \(\text{B}\) 类格,则让该 \(\text{A}\) 类格向 \(\text{B}\) 类格连容量为 \(\infty\) 的边。这样便有了一张二分图啦。

由最小割最大流定理,直接在该二分图上跑最大流得结果 \(flow\),这个最小割(也就是 \(flow\))就是该棋盘上最少的不能放置骑士的空格子个数。最后简单利用容斥原理知 \(n^2 - m - flow\) 即为答案。

#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
using namespace std;
const int N = 5e5+5;
const int INF = 0x3f3f3f3f;
int m,n,St,Ed,dis[N];
int dx[]={-2,-2,-1,-1,1,1,2,2};
int dy[]={1,-1,2,-2,2,-2,1,-1};
bool a[205][205];queue<int> q;
int tot=1,head[N],to[N],wei[N],nxt[N];
int read(){
    int su=0,pp=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
    return su*pp;
}
int Node(int x,int y){return (x-1)*n+y;}
void Add_edge(int x,int y,int z){
    ++tot,to[tot]=y,wei[tot]=z;
    nxt[tot]=head[x],head[x]=tot;
    ++tot,to[tot]=x,wei[tot]=0;
    nxt[tot]=head[y],head[y]=tot;return;
}
bool BFS_Canit(){
    for(int i=1;i<=Ed;i++)dis[i]=0;
    while(!q.empty())q.pop();
    dis[St]=1,q.push(St);
    while(!q.empty()){
        int u=q.front();q.pop();
        for(int i=head[u];i;i=nxt[i]){
            int v=to[i],w=wei[i];
            if(!w||dis[v])continue;
            dis[v]=dis[u]+1;
            if(v==Ed)return 1;q.push(v);
        }
    }return 0;
}
int DFS_Cntflow(int u,int flow){
    if(u==Ed)return flow;int now=flow;
    for(int i=head[u];i;i=nxt[i]){
        if(now<=0)break;
        int v=to[i],w=wei[i];
        if(!w||dis[v]!=dis[u]+1)continue;
        int tmp=DFS_Cntflow(v,min(now,w));
        if(!tmp)dis[v]=0;
        else wei[i]-=tmp,wei[i^1]+=tmp,now-=tmp;
    }return flow-now;
}
int Sol_Dinic(){
    int res=0,flow=0;
    while(BFS_Canit())
        while(flow=DFS_Cntflow(St,INF))res+=flow;
    return res;
}
int main(){
    n=read(),m=read();
    for(int i=1;i<=m;i++){
        int x=read(),y=read();
        a[x][y]=1;
    }St=n*n+1,Ed=St+1;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++){
            if(a[i][j])continue;
            if((i+j)%2==1)Add_edge(St,Node(i,j),1);
            else Add_edge(Node(i,j),Ed,1);
        }
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++){
            if((i+j)%2==0)continue;
            for(int k=0;k<8;k++){
                int x=i+dx[k],y=j+dy[k];
                if(x<1||x>n||y<1||y>n||a[x][y])continue;
                Add_edge(Node(i,j),Node(x,y),INF);
            }
        }
    cout<<n*n-m-Sol_Dinic()<<"\n";
    return 0;
}

P2774 方格取数问题

我要抗议!我觉得这题和上题难度没差别!!凭什么这题是紫而上题只有蓝!!!

一样是二分图,和上题的分类方案一样:将方格按照横纵坐标之和(即 \(x+y\))的奇偶性将其分成两类格子,令奇数格为 \(\text{A}\) 类格,偶数格为 \(\text{B}\) 类格。同样可以发现同类方格的选择互不相干。

接下来是建模。先让源点向所有 \(\text{A}\) 类格连边,所有 \(\text{B}\) 类格向汇点连边,容量为自身格子上的值。若某个 \(\text{A}\) 类格的选择影响到另一个 \(\text{B}\) 类格的选择(即这两个格子中要么都不选要么只选一个,若同时选则不满足题目限制),则让该 \(\text{A}\) 类格向该 \(\text{B}\) 类格连边,容量为 \(\infty\)。

这样就构造完成了二分图。利用最小割最大流定理在该图上跑最大流得结果 \(flow\),和上题类似的,这里的最小割(也就是 \(flow\))就是该棋盘上最小的不能选的方格上的数值之和。最终的答案就是所有方格上数值之和 \(sum\) 减去 \(flow\)。

#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
using namespace std;
const int N = 5e5+5;
const LL INF = 0x3f3f3f3f3f3f3f3f;
LL n,m,sum,St,Ed,dis[N];
LL dx[]={1,0,-1,0};
LL dy[]={0,1,0,-1};
bool a[205][205];queue<LL> q;
LL tot=1,head[N],to[N],wei[N],nxt[N];
LL read(){
    LL su=0,pp=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
    return su*pp;
}
int Node(int x,int y){return (x-1)*m+y;}
void Add_edge(int x,int y,LL z){
    ++tot,to[tot]=y,wei[tot]=z;
    nxt[tot]=head[x],head[x]=tot;
    ++tot,to[tot]=x,wei[tot]=0;
    nxt[tot]=head[y],head[y]=tot;return;
}
bool BFS_Canit(){
    for(int i=1;i<=Ed;i++)dis[i]=0;
    while(!q.empty())q.pop();
    dis[St]=1,q.push(St);
    while(!q.empty()){
        int u=q.front();q.pop();
        for(int i=head[u];i;i=nxt[i]){
            LL v=to[i],w=wei[i];
            if(!w||dis[v])continue;
            dis[v]=dis[u]+1;
            if(v==Ed)return 1;q.push(v);
        }
    }return 0;
}
LL DFS_Cntflow(int u,LL flow){
    if(u==Ed)return flow;LL now=flow;
    for(int i=head[u];i;i=nxt[i]){
        if(now<=0)break;
        LL v=to[i],w=wei[i];
        if(!w||dis[v]!=dis[u]+1)continue;
        LL tmp=DFS_Cntflow(v,min(now,w));
        if(!tmp)dis[v]=0;
        else wei[i]-=tmp,wei[i^1]+=tmp,now-=tmp;
    }return flow-now;
}
LL Sol_Dinic(){
    LL res=0,flow=0;
    while(BFS_Canit())
        while(flow=DFS_Cntflow(St,INF))res+=flow;
    return res;
}
int main(){
    n=read(),m=read();
    St=n*m+1,Ed=St+1;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++){
            LL w=read();sum+=w;
            if((i+j)%2==1)Add_edge(St,Node(i,j),w);
            else Add_edge(Node(i,j),Ed,w);
        }
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++){
            if((i+j)%2==0)continue;
            for(int k=0;k<4;k++){
                int x=i+dx[k],y=j+dy[k];
                if(x>=1&&x<=n&&y>=1&&y<=m)
                    Add_edge(Node(i,j),Node(x,y),INF);
            }
        }
    cout<<sum-Sol_Dinic()<<"\n";
    return 0;
}
posted @ 2025-12-01 17:43  嘎嘎喵  阅读(79)  评论(0)    收藏  举报