【知识点复习】网络流

基础知识

网上博客有很多,这里就没必要再赘述了。

贴一份 \(yxc\) 的笔记:

1. 基本概念
    1.1 流网络,不考虑反向边
    1.2 可行流,不考虑反向边
        1.2.1 两个条件:容量限制、流量守恒
        1.2.2 可行流的流量指从源点流出的流量 - 流入源点的流量
        1.2.3 最大流是指最大可行流
    1.3 残留网络,考虑反向边,残留网络的可行流f' + 原图的可行流f = 原题的另一个可行流
        (1) |f' + f| = |f'| + |f|
        (2) |f'| 可能是负数
    1.4 增广路径
    1.5 割
        1.5.1 割的定义
        1.5.2 割的容量,不考虑反向边,“最小割”是指容量最小的割。
        1.5.3 割的流量,考虑反向边,f(S, T) <= c(S, T)
        1.5.4 对于任意可行流f,任意割[S, T],|f| = f(S, T)
        1.5.5 对于任意可行流f,任意割[S, T],|f| <= c(S, T)
        1.5.6 最大流最小割定理
            (1) 可以流f是最大流
            (2) 可行流f的残留网络中不存在增广路
            (3) 存在某个割[S, T],|f| = c(S, T)
    1.6. 算法
        1.6.1 EK O(nm^2)
        1.6.2 Dinic O(n^2m)
    1.7 应用
        1.7.1 二分图
            (1) 二分图匹配
            (2) 二分图多重匹配
        1.7.2 上下界网络流
            (1) 无源汇上下界可行流
            (2) 有源汇上下界最大流
            (3) 有源汇上下界最小流
        1.7.3 多源汇最大流

最大流

个人认为比较重要的几个点就是:

  • 建的是原图的残留网络

每次增大就相当于减小残留网络中的容量。

  • 分层思想

保证流的顺序。

  • 当前弧优化

别的我不知道,我只知道如果没有它你会死翘翘。

详细分析也不讲啦,这里推荐一篇证明博客 -> 出门右转

点击查看代码
inline void add(int x, int y, int w) {
    e[++tot] = (node){ y, head[x] }, f[tot] = w, head[x] = tot;
    e[++tot] = (node){ x, head[y] }, head[y] = tot;
}
inline void read(int &x) {
    x = 0;
    char s = getchar();
    while (s < '0' || s > '9') s = getchar();
    while (s <= '9' && s >= '0') {
        x = x * 10 + s - '0';
        s = getchar();
    }
}
inline bool bfs() {
    memset(dep, -1, sizeof(dep));
    dep[S] = 0, cur[S] = head[S];
    queue<int> q;
    q.push(S);
    int now, ver;
    while (!q.empty()) {
        now = q.front();
        q.pop();
        for (rt i = head[now]; i; i = e[i].nex) {
            ver = e[i].to;
            if (dep[ver] == -1 && f[i]) {
                dep[ver] = dep[now] + 1, cur[ver] = head[ver];
                if (ver == T)
                    return 1;
                q.push(ver);
            }
        }
    }
    return 0;
}
inline int find(int x, int limit) {
    if (x == T)
        return limit;
    int flow = 0, tmp, ver;
    for (rt i = cur[x]; i && flow < limit; i = e[i].nex) {
        cur[x] = i, ver = e[i].to;
        if (dep[ver] == dep[x] + 1 && f[i]) {
            tmp = find(ver, min(limit - flow, f[i]));
            if (!tmp)
                dep[ver] = -1;
            f[i] -= tmp, f[i ^ 1] += tmp, flow += tmp;
        }
    }
    return flow;
}
inline int dinic() {
    int res = 0, flow;
    while (bfs())
        while (flow = find(S, inf)) res += flow;
    return res;
}

费用流

费用流是基于网络流,只是多了一个费用的条件限制。

算法核心应该就是利用 \(spfa\) 可以处理负权的功能进行最短路。

