网络流进阶与实例分析

网络流进阶

暴力的建模

例题1【Luogu P2756】飞行员配对方案问题

题目传送门

二分图最大匹配例题。

二分图的坑下面再补。

思考在外籍飞行员和英国飞行员之间连边。如果这条边有流,说明两人配对。

每个人只能配一次对这个条件怎么加以限制呢?

这里需要用到一种基础技巧——

超级源汇

钦定一个源点和一个汇点。注意不要越数组的界。

从源点向外籍飞行员连边,容量为1. 从英国飞行员向汇点连边,容量也为1.

这样子就能限制每个外籍飞行员和每个英国飞行员只能配对一次这个条件了。

连完边跑最大流即可。

这种题目是求之不得的,因为题目中已经告诉了我们连边的方法。但是其他题目就没这么好心了。

输出方案采用的是增广时记录的方式。还有一种下面再讲。

#include<bits/stdc++.h>

#define debug cout << "jxztql" << endl

using namespace std;

const int N = 1000007;

int n, m, s = 1008, t = 1009, cnt = 1, mi, ans = 0;

struct dartou{
    int to, val, next;
}e[N];

struct dat{
    int fa, num;
}pre[N];

bool vis[N];

int connect[N], dt[N];

void add(int x, int y, int value){
    cnt++;
    e[cnt].to = y;
    e[cnt].val = value;
    e[cnt].next = dt[x];
    dt[x] = cnt;
}

bool /*SPFA*/dijstra(){
    memset(pre, 0, sizeof(pre));
    memset(vis, 0, sizeof(vis));
    queue<int> q;
    q.push(s);
    vis[s] = 1;
    while(!q.empty()){
        int u = q.front();
        q.pop();
        for(int i = dt[u]; i; i = e[i].next){
            int v = e[i].to;
            if(e[i].val != 0 && !vis[v]){
                pre[v].fa = u;
                pre[v].num = i;
                if(v == t)return 1;
                vis[v] = 1;
                q.push(v);
            }
        }
    }
    return 0;
}

void EK(){
    while(dijstra()){
        mi = 0x3f3f3f3f;
        for(int i = t; i != s; i = pre[i].fa)mi = min(mi, e[pre[i].num].val);
        for(int i = t; i != s; i = pre[i].fa){
            connect[pre[i].fa] = i;
            e[pre[i].num].val -= mi;
            e[pre[i].num ^ 1].val += mi;
        }ans += mi;
    }
}

int main(){
    cin >> m >> n;
    for(int i = 1; i <= n; i++){
        add(i + n, t, 1);
        add(t, i + n, 0);
    }
    for(int i = 1; i <= m; i++){
        add(i, s, 0);
        add(s, i, 1);
    }
    for(; ;){
        int a, b;
        cin >> a >> b;
        if(a == -1 && b == -1)break;
        add(a, b + n, 1);
        add(b + n, a, 0);
    }
    EK();
    if(ans)cout << ans << endl;
    else cout << "No Solution!" << endl;
    for(int i = 1; i <= n; i++){
        if(connect[i])
            cout << i << " "<< connect[i] - n<< endl;
    }
}
练习

没有。题目少。

有技巧的建模

不是很建议第二梯队同学食用。

这种题目包装的极其严实(当然有一些不是),通常需要一些技巧。这部分以讲解例题为主。

例题2【Luogu P4016】【网络流24题】负载平衡问题

题目传送门

