网络流基础知识学习(初级)

前言

  1. 前面很多东西需要深入理解,建议先看视频,网络流雀食挺烧脑的。
  2. 后面发现网络流就是全套模板,最多Dinic,偶尔卡(不知道正式赛会不会卡)那就ISAP。遇到的时候,难点就是如何建图(比较抽象)。

前置知识——链式前向星

  1. 之前听说会vector就ok了,但是貌似网络流有些地方只能前向星emmm
  2. 链式前向星没有排序,前向星有排序。
  3. 深度理解链式前向星(学起来,很快啊!!)

模板

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
const int M = 1e5 + 10;

int cnt, head[N];  // cnt初始化为0,head初始化为-1
//结构体
struct Edge {
    int to, w, next;
    Edge(int to = 0, int w = 0, int next = 0) : to(to), w(w), next(next) {}
} edge[M];
//加边
void add(int u, int v, int w) {
    edge[cnt] = Edge(v, w, head[u]), head[u] = cnt++;  //正向边
    edge[cnt] = Edge(u, w, head[v]), head[v] = cnt++;
    //反向边(注意权值,按题目要求)
    /*    建议全部从0开始,网络流那里从1开始好像就有问题cnt从1开始:edge[++cnt]=Edge(v,w,head[u]),head[u]=cnt;*/
}
//遍历:for(int i=head[u];~i;i=edge[i].next)
void init() { memset(head, 0, sizeof(head)), cnt = 0; }

signed main() {
    init();
    return 0;
}

一、网络流基础概念

  1. 可行流:在容量网络G中满足以下条件的网络流 f f f,称为可行流。
    1. 弧流量限制条件 0 < = f ( u , v ) < = c ( u , v ) 0<=f(u,v)<=c(u,v) 0<=f(u,v)<=c(u,v);
    2. 平衡条件:即流入一个点的流量要等于流出这个点的流量,(源点和汇点除外)。
  2. 最大流:在所有的可行流中,流量最大的流(可能不是唯一的)。
  3. 最小割:割其实就是删边的意思,当然最小割就是割掉 X 条边来让 S 跟 T 不互通。我们要求 X 条边加起来的流量综合最小。这就是最小割问题。
  4. 最小费用最大流:每条边都有一个费用,代表单位流量流过这条边的开销。我们要在求出最大流的同时,要求花费的费用最小。
  5. 其他名词
    1. 零流:若网络流上每条弧上的流量都为0,则该网络流称为零流。
    2. 伪流:如果一个网络流只满足弧流量限制条件,不满足平衡条件,则这种网络流为伪流,或称为容量可行流.(在预流推进优化算法中使用)。
    3. 增广路:在原图 G 中若一条从源点到汇点的路径上所有边的 剩余容量都大于 0,这条路被称为增广路。
    4. 残量网络:可以理解为,残量网络中包括了那些还剩了流量空间的边构成的图,也包括虚边(即反向边)。
    5. 网络流的流量:从源点出去或者进入汇点的总流量。

二、网络流中的常见问题(最大流,最小割,费用流)

三、最大流:

  1. 概述
    1. 我们有一张图,要求从源点流向汇点的最大流量(可以有很多条路到达汇点),就是我们的最大流问题。
    2. 最大流算法可以解决很多实际的问题,比如二分图的最大匹配数就等于网络的最大流量。

Ford-Fulkerson 增广路算法:该方法通过寻找增广路来更新最大流,有 EK,dinic,SAP,ISAP 主流算法

  1. 精华部分:利用反向边,使程序有了一个后悔和改正的机会。

