网络流建模技巧

网络流的题目主要在于如何建模 总结一下建模技巧

主要来源于网络流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的流就是一条路径,跑最大流即可。

网络流建模很重要的一点是理解流在题目中代表什么

这道题一条流就代表一个最长不下降子序列

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值