环形均分纸牌板子题(逃

但是如果用网络流做呢?

把货物看作流量,交换次数看作费用。于是流量的转移就可以看作货物的搬运

我们可以知道,分完之后每个仓库的货物数量都是总数量的平均值

于是,货物数量大于平均值的仓库就需要拿出流量;货物数量小于平均值的仓库就需要接收流量。

这样子我们就可以建二分图跑最小费用最大流了。

二分图这先留个坑后面再填。

将货物数小于平均值的仓库与源点连边,流量为差值,费用为0.(不是仓库之间的搬运不需要费用)

将货物数大于平均值的仓库与汇点连边,流量为差值,费用为0.(同上)

相邻的仓库之间连边,流量为inf(可以无限搬运),费用为1.

然后跑最小费用最大流即可。

#include<bits/stdc++.h>

#define debug cout << "jxztql" << endl

using namespace std;

const int N = 1000007;

struct dartou{
    int to, val, w, next;
}e[N];

struct dat{
    int fa, num;
}pre[N];

int dt[N], dis[N], a[N], sum = 0;;

bool vis[N];

int n, m, s, t, cnt = 1, maxflow = 0, cost = 0, mi;

void add(int x, int y, int value, int wa){
    cnt++;
    e[cnt].to = y;
    e[cnt].val = value;
    e[cnt].w = wa;
    e[cnt].next = dt[x];
    dt[x] = cnt;
}

bool /*SPFA*/dijstra(){
    memset(pre, 0, sizeof(pre));
    memset(vis, 0, sizeof(vis));
    memset(dis, 0x3f, sizeof(dis));
    queue<int> q;
    q.push(s);
    vis[s] = 1;
    dis[s] = 0;
    while(!q.empty()){
        register int u = q.front();
        vis[u] = 0;
        q.pop();
        for(register int i = dt[u]; i; i = e[i].next){
            register int v = e[i].to, w = e[i].w;
            if(e[i].val > 0 && dis[v] > dis[u] + w){
                dis[v] = dis[u] + w;
                pre[v].fa = u;
                pre[v].num = i;
                if(!vis[v]){
                    q.push(v);
                    vis[v] = 1;
                }
            }
        }
    }
    return dis[t] != 0x3f3f3f3f;
}

void EK(){
    while(dijstra()){
        mi = 0x3f3f3f3f;
        for(int i = t; i != s; i = pre[i].fa)mi = min(mi, e[pre[i].num].val);
        for(int i = t; i != s; i = pre[i].fa){
            e[pre[i].num].val -= mi;
            e[pre[i].num ^ 1].val += mi;
        }
        maxflow += mi;
        cost += mi * dis[t];
    }
}

int main(){
    cin >> n;
    s = n * 2 + 1, t = s + 1;
    for(int i = 1; i <= n; i++){
        cin >> a[i];
        sum += a[i];
    }
    sum /= n;
    for(int i = 1; i <= n; i++)
        if(a[i] > sum)add(s, i, a[i] - sum, 0), add(i, s, 0, 0);
        else add(i, t, sum - a[i], 0), add(t, i, 0, 0);
    for(int i = 2; i <= n; i++){
        add(i - 1, i, 0x3f3f3f3f, 1), add(i, i - 1, 0, -1);
        add(i, i - 1, 0x3f3f3f3f, 1), add(i - 1, i, 0, -1);
    }
    add(1, n, 0x3f3f3f3f, 1); add(n, 1, 0, -1);
    add(n, 1, 0x3f3f3f3f, 1); add(1, n, 0, -1);
    EK();
    cout << cost ;
}

此题EK可过(是当时我懒得写dinic

例题3【Luogu P3254】【网络流24题】圆桌问题

题目传送门

把人看作流量。

从源点向每个单位连一条流量为单位人数的边。

由于每张桌子只能坐一个同一单位的人,从每个单位向每张桌子连一条流量为1的边。

从每张桌子向汇点连一条流量为桌子最大人数的边。

然后跑最大流即可。

这道题还让我们输出方案。如何处理?

是否有可行方案:将人总数拿来与最大流结果(就餐人数)比较,如果就餐人数小于总人数就代表没有可行方案。

上面已经讲过可以在增广时记录,但是由于dinic是多路增广,所以没有专门的结构体来记录增广路。

故沿着增广路记录不可行。

那怎么办呢?

从建图方式考虑。

每个单位向每张桌子连一条流量为1的边

所以我们可以在跑完最大流时遍历所有的这种边,判断该边是否满流,满流就输出。

(由于篇幅原因只放建图代码)

for(register int i = 1; i <= m; i++){
        register int tm;
        cin >> tm;
        add(s, i, tm);
        add(i, s, 0);
        sum += tm;
    }
    for(register int i = 1; i <= n; i++){
        register int tm;
        cin >> tm;
        add(i + m, t, tm);
        add(t, i + m, 0);
    }
    for(register int i = 1; i <= m; i++)
        for(register int j = 1; j <= n; j++)
            add(i, j + m, 1), add(j + m, i, 0);
    if(sum > dinic()){cout << 0 << endl; return 0;}
    cout << 1 << endl;
    cnt += (n + m) << 1;
    for(register int i = 1; i <= m; i++){
        for(register int j = 1; j <= n; j++){
            if(!e[cnt].f)cout << j << " ";
            cnt += 2;
        }
        cout << endl;
    }
练习1【Luogu P2763】【网络流24题】试题库问题

好像跟上一题没什么区别。

把试题看作流量。

由于每道题只能用一次,从源点向每道题连一条容量为1的边。

从每道题向可对应的类型连一条容量为1的边。

由于每种类型有题数限制,再从每种类型向汇点连一条容量为该类型题数的边。

小问题

从每道题向对应类型连边的容量可不可以是inf?

源点向题目的连边已经限制了题目数量,题到类型的连边能不能乱连?

答:不可以。原因不得而知。

61838.png

总结
  1. 超级源汇主要是为了限制某些条件,且在几乎所有题中都是必要的。
  2. 做这类题需要较为强大的解包装能力。
  3. 总要把一些东西看作流量来处理,建图是关键。

取个标题好难啊

例题1 【Luogu P2762】【网络流24题】太空飞行计划问题

题目传送门

此处出现了一个模型:最大权闭合子图

定义一个带点权的有向图,点权可正可负,求一个权值和最大的子图,使得每个点的后继都在这个子图中。

SouthEast

这类问题可以用网络流解决。

补一个定理:最大流 = 最小割

割:现在有一个人在某张网络流图上面砍几刀,使得s和t不再联通。砍掉的所有边的容量和就是一个割。

由于容量限制,任意流一定小于等于任意割。

所以当割最小时,流量一定最大(并不想证明)

解法

这个问题可以转化为最小割问题,用网络流解决。

从源点s向每个正权点连一条容量为权值的边,每个负权点向汇点t连一条容量为权值的绝对值的边,有向图原来的边容量全部为无限大。

求它的最小割,割掉后,与源点s连通的点构成最大权闭合子图,权值为(正权值之和-最小割)。

SouthEast

下面证明一下:

  因为割集中所有的边,不是连接在s上,就是连接在t上;

  我们记割集中,所有连接在s上的边的权值和为x1,所有连接在t上的边的权值和为x2,而割集中所有边权值和为X=x1+x2;

  又,记图S中所有点的权值和为W,记其中正权值之和为w1,负权值之和为 - w2,故W = w1 - w2;

  而 W + X = w1 - w2 + x1 + x2,由于x2 = w2

(因为图S中所有负权值的点,必然连接到t点,而图S必然要与t分割开;故割集中,“连接在t点上的边权值和”就是“图S中所有负权值点的权值之和,取负”)

  因而W + X = w1 + x1;

  而显然的,w1 + x1是整个图中所有正权值之和,记为SUM;

  故W = SUM - X,即 “图S中所有点的权值和” = “整个图中所有正权值之和” - “割集中所有边权值和”;

  然后,因为SUM为定值,只要我们取最小割,则“图S中所有点的权值和”就是最大的,即此时图S为图S为最大权闭合子图;

引用:最大权闭合子图 - [求最大点权的闭合子图]

这样子这道题就可以过掉了。

输入方式题目下方已经给出,复制代码即可。

例题2 【Luogu P2774】【网络流24题】方格取数问题

题目传送门

我们发现对于一个点,如果选了,周围四个点就不能选。(这不是废话)

所以我们考虑建出一个二分图(这次没有出

考虑对棋盘进行黑白染色。横纵坐标和为偶数的为白点,反之则为黑点。这样子一个黑点四周肯定都是白点。

假设我们都取,再求出最小割,把点权和减掉最小割=最大流就是答案了。

从源点向黑点连容量为点权的边,白点向汇点连容量为点权的边。

黑点与相邻白点之间连容量为inf的边。跑一遍最大流。

证明其正确性?

设现在有互斥方格\(a, b\),最小割一定会割掉\(S -> a\) 或者 \(b -> T\) 两者之一。

那么就表示这两个方格的其中一个不选了。

证毕。

int as(int i, int j){return (i - 1) * m + j;}

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= m; j++){
            cin >> a;
            if((i + j) & 1)
                add(s, as(i, j), a);
            else add(as(i, j), t, a);
            sum += a;
        }
    }
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= m; j++){
            if((i + j) & 1){
                for(int k = 0; k < 4; k++){
                    int xx = i + x[k], yy = j + y[k];
                    if(xx < 1 || xx > n || yy < 1 || yy > m)continue;
                    add(as(i, j), as(xx, yy), inf);
                }
            }
        }
    }
    sum -= dinic();
    cout << sum << endl;
}

