引出:
求出一个二分图的最大匹配。
二分图匹配怎么求?
可以用匈牙利算法:遍历每个点,查找增广路,若找到增广路,则修改匹配集合和匹配数;否则终止算法,返回最大匹配数。这样时间复杂度是 (mathcal{O}(nm)) 的。代码实现:
int con[N * N];
bool vis[N * N];
bool Hungary(int u)
{
for (int i = head[u]; i; i = e[i].nxt)
{
int v = e[i].to;
if (vis[v]) continue;
vis[v] = 1;
if (!con[v] || Hungary(con[v]))
{
con[v] = u;
return 1;
}
}
return 0;
}
二分图有很多优秀的性质:
最大匹配数=最小点覆盖数,最大独立集与最大匹配数互补,DAG最小路径覆盖=DAG原图节点数-对应二分图最大匹配。
也就是说用简简单单小 DFS 就能快速地求出这么多问题!但是这不够优秀,我们用网络最大流解决二分图一类的问题可以达到 (mathcal{O}(msqrt{n}))。所以在这里介绍一下网络流的解法之类。
正文:
当然网络流不仅局限于二分图的问题,它还比匈牙利更加灵活。
在开始之前,先了解关于网络流的一些基本概念吧。
基本概念:
网络流图:
- 有且仅有一个入度为 (0) 的源点 (S)。
- 有且仅有一个出度为 (0) 的汇点 (T)。
- 每条有向边都有流量上限。
可行流:
- 每个点((S,T) 除外)满足流入等于流出,每条边满足流入等于流出且不超过流量上限。
1.从源点出发的流全经过网络到汇点。
饱和弧:
饱和弧是指在一个可行流中,一条通过流量等于流量上限的边。若是小于,这条边就叫非饱和弧。
零弧:
零弧是指在一个可行流中,一条通过流量为零的边。
前向弧和后向弧:
将网络流图看作是无向图的情况下,前向弧是指一条方向是和源点流向汇点方向一致的边,后向弧则反之。
后向边:
和有向图的有向边方向相反的边。
增广路径:
一段路径满足:
- 所有前向弧都是非饱和弧。
- 所有后向弧都是非零弧。
残量网络:
对于某一条边,还能再有多少流量经过。
最大流:
每次用 BFS 找一条最短的增广路径,然后沿着这条路径修改流量值(实际修改的是残量网络的边权)。当没有增广路时,算法停止,此时的流就是最大流。
然后这个过程可以用 Dinic 优化:
- 分层图优化:Dinic 算法首先对图进行一次 BFS,然后在其生成的分层图中多次 DFS。这样就切断了原有图中的许多不必要连接。这样我们可以进行多路增广,减少了调用增广函数的次数。
- 当前弧优化:每次 DFS 完,找到路径中容量最小的一条。在这条边之前的路径的容量大于等于这条边的容量。那么从这条边之前的点,可能引发出别的增广路径。
- -1 优化:在同一次 DFS 中,如果从一个点引发不出任何增广路径,就让它在层次图里抹去。
代码:
int n, m, s, t, tot;
struct edge
{
int y, w, op, next;
} e[M];
int head[N];
void Add(int x, int y, int w)
{
e[++tot] = (edge){y, w, tot + 1, head[x]};
head[x] = tot;
e[++tot] = (edge){x, 0, tot - 1, head[y]};
head[y] = tot;
}
int dis[N];
queue <int> que;
bool bfs()
{
while(!que.empty())que.pop();
memset(dis, 60, sizeof(dis));
dis[s] = 0;
que.push(s);
while(!que.empty())
{
int x = que.front();que.pop();
for (int i = head[x]; i; i = e[i].next)
{
int y = e[i].y;
if(dis[y] >= dis[x] + 1 && e[i].w)
{
dis[y] = dis[x] + 1;
if(y == t) return 1;
que.push(y);
}
}
}
return 0;
}
ll dfs(int x, ll f)
{
if(x == t) return f;
ll sum = 0;
for (int i = head[x]; i; i = e[i].next)
{
int y = e[i].y;
if(dis[y] == dis[x] + 1 && e[i].w)
{
ll f2 = dfs(y, min(e[i].w * 1ll, f - sum));
if (!f2) dis[y] = -1; //-1优化
e[i].w -= f2;
e[e[i].op].w += f2;
sum += f2;
if (sum == f) break;
}
}
return sum;
}
ll dinic()
{
ll sum = 0;
while(bfs()){sum += dfs(s, 1010580540);}
return sum;
}
最小割:
首先,割是一组边,如果去掉割边,(S,T) 就不会联通,那么最小割就是这组边的最小价值。
其次,最小割等于最大流,不会证明。
费用流:
BFS 换成 SPFA。
int n, m, s, t, tot;
struct edge
{
int y, w, z, op, next;
} e[M];
int head[N];
void Add(int x, int y, int w, int z)
{
e[++tot] = (edge){y, w, z, tot + 1, head[x]};
head[x] = tot;
e[++tot] = (edge){x, 0, -z, tot - 1, head[y]};
head[y] = tot;
}
int dis[N], incf[N], pre[N];
bool vis[N];
queue <int> que;
bool spfa()
{
while(!que.empty())que.pop();
memset(dis, 60, sizeof(dis));
memset(vis, 0, sizeof(vis));
dis[s] = 0;
que.push(s);
vis[s] = 1;
incf[s] = 1 << 30;
while(!que.empty())
{
int x = que.front();que.pop();vis[x] = 0;
for (int i = head[x]; i; i = e[i].next)
{
int y = e[i].y;
if(dis[y] > dis[x] + e[i].z && e[i].w)
{
dis[y] = dis[x] + e[i].z;
incf[y] = min(incf[x], e[i].w);
pre[y] = i;
if(!vis[y]) vis[y] = 1, que.push(y);
}
}
}
if (dis[t] == 1010580540)
return 0;
else
return 1;
}
int maxflow, mincost;
void MCMF()
{
while(spfa())
{
int x = t;
maxflow += incf[t];
mincost += dis[t] * incf[t];
int i;
while(x != s)
{
i = pre[x];
e[i].w -= incf[t];
e[e[i].op].w += incf[t];
x = e[e[i].op].y;
}
}
}