注意点:

  • 流量处理时要注意一定是“照顾弱小”。但是初始值为 \(inf\)

  • 利用链式前向星的特性记录路径就很妙

点击查看代码
inline void add(int a, int b, int c, int d) {
    to[++tot] = b, f[tot] = c, w[tot] = d, nex[tot] = head[a], head[a] = tot;
    to[++tot] = a, w[tot] = -d, nex[tot] = head[b], head[b] = tot;
}
inline bool spfa() {
    int now, ver;
	memset(d,0x3f,sizeof(d));
    memset(incf,0,sizeof(incf));
	ss = 0, tt = 1, q[0] = S, d[S] = 0, incf[S] = inf;
    while(ss != tt) {
        now = q[ss ++], vis[now] = 0;
        if(ss == N) ss = 0;
        for(rt i = head[now]; i; i = nex[i]) {
            ver = to[i];
            if(f[i] && d[now] + w[i] < d[ver]) {
                d[ver] = d[now] + w[i], pre[ver] = i, incf[ver] = min(f[i], incf[now]);
                if(!vis[ver]) {
                    q[tt ++] = ver, vis[ver] = 1;
                    if(tt == N) tt = 0;
                }
            }
        }
    }
    return incf[T] > 0;
}
inline int EK() {
    int cost = 0,flow;
    while(spfa()) {
        flow = incf[T], cost += flow * d[T];
        for(rt i = T; i != S; i = to[pre[i] ^ 1])
            f[pre[i]] -= flow, f[pre[i] ^ 1] += flow;
    }
    return cost;
}

建图总结

其实大家都可以发现,网络流难点在于建图,只要把图建对,走遍天下都不怕。

然而建图都是有规律可循的,所以总结一发。

一、常规建图(又名直接来)

现在一般都不会出现直接建图的情况了。。。

飞行员配对方案问题

一共有 \(n\) 个飞行员,其中有 \(m\) 个外籍飞行员和 \((n - m)\) 个英国飞行员,外籍飞行员从 \(1\)\(m\) 编号英国飞行员从 \(m + 1\)\(n\) 编号。 对于给定的外籍飞行员与英国飞行员的配合情况,试设计一个算法找出最佳飞行员配对方案,使皇家空军一次能派出最多的飞机。

\(Solution\):

直接将外籍飞行员和可以配合的英国飞行员连边, 每个外籍向源点,英籍向汇点连容量为1的边,以控制每人只能配对一次。

建图

	for(rt i = 1; i <= m; i ++) add(s,i,1);
	for(rt i = m + 1; i <= n; i ++) add(i,t,1);
	read(a), read(b);
	while(~a && ~b) {
		add(a,b,1);
		read(a),read(b);
	}

舞会

某学校要召开一个舞会。已知学校所有 \(n\) 名学生中,有些学生曾经互相跳过舞。当然跳过舞的学生一定是一个男生和一个女生。在这个舞会上,要求被邀请的学生中的任何一对男生和女生互相都不能跳过舞。求这个舞会最多能邀请多少个学生参加。

\(Solution\):

反向思考,题目要求最多邀请人数,其实就是求最小不邀请人数,那么就转化成了求所有相互跳过舞的人中的最小割

注意,因为题目没有给定男女生的具体情况,所以要先做染色处理,确定男女生,不同性别连向不同端点,跳过舞的一对就直接连边即可。