拆点

这是个专题。

例题1【Luogu P2754】【网络流24题】【CTSC1999】家园

题目传送门

又遇到一个小技巧。

考虑按时间拆点。

这里我们枚举时间t。令t时刻的i号空间站为\((t,i)\)

遍历每一架太空船,在\(t - 1\)时刻太空船停靠的太空站与\(t\)时刻停靠的太空站之间连一条容量为太空船容量的边。

再遍历每一个太空站,在\(t - 1\)时刻的太空站与\(t\)时刻的太空站之间连一条容量为inf的边。

再建立超级源汇。

从源点向\(t\)时刻的地球连一条容量为inf的边,从\(t\)时刻的月球向汇点连一条容量为inf的边。

注意!!!\(t = 0\)时,也要执行上述加粗字体中的内容(我因为这个wa了半天)。

不用每次枚举一个时间就重新建图,直接在残量网络上接着跑,并累加答案就行了。

int num(int tim, int j){return tim * n + j;}

int main(){
    cin >> n >> m >> k;
    n += 2;
    for(int i = 1; i <= m; i++){
        cin >> h[i] >> p[i];
        for(int j = 0; j < p[i]; j++){
            cin >> men[i][j];
            men[i][j] += 2;
        }
    }
    for(int t = 0; t <= 600; t++){
        add(s, num(t, 2), inf);
        add(num(t, 1), T, inf);
        if(t){
            for(int i = 1; i <= m; i++)
                add(num(t - 1, men[i][(t - 1) % p[i]]), num(t, men[i][t % p[i]]), h[i]);
            for(int i = 1; i <= n; i++)
                add(num(t - 1, i), num(t, i), inf);
            ans += dinic();
            if(ans >= k){cout << t << endl; return 0;}
        }
    }
    cout << 0 << endl;
}

