使用一个例子学习最大流和匹配问题,假如某地有n个出租车司机和m个正在打车的乘客,他们应该如何匹配;
#include <iostream> // 用于输入输出
#include <vector> // 用于动态数组
#include <queue> // 用于广度优先搜索
#include <limits> // 用于获取整型最大值
#include <algorithm> // 用于使用std::fill和std::min函数// 图类,用于表示网络流图
class Graph {
private:int V; // 顶点数std::vector<std::vector<int>> capacity; // 容量矩阵std::vector<std::vector<int>> adj; // 邻接表public:// 构造函数,初始化图Graph(int V) : V(V) {capacity.resize(V, std::vector<int>(V, 0)); // 初始化容量矩阵adj.resize(V); // 初始化邻接表}// 添加边到图中void addEdge(int u, int v, int cap) {adj[u].push_back(v); // 添加正向边adj[v].push_back(u); // 添加反向边(用于残余网络)capacity[u][v] = cap; // 设置正向边的容量}// 广度优先搜索,寻找增广路径int bfs(int s, int t, std::vector<int>& parent) {std::fill(parent.begin(), parent.end(), -1); // 初始化父节点数组parent[s] = -2; // 标记源点已访问std::queue<std::pair<int, int>> q; // 用于BFS的队列,存储节点和到达该节点的流量q.push({s, std::numeric_limits<int>::max()}); // 将源点入队while (!q.empty()) {int cur = q.front().first; // 当前节点int flow = q.front().second; // 到达当前节点的流量q.pop();for (int next : adj[cur]) { // 遍历当前节点的所有邻接节点if (parent[next] == -1 && capacity[cur][next]) { // 如果邻接节点未访问且有剩余容量parent[next] = cur; // 记录父节点int new_flow = std::min(flow, capacity[cur][next]); // 计算新的流量if (next == t) // 如果到达汇点return new_flow; // 返回找到的增广路径的流量q.push({next, new_flow}); // 将邻接节点入队}}}return 0; // 没有找到增广路径,返回0}// 计算最大流int maxFlow(int s, int t) {int flow = 0; // 总流量std::vector<int> parent(V); // 用于存储增广路径int new_flow;// 当还能找到增广路径时while (new_flow = bfs(s, t, parent)) {flow += new_flow; // 增加总流量int cur = t;while (cur != s) { // 更新残余网络int prev = parent[cur];capacity[prev][cur] -= new_flow; // 正向边减少容量capacity[cur][prev] += new_flow; // 反向边增加容量cur = prev;}}return flow; // 返回最大流}
};// 出租车匹配器类
class TaxiMatcher {
private:int n, m; // n个司机,m个乘客Graph graph; // 用于表示匹配问题的图public:// 构造函数,初始化匹配器TaxiMatcher(int n, int m) : n(n), m(m), graph(n + m + 2) {int source = 0; // 源点int sink = n + m + 1; // 汇点// 添加从源点到所有司机的边,容量为1for (int i = 1; i <= n; i++) {graph.addEdge(source, i, 1);}// 添加从所有乘客到汇点的边,容量为1for (int i = 1; i <= m; i++) {graph.addEdge(n + i, sink, 1);}}// 添加可能的匹配void addPossibleMatch(int driver, int passenger) {graph.addEdge(driver, n + passenger, 1); // 添加从司机到乘客的边,容量为1}// 寻找最大匹配数int findMaxMatches() {return graph.maxFlow(0, n + m + 1); // 计算从源点到汇点的最大流}
};int main() {int n, m;std::cout << "请输入出租车司机数量: ";std::cin >> n;std::cout << "请输入旅客数量: ";std::cin >> m;TaxiMatcher matcher(n, m); // 创建匹配器std::cout << "请输入可能的匹配(司机编号 旅客编号),输入 -1 -1 结束:\n";while (true) {int driver, passenger;std::cin >> driver >> passenger;if (driver == -1 && passenger == -1) break;matcher.addPossibleMatch(driver, passenger); // 添加可能的匹配}int maxMatches = matcher.findMaxMatches(); // 计算最大匹配数std::cout << "最大匹配数量: " << maxMatches << std::endl;return 0;
}
算法思想解释:
- 这段代码实现了最大流算法(Ford-Fulkerson方法的Edmonds-Karp实现)来解决二分图最大匹配问题。
- 二分图最大匹配问题被转化为最大流问题:
- 创建一个源点(source)和一个汇点(sink)。
- 从源点到每个司机添加一条容量为1的边。
- 从每个乘客到汇点添加一条容量为1的边。
- 对于每个可能的匹配(司机到乘客),添加一条容量为1的边。
- 使用广度优先搜索(BFS)来寻找增广路径。这保证了算法的时间复杂度为O(VE^2),其中V是顶点数,E是边数。
- 通过反复寻找增广路径并更新残余网络,直到无法找到更多的增广路径,从而计算出最大流。
- 最终的最大流值就是最大匹配数,因为每条从源点到汇点的路径代表一个匹配。
这种方法巧妙地将二分图最大匹配问题转化为了网络流问题,利用了最大流-最小割定理来解决。它不仅可以处理完美匹配,也可以处理不完全匹配的情况,是一个非常通用和强大的方法。
备注:
这段代码使用的是 Edmonds-Karp 算法,它是 Ford-Fulkerson 方法的一种实现。我会详细解释这个算法,并给出一个例子。
最大流算法解释:
- Ford-Fulkerson 方法: 这是一种用于寻找网络中最大流的迭代方法。其基本思想是:
- 只要存在一条从源点到汇点的增广路径(即还有剩余容量的路径),就沿着这条路径增加流量。
- 重复这个过程,直到找不到增广路径为止。
- Edmonds-Karp 算法: 这是 Ford-Fulkerson 方法的一个具体实现,其特点是使用广度优先搜索(BFS)来寻找增广路径。这保证了算法的时间复杂度为 O(VE^2),其中 V 是顶点数,E 是边数。
算法步骤:
- 初始化流为 0。
- 使用 BFS 寻找一条从源点到汇点的增广路径。
- 确定这条路径上的最小剩余容量。
- 将这个最小容量加到总流量上。
- 更新残余网络:沿着增广路径,正向边的容量减少,反向边的容量增加。
- 重复步骤 2-5,直到找不到增广路径。
下面详细解释一下类:
1.graph类
class Graph {
private:int V; // 顶点数std::vector<std::vector<int>> capacity; // 容量矩阵std::vector<std::vector<int>> adj; // 邻接表public:Graph(int V); // 构造函数void addEdge(int u, int v, int cap); // 添加边int bfs(int s, int t, std::vector<int>& parent); // 广度优先搜索int maxFlow(int s, int t); // 计算最大流
};
解释:
V
存储图中的顶点数。capacity
是一个二维vector,表示边的容量。capacity[u][v]
表示从顶点u到顶点v的边的容量。adj
是邻接表,用于存储图的结构。addEdge
方法用于添加边到图中。bfs
方法实现了广度优先搜索,用于寻找增广路径。maxFlow
方法实现了 Edmonds-Karp 算法,计算从源点到汇点的最大流。
2.TaxiMatcher 类
TaxiMatcher 类是一个高层抽象,它使用 Graph 类来解决出租车匹配问题。
class TaxiMatcher {
private:int n, m; // n个司机,m个乘客Graph graph; // 用于表示匹配问题的图public:TaxiMatcher(int n, int m); // 构造函数void addPossibleMatch(int driver, int passenger); // 添加可能的匹配int findMaxMatches(); // 寻找最大匹配数
};
解释:
1.n和m分别表示司机司机数量和乘客数量。
2.graph是一个graph对象,用于表示整个匹配问题。
3.构造函数创建了一个n+m+2个顶点的图(n个司机,m个乘客,1个源点,一个汇点)
4.addossibleMatch方法用于添加一个可能的司机-乘客匹配。
5.findMaxMatches 方法通过调用graph的maxFlow方法来计算最大匹配数。
这两个类的协作方式:
1.Taximatch在构建时构建一个特殊的结构graph:
- 顶点 0 作为源点
- 顶点 1 到 n 表示司机
- 顶点 n+1 到 n+m 表示乘客
- 顶点 n+m+1 作为汇点
- 从源点到每个司机添加容量为1的边
- 从每个乘客到汇点添加容量为1的边
2.当调用addPossibleMatch时,TaxiMatcher调用Graph中的maxFlow方法,计算从源点到汇点的最大流,这个最大流值就是最大匹配数。