Edmond-Karp 动能算法(EK 算法)

  1. 这个算法很简单,就是BFS找增广路,然后对其进行增广。
  2. 复杂度
    ​​​​在这里插入图片描述
    在这里插入图片描述
  3. 讲解
    1. 精髓:建反向边,给予后悔的机会。(理解这里很重要)
    2. 怎么实现后悔1:最开始建边的时候建立正向边,权值为 w w w,反向边权值为0(注意如果矩阵就g[a][b]+=w,g[b][a]+=0,链式前向星的话就只需要每次add(a,b,w),add(b,a,0)即可——视作两点之间可以有很多条边)。
    3. 怎么后悔2:每次更新残余网络的时候增广路上的正方向+=minn,反方向-=minn。使某两点的正向边与反向边之和始终为两边的容量和,并且任意一边的值都是剩下的容量以及反向边通过的流量,所以也并不需要考虑最开始是否能够环形流动(正反边都有容量)。
    4. 如果使邻接矩阵的话,反向边就是从g[a][b]变为g[b][a]。如果是链式前向星,那就在加边的时候连续加入正向和反向边,然后在用的时候直接i xor 1就可以了(i为边的编号)。不过注意,cnt应该从02开始设置,否则就达不到这种效果,因为总是要 (某偶数,该偶数+1)才能互相异或1得到对方。
    5. 最后注意,链式前向星的时候,增广路的前一个节点的记录方式也与邻接矩阵有一点不一样:x的前一个点到x点这条边的序号记为id[x]=i
    6. ​感觉总结的挺好的,唯一不好的是没有图。可以再找博客参考,比如oi-wiki
  4. 题目P2740 [USACO4.2]草地排水Drainage Ditches
    1. 题解:EK增广路,最大流模板。
    2. 题意 1 ≤ n ≤ 200 1\le n\le 200 1n200条水管 1 ≤ m ≤ 200 1\le m\le 200 1m200个点,源点为1,汇点为m(a,b,c)表示有ab容量为c的水管。求最大流。说明,两点之间可能不止一条水管;可能会出现水环形流动的情形(好像没啥卵用)。

矩阵实现模板

#include <bits/stdc++.h>

#define read(x) scanf("%d", &x)
#define print(a, c) printf("%d%c", a, c)
using namespace std;
const int M = 2e2 + 10;
const int INF = 2e9;
//这里还没被坑过emmm,但总感觉需要注意一下,万一最小值恰好可以比1e9大一点点

int n, m, a, b, c;
int g[M][M];
int s, t;

int vis[M], pre[M], mi[M];
bool bfs() {
    memset(vis, 0, sizeof(vis));
    // pre,mi不用初始话整个数组。而且注意,这里不应该只初始化1~n,因为m才是节点的总数
    queue<int> q;
    q.push(s), vis[s] = 1, mi[s] = INF;  // mi只需要初始这里,pre则啥都不必
    while (!q.empty()) {
        int x = q.front();
        q.pop();
        // 1~m才是节点!n只是边数
        for (int i = 1; i <= m; i++) {
            if (!vis[i] && g[x][i] > 0) {
                pre[i] = x, mi[i] = min(mi[x], g[x][i]);
                vis[i] = 1, q.push(i);
                if (i == t) return true;
            }
        }
    }
    return false;
}
int EK() {
    int maxflow = 0;
    while (bfs()) {
        int x = t;
        while (x != s) {
            g[pre[x]][x] -= mi[t];
            g[x][pre[x]] += mi[t];
            // g[a][b]+g[b][a]守恒。我们可以不考虑是否两个方向都有容量,只要满足守恒条件即可(很容易理解的:总之g[b][a]表示g[b][a]剩下的容量以及g[a][b]经过的流量之和,永远不会超过它们的和)
            x = pre[x];
        }
        maxflow += mi[t];
    }
    return maxflow;
}
signed main() {
    read(n), read(m);
    s = 1, t = m;
    for (int i = 1; i <= n; i++) {
        read(a), read(b), read(c);
        g[a][b] += c;
        //残余网络中的g[b][a]表示使用的这个方向的流量,g[a][b]表示剩下的这个方向的流量
    }
    print(EK(), '\n');
    return 0;
}

链式前向星实现模板