再提一句。正解要用并查集维护是否能到达月球。

但是也可以枚举时间直到一个很大的数,如果还不出解就输出无解。

例题2【Luogu P4013】【网络流24题】数字梯形问题

题目传送门

这题的思路较为显然。

将每个数字拆成两个点\(x\)\(x'\)

对于第一个问题,有以下几个步骤:

step 1:我们从源点向最上层的数的入点连一条容量为1,费用为0的边,表示每个数只能用一次。

step 2:从\(x\)\(x'\)连一条容量为1,费用为点权的边,表示每个数只能选取一次。

step 3:从每个数的出点向它能到达的数的入点连一条容量为1,费用为0的边,表示一条路径。

step 4:从底层每个数的出点向汇点连一条容量为1,费用为点权的边。

对于第二个问题,我们只需要将\(x\)\(x'\)之间的边容量改成inf,表示每个数可以选取无数次。

同时将step 4中的边容量改成inf,表示最后一行的数也能选取无数次。

对于第三个问题,在第二问的基础上将step 3中的容量改成inf即可。

对于每一个问题,跑最大费用最大流。

int main(){
    cin >> m >> n;
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m + i - 1; j++){
            cin >> num[i][j];
            jbk[i][j] = ++sum;
        }
    for(int i = 1; i <= m; i++)
        add(s, i, 1, 0);
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m + i - 1; j++){
            add(jbk[i][j], jbk[i][j] + sum, 1, -num[i][j]);
            if(i != n){
                add(jbk[i][j] + sum, jbk[i + 1][j], 1, 0);
                add(jbk[i][j] + sum, jbk[i + 1][j + 1], 1, 0);
            }
        }
    for(int i = 1; i <= m + n - 1; i++)
        add(jbk[n][i] + sum, t, 1, 0);
    dinic();
    cout << -cost << endl;
    
    
    memset(head, 0, sizeof(head));
    memset(e, 0, sizeof(e));
    tmp = 1, cost = 0;
    for(int i = 1; i <= m; i++)
        add(s, i, 1, 0);
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m + i - 1; j++){
            add(jbk[i][j], jbk[i][j] + sum, inf, -num[i][j]);
            if(i != n){
                add(jbk[i][j] + sum, jbk[i + 1][j], 1, 0);
                add(jbk[i][j] + sum, jbk[i + 1][j + 1], 1, 0);
            }
        }
    for(int i = 1; i <= m + n - 1; i++)
        add(jbk[n][i] + sum, t, inf, 0);
    dinic();
    cout << -cost << endl;
    
    
    memset(head, 0, sizeof(head));
    memset(e, 0, sizeof(e));
    tmp = 1, cost = 0;
    for(int i = 1; i <= m; i++)
        add(s, i, 1, 0);
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m + i - 1; j++){
            add(jbk[i][j], jbk[i][j] + sum, inf, -num[i][j]);
            if(i != n){
                add(jbk[i][j] + sum, jbk[i + 1][j], inf, 0);
                add(jbk[i][j] + sum, jbk[i + 1][j + 1], inf, 0);
            }
        }
    for(int i = 1; i <= m + n - 1; i++)
        add(jbk[n][i] + sum, t, inf, 0);
    dinic();
    cout << -cost << endl;
}
例题3 【Luogu P2045】方格取数加强版

