引子
曾经很多次看过最大流的模板,基础概念什么的也看了很多遍。也曾经用过强者同学的板子,然而却一直不会网络流。虽然曾经尝试过写,然而即使最简单的一种算法也没有写成功过,然后对着强者大神的代码一点一点的照猫画虎,A了一题。然而这并没有什么用,实际上我还是不会呀。过一阵子就写不出来了,所以那个时候的A应该就是对照着换了换变量吧。持续性萎靡不振,间歇性踌躇满志的我觉得是时候不看资料尤其是不看他人代码完全的自己写一道模板题了。
题目
hihocoder 1369 http://hihocoder.com/problemset/problem/1369
hdu 3549 http://acm.hdu.edu.cn/showproblem.php?pid=3549
两道数据范围极小的模板练习题
最大流问题
不想过多的重复网上已有的基础概念。只想叙述一下我的理解。
把边都想象成仅可以单向流动的水管(虽然这有点奇怪,或者换成单向车道没那么奇怪)。问题就是求从源点到汇点最多流多少水。
流量网络就是每条边流多少水构成的图。
显然通过每条边的限制不能超过该边的容量限制。
残余网络就是每条边还可以流多少水。
最为重要的是反向边的理解了。
当我选择对于在边 增加其一个流量 ,那么u到v的新的残余容量就是这个操作之前残余流量减去 ,即
同时增加对v到u的残余流量(如果一开始v到u没有边就是容量为0)增加f.
这个容量的增加如何理解呢?
如果选择给v到u一个 的流量,相当于原本变化前的图中u到v的 这个流量少流
只要 ,我们都可以在原图中通过少流的方式构造出等价的实现,所以我们当我们给 增加一个 的流量的同时,同时给 增加一个 的容量, 避免我们先陷入后面发现 流太多了,需要应该少流一点点,却不知道怎么写判断,怎么回溯,怎么后悔的尴尬境地。 有了反向边,一个无须判断过多讨论,多流了的情况自然的蕴含在找可行流的时候通过给边 一个流而把多流的流量流回去。
Ford-Fulkerson算法
下面说说Ford-Fulkerson算法
寻找问题的解的时候是基于残留网络。
初始的残留网络就是容量网络。
Ford-Fulkerson算法是不断的在当前的残留网络中找一条从源点到汇点的路径,这条路径上的每个容量值需要大于0。如果有这样的一条路径 ,那么记 ,显然可以给路径上的每条边一个flow的流量。来增加我们的可行流的总量。因此,这样的路径称之为增广路。
由于我们是基于残留网络求解,同时要增加反向边。我们直接给出对残留网络的修改:
路径上的每条边 的残余流量减少 ,同时 的残余流量加上
Fold-Fulkerson算法就是不断的在残余网络中找增广路,然后更新残留网络,直到找不到增广路为止。当找不到时,我们就已经得到最大流了。
至于时间复杂度,如果边的容量是无理数的话,Ford-Fulkerson算法甚至可能一直运行下去无法结束。但是如果容量值是整数,那么由于找到一条增广路的时间最多是 ,每次找到的增广路的流量值至少是1,那么时间复杂度应该是 .其中 是最后最大流问题的解。具有保证终止和且运行时间与最大流量值无关的的的Ford-Fulkerson算法的变体是Edmonds–Karp算法。
以上这段参考了维基百科。如果有理解错误的地方希望大家指出。
Edmonds–Karp算法
如上所述,Edmonds–Karp算法可以说是一种Ford-Fulkerson算法的具体化,或者说是一种变体。Ford-Fulkerson算法并没有具体叙述如何找增广路,所以可以采用dfs,bfs。而Edmonds–Karp算法大体与Ford-Fulkerson算法相同,只是要求找出的最短路是当前残留网络中边数最小的路径。即找一条最短路(边数的最短)。其寻找最短路的方式是采用BFS。
其时间复杂度是:
#include <cstdlib>
#include <cstring>
#include <algorithm>
#include <iostream>
#include <vector>
#include <queue>
#include <cmath>
#include <map>
using namespace std;
int n,m;
int pointCount,edgeCount;
const int kMaxPointCount = 500;
const int kMaxInt = 0x7fffffff;
typedef map<int,int> toVCapacity; // 使用map,隐含:不能有重边,因此对于重边直接容量合并
toVCapacity edgeFrom[kMaxPointCount+2]; // 下标从0开始
// hihocoder 1369
// hdu 3549
// reverseWay应当保存的是反过来的增广路径的节点,不包括汇点end
// 大概这种形式 end-v1-v2-v3-v4-v5-...-start (边是反过来的,(v1,end)是一条边) 保存的节点不包含end
bool getAWayByBfs(int pointCount,int edgeCount,int start,int end,toVCapacity remainCapacityFrom[],int pre[],vector<int>&reverseWay,int flows[])
{
const int notVisited = -1;/*-1 一个特殊数字,表示还没有访问,由于要使用memset,不可替换成其它数字*/
const int noPre = -2; /*一个标记没有前置节点的特殊数字而已*/
reverseWay.clear();
memset(pre,notVisited,sizeof(int)*pointCount);
queue<int>q;
flows[start] = kMaxInt; pre[start] = noPre; q.push(start);
int u,v,remainCapacity;
toVCapacity::iterator edgeIt,tmpEndEdgeIt;
bool reachEndFlag = false;
while ((!reachEndFlag) && (!q.empty())) {
u = q.front(); q.pop();
edgeIt = remainCapacityFrom[u].begin();
tmpEndEdgeIt = remainCapacityFrom[u].end();
while (edgeIt != tmpEndEdgeIt) {
v = edgeIt->first;
remainCapacity = edgeIt->second;
if (notVisited == pre[v] && remainCapacity > 0) {
flows[v] = min(flows[u],remainCapacity);
pre[v] = u; q.push(v);
if (v == end) {
reachEndFlag = true;
break;
}
}
++edgeIt;
}
}
if (pre[end] == notVisited)
return false;
v = end;
while (v != start) {
//cout<<v<<" ";
v = pre[v];
reverseWay.push_back(v);
}
//cout<<"end = "<<end<<"flow = "<<flows[end]<<" "<<"\n";
return true;
}
void addCapacity(toVCapacity remainCapacityFrom[],int u,int v,int addition)
{
toVCapacity::iterator it;
it = remainCapacityFrom[u].find(v);
if (it == remainCapacityFrom[u].end()) {
if (addition > 0) {
remainCapacityFrom[u].insert(pair<int,int>(v,addition));
} else if (addition < 0) {
//cout<<"capacity error"<<"\n";
//exit(0);
} // == 0, ignore
} else {
it->second += addition;
if (it->second == 0) {
remainCapacityFrom[u].erase(it);
} else if (it->second < 0) {
//cout<<"capacity error"<<"\n";
//exit(0);
} // else ... ignore
}
}
int getMaxFlow(int pointCount,int edgeCount,int start,int end,toVCapacity remainCapacityFrom[])
{
int maxFlow = 0;
int flows[pointCount+2],flow;
int pre[pointCount+2];
vector<int>reverseWay;
vector<int>::iterator way_it;
int u,v;
while (1) {
// find a path
// if can,then we modify,or we break out
if (!getAWayByBfs(pointCount,edgeCount,start,end,remainCapacityFrom,pre,reverseWay,flows))
break;
// reverseWay保存的是反过来的增广路径的节点,不包括汇点end
flow = flows[end];
v = end; way_it = reverseWay.begin();
while (way_it != reverseWay.end()) {
u = *way_it;
addCapacity(remainCapacityFrom,u,v,-flow);
addCapacity(remainCapacityFrom,v,u,flow);
v = u; ++way_it;
}
maxFlow += flow;
}
return maxFlow;
}
int main1()
{
char ln = '\n';
cin>>n>>m; // 点数 边数
pointCount = n; edgeCount = 2*m;
int u,v,c;
// 输入数据的点的下标是从1开始,源点为1,汇点为n
for (int i = 0; i< pointCount; ++i)
edgeFrom[i].clear();
for (int i = 0;i < m; ++i) {
cin>>u>>v>>c;
--u,--v;
//edgeFrom[u].insert(pair<int,int>(v,c));
//上面这样写当有重边的时候会导致错误,因为假设
// 1 2 3
// 1 2 6
// 加进去的边只有1 2 3 第二次 1 2 6的时候的insert是无效的,不执行的
// 如果改成下标方式的插入元素,则第一次加进去的1 2 3将会被覆盖掉所以直接调用上面写
// 因为这个wa了好久
// 因此加边还需要判断原本是否有边的存在,懒得写判断,就直接使用原本修改的时候用的addCapacity函数了
addCapacity(edgeFrom,u,v,c);
}
int ans = getMaxFlow(pointCount,edgeCount,0,pointCount-1,edgeFrom);
cout<<ans<<ln;
return 0;
}
int main()
{
// for hdu
/*int T; std::ios::sync_with_stdio(false); cin.tie(0); cin>>T; for (int i = 1; i <= T; ++i) { cout<<"Case "<<i<<": "; main1(); }*/
//for hihocoder
main1();
return 0;
}