#include <bits/stdc++.h>
#define read(x) scanf("%d", &x)
#define print(a, c) printf("%d%c", a, c)
using namespace std;
const int N = 2e2 + 10;
const int M = 2e2 + 10;
const int INF = 1e9;
struct Edge {
    int to, w, next;
    Edge(int _to = 0, int _w = 0, int _next = 0)
        : to(_to), w(_w), next(_next) {}
} edge[N << 1];    // n为边的数量
int head[M], cnt;  // m才为点的数量
int n, m, a, b, c, s, t;
void add(int u, int v, int w) {
    edge[cnt] = Edge(v, w, head[u]), head[u] = cnt++;
    edge[cnt] = Edge(u, 0, head[v]), head[v] = cnt++;  //反向边
}
int vis[M], id[M], mi[M];
bool bfs() {
    memset(vis, 0, sizeof(vis));
    queue<int> q;
    q.push(s), vis[s] = 1, mi[s] = INF;
    while (!q.empty()) {
        int x = q.front();
        q.pop();
        for (int i = head[x]; ~i; i = edge[i].next) {
            if (edge[i].w > 0) {
                int y = edge[i].to;
                if (vis[y]) continue;
                mi[y] = min(mi[x], edge[i].w);
                id[y] = i;
                q.push(y), vis[y] = 1;
                if (y == t) return true;
            }
        }
    }
    return false;
}
int EK() {
    int res = 0, x;
    while (bfs()) {
        x = t;
        while (x != s) {
            int i = id[x];
            //增广路中x表示一个点,id[x]表示x_pre(增广路中x的前一个点)到x这条边的序号
            edge[i].w -= mi[t];
            edge[i ^ 1].w += mi[t];
            //反正正向边和反向边之和不大于容量。(环形咋办???)
            x = edge[i ^ 1].to;
        }
        res += mi[t];
    }
    return res;
}
signed main() {
    read(n), read(m);
    // init
    s = 1, t = m;
    memset(head, -1, sizeof(head));
    for (int i = 1; i <= n; i++) {
        read(a), read(b), read(c);
        add(a, b, c);  //, add(b, a, 0);  //不是邻接矩阵的话是不用考虑重边的
    }
    print(EK(), '\n');
    return 0;
}
/*
5 4
1 2 40
1 4 20
2 4 20
2 3 30
3 4 10
*/

Dinic算法

  1. 参考博客
    1. 最大流(Edmonds-Karp,Dinic,ISAP,Push-Relabel 预流推进算法)
    2. EK不够快?再学个Dinic吧
  2. 实现过程
    1. 每次增广前,通过bfs分层。这源点层数为0,其他点的层数为离源点的最近距离(前提条件:路径容量全大于0);
    2. 然后bfs找最短增广路(基于bfs处理的dep一层一层增广(也要满足容量条件),增广路长度都为dep[t])。每次dfs最大流答案加上流过点s的最大流量(具体操作见代码——递归实现)!!
    3. 重复1,2操作直到不存在到汇点的增广路。
  3. Dinic的两个优化:多路增广和当前弧优化
    1. 多路增广:每次找到一条增广路的时候,如果残余流量没有用完怎么办呢?我们可以利用残余部分流量,再找出一条增广路。这样就可以在一次 DFS 中找出多条增广路,大大提高了算法的效率。
    2. 具体说明:蓝色表示点的深度dep。dfs的时候我们一下子找到四条增广路:s->1->4->t,s->1->2->t,s->2->3->t,s->1->3->t
      ​​在这里插入图片描述
    3. 当前弧优化:如果一条边已经被增广过,那么它就没有可能被增广第二次。那么,我们下一次进行增广的时候,就可以不必再走那些已经被增广过的边。(具体证明看不懂,操作就简单了——bfs时预处理(now[v]=head[v]),dfs时now[v]=i即可)
    4. 注意(踩过坑):​
      当前弧优化建议放在bfs里面,虽然也可以放在Dinic中的循环里面(需要整体赋值),但是一是时间复杂度显然会变高一点,二是最开始的bfs之前也要整体赋值一下,否则bfs中i的开头要用head[i]才行。(网络流中首先注意到这个问题,做了kuangbin专题的第一题之后才全然搞明白这个是干什么的,优化在哪里——只优化在dfs中,不能优化在bfs中,用于替代head作为一个节点的链式前向星的结尾。一条边增广过,就没有可能再增广了(原来这里了解的也还是不够。总之知道怎么使用了!!)。bfs中只是可以初始化罢了,一定注意每次多路增广的时候都要初始化!)
  4. 复杂度
    1. 具体时间复杂度 O ( n 2 m ) O(n^2m) O(n2m),事实上,往往达不到这个复杂度。据oi大佬说(EK不够快?再学个Dinic吧)​学会Dinic就足够了。
    在这里插入图片描述
    1. 在求解二分图最大匹配问题时,Dinic算法的时间复杂度是 O ( m n ) O(m\sqrt{n}) O(mn )