题目传送门

这题非常友好,没有\(n, m\)。(日常m写成n调半天)

曾经有一道叫做方格取数的题目,每次找两条路径,正解是dp。

但是显然这道题无法用dp解决。

继续考虑拆点。将每个点拆成\(in(i,j), out(i,j)\)

从源点向\(in(1,1)\)连一条容量为k的边,表示有k条路径。

\(out(n,n)\)向汇点连一条容量为k的边,表示接受到k条路径。

对于一个点\(x,y\)

\(in(i,j)\)\(out(i,j)\)连两条边:

一条容量为1,费用为点权的相反数(跑的是最大费用最大流),表示每个点只能取一次。

一条容量为inf,费用为0,表示每个点可以经过无数次。

再从\(out(i,j)\)向相邻的格子的入点连容量为inf,费用为0的边,跑最小(大)费用最大流即可。

int main(){
    cin >> n >> m >> k;
    n += 2;
    for(int i = 1; i <= m; i++){
        cin >> h[i] >> p[i];
        for(int j = 0; j < p[i]; j++){
            cin >> men[i][j];
            men[i][j] += 2;
        }
    }
    for(int t = 0; t <= 500; t++){
        add(s, num(t, 2), inf);
        add(num(t, 1), T, inf);
        if(t){
            for(int i = 1; i <= m; i++)
                add(num(t - 1, men[i][(t - 1) % p[i]]), num(t, men[i][t % p[i]]), h[i]);
            for(int i = 1; i <= n; i++)
                add(num(t - 1, i), num(t, i), inf);
            ans += dinic();
            if(ans >= k){cout << t << endl; return 0;}
        }
    }
    cout << 0 << endl;
}
例题4【Luogu P3159】【CQOI2012】交换棋子
输入样例1 输出样例1
3 3                                              4
110
000
001
000
110
100
222
222
222
数据范围

\(1 <= m,n <= 20\)

思路的确定

首先当我们看到这种数据范围的时候,

