【知识点复习】网络流
基础知识
网上博客有很多,这里就没必要再赘述了。
贴一份 \(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秒。以下操作,每秒按顺序执行
- 在第i秒开始的时候,阿尔托利亚·潘德拉贡在方格(x,y)上,她可以拿走(x,y)中的宝石。
- 在偶数秒,阿尔托利亚·潘德拉贡周围四格的宝石会消失
- 若阿尔托利亚·潘德拉贡第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的女友。
\(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;
}