Dinic模板(应该是最长用到到的,比ISAP慢不了多少)

//题目仍然是上面EK的那个题目。
#include <bits/stdc++.h>

// #define int long long
#define read(x) scanf("%d", &x)
#define print(a, c) printf("%d%c", a, c)
#define pb push_back
#define dbg(x) cout << #x << "===" << x << endl
using namespace std;
const int M = 2e2 + 10;
const int INF = 1e9 + 10;

struct Edge {
    int to, w, next;
    Edge(int to = 0, int w = 0, int next = 0) : to(to), w(w), next(next) {}
} e[M << 1];
int n, m, a, b, c;
int s, t;
int head[M], cnt;
void add(int u, int v, int w) {
    e[cnt] = Edge(v, w, head[u]), head[u] = cnt++;
    e[cnt] = Edge(u, 0, head[v]), head[v] = cnt++;
}
int dep[M], now[M];
// bfs使dfs按照dep来增广,得到优化(具体证明就看不太懂了)。
bool bfs() {
    memset(dep, -1, sizeof(dep));
    queue<int> q;
    q.push(s), dep[s] = 0;
    now[s] = head[s];  //当前弧优化(初始化)
    while (!q.empty()) {
        int x = q.front();
        q.pop();
        for (int i = now[x]; ~i; i = e[i].next) {
            int v = e[i].to;
            if (dep[v] == -1 && e[i].w > 0) {
                dep[v] = dep[x] + 1;
                now[v] = head[v];  //当前弧优化(初始化)。
                q.push(v);
                if (v == t) return true;
                //遇到v==t即退出,因为<dep[v]的已经遍历完了,dfs的增广路不可能经过深度>=dep[v]的点
            }
        }
    }
    return false;
}
//一次dfs可能有几条增广路:见图
int dfs(int x, int flow) {
    if (x == t) return flow;
    int ans = 0;
    // flow表示x点还能操作的流量,ans表示增广的这几路后流出x总最大流
    // flow不剩下了,就没必要增广了
    for (int i = now[x]; ~i && flow; i = e[i].next) {
        int v = e[i].to;
        now[x] = i;
        //当前弧优化(这里才能得到优化,bfs中的只是初始化:另now[x]=head[x])
        if (dep[v] == dep[x] + 1 && e[i].w > 0) {
            int tmp = dfs(v, min(flow, e[i].w));
            // tmp为流过点v的最大流量
            if (tmp == 0) dep[v] = -1;  //剪枝,去掉增广完毕的点
            e[i].w -= tmp;
            e[i ^ 1].w += tmp;
            ans += tmp;
            flow -= tmp;  //流过当前点(x)的流量累加(回溯)
        }
    }
    return ans;  //最后返回的是这几路增广的流量和
}
int Dinic() {
    int maxflow = 0;
    // bfs为真就一直dfs多路增广
    while (bfs()) maxflow += dfs(s, INF);
    return maxflow;
}
signed main() {
    read(n), read(m);
    memset(head, -1, sizeof(head));
    s = 1, t = m;
    for (int i = 1; i <= n; i++) {
        read(a), read(b), read(c);
        add(a, b, c);
    }
    print(Dinic(), '\n');
    return 0;
}