点击查看代码
#include <cstdio>
#include <queue>
#include <cstring>
#include <algorithm>
using namespace std;
#define int long long
#define rt register int
const int N = 1010, M = 1e4, inf = 1e9;
int n, m, S, T, tot2,tot = 1, head[N], cur[N], f[M], dep[N], H[N];
bool vis[N];
struct node {
    int to, nex;
} e[M],E[M];
inline void add2(int x,int y,int w) {
	E[++tot2] = (node) {y,H[x]}, H[x] = tot2;
	E[++tot2] = (node) {x,H[y]}, H[y] = tot2;
}
inline void add(int x, int y, int w) {
    e[++tot] = (node){ y, head[x]}, f[tot] = w, head[x] = tot;
    e[++tot] = (node){ x, head[y]}, head[y] = tot;
}
inline void read(int &x) {
    x = 0;
    char s = getchar();
    while (s < '0' || s > '9') s = getchar();
    while (s <= '9' && s >= '0') {
        x = x * 10 + s - '0';
        s = getchar();
    }
}
inline bool bfs() {
    memset(dep, -1, sizeof(dep));
    dep[S] = 0, cur[S] = head[S];
    queue<int> q;
    q.push(S);
    int now, ver;
    while (!q.empty()) {
        now = q.front();
        q.pop();
        for (rt i = head[now]; i; i = e[i].nex) {
            ver = e[i].to;
            if (dep[ver] == -1 && f[i]) {
                dep[ver] = dep[now] + 1, cur[ver] = head[ver];
                if (ver == T)
                    return 1;
                q.push(ver);
            }
        }
    }
    return 0;
}
inline int find(int x, int limit) {
    if (x == T)
        return limit;
    int flow = 0, tmp, ver;
    for (rt i = cur[x]; i && flow < limit; i = e[i].nex) {
        cur[x] = i, ver = e[i].to;
        if (dep[ver] == dep[x] + 1 && f[i]) {
            tmp = find(ver, min(limit - flow, f[i]));
            if (!tmp)
                dep[ver] = -1;
            f[i] -= tmp, f[i ^ 1] += tmp, flow += tmp;
        }
    }
    return flow;
}
inline int dinic() {
    int res = 0, flow;
    while (bfs())
        while (flow = find(S, inf)) res += flow;
    return res;
}
inline void work(int x,int v) {
	vis[x] = 1;
	if(!v) add(S,x,1);
	else add(x,T,1);
	int k;
	for(rt i = H[x]; i; i = E[i].nex) {
		k = E[i].to;
		if(!v) add(x,k,1);
		else add(k,x,1);
		if(!vis[k]) work(k,1 - v); 
	} 
}
signed main() {
    read(n),read(m);
	int a,b;
	S = n + 1, T = S + 1;
	vis[S] = vis[T] = 1;
	for(rt i = 1; i <= m; i ++) {
		read(a),read(b);
		add2(a,b,1);
	}
	for(rt i = 0; i < n; i ++) {
		if(!vis[i]) work(i,0);
	}
	printf("%d\n",n - dinic()); 
	return 0;
}

二、拆点(网络流一大热点)

就不用介绍了吧。

主要是用来控制每个点的使用次数的。

奶牛食品

FJ的奶牛们只吃各自喜欢的一些特定的食物和饮料,除此之外的其他食物和饮料一概不吃。某天FJ为奶牛们精心准备了一顿美妙的饭食,但在之前忘记检查奶牛们的菜单,这样显然是不能不能满足所有奶牛的要求。但是FJ又不愿意为此重新来做,所以他还是想让尽可能多的牛吃到他们喜欢的食品和饮料。FJ提供了F (编号为1、2、…、F)种食品并准备了D (编号为1、2、…、D)种饮料, 他的N头牛(编号为1、2、…、N)都已决定了是否愿意吃某种食物和喝某种饮料。FJ想给每一头牛一种食品和一种饮料,使得尽可能多的牛得到喜欢的食物和饮料。每一种食物和饮料只能由一头牛来用。例如如果食物2被一头牛吃掉了,没有别的牛能吃到食物2。

\(Solution\):

传统思路,将食物、牛、饮料按顺序建成网络流,但是因为控制了数量,所以将奶牛割成两个点,前一个点连源点,后一个点连汇点,两个点之间连容量为1的边,就能保证每头牛只消耗一份食物和饮料。