这不是标准状压吗?

但是这是网络流。显然不能状压。爆搜肯定惨死。

看到这种题,就会想到从起始棋盘向目标状态连边。交换次数自然就是费用。

同时,棋子的交换也可以视为流量的转移。

于是我们给这道题定了性:最小费用最大流

但是有一些小问题:比如我们选取了一条路径:\((1,1) -> (2,2)\)

我们需要交换两次:\(swap((1,1),(1,2))\) \(swap((1,2),(2,2))\)

于是中间的节点使用了两次的交换次数。
如果我们只是把点拆成一条边,很难处理这个节点到底是中间点、起始点还是目标点。

所以我们考虑拆点。

拆点

这道题采用了一个蛇皮的方法,当时我没有想到,应该是我太菜了。

基础的拆点可以将一个节点拆成入点和出点。但是这道蛇皮的题是要将每个格子拆成三个点的:\(in\),\(mid\),\(out\)

无标题

我们把起始状态中每一个有黑棋子的点连向原点,目标状态中每一个有黑棋子的点连向汇点。

把这些点向拆完的格子连边。

从每一个格子的\(out\)点向与这个格子联通的格子的\(in\)点连边。

现在有以下几种情况。我们用\(<a,b>\)这个数对表示起始是否为黑,目标是否为黑。

\(<1,0>\)

无标题

图中蓝色的边表示水流方向。

解释一下:起始状态下有黑子,但是目标状态没有黑子,所以\(mid -> end\) 这条边流量为0。

这个黑子(也可以视为水流)就会流向\(out\),向下一个格子的\(in\)流去。

\(<0,1>\)

无标题

起始状态没有黑子。没有流量流向mid。

但是目标状态有黑子。所以只会有从上一个格子跑过来的棋子流向\(end\)\(mid -> end\) 流量为1。

\(<0,0>\)

无标题

这个格子只能当中转站。

\(<1,1>\)

无标题

一通狂流即可。

小提问

碰到\(<1,1>\)能不能直接不连呢?

显然答案是否定的。这个点还可以当中转点。


至此,连边部分就结束了。

等等,还有一些小问题!

小问题

\(in -> mid\) 以及 \(mid -> out\) 的流量怎么办?

考虑将\(m_{i,j}\)均等分配即可。

这样子这题就可以过了。

等等,还有一些小问题!

真的可以过吗?

#1AC6ms/3364KB #2AC20ms/3376KB #3AC6ms/3356KB #4WA #5WA #6WA #7WA #8WA #9WA #10WA

(我不会用笔记本截图)

\(m_{i,j}\)是奇数的时候,交换次数就会莫名其妙地少掉一个。

所以我们要考虑将这一个流量给到哪里。继续分类讨论。

\(<0,0>\)

由于这个点只能当中转站,所以不能分配这一个流量。要保证\(in -> mid\)\(mid -> out\)的流量相同。要不然就会出现一个黑点流进来却流不出去,或者是黑点流不进来\(mid -> out\)却莫名其妙多出来一个流量。

\(<1,1>\)

同上。

\(<1,0>\)

除了均分的交换次数之外,有一个黑点需要流出去。所以分配给\(mid -> out\)这条边。

\(<0,1>\)

除了均分的交换次数之外,有一个黑点要流进来。所以分配给\(in -> mid\)这条边。

这样子就可以过了。

#include<bits/stdc++.h>

using namespace std;

const int N = 3e5;
const int inf = 1e9;

int cnt = 1, sum = 0, n, m, s = 1e4, t = s + 1, cost = 0, tmp = 0, dd = 0;

struct dt{
    int to, val, w, next;
}e[N];

int dt[N], dep[N], cur[N], dis[N];

char beg[21][21], lst[21][21], f[21][21];

bool vis[N];

void add(int x, int y, int value, int wa){
    cnt++;
    e[cnt].to = y;
    e[cnt].val = value;
    e[cnt].w = wa;
    e[cnt].next = dt[x];
    dt[x] = cnt;
    cnt++;
    e[cnt].to = x;
    e[cnt].val = 0;
    e[cnt].w = -wa;
    e[cnt].next = dt[y];
    dt[y] = cnt;
}