ISAP

  1. 总之:就是在Dinic上的优化(而且优化还不少)。每次分层变为只分层一次(只bfs一次),其他分层在dfs中完成。
  2. 参考博文
    1. 理解算法思想oi-wiki究级的最大流算法:ISAP与HLPP
    2. 代码实现最大流(Edmonds-Karp,Dinic,ISAP,Push-Relabel 预流推进算法)究级的最大流算法:ISAP与HLPP
  3. 复杂度:ISAP O ( n 2 m ) O(n^2m) O(n2m)——一般达不到这个复杂度。该算法复杂度比Dinic略优越(一般来说效果会好一些)。
  4. 补充:增广路算法,复杂度都挺玄学的。
    在这里插入图片描述
  5. ISAP和Dinic的两点不同跑反图、不同的分层操作
    1. 跑反图
      在这里插入图片描述
    2. 并不会每次都跑BFS
      在这里插入图片描述
  6. 具体操作
    1. 分层具体操作
      在这里插入图片描述
  7. 优化:当前弧优化和GAP优化
    在这里插入图片描述
  8. 具体操作2:没想到这么简单,写起来比Dinic都还简单的既视感
    1. bfs只需要一次,bfs只需要分层整个图(反向图),不需要考虑权值,也不用特意判断是否s,t连通(dep[s]=0即不连通)
    2. dfs中前面部分与Dinic几乎完全一样(ISAP没有剪枝)
    3. 只是在遍历完x之后,才dep[x]++,使x与相连的深度为dep[x]-1的分割开(开始考虑深度为dep[x]的,同时dep[x]+=1)。
    4. 注意初始化条件,dep[x]=0,是有实际意义的(到不了)。

ISAP模板(终于进入正题了?&比Dinic快的板子)