点击查看代码
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
#include<algorithm>
using namespace std;
#define rt register int
#define int long long
const int N = 405,M = 1e5 + 5;
int n,f,d,t,tot = 1,head[N],l[N],cur[N];
queue<int> q;
struct node {
	int to,nex,c;
}e[M];
inline void add(int x,int y,int w) {
	e[++tot] = (node) {y,head[x],w}, head[x] = tot;
	e[++tot] = (node) {x,head[y],0}, head[y] = tot;
}
inline void read(int &x) {
	x = 0; char s = getchar();
	while(s < '0' || s > '9') s = getchar();
	while(s <= '9' && s >= '0') { x = x * 10 + s - '0'; s = getchar();}
}
inline bool bfs() {
	memset(l,-1,sizeof(l));
	memcpy(cur,head,sizeof(head));
	while(!q.empty()) q.pop();
	q.push(1), l[1] = 0;
	int now;
	while(!q.empty()) {
		now = q.front(), q.pop();
		for(rt i = head[now]; i; i = e[i].nex) {
			if(e[i].c > 0 && l[e[i].to] == -1) {
				l[e[i].to] = l[now] + 1;
				q.push(e[i].to);
				if(e[i].to == t) return 1;
			}
		}
	}
	return 0;
}
inline int dfs(int x,int y) {
	if(x == t) return y;
	int res = 0,tmp;
	for(rt i = cur[x]; i; i = e[i].nex) {
		if(e[i].c > 0 && l[e[i].to] == l[x] + 1) {
			tmp = dfs(e[i].to,min(e[i].c,y - res));
			if(tmp) {
				res += tmp;
				e[i].c -= tmp;
				e[i ^ 1].c += tmp;
				if(res == y) break;
			}
		}
	}
	if(res != y) l[x] = -1;
	return res;
}
inline int dinic() {
	int ans = 0;
	while(bfs()) ans += dfs(1,1e9);
	return ans;
}
signed main() {
	read(n), read(f), read(d);
	int a,b,c;// f                 n1                 n2                                   d                         t 
	//源点1   食物左边        奶牛中间              奶牛2号                            饮料右边                     汇点 
	// 1     2 ~ f + 1    f + 2 ~ f + 1 + n    f + 2 + n  ~ f + 1 + n * 2      f + 2 + n * 2 ~ f + 1 + 2 * n + d    f + 2 + n * 2 + d
	for(rt i = 2; i <= f + 1; i ++) add(1,i,1);
	for(rt i = f + 2; i <= f + n + 1; i ++) {
		read(a),read(b);
		while(a--) {//食物 
			read(c);
			add(c + 1,i,1);
		}
		while(b--) {//饮料 
			read(c);
			add(i + n,c + 2 * n + f + 1,1);
		}
		add(i,i + n,1);
	}
	t = 2 + f + n * 2 + d;
	for(rt i = f + 2 * n + 2; i < t; i ++) add(i,t,1);
	printf("%d\n",dinic());
	return 0;
}

三、分点(自定义)

其实我也不知道这个东东叫啥。

特点:利用题目给定的连边条件,将不同情况的点连向不同端点(感性理解一下)。

王者之剑

这是在阿尔托利亚·潘德拉贡成为英灵前的事情,她正要去拔出石中剑成为亚瑟王,在这之前她要去收集一些宝石。
宝石排列在一个n*m的网格中,每个网格中有一块价值为v(i,j)的宝石,阿尔托利亚·潘德拉贡可以选择自己的起点。
开始时刻为0秒。以下操作,每秒按顺序执行

  1. 在第i秒开始的时候,阿尔托利亚·潘德拉贡在方格(x,y)上,她可以拿走(x,y)中的宝石。
  2. 在偶数秒,阿尔托利亚·潘德拉贡周围四格的宝石会消失
  3. 若阿尔托利亚·潘德拉贡第i秒开始时在方格(x,y)上,则在第i+1秒可以立即移动到(x+1,y),(x,y+1),(x-1,y)或(x,y-1)上,也可以停留在(x,y)上。

求阿尔托利亚·潘德拉贡最多可以获得多少价值的宝石

\(Solution\):

