强烈推荐:北京大学ACM暑期课讲义-网络流,讲的很精华,慢慢看比较容易看懂。
最大流即有向图源点到汇点的最大流量。
各种算法的核心思想就是寻找增广路,核心思想就像是贪心,增广路的寻找是搜索(bfs,dfs),再加上各种技巧优化。
还有很出名的最大流最小割定理,st2点间最大流=st两点间的最小割。很容易接受的定理..
以poj1273为例:
O(m*n^2)的dinic算法:
#include <bits/stdc++.h>
using namespace std;
#define inf 11111111
const int N = 255;
struct
{
int v, w, ne;
} ed[444];
int cnt, co, id[N], flor[N], cur[N];
//co为点数,遍历所有点时用到(函数中唯一需要改的地方)
void add(int a, int b, int x)
{
//加边一次加4个,直接调用就好
ed[cnt].v = b, ed[cnt].w = x;
ed[cnt].ne = id[a], id[a] = cnt++;
ed[cnt].v = a, ed[cnt].w = 0;
ed[cnt].ne = id[b], id[b] = cnt++;
}
int bfs(int s, int t)
{
queue<int> q;
memset(flor, 0, sizeof(flor));
flor[s] = 1, q.push(s);
while (q.size())
{
int now = q.front();
q.pop();
if (now == t)
return 1;
for (int i = id[now]; ~i; i = ed[i].ne)
{
int to = ed[i].v;
if (flor[to] == 0 && ed[i].w > 0)
{
flor[to] = flor[now] + 1;
q.push(to);
if (to == t)
return 1;
}
}
}
return flor[t] != 0;
}
int dfs(int s, int t, int value)
{
int ret = value, a;
if (s == t || value == 0)
return value;
for (int &i = cur[s]; ~i; i = ed[i].ne)
{
int to = ed[i].v;
if (flor[to] == flor[s] + 1 && (a = dfs(to, t, min(ret, ed[i].w))))
{
ed[i].w -= a, ed[i ^ 1].w += a;
ret -= a;
if (!ret)
break;
}
}
if (ret == value)
flor[s] = 0;
return value - ret;
}
int dinic(int s, int t)
{
int ret = 0;
while (bfs(s, t))
{
for (int i = 1; i <= co; i++)//遍历所有点,这里是需要修改的!!
cur[i] = id[i];
ret += dfs(s, t, inf);
}
return ret;
}
int main()
{
int a, b, c, n, m;
while (~scanf("%d%d", &m, &n))
{
memset(id, -1, sizeof(id)), cnt = 0;
for (int i = 0; i < m; i++)
{
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);
}
co = n;
printf("%d\n", dinic(1, n));
}
return 0;
}
最大流几个注意点:
矩阵存边的话重边需要加上
初始化最大值时要大于最终的最大流(比如所有流量之和)
例题:
Poj 1459
多源点多汇点的最大流
有多源点多汇点的话,并不是类似最短路的让多点进queue,而是加一个超级源点和超级汇点,超级点和源/汇点之间连边为inf的有向边。
读入数据有点恶心,用dinic模板时注意dinic函数while()下循环的范围为最小下标到最大下标(包括后来加的点)
多源多汇的情况,加上超级源汇点就行。
Poj 3436
看似和图论没什么关系,但可以归于类似最大流的最优化问题。每个机器是一个节点,机器的输出符合另一个机器的输入则建立一条有向边。
首先肯定是建立超级源点汇点,源点连输入没有1的机器,汇点连输入只有1的机器。
这样相当于是求最小点割集(注意是割点不是割边)
建图策略是拆点,把每个点拆成出点和入点,入点唯一的出边指向该点的出点,权值为该点的q值,其他边权值全为最大值(比最大边大就行)(这也是求最小点割的方法)
网上有不拆点的建图方法,但好像是错的。(这题数据水..)
所以点带权值求最小点割的情况,拆一下点跑最大流就行。
求最大流的具体情况,就先把原图备份上,跑完最大流后对比这个图和原图,同一条边权值不同的就是最大流经过的点,流量就是2边的差值。
Poj 2112
相当于已知c个奶牛到k个挤奶机的距离,每个挤奶机最多有m个奶牛,求每个奶牛都到达一个挤奶机,走路最多的奶牛最少走多少步。
看似和图论没什么关系,其实也可以归于最大流的数学表达。
先对原图floyd求全局最短路。
发现对于每个ans,可以用最大流确定其是否可行,具体是:
对于每个ans,建立超级源点,汇点,分别连奶牛,挤奶机,边长分别是1,m,map里每个奶牛到挤奶机的距离小于等于ans的边都加入(边长为1,大一点也行,反正前面的奶牛最大流已经限定为1了),求最大流,如果最大流为c(所有奶牛都能到),则可行。
二分答案ans,找到可行下界。
二分答案+用网络流check也是一种图论解决问题的思路,试着想象一下最大流的数学本质兴许有用。(类似多条件限制下的最优化)
最好用dinic模板,几乎不会被卡时间。
就是有下界的最大流(上界本来就有)。
算法思想是通过构图把他转化为一般的最大流。(ppt上讲的很清楚)
具体流程是:
首先把每条边拆成必要边和不必要边(必要边是下界流量,不必要边是上界-下界)
加点x,y,将每个不必要边u->v拆成u->x和y->v,再加边原汇点->原源点,容量为无穷。此时新图为y是源点x是汇点的最大流图。
若最大流<所有流入x的边之和(即最大流小于流量下界和),则原图无解。
否则,记sum1为从s点流出的流量和。
在残余网络里,去掉st之间的所有边(就只有上一步加的无穷边和反向0边这2个边),再做一次s到t的最大流(xy点存在与否并不影响,因为这2点只有出或入,并不能传递流量),最大流为sum2。
有下界的最大流流量即sum1 + sum2。
要求最大流的具体信息的话,第一次跑完存图为G1,第二次跑完图为G2,原图下界为LC,
则原图i->j流量为:G1[i][j] – G2[i][j] + LC[i][j]
有重边的话:注意流量和下界都要累加,合并为1条边。
Poj 2396
非常神奇的题目了,看上去和图论没什么关系系列(但据说是比较明显的图论题目)
给一个200*20以内的矩阵,给出每行每列的和,及一些元素的大小关系(<=>),求任意一个满足所有要求的矩阵,或判断不可能。
用网络流模型,每行每列对应一个点,加超级源点汇点,各连接所有行和列,上下界均为该行(列)的和。再把每行对每列连接起来(就像完全二分图),下界为0上界为inf。对于每个点(i,j)的约束,更新i行->j列的流量上下界(相等则上下界均为该值)。
求出任意一个可行流,i->j的流量就是最终矩阵(i,j)元素的值。
是求可行流而不是最大流,所以只用做一次最大流(因为源点汇点的流量都满足上界=下界,第二次并不会更新最大流,sum2必定为0)
此时(一次dinic后i->j的流量)+(i->j的流量下界)即为原图该边的流量。
坑点是给的条件可能会出现悖论(直接输出不可能),且下界可能会为负数(此时处理为0)。
具体实现的话,在原最大流模板下加一个有下界的最大流函数,用二维矩阵存上下界,在函数内建图,跑2遍最大流记录sum1,sum2,并返回即可。
模板(上面和dinic一样,只多了一行数组定义):
#include <bits/stdc++.h>
using namespace std;
#define inf 11111111
const int N = 255;
struct
{
int v, w, ne;
} ed[444], ed1[444];
int cnt, co, id[N], flor[N], cur[N];
//co为点数,遍历所有点时用到(函数中唯一需要改的地方)
int mi[N][N], ma[N][N];
void add(int a, int b, int x)
{
//加边一次加4个,直接调用就好
ed[cnt].v = b, ed[cnt].w = x;
ed[cnt].ne = id[a], id[a] = cnt++;
ed[cnt].v = a, ed[cnt].w = 0;
ed[cnt].ne = id[b], id[b] = cnt++;
}
int bfs(int s, int t)
{
queue<int> q;
memset(flor, 0, sizeof(flor));
flor[s] = 1, q.push(s);
while (q.size())
{
int now = q.front();
q.pop();
if (now == t)
return 1;
for (int i = id[now]; ~i; i = ed[i].ne)
{
int to = ed[i].v;
if (flor[to] == 0 && ed[i].w > 0)
{
flor[to] = flor[now] + 1;
q.push(to);
if (to == t)
return 1;
}
}
}
return flor[t] != 0;
}
int dfs(int s, int t, int value)
{
int ret = value, a;
if (s == t || value == 0)
return value;
for (int &i = cur[s]; ~i; i = ed[i].ne)
{
int to = ed[i].v;
if (flor[to] == flor[s] + 1 && (a = dfs(to, t, min(ret, ed[i].w))))
{
ed[i].w -= a, ed[i ^ 1].w += a;
ret -= a;
if (!ret)
break;
}
}
if (ret == value)
flor[s] = 0;
return value - ret;
}
int dinic(int s, int t)
{
int ret = 0;
while (bfs(s, t))
{
for (int i = 1; i <= co; i++)//遍历所有点,这里是需要修改的!!
cur[i] = id[i];
ret += dfs(s, t, inf);
}
return ret;
}
int bound_flow(int s, int t)
{
//由mi ma数组求有上界的网络流 co为遍历所有点的参数,需要提前传入
memset(id, -1, sizeof(id));
int i, j, x = co + 1, y = co + 2;
int sum = 0, sum1 = 0, sum2 = 0, s1 = 0;
for (i = 0; i <= co; i++)
for (j = 0; j <= co; j++)
if (mi[i][j] != 0)//拆必要边
{
if (i == s)
s1 += mi[i][j];
add(i, x, mi[i][j]), add(y, j, mi[i][j]);
sum += mi[i][j];//必要边之和
}
for (i = 0; i <= co; i++)
for (j = 0; j <= co; j++)
if (ma[i][j] - mi[i][j] > 0)//加不必要边
{
if (i == s)
s1 += ma[i][j] - mi[i][j];
add(i, j, ma[i][j] - mi[i][j]);
}
add(t, s, inf);//加无穷边
memcpy(ed1, ed, sizeof(ed));
co = co + 2;//改点数
int temp = dinic(y, x);
if (temp != sum)
return -1;
i = s;
for (j = id[i]; ~j; j = ed[j].ne)
if (ed[j].w < ed1[j].w)
sum1 += ed1[j].w - ed[j].w;//计算sum1
for (j = id[i]; ~j; j = ed[j].ne)
if (ed[j].v == t)
ed[j].w = 0;
for (j = id[t]; ~j; j = ed[j].ne)
if (ed[j].v == s)
ed[j].w = 0;//删st边
sum2 = dinic(s, t);
return sum1 + sum2;
}
最小费用最大流:对于每条有向边i->j,都有一个单位费用和流量(流量每多1都要多一个单位费用),求s到t的最大流中,总费用最小的。
原理类似贪心,每次寻找费用最短路,并分配最大可能流量。
因为存在负边(取消流时产生的负边),不能用dij求最短路,而是用spfa。所以复杂度比较玄学,上界大概是最大流量*单次spfa。
以poj2135为例:
求无向图1->n->1的不重复最短路径。
叒是看似和网络流没关系的。相当于求1->n 的2条不相交边的路径的和的最小。
建立源点汇点,源点连到1一条流量为2(表示2条路)费用为0的有向边,汇点同理连到n。
对于每条费用为c的无向边i<->j,,建立i->j和j->i的流量为1费用c的边(带0边)。
这时候源点到汇点的最小费用就是所求最短路。
#include <bits/stdc++.h>
using namespace std;
const int inf = 1 << 28, MAXN = 1005, MAXM = 10005 << 2;
struct
{
int s, to, ne, ca, va;//费用va 容量ca
} ed[MAXM];
int id[MAXN], pre[MAXN], dis[MAXN], cnt;
bool vis[MAXN];
void addedge(int a, int b, int v, int c)
{
//加边和反向0边
ed[cnt].to = b, ed[cnt].s = a, ed[cnt].va = v;
ed[cnt].ca = c, ed[cnt].ne = id[a], id[a] = cnt++;
ed[cnt].to = a, ed[cnt].s = b, ed[cnt].va = -v;
ed[cnt].ca = 0, ed[cnt].ne = id[b], id[b] = cnt++;
}
bool spfa(int s, int t, int nnum)
{
//[0,nnum]中s到t是否存在最短路
memset(vis, 0, sizeof(vis));
memset(pre, -1, sizeof(pre));
for (int i = 0; i <= nnum; i++)
dis[i] = inf;
queue<int> que;
que.push(s);
dis[s] = 0, vis[s] = 1;
while (!que.empty())
{
int temp = que.front();
que.pop(), vis[temp] = 0;
for (int i = id[temp]; ~i; i = ed[i].ne)
if (ed[i].ca)
{
int ne = ed[i].to;
if (dis[temp] + ed[i].va < dis[ne])
{
dis[ne] = dis[temp] + ed[i].va;
pre[ne] = i;
if (!vis[ne])
vis[ne] = 1, que.push(ne);
}
}
}
return dis[t] != inf;
}
int getMincost(int s, int t, int nnum)
{
//[0,nnum]中s到t的最小费用最大流的最小费用
int ans_flow = 0, ans_cost = 0, temp, minc;
while (spfa(s, t, nnum))
{
temp = t;
minc = inf;
while (pre[temp] != -1)
{
minc = min(ed[pre[temp]].ca, minc);
temp = ed[pre[temp]].s;
}
temp = t;
while (pre[temp] != -1)
{
ed[pre[temp]].ca -= minc;
int ss = pre[temp] ^ 1;
ed[ss].ca += minc;
temp = ed[pre[temp]].s;
}
ans_cost += dis[t] * minc;
}
return ans_cost;
}
int main()
{
int i, a, b, v, s, t, n, m;
while (~scanf("%d%d", &n, &m))
{
memset(id, -1, sizeof(id)), cnt = 0;
memset(ed, 0, sizeof(ed));
for (i = 0; i < m; i++)
{
scanf("%d%d%d", &a, &b, &v);
addedge(a, b, v, 1);
addedge(b, a, v, 1);
}
s = n + 1, t = n + 2;
addedge(s, 1, 0, 2);
addedge(n, t, 0, 2);
printf("%d\n", getMincost(s, t, t));
}
return 0;
}
费用流的常见思路是构造网络流模型,其最大流是题目已经确定的或很快能算出来的不大的数(因为复杂度里有最大流),并给每条边一个费用。其费用就是要求的最优化答案。
构图比较难...