// Luogu _P2740 [USACO4.2]草地排水Drainage Ditches
#include <bits/stdc++.h>
// #define int long long
#define read(x) scanf("%d", &x)
#define print(a, c) printf("%d%c", a, c)
#define dbg(x) cout << #x << "===" << x << endl
using namespace std;
const int N = 2e2 + 10;
const int M = 4e2 + 10;  //还是直接放在这里操作更舒服 M<<1多少容易弄错
const int INF = 1e9 + 7;
int n, m, s, t, a, b, c;
struct Edge {
    int v, w, next;
    Edge(int v = 0, int w = 0, int next = 0) : v(v), w(w), next(next) {}
} e[M];
int head[N], cnt, now[N];
void add(int u, int v, int w) {
    e[cnt] = Edge(v, w, head[u]), head[u] = cnt++;
    e[cnt] = Edge(u, 0, head[v]), head[v] = cnt++;
}
int dep[N], gap[N];  // gap优化:貌似优化力度挺大的
void bfs() {
    for (int i = 0; i <= n; i++) dep[i] = gap[i] = 0, now[i] = head[i];
    queue<int> q;
    q.push(t), dep[t] = 1, gap[dep[t]]++;
    while (!q.empty()) {
        int x = q.front();
        q.pop();
        for (int i = head[x]; ~i; i = e[i].next) {
            int v = e[i].v;
            if (!dep[v]) dep[v] = dep[x] + 1, q.push(v), gap[dep[v]]++;
        }
    }
}
int dfs(int x, int flow) {
    if (x == t) return flow;
    int ans = 0;
    for (int i = now[x]; ~i; i = e[i].next) {
        int v = e[i].v;
        now[x] =
            i;  //怎么总是忘了弧优化的操作?甚至i=head[i]都来了?唉,还是太菜了
        if (dep[x] == dep[v] + 1) {
            int tmp = dfs(v, min(flow, e[i].w));
            //对比Dinic,这里没有tmp==0则dep[v]=INF。如果有这里的剪枝,很明显就破坏了dep的值。就需要从新bfs更新dep了
            e[i].w -= tmp;
            e[i ^ 1].w += tmp;
            ans += tmp;
            flow -= tmp;
            if (!flow) return ans;
            // x没有跑完,不能够像Dinic一样直接跳出去返回ans,v点是跑完了,但是上一层dfs已经修改了dep[v]
            // Dinic是~i&&flow。其实Dinic也可以像这里一样写
        }
    }
    //上面的操作与Dinic几乎一模一样(与Dinic还是有一点差别的,没有剪枝!!)
    //以下理解来源于https://www.luogu.com.cn/blog/ONE-PIECE/jiu-ji-di-zui-tai-liu-suan-fa-isap-yu-hlpp
    //到这里的时候,x流出去的点已经全部流过了
    //但是从前面流过的点还有剩余。更改dep使x与x流出的点分开
    // emmm,虽然没有完全理解,但是知道怎么操作,虽然知道怎么操作,但是没有完全理解
    //过一段时间再来看看
    gap[dep[x]]--;  //为什莫是这个地方dep[x]++?
    if (gap[dep[x]] == 0) dep[s] = n + 1;
    ++gap[++dep[x]], now[x] = head[x];  //当前弧优化,在每次分层之后都要回到head
    return ans;
}
int ISAP() {
    int res = 0;
    bfs();
    // dbg(dep[s]);bfs有没有错误
    while (dep[s] <= n && dep[s]) res += dfs(s, INF);
    return res;
}
void init() {
    memset(head, -1, sizeof(head)), cnt = 0;
    read(m), read(n);  // m为边数,n为点数
    s = 1, t = n;
    for (int i = 1; i <= m; i++) {
        read(a), read(b), read(c);
        add(a, b, c);
    }
}
signed main() {
    init();
    int maxflow = ISAP();
    print(maxflow, '\n');
    return 0;
}

& HLPP(Push-Relabel 预流推进算法)

在这里插入图片描述

四、最小割

  1. 概述:将某s,t分为两部分需要切除的边的权值的最小值。
  2. 解法:最小割在数值上与最大流相等,但在本身性质上与网络流无任何关系。(所以将题目转化为最小割问题之后,Dinic一下即可。)
  3. 模板:略了,和Dinic一样

UVA1660 电视网络 Cable TV Network点转化为边,无代码

  1. 这一题也算是最小割裸题,但是不够裸,做模板还是不太方便。
  2. 题意:去掉一些点使整个图不连通。问最少去掉多少个点。点的个数不超过50。
  3. 题解点一下:点转化为边,无向图变有向图,权值为1,原边为INF,然后跑一下Dinic即可。如图:x'->y,y'->x转化为原边中的x->y,y->x
    在这里插入图片描述

五、费用流

最小费用最大流

  1. 概述:最小费用最大流——费用最小的最大流
  2. 操作
    1. 只需要在EK/Dinic的操作中改一下增广路的顺序就ok了——每次增广花费最短的路。具体操作可以spfa,dijkstra。
    2. 一般不带负权,spfa,dijkstra都可以解决。带负权,就不知道怎么解决了emm(等遇到题目再说)
    3. Dinic也可以多路增广,按层数改为按最短路。注意当前弧优化不能想Dinic求最大流一样,因为这里的当前弧优化不想求最大流,它只会增,不会减(这里是凭记忆,具体也没太了解,甚至不知道说的对不对,总之注意)。

结尾:之前还学了很多不是很有用的东西,略了略了(就算能用上大概率是还没有kuangbin板子好用的)

在这里插入图片描述
在这里插入图片描述