稍微观察就会发现,每个格点与他周围的四个格点的坐标和奇偶性不同,说明相同奇偶性的格点不会相互排斥,也就是可以同时选。所以将不同奇偶性连向不同端点就好了。

如果坐标和为偶数(因为偶数与汇点相连),从当前点向周围四个点连边,边权为正无穷,意为连接的两个点不能同时选择。

点击查看代码
#include<cstdio>
#include<cstring>
#include<queue>
#include<algorithm>
using namespace std;
#define rt register int
#define int long long
const int N = 1e4 + 10, M = 1e5 + 10,inf = 2e9;
int n,m,S,T,tot = 1,head[N],cur[N],dep[N],f[M],ans;
int a[4][2] = {{-1,0},{0,-1},{0,1},{1,0}};
struct node {
	int to,nex;
}e[M];
inline void add(int x,int y,int w) {
	e[++tot] = (node) {y,head[x]}, f[tot] = w, head[x] = tot;
	e[++tot] = (node) {x,head[y]}, head[y] = tot;
}
inline void read(int &x) {
	x = 0;
	int ff = 1; char s = getchar();
	while(s < '0' || s > '9') {s = getchar();}
	while(s <= '9' && s >= '0') { x = x * 10 + s - '0'; s = getchar();}
	x *= ff;
}
inline bool bfs() {
	memset(dep,-1,sizeof(dep));
	dep[S] = 0, cur[S] = head[S];
	queue<int> q;
	q.push(S);
	int now,ver;
	while(!q.empty()) {
		now = q.front();
		q.pop();
		for(rt i = head[now]; i; i = e[i].nex) {
			ver = e[i].to;
			if(dep[ver] == -1 && f[i]) {
				dep[ver] = dep[now] + 1, cur[ver] = head[ver];
				if(ver == T) return 1;
				q.push(ver);
			}
		}
	} 
	return 0;
}
inline int find(int x,int limit) {
	if(x == T) return limit;
	int ver,flow = 0,tmp;
	for(rt i = cur[x]; i && flow < limit; i = e[i].nex) {
		cur[x] = i, ver = e[i].to;
		if(dep[ver] == dep[x] + 1 && f[i]) {
			tmp = find(ver,min(limit - flow,f[i]));
			if(!tmp) dep[ver] = -1;
			f[i] -= tmp, f[i ^ 1] += tmp, flow += tmp;
		}
	}
	return flow;
}
inline int dinic() {
	int res = 0,flow;
	while(bfs()) while(flow = find(S,inf)) res += flow;
	return res;
}
inline int pos(int x,int y) {
	return (x - 1) * m + y;
}
signed main() {
	read(n), read(m);
	S = n * m + 1,T = n * m + 2;
	int x;
	for(int i = 1; i <= n; i ++) {
		for(int j = 1; j <= m; j ++) {
			read(x);
			ans += x;
			if((i + j) & 1) add(pos(i,j),T,x);
			else {
				add(S,pos(i,j),x);
				for(int k = 0; k < 4; k ++) {
					int tx = i + a[k][0], ty = j + a[k][1];
					if(tx >= 1 && tx <= n && ty >= 1 && ty <= m) {
						add(pos(i,j),pos(tx,ty),inf);

					}
				}
			}
		}
	}
	printf("%lld",ans - dinic());
	return 0;
}

wyc的奇妙女友

wyc 想出一道二分图(最大流),于是他找了女友们
单身多年的 wyc 终于找到了女朋友,而且一次性找到了 \(inf\) 个,wyc 很喜欢她们里的所有人,但是害怕她们之间互相争斗导致自己后宫着火,wyc 又十分喜欢挑战人类极限,想让一片街区有最多的女友可以让他肆意妄为,为了满足他的欲望,他找到了你来请你帮帮他。街区中的一个位置可以看做一个点。由于看上了 wyc,wyc 的女友们视野变得十分奇怪,她们只能看到目字型的位置,例如:如果她在点(5,5)上,她可以看到(6,8),(8,6)....(共八个)的点,如果 wyc 的一个女友看到了他的其他女友,那么她就会意识到wyc不仅有一个女友,wyc就会遭到迫害。
现在 wyc 的女友们都在逛 gai,已知街区大小为 \(n*m\)(棋盘),其中有 \(k\) 个点没有 wyc 的女友,请问这条街区上最多可以有多少wyc的女友。