bool /*SPFA*/dijstra(){
    memset(vis, 0, sizeof(vis));
    memset(dis, 0x3f, sizeof(dis));
    for(int i = 0; i <= N; i++)cur[i] = dt[i];
    queue<int> q;
    q.push(s);
    vis[s] = 1;
    dis[s] = 0;
    while(!q.empty()){
        int u = q.front();
        vis[u] = 0;
        q.pop();
        for(int i = dt[u]; i; i = e[i].next){
            int v = e[i].to, w = e[i].w;
            if(e[i].val > 0 && dis[v] > dis[u] + w){
                dis[v] = dis[u] + w;
                //cout << v << " " << pre[v].fa << endl;
                if(!vis[v]){
                    q.push(v);
                    vis[v] = 1;
                }
            }
        }
    }
    return dis[t] < 0x3f3f3f3f;
}

int dfs(int now, int low){
    if(now == t) return low;
    int flow = 0;
    vis[now] = 1;
    for(int i = cur[now]; i; i = e[i].next){
        cur[now] = i;
        int v = e[i].to;
        if(!vis[v] && e[i].val && dis[v] == dis[now] + e[i].w){
            if(flow = dfs(v, min(low, e[i].val))){
                e[i].val -= flow;
                e[i ^ 1].val += flow;
                cost += e[i].w * flow;
                return flow;
            }
        }
    }
    return 0;
}

int dinic(){
    int minflow, maxflow = 0;
    while(dijstra()){
        while(minflow = dfs(s, inf)){
            memset(vis, 0, sizeof(vis));
            maxflow += minflow;
        }
    }
    return maxflow;
}

int begin(int i, int j){return (i - 1) * m + j;}
int last(int i, int j){return n * m * 1 + (i - 1) * m + j;}
int in(int i, int j){return n * m * 2 + (i - 1) * m + j;}
int mid(int i, int j){return n * m * 3 + (i - 1) * m + j;}
int out(int i, int j){return n * m * 4 + (i - 1) * m + j;}

int x[8] = {0, 0, 1, -1, 1, 1, -1, -1}, y[8] = {1, -1, 0, 0, 1, -1, 1, -1};

int main(){
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++){
            char c;
            cin >> c;
            beg[i][j] = c - '0';
        }
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++){
            char c;
            cin >> c;
            lst[i][j] = c - '0';
        }
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++){
            char c;
            cin >> c;
            f[i][j] = c - '0';
        }
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= m; j++){
            if(beg[i][j] == lst[i][j]){
                add(in(i, j), mid(i, j), f[i][j] >> 1, 0);
                add(mid(i, j), out(i, j), f[i][j] >> 1, 0);
            }
            else{
                if(beg[i][j] == 1){
                    add(in(i, j), mid(i, j), f[i][j] >> 1, 0);
                    add(mid(i, j), out(i, j), (f[i][j] + 1) >> 1, 0);
                }
                if(lst[i][j] == 1){
                    add(in(i, j), mid(i, j), (f[i][j] + 1) >> 1, 0);
                    add(mid(i, j), out(i, j), f[i][j] >> 1, 0);
                }
            }
            
            if(beg[i][j] == 1){
                tmp++;
                add(s, begin(i, j), 1, 0);
                add(begin(i, j), mid(i, j), 1, 0);
            }
            if(lst[i][j] == 1){
                dd++;
                add(last(i, j), t, 1, 0);
                add(mid(i, j), last(i, j), 1, 0);
            }
            for(int k = 0; k < 8; k++){
                int xx = i + x[k], yy = j + y[k];
                if(xx < 1 || xx > n || yy < 1 || yy > m) continue;
                add(out(i, j), in(xx, yy), inf, 1);
            }
        }
    }
    if(tmp != dinic()) cout << -1 << endl;
    else cout << cost << endl;
}

转载于:https://www.cnblogs.com/ironwheel/p/11189170.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值