#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <map>
#include <queue>
#include <string>
// #define int long long
#define read(x) scanf("%d", &x)
#define print(a, c) printf("%d%c", a, c)
#define dbg(x) cout << #x << "===" << x << endl
using namespace std;
const int N = 3e2 + 10;
const int M = 1e4 + 10;
const int INF = 1e9 + 10;
int n, m, k;
int s, t;
struct Edge {
    int v, w, co, next;
    Edge(int v = 0, int w = 0, int co = 0, int next = 0)
        : v(v), w(w), co(co), next(next) {}
} e[M];
int head[N], now[N], cnt;
void add(int u, int v, int w, int co) {
    e[cnt] = Edge(v, w, co, head[u]), head[u] = cnt++;
    e[cnt] = Edge(u, 0, -co, head[v]), head[v] = cnt++;
}

int maxflow, mincost;
int dis[N], vis[N], incf[N], pre[N];
bool spfa() {
    for (int i = 1; i <= t; i++) dis[i] = INF, incf[i] = vis[i] = 0;
    queue<int> q;
    q.push(s), vis[s] = 1, dis[s] = 0, incf[s] = INF;
    while (!q.empty()) {
        int x = q.front();
        q.pop();
        vis[x] = 0;
        for (int i = head[x]; ~i; i = e[i].next) {
            int v = e[i].v;
            if (dis[x] + e[i].co < dis[v] && e[i].w) {
                dis[v] = dis[x] + e[i].co;
                pre[v] =
                    i;  // i为正边,v=e[pre[v]].v为后继点,e[pre[v]^1]为前继点x
                incf[v] = min(incf[x], e[i].w);
                if (!vis[v]) q.push(v), vis[v] = 1;
            }
        }
    }
    // return incf[t] > 0;
    return dis[t] != INF;  //都可以
}
void MCMF() {
    while (spfa()) {
        maxflow += incf[t];
        mincost += incf[t] * dis[t];
        for (int i = t; i != s; i = e[pre[i] ^ 1].v) {
            e[pre[i]].w -= incf[t];      //正边的 w
            e[pre[i] ^ 1].w += incf[t];  //逆向边的 w
        }
    }
}
int a[55][55], b[55][55], c[55][55][55];
void init() {
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= k; j++) read(a[i][j]);
    for (int i = 1; i <= m; i++)
        for (int j = 1; j <= k; j++) read(b[i][j]);
    for (int ii = 1; ii <= k; ii++)
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= m; j++)
                read(c[ii][i][j]);  //表示将ii运到j->i的单价
}
int solve() {
    int res = 0;
    for (int ii = 1; ii <= k; ii++) {
        memset(head, -1, sizeof(head)), cnt = 0, maxflow = mincost = 0;
        //源点->供应点->(有的)第ii件商品
        s = m + m + n + n + 1, t = s + 1;
        for (int i = 1; i <= m; i++)
            add(s, i, INF, 0), add(i, m + i, b[i][ii], 0);

        //(需要的)ii件商品->店主->汇点
        int sum = 0;
        for (int i = 1; i <= n; i++)
            add(m + m + n + i, t, INF, 0),
                add(m + m + i, m + m + n + i, a[i][ii], 0), sum += a[i][ii];
        //供应的ii->需要的ii
        for (int i = 1; i <= m; i++)
            for (int j = 1; j <= n; j++)
                add(m + i, m + m + j, INF, c[ii][j][i]);
        MCMF();
        if (maxflow != sum)
            return -1;
        else
            res += mincost;
    }
    return res;
}
signed main() {
    while (scanf("%d%d%d", &n, &m, &k) != EOF) {
        if (n == 0 && m == 0 && k == 0) break;
        init();
        print(solve(), '\n');
    }
    return 0;
}
/*
1 3 3
1 1 1
0 1 1
1 2 2
1 0 1
1 2 3
1 1 1
2 1 1

1 1 1
3
2
20

0 0 0
output:::
4
-1
*/
  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值