网路流
最大流问题:给出一个联通的不带边权的有向图,给出源点s和汇点t,将边当作一条有容量限制(单位时间内只能流固定容量的水)的带方向(水流只能从向边的方向流动)的水管。求解单位时间内从s到t最多能流动多少水量。
Ford-Fulkerson算法:
- 思路: Ford-Fulkerson算法是一种贪心算法,该算法的核心在于当流量f经过边e的时候,同时建立e的一条容量为f的反向边,然后利用满足f(e) < c(e)的e或者e对应的反向边rev(e),寻找一条s到t的路径,找到这样一条路径并让路径中的边的容量自减f,同时对应反向边的容量加f,这条路径称为增广路这样处理过后的网络成为残余网络。不断沿着残余网络寻找增广路增加流量,直到不能增广,最后的流量便是最大流。
黑色为原图的边,红色为反向边,这里并非一个全图,只是残余网络的一部分的一部分。
在残余网络上新增一条边,可以看出反向边的作用就是可以将流回退回去,然后寻找更多的增广路。
算法实现:
const int INF = 0x3f3f3f3f;
const int MAX_V = 300;
struct edge {
int to, cap, rev;
};
vector<edge> G[MAX_V];
bool used[MAX_V];
void add_edge(int from, int to, int cap) {
G[from].push_back((edge){to, cap, G[to].size()});
G[to].push_back((edge){from, 0, G[from].size() - 1});
}
int dfs(int v, int t, int f) {
if(v == t) return f;
used[v] = true;
for(int i = 0; i < G[v].size(); i++) {
edge &e = G[v][i];
if(!used[e.to] && e.cap > 0) {
int d = dfs(e.to, t, min(f, e.cap));
if(d > 0) {
e.cap -= d;
G[e.to][e.rev].cap += d;
return d;
}
}
}
return 0;
}
int max_flow(int s, int t) {
int flow = 0;
for(;;) {
memset(used, 0, sizeof(used));
int f = dfs(s, t, INF);
if(f > 0) flow += f;
else {
return flow;
}
}
}
二分图
- 概念: 二分图其实就是特殊的网络流,可以用网络流的算法来解决,手动增加一个s,和一个t,然后建图,便可以转化为网络流问题,因为二分图的特殊性,因此还有更高效的匈牙利算法。
- 特性:二分图有一个很重要的特性,在二分图中|最小点覆盖| = |最大匹配|,这个等式的证明思考了很久,也参考了网上很多博客,但总觉得很多证明写的太过复杂,这里看到了一个很简单的证明。
证明:摘自http://www.cnblogs.com/rainydays/archive/2011/03/03/1969543.html
首先,最小点集覆盖一定>=最大匹配,因为假设最大匹配为n,那么我们就得到了n条互不相邻的边,光覆盖这些边就要用到n个点。现在我们来思考为什么最小点击覆盖一定<=最大匹配。任何一种n个点的最小点击覆盖,一定可以转化成一个n的最大匹配。因为最小点集覆盖中的每个点都能找到至少一条只有一个端点在点集中的边(如果找不到则说明该点所有的边的另外一个端点都被覆盖,所以该点则没必要被覆盖,和它在最小点集覆盖中相矛盾),只要每个端点都选择一个这样的边,就必然能转化为一个匹配数与点集覆盖的点数相等的匹配方案。所以最大匹配至少为最小点集覆盖数,即最小点击覆盖一定<=最大匹配。综上,二者相等。
- 匈牙利算法:
const int INF = 0x3f3f3f3f;
const int MAX_V = 1000;
int V; // 顶点数
vector<int> G[MAX_V]; // 图的邻接表表示
int match[MAX_V]; // 所匹配的顶点
bool used[MAX_V]; // DFS中用到的访问标记
// 向图中增加一条连接u和v的边
void add_edge(int u, int v) {
G[u].push_back(v);
G[v].push_back(u);
}
// 通过DFS寻找增广路
bool dfs(int v) {
used[v] = true;
for(int i = 0; i < G[v].size(); i++) {
int u = G[v][i], w = match[u];
if(w < 0 || !used[w] && dfs(w)) {
match[v] = u;
match[i] = v;
return true;
}
}
return false;
}
int bipartite_matching() {
int res = 0;
memset(match, -1, sizeof(match));
for(int v = 0; v < V; v++) {
if(match[v] < 0) {
memset(used, 0, sizeof(used));
if(dfs(v)) {
res++;
}
}
}
return res;
}
最小费用流
- 基础:最小费用流个人认为就是网络流的扩展,仍然是采用增广路的思想,因为加入了边权,所以按着最短路增广就可以了,因为残余网络会出现负边所以应该采用Bellman-Ford算法求最短路。引入势的概念后可以用Dijkstra求最短路。势的想法感觉真是极妙的,从代码的实现来看也是极为精巧的。
- Bellman-Ford求最短路的方法:
const int INF = 0x3f3f3f3f;
const int MAX_V = 1000;
struct edge { int to, cap, cost, rev; };
int V; //顶点数
vector<edge> G[MAX_V]; //图的邻接表示
int dist[MAX_V]; //最短距离
int prevv[MAX_V], preve[MAX_V]; //最短路中的前驱节点和对应的边
void add_edge(int from, int to, int cap, int cost) {
G[from].push_back((edge){to, cap, cost, G[to].size()});
G[to].push_back((edge){from, 0, -cost, G[from].size() - 1});
}
int min_cost_flow(int s, int t, int f) {
int res = 0;
while(f > 0) {
//Bellman-Ford
fill(dist, dist + V, INF);
dist[s] = 0;
bool update = true;
while(update) {
update = false;
for(int v = 0; v < V; v++) {
if(dist[v] == INF) continue;
for(int i = 0; i < G[v].size(); i++) {
edge &e = G[v][i];
if(e.cap > 0 && dist[e.to] > dist[v] + e.cost) {
dist[e.to] = dist[v] + e.cost;
prevv[e.to] = v;
preve[e.to] = i;
update = true;
}
}
}
}
if(dist[t] == INF) {
return -1;
}
int d = f;
for(int v = t; v != s; v = prevv[v]) {
d = min(d, G[prevv[v]][preve[v]].cap);
}
f -= d;
res += d * dist[t];
for(int v = t; v != s; v = prevv[v]) {
edge &e = G[prevv[v]][preve[v]];
e.cap -= d;
G[e.to][e.rev].cap += d;
}
}
return res;
}
- Dijkstra 求最短路的方法:
const int MAX_V = 1000;
const int INF = 0x3f3f3f3f;
typedef pair<int, int> P;
struct edge { int to, cap, cost, rev; };
int V; //顶点数
vector<edge> G[MAX_V]; //图的邻接表示
int h[MAX_V]; //顶点的势
int dist[MAX_V]; //最短距离
int prevv[MAX_V], preve[MAX_V]; //最短路中的前驱节点和对应的边
void add_edge(int from, int to, int cap, int cost) {
G[from].push_back((edge){to, cap, cost, G[to].size()});
G[to].push_back((edge){from, 0, -cost, G[from].size() - 1});
}
// 求解从s到t流量为f的最小费用流
// 如果没有流量为f的流,则返回-1
int min_cost_flow(int s, int t, int f) {
int res = 0;
fill(h, h + V, 0); //初始化h
while(f > 0) {
// 使用Dijkstra算法更新h
priority_queue<P, vector<P>, greater<P> > que;
fill(dist, dist + V, INF);
dist[s] = 0;
que.push(P(0, s));
while(!que.empty()) {
P p = que.top(); que.pop();
int v = p.second;
if(dist[v] < p.first) continue;
for(int i = 0; i < G[v].size(); i++) {
edge &e = G[v][i];
if(e.cap > 0 && dist[e.to] > dist[v] + e.cost + h[v] - h[e.to]) {
dist[e.to] = dist[v] + e.cost + h[v] - h[e.to];
prevv[e.to] = v;
preve[e.to] = i;
que.push(P(dist[e.to], e.to));
}
}
}
if(dist[t] == INF) {
// 不能再增广
return -1;
}
for(int v = 0; v < V; v++) h[v] += dist[v]; //因为加入了势,此时dist[v]是最短距离加上 h[s] - h[v],
//因为h[s]恒为0, 所以这样就能保证 h[v] 总为s到v的最短距离。
// 沿s到t的最短路尽量增广
int d = f;
for(int v = t; v != s; v = prevv[v]) {
d = min(d, G[prevv[v]][preve[v]].cap);
}
f -= d;
res += d * h[t];
for(int v = t; v != s; v = prevv[v]) {
edge &e = G[prevv[v]][preve[v]];
e.cap -= d;
G[v][e.rev].cap += d;
}
}
return res;
}