n70Hmt.jpg

\(Solution\):

做题思维不能太固化,我拿到题就直接用坐标和建边,但是肯定是错的。观察图片,我们发现人所在的位置与她所能看见的位置的横、纵坐标的奇偶性各不相同,所以利用横或纵坐标来建边即可。

点击查看代码
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
const int N = 40010, M = 4e5 + 10, inf = 1e9;
int n,m,K,tot = 1,S,T,sum,head[N],f[M],cur[N],dep[N],a[8][2] = {{-3,-1},{-1,-3},{1,-3},{3,-1},{3,1},{1,3},{-1,3},{-3,1}};
bool flag[210][210];
struct node {
	int to,nex;
}e[M];
void add(int x,int y,int w) {
	e[++tot] = (node) {y,head[x]}, f[tot] = w, head[x] = tot;
	e[++tot] = (node) {x,head[y]}, head[y] = tot;
}
bool bfs() {
	memset(dep,-1,sizeof(dep));
	dep[S] = 0,cur[S] = head[S];
	queue<int> q;
	q.push(S);
	while(!q.empty()) {
		int now = q.front();
		q.pop();
		for(int i = head[now]; i; i = e[i].nex) {
			int ver = e[i].to;
			if(dep[ver] == -1 && f[i]) {
				dep[ver] = dep[now] + 1;
				cur[ver] = head[ver];
				if(ver == T) return 1;
				q.push(ver);
			}
		}
	}
	return 0;
}
int find(int x,int limit) {
	if(x == T) return limit;
	int flow = 0,tmp;
	for(int i = cur[x]; i && flow < limit; i = e[i].nex) {
		int ver = e[i].to;
		cur[x] = i;
		if(dep[ver] == dep[x] + 1 && f[i]) {
			tmp = find(ver,min(limit - flow,f[i]));
			if(!tmp) dep[ver] = -1;
			f[i] -= tmp, f[i ^ 1] += tmp, flow += tmp;
		}
	}
	return flow;
}
int dinic() {
	int res = 0, flow;
	while(bfs()) while(flow = find(S,inf)) res += flow;
	return res;
}
int pos(int x,int y) {
	return (x - 1) * n + y;
}

int main() {
	scanf("%d %d %d",&n,&m,&K);
	int u,v;
	S = n * m + 1, T = S + 1;
	for(int i = 1; i <= K ; i ++) {
		scanf("%d %d",&u,&v);
		flag[u][v] = 1;
	}
	for(int i = 1; i <= n; i ++) {
		for(int j = 1; j <= m; j ++) {
			if(flag[i][j]) continue;
			sum ++;
			if(j & 1) {
				add(S,pos(i,j),1);
				for(int k = 0; k < 8; k ++) {
					int tx = i + a[k][0], ty = j + a[k][1];
					if(!flag[tx][ty] && tx >= 1 && ty >= 1 && tx <= n && ty <= m) {
						add(pos(i,j),pos(tx,ty),inf);
					}
				}
			}
			else add(pos(i,j),T,1); 
		} 
	}
	printf("%d\n",sum - dinic());
	return 0;
} 

四、位置建边

其实就是将二维图转化成一维建图。

深海机器人问题

