网络流的题目主要在于如何建模 总结一下建模技巧
主要来源于网络流24题,平时训练遇到的一些网络流题目,《算法竞赛进阶指南》
最大流
1.一般几百的数据范围可以联想到网络流
2.最大流=最小割
网络的割指割去一些边之后S与T不联通。边容量最小的割称为最小割
网络的最大流量=最小割
3.Dinic模板
#include <bits/stdc++.h>
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
#define rep(i, a, b) for(int i = (a); i < (b); i++)
using namespace std;
typedef long long ll; //此模板的最大流开long long
const int N = 210; //总点数
struct Edge { int from, to, flow; }; //flow表示这条边的容量 若容量为1,流过1之后那么容量减1变为0 如二分匹配输出方案,flow=0的边就是匹配边
vector<Edge> edge;
vector<int> g[N];
int d[N], cur[N]; //d数组表示bfs分层的深度
int n, m, s, t; //s t是源点和汇点 cur是一个优化表示当前点到哪一个节点了,减少了很多遍历
void add(int from, int to, int flow)
{
edge.push_back(Edge{from, to, flow});
g[from].push_back(edge.size() - 1);
edge.push_back(Edge{to, from, 0});
g[to].push_back(edge.size() - 1);
}
bool bfs() //bfs标记深度优化 dfs时只能从这一层搜到下一层
{
memset(d, 0, sizeof(d));
queue<int> q;
q.push(s);
d[s] = 1;
while(!q.empty())
{
int u = q.front(); q.pop();
for(auto x: g[u])
{
Edge e = edge[x];
if(!d[e.to] && e.flow) //记住考虑的是残量网络内的图 这条边还可以流才搜
{
d[e.to] = d[u] + 1;
q.push(e.to);
}
}
}
return d[t]; //如果h[t]为0说明不联通,此时已经是最大流,整个算法结束
}
ll dfs(int u, ll in) //in表示当前节点流入的流量
{
if(u == t) return in; //找到汇点就return
ll out = 0; //out表示这个节点流出的流量
for(int& i = cur[u]; i < g[u].size(); i++) //cur优化 注意这里引用
{
Edge& e = edge[g[u][i]]; //这里要引用,因为要修改
if(d[u] + 1 == d[e.to] && e.flow) //dfs时有的流才流
{
ll f = dfs(e.to, min((ll)e.flow, in));
e.flow -= f;
edge[g[u][i] ^ 1].flow += f; //反向边容量增加
out += f; in -= f;
if(in == 0) break;
}
}
return out;
}
void build() //建模,修改输入输出,如何加边,数据范围
{
scanf("%d%d%d%d", &n, &m, &s, &t);
while(m--)
{
int from, to, flow;
scanf("%d%d%d", &from, &to, &flow);
add(from, to, flow); //加边
}
}
int main()
{
build();
ll ans = 0;
while(bfs())
{
memset(cur, 0, sizeof(cur));
ans += dfs(s, 1e18);
}
printf("%lld\n", ans);
return 0;
}
费用流
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
typedef long long ll;
const int N = 5e3 + 10;
struct Edge { int from, to, flow, cost; };
vector<Edge> edge;
vector<int> g[N];
int p[N], d[N], vis[N], n, m, s, t; //f[i]表示点i流入的流量
ll f[N];
//p数组用来回溯
void add(int from, int to, int flow, int cost)
{
edge.push_back(Edge{from, to, flow, cost});
g[from].push_back(edge.size() - 1);
edge.push_back(Edge{to, from, 0, -cost});
g[to].push_back(edge.size() - 1);
}
bool spfa()
{
memset(vis, 0, sizeof(vis));
memset(d, 0x3f, sizeof d);
queue<int> q; q.push(s);
d[s] = 0; f[s] = 1e18;
vis[s] = 1;
while(!q.empty())
{
int u = q.front(); q.pop();
vis[u] = 0;
for(auto x: g[u])
{
Edge e = edge[x];
int v = e.to;
if(e.flow && d[v] > d[u] + e.cost)
{
d[v] = d[u] + e.cost;
p[v] = x;
f[v] = min(f[u], (ll)e.flow);
if(!vis[v]) { vis[v] = 1; q.push(v); }
}
}
}
return d[t] < 1e9; //最后联通
}
void build() //建模
{
scanf("%d%d%d%d", &n, &m, &s, &t);
while(m--)
{
int from, to, flow, cost;
scanf("%d%d%d%d", &from, &to, &flow, &cost);
add(from, to, flow, cost);
}
}
int main()
{
build();
ll maxflow = 0, mincost = 0;
while(spfa()) //bfs改成spfa 每次找一条花费最小的增广路增广路
{ //之后不需要dfs 因为只有一条增广路
maxflow += f[t]; //更新答案
mincost += 1LL * f[t] * d[t];
for(int u = t; u != s; u = edge[p[u]].from)
{
edge[p[u]].flow -= f[t]; //找到回溯后修改容量
edge[p[u] ^ 1].flow += f[t];
}
}
printf("%lld %lld\n", maxflow, mincost);
return 0;
}
题目
代码就不放了,都在我之前的博客。主要讲关键思路
POJ 1966 (点边转化 + 最小割)
题意:给定一张无向图,求最少去掉多少个点,可以使图不联通 N <= 50
首先不连通,那一定存在两个点不联通。N很小,我们可以直接枚举这两个点
当然这两个点不可以直接相连,这样怎么删都不行
那么问题就变成了,最少删除多少个点,使得S和T不连通
这个和最小割很像,最小割是最少删除容量多少的边使得S和T不连通
最小割是删边,这道题是删点
那么就用到网络流常用的点边转化的技巧了,这里就是把点化为边,也就是拆成两个点,出点和入点,中间连一条边
建模如下
1.每个点拆成入点和出点,中间入点向出点连一条容量为1的边(起点和终点不用连边)
2.原图中有边(u, v) 那就u出连v入,v出连u入,因为是无向图。容量为正无穷
然后以S出位源点,T为汇点,跑最大流。最大流=最小割,跑出来这个值就是最小要删的点数
一般要用到最小割的题目,建模的时候边的容量都很有特点,关键信息边容量设为1,其他设为无穷,使得一定会删那些关键的地方。
这道题只能删点,点容量设为1,不能删边,容量设为无穷。
任何经过点x的路径,都必须经过x入到x出这条边,这条边删去后就相当于删去这个点
POJ 3422(点边转化加次数限制)
题意:一个N * N的矩形,每个矩形都有一个权值
从左上角到右下角选k条路径,每次只能向右或向下,到一个点可以获得点上的权值,但只能获得一次。求能获得的最大值。N <= 50 K <= 10
这道题的特点是,有限制的条件下求最大权值,这种问题很适合用费用流解决
这个限制可以通过最大流来构造。这道题而言,限制是一个权值只能取一次
这道题的k条路可以视作k条流量为1的流
首先是常见的点边转化,一个点有权值,那就拆成两个点,入点向出点连一条边,费用为权值
怎么体现只能取一次呢
连两条边,一条边容量为1,费用为权值,一条边容量为K - 1费用为0
然后每个点向右和向下连一条容量为k费用为0的边
然后以(1,1)的入点为源点,(N,N)的出点为汇点,跑费用流就是答案
这道题的最大流的容量限制体现了题目对于权值选取的限制,这是关键
P2756 飞行员配对方案问题 (最大流实现二分图最大匹配+输出方案)
题意:英国人和外国人进行配对,求最大匹配数和方案
用最大流算法实现二分图最大匹配比匈牙利更快
匈牙利是点乘边的时间复杂度
输出方案的话就遍历每条边,容量为0的就是匹配边
for(auto e: edge)
if(!e.flow && e.from < e.to && e.from != s && e.to != t) //排除反向边
printf("%d %d\n", e.from, e.to);
P4016 负载平衡问题(建模)
题意:n个仓库,位置为环形。每个仓库有wi的货物,货物只能在相邻仓库运输。要使得每个苍梧货物相等,求最小运输量
有限制下的最优权值问题
限制是最后要一样,最优权值是最小搬运
其实搬运的过程可以联想到流
首先先算出每个点需要给别人还是别人给
如果是给别人,那就从源点向其连边,表示有这么多流量要给出去
如果是别人给,那就从这个点向汇点连边,表示从别人拿到这么多流量
因为只有相邻的能运输,所以相邻的点连边,因为是无向所以要连两条。容量为无穷费用为1
无穷是为了能最大流能流满,费用为1其实就是计算相邻之间有多少流,符合题意
P2762 太空飞行计划问题(最大权闭合子图)
题意:有实验和器材。一个实验可以赚ai的钱,器材需要花费bi的钱。一个实验需要一些固定的仪器。如何选择实验来最大化赚的钱
最大权闭合子图是个经典问题
在有向图中,一个点选了其后继必须选择,则是闭合的
每个权有点权,最大化点权和,就是最大权闭合子图
这道题而言,选了某个实验就必须选其器材,实验权值为正,器材权值为负
解决方法如下:
源点向实验连容量为权值的边,器材向汇点连容量为权值的点,中间连实验到器材的边,容量为正无穷。
跑最大流=最小割
答案就是正点权和-最小割
可以这么理解,我们先把全部的实验都选了
然后我们要减少一些实验使得权值最大
这时减去的值等于不选的实验少获得的利益,以及选了的实验器材的成本
刚好就对应两侧不是正无穷的边
这些边容量之和就是要减去的值
同时根据定义,删去这些边后是不联通的,意味着所有选了的实验,它的器材之后的边都被割了,符合要求。
输出方案的话,也就是看了选了哪些实验和仪器,利用d数组
如果d数组不为0,说明被选过
_for(i, 1, m) if(d[i]) printf("%d ", i); puts("");
_for(i, 1, n) if(d[i + m]) printf("%d ", i); puts("");
P2774 方格取数问题(二分图最大权独立集)
题意:n * n的矩阵,每个点有权值。选一些点使得权值和最大,选的点不能相邻
用坐标之和的奇偶性变成二分图,相邻的点连边
这样就转化为了二分图最大权独立集的问题
二分图最大权独立集指的是每个点有权值,两个点若有边,那么最多只能选一个节点。求选哪些节点使得权值和最大
方法是最小割,类似上一题的建图方法
Direction Setting(边化点)
题意:一个无向图,每个点有一个ai。现在需要给每条边设置方向。
di为入度 使得 max(0, di - ai)之和最小 n,m <= 200
n m比较小联想到网络流,最小联想到费用流
这道题用到了边化点,用点来代表边的属性
难点在于如何表示选择方向,这时这个方向会使某个点入度加1
这就是边的性质,为了表现这个边的性质,新加入一个点
这个点向两个端点都连一个容量为1费用为0的点,同时源点向其连容量为1费用为0的点
这样就表示了选方向增加入度这个操作
那么怎么表示max(0, di - ai)?
用两条边来表示,每一个点向汇点连一条容量为ai费用为0的边,再连一条容量无穷费用为1的边
这样因为是最小费用所以肯定会先跑满费用为0的边。
最后的最小费用就是答案
P3358 最长k可重区间集问题(网络流的串并联)
有n个开区间,要你选一些区间,满足一个点最多被覆盖k次,覆盖范围最长
同样是有限制的条件下求最优权值,最大流体现限制,费用体现最优权值
这个题很有趣,用了电路里面的串联并联的思想
对于一个区间,源点到l连一条容量为1费用为0的边,l到r连一条容量为1,费用为区间长度的边,r到汇点连一条容量为1费用为0的边
这样流过就表示选这个区间
关键在于怎么体现k的限制
也就是相交的区间不能大于k
可以用串联并联的思想,一个区间看作一个电阻
首先源点限制容量为k,可以用两个点中间连一条容量为k的边。
那么上面那种建模方式就是n个区间并联。
并联就是有限制的。如果不相交,就无限制,就是串联,表示可以一条流流过这两个区间
所以就枚举所有不相交的区间,ri到lj连一条容量为1费用为0的边
这样跑费用流就是答案
P2766 最长不下降子序列问题 (限制路径上点经过的次数)
给出一个序列
若每个点只能用一次,计算最多有多少个最长不下降子序列
若某些点可以用xi次,某些点可以用无限次呢?
思路是先求dp数组,表示以i为结尾最长的不下降子序列长度是多少
然后根据dp数组分层,这样就变成了一层一层的点
层与层之间,如果dp是可以转移的那就连一条容量为1的边
题目要求点的经过次数,所以进行拆点,中间连一条边,容量为可以使用的次数
这样,一条容量为1的流就是一条路径,跑最大流即可。
网络流建模很重要的一点是理解流在题目中代表什么
这道题一条流就代表一个最长不下降子序列