深海资源考察探险队的潜艇将到达深海的海底进行科学考察。潜艇内有多个深海机器人。潜艇到达深海海底后,深海机器人将离开潜艇向预定目标移动。深海机器人在移动中还必须沿途采集海底生物标本。沿途生物标本由最先遇到它的深海机器人完成采集。每条预定路径上的生物标本的价值是已知的,而且生物标本只能被采集一次。
本题限定深海机器人只能从其出发位置沿着向北或向东的方向移动,而且多个深海机器人可以在同一时间占据同一位置。
用一个 \(P\times Q\) 网格表示深海机器人的可移动位置。西南角的坐标为 \((0,0)\),东北角的坐标为 \((Q,P)\)
给定每个深海机器人的出发位置和目标位置,以及每条网格边上生物标本的价值。
计算深海机器人的最优移动方案, 使深海机器人到达目的地后,采集到的生物标本的总价值最高。

\(Solution\):

给二维图上的每个点重新编号,建立点和点之间的权值关系就好了。

点击查看代码
#include<cstring>
#include<algorithm>
#include<cstdio>
using namespace std;
#define rt register int
const int N = 5010, M = 100010, inf = 1e8;
int n, m, S, T;
int head[N], to[M], f[M], w[M], nex[M], tot, q[N], d[N], pre[N], incf[N];
bool vis[N];
void add(int a, int b, int c, int d) {
    to[tot] = b, f[tot] = c, w[tot] = d, nex[tot] = head[a], head[a] = tot ++;
    to[tot] = a, f[tot] = 0, w[tot] = -d, nex[tot] = head[b], head[b] = tot ++;
}
bool spfa() {
    int ss = 0, tt = 1, ver, now;
    memset(d, 0x3f, sizeof(d) );
    memset(incf, 0, sizeof(incf) );
    q[0] = S, d[S] = 0, incf[S] = inf;
    while (ss != tt) {
        now = q[ss ++ ];
        if(ss == N) ss = 0;
        vis[now] = 0;
        for(rt i = head[now]; ~i; i = nex[i]) {
            ver = to[i];
            if(f[i] && d[now] + w[i] < d[ver]) {
                d[ver] = d[now] + w[i], pre[ver] = i;
                incf[ver] = min(f[i], incf[now]);
                if(!vis[ver]) {
                    q[tt ++ ] = ver;
                    if (tt == N) tt = 0;
                    vis[ver] = 1;
                }
            }
        }
    }
    return incf[T] > 0;
}
int EK() {
    int cost = 0,now;
    while(spfa()) {
        now = incf[T], cost += now * d[T];
        for(rt i = T; i != S; i = to[pre[i] ^ 1]) {
            f[pre[i]] -= now;
            f[pre[i] ^ 1] += now;
        }
    }
    return cost;
}
inline void read(int &x) {
	x = 0;
	int ff = 1; char s = getchar();
	while(s < '0' || s > '9') {if(s == '-') ff = -1; s = getchar(); }
	while(s <= '9' && s >= '0') { x = x * 10 + s - '0'; s = getchar();}
	x *= ff;
}
inline int pos(int x,int y) {
	return m * (x - 1) + y; 
}
int main() {
	memset(head,-1,sizeof(head));
	int a,b,x,y,w; 
	read(a), read(b), read(n), read(m); 
	n ++, m ++; 
	S = n * m + 1, T = S + 1;
	for(rt i = 1; i <= n; i ++) {
		for(rt j = 1; j < m; j ++) {
			read(x);
			y = pos(i,j);
			add(y,y + 1,1,-x);
			add(y,y + 1,inf,0);
		}
	}
	for(rt j = 1; j <= m; j ++) {
		for(rt i = 1; i < n; i ++) {
			read(x);
			y = pos(i,j);
			add(y,y + m,1,-x);
			add(y,y + m,inf,0);
		}
	}
	for(rt i = 1; i <= a; i ++) {
		read(w), read(x), read(y);
		add(S,pos(x + 1,y + 1),w,0);
	}
	for(rt i = 1; i <= b; i ++) {
		read(w), read(x), read(y);
		add(pos(x + 1,y + 1),T,w,0);
	}
	printf("%d",-EK());
    return 0;
}
posted @ 2021-01-16 15:57  Spring-Araki  阅读(103)  评论(0编辑  收藏  举报