图论是数学的一个分支,它以图为研究对象,研究顶点和边组成的图形的数学理论和方法。在IO竞赛中,图论问题占据了非常重要的地位,几乎在每一场重要的竞赛中都会出现图论相关的题目。掌握图论算法对于在IO竞赛中取得好成绩至关重要。本文将从图的基本概念出发,深入解析各类图论算法的原理和应用,并通过大量实例帮助读者全面掌握这一重要领域。
路径问题 → 连通性问题 → 匹配问题 → 覆盖问题 → 着色问题 → 网络流问题算法类型 | 常见问题 | 时间复杂度 | 适用场景 |
|---|---|---|---|
最短路径 | 单源最短路径、多源最短路径 | O(m log n) ~ O(n³) | 寻找两点间的最短路径 |
最小生成树 | 稠密图、稀疏图 | O(m log n) ~ O(m + n log n) | 构建最小代价的连通网络 |
拓扑排序 | 有向无环图的排序 | O(m + n) | 任务调度、依赖关系处理 |
二分图匹配 | 最大匹配、完美匹配 | O(E√V) ~ O(VE) | 资源分配、工作安排 |
网络流 | 最大流、最小割、最小费用流 | O(E²V) ~ O(EV²) | 流量优化、资源分配 |
目录
├── 第一章:图的基本概念与表示方法
├── 第二章:最短路径算法
├── 第三章:最小生成树算法
├── 第四章:图的连通性算法
├── 第五章:拓扑排序与有向无环图
├── 第六章:二分图与匹配算法
├── 第七章:网络流算法
├── 第八章:图论高级算法与技巧
└── 第九章:图论算法实战训练图是由顶点(Vertex)和边(Edge)组成的一种数据结构,用于表示元素之间的关系。在图论中,我们通常用G=(V,E)来表示一个图,其中V是顶点的集合,E是边的集合。
图的基本术语:
图可以根据不同的标准进行分类:
在计算机中,图通常有以下几种表示方法:
邻接矩阵是一个n×n的二维数组,其中n是顶点的数量。矩阵的元素A[i][j]表示顶点i和顶点j之间的边的情况。
邻接矩阵的优点是查询两个顶点之间是否有边的时间复杂度为O(1),缺点是空间复杂度为O(n²),对于稀疏图来说会浪费大量空间。
邻接矩阵的C++实现:
#include <iostream>
#include <vector>
#include <climits>
using namespace std;
const int INF = INT_MAX; // 表示无穷大
class Graph {
private:
int V; // 顶点数
vector<vector<int>> adjMatrix; // 邻接矩阵
public:
Graph(int vertices) : V(vertices) {
adjMatrix.resize(V, vector<int>(V, INF));
for (int i = 0; i < V; i++) {
adjMatrix[i][i] = 0; // 自己到自己的距离为0
}
}
void addEdge(int u, int v, int weight) {
adjMatrix[u][v] = weight;
}
void printAdjMatrix() {
for (int i = 0; i < V; i++) {
for (int j = 0; j < V; j++) {
if (adjMatrix[i][j] == INF) {
cout << "INF ";
} else {
cout << adjMatrix[i][j] << " ";
}
}
cout << endl;
}
}
};
int main() {
Graph g(5);
g.addEdge(0, 1, 10);
g.addEdge(0, 2, 3);
g.addEdge(1, 2, 1);
g.addEdge(1, 3, 2);
g.addEdge(2, 1, 4);
g.addEdge(2, 3, 8);
g.addEdge(2, 4, 2);
g.addEdge(3, 4, 7);
g.addEdge(4, 3, 9);
cout << "邻接矩阵表示:" << endl;
g.printAdjMatrix();
return 0;
}邻接表是一种链式存储结构,它为每个顶点维护一个链表,链表中存储与该顶点相邻的所有顶点及对应的边的权重。
邻接表的优点是空间复杂度为O(V+E),对于稀疏图来说比较节省空间,缺点是查询两个顶点之间是否有边的时间复杂度为O(V)。
邻接表的C++实现:
#include <iostream>
#include <vector>
using namespace std;
struct Edge {
int to; // 目标顶点
int weight; // 边的权重
Edge(int t, int w) : to(t), weight(w) {}
};
class Graph {
private:
int V; // 顶点数
vector<vector<Edge>> adjList; // 邻接表
public:
Graph(int vertices) : V(vertices) {
adjList.resize(V);
}
void addEdge(int u, int v, int weight) {
adjList[u].emplace_back(v, weight);
}
void printAdjList() {
for (int i = 0; i < V; i++) {
cout << "顶点" << i << "的邻接点:";
for (const Edge& edge : adjList[i]) {
cout << "(" << edge.to << ", " << edge.weight << ") ";
}
cout << endl;
}
}
};
int main() {
Graph g(5);
g.addEdge(0, 1, 10);
g.addEdge(0, 2, 3);
g.addEdge(1, 2, 1);
g.addEdge(1, 3, 2);
g.addEdge(2, 1, 4);
g.addEdge(2, 3, 8);
g.addEdge(2, 4, 2);
g.addEdge(3, 4, 7);
g.addEdge(4, 3, 9);
cout << "邻接表表示:" << endl;
g.printAdjList();
return 0;
}边列表是一种简单的图表示方法,它使用一个列表来存储图中的所有边。每条边通常表示为一个三元组(u, v, w),其中u和v是边的两个顶点,w是边的权重(对于无权图,可以省略w)。
边列表的优点是简单直观,缺点是查询两个顶点之间是否有边的时间复杂度为O(E),不适合频繁查询边的情况。
边列表的C++实现:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
struct Edge {
int from; // 起始顶点
int to; // 目标顶点
int weight; // 边的权重
Edge(int f, int t, int w) : from(f), to(t), weight(w) {}
// 用于排序
bool operator<(const Edge& other) const {
return weight < other.weight;
}
};
class Graph {
private:
int V; // 顶点数
vector<Edge> edges; // 边列表
public:
Graph(int vertices) : V(vertices) {}
void addEdge(int u, int v, int weight) {
edges.emplace_back(u, v, weight);
}
void printEdges() {
for (const Edge& edge : edges) {
cout << edge.from << " -> " << edge.to << " (" << edge.weight << ")" << endl;
}
}
};
int main() {
Graph g(5);
g.addEdge(0, 1, 10);
g.addEdge(0, 2, 3);
g.addEdge(1, 2, 1);
g.addEdge(1, 3, 2);
g.addEdge(2, 1, 4);
g.addEdge(2, 3, 8);
g.addEdge(2, 4, 2);
g.addEdge(3, 4, 7);
g.addEdge(4, 3, 9);
cout << "边列表表示:" << endl;
g.printEdges();
return 0;
}图的遍历是指从图中的某个顶点出发,按照某种顺序访问图中的所有顶点,且每个顶点只被访问一次。图的遍历算法主要有两种:深度优先搜索(DFS)和广度优先搜索(BFS)。
深度优先搜索是一种递归的遍历算法,它从一个顶点出发,尽可能深地搜索图的分支,直到无法继续前进时,才回溯到上一个顶点,继续搜索其他分支。
DFS的基本步骤:
DFS的C++实现:
#include <iostream>
#include <vector>
using namespace std;
void dfs(int u, const vector<vector<int>>& adj, vector<bool>& visited) {
visited[u] = true;
cout << u << " ";
for (int v : adj[u]) {
if (!visited[v]) {
dfs(v, adj, visited);
}
}
}
int main() {
int V = 5;
vector<vector<int>> adj(V);
// 构建邻接表
adj[0].push_back(1);
adj[0].push_back(2);
adj[1].push_back(0);
adj[1].push_back(2);
adj[1].push_back(3);
adj[2].push_back(0);
adj[2].push_back(1);
adj[2].push_back(3);
adj[2].push_back(4);
adj[3].push_back(1);
adj[3].push_back(2);
adj[3].push_back(4);
adj[4].push_back(2);
adj[4].push_back(3);
vector<bool> visited(V, false);
cout << "DFS遍历结果:";
dfs(0, adj, visited);
cout << endl;
return 0;
}广度优先搜索是一种层次遍历算法,它从一个顶点出发,先访问该顶点的所有邻接顶点,然后再依次访问这些邻接顶点的邻接顶点,直到访问完图中的所有顶点。
BFS的基本步骤:
BFS的C++实现:
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
void bfs(int start, const vector<vector<int>>& adj, vector<bool>& visited) {
queue<int> q;
q.push(start);
visited[start] = true;
while (!q.empty()) {
int u = q.front();
q.pop();
cout << u << " ";
for (int v : adj[u]) {
if (!visited[v]) {
visited[v] = true;
q.push(v);
}
}
}
}
int main() {
int V = 5;
vector<vector<int>> adj(V);
// 构建邻接表
adj[0].push_back(1);
adj[0].push_back(2);
adj[1].push_back(0);
adj[1].push_back(2);
adj[1].push_back(3);
adj[2].push_back(0);
adj[2].push_back(1);
adj[2].push_back(3);
adj[2].push_back(4);
adj[3].push_back(1);
adj[3].push_back(2);
adj[3].push_back(4);
adj[4].push_back(2);
adj[4].push_back(3);
vector<bool> visited(V, false);
cout << "BFS遍历结果:";
bfs(0, adj, visited);
cout << endl;
return 0;
}最短路径问题是图论中的经典问题,它要求在加权图中找到从一个顶点到另一个顶点的路径,使得路径上的边的权重之和最小。最短路径问题可以分为以下几种类型:
Dijkstra算法是一种用于解决单源最短路径问题的贪心算法,它要求图中的边的权重都是非负的。Dijkstra算法的基本思想是每次从未访问的顶点中选择距离最小的顶点,然后更新其邻接顶点的距离。
Dijkstra算法的基本步骤:
Dijkstra算法的C++实现:
#include <iostream>
#include <vector>
#include <queue>
#include <climits>
using namespace std;
struct Edge {
int to;
int weight;
Edge(int t, int w) : to(t), weight(w) {}
};
vector<int> dijkstra(int start, const vector<vector<Edge>>& graph) {
int n = graph.size();
vector<int> dist(n, INT_MAX);
vector<bool> visited(n, false);
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
dist[start] = 0;
pq.push({0, start});
while (!pq.empty()) {
int u = pq.top().second;
int d = pq.top().first;
pq.pop();
if (visited[u]) continue;
visited[u] = true;
for (const Edge& edge : graph[u]) {
int v = edge.to;
int weight = edge.weight;
if (dist[u] != INT_MAX && dist[u] + weight < dist[v]) {
dist[v] = dist[u] + weight;
pq.push({dist[v], v});
}
}
}
return dist;
}
int main() {
int V = 5;
vector<vector<Edge>> graph(V);
// 构建图
graph[0].emplace_back(1, 10);
graph[0].emplace_back(2, 3);
graph[1].emplace_back(2, 1);
graph[1].emplace_back(3, 2);
graph[2].emplace_back(1, 4);
graph[2].emplace_back(3, 8);
graph[2].emplace_back(4, 2);
graph[3].emplace_back(4, 7);
graph[4].emplace_back(3, 9);
vector<int> dist = dijkstra(0, graph);
cout << "从顶点0到各顶点的最短距离:" << endl;
for (int i = 0; i < V; i++) {
if (dist[i] == INT_MAX) {
cout << "顶点" << i << ": 不可达" << endl;
} else {
cout << "顶点" << i << ": " << dist[i] << endl;
}
}
return 0;
}Bellman-Ford算法是一种用于解决单源最短路径问题的动态规划算法,它可以处理含有负权边的图,但不能处理含有负权回路的图。Bellman-Ford算法的基本思想是通过松弛操作逐步逼近最短路径。
Bellman-Ford算法的基本步骤:
Bellman-Ford算法的C++实现:
#include <iostream>
#include <vector>
#include <climits>
using namespace std;
struct Edge {
int from;
int to;
int weight;
Edge(int f, int t, int w) : from(f), to(t), weight(w) {}
};
vector<int> bellmanFord(int start, int V, const vector<Edge>& edges) {
vector<int> dist(V, INT_MAX);
dist[start] = 0;
// 进行V-1次松弛操作
for (int i = 0; i < V - 1; i++) {
for (const Edge& edge : edges) {
int u = edge.from;
int v = edge.to;
int weight = edge.weight;
if (dist[u] != INT_MAX && dist[u] + weight < dist[v]) {
dist[v] = dist[u] + weight;
}
}
}
// 检查是否存在负权回路
for (const Edge& edge : edges) {
int u = edge.from;
int v = edge.to;
int weight = edge.weight;
if (dist[u] != INT_MAX && dist[u] + weight < dist[v]) {
cout << "图中存在负权回路" << endl;
return {};
}
}
return dist;
}
int main() {
int V = 5;
vector<Edge> edges;
// 构建图
edges.emplace_back(0, 1, -1);
edges.emplace_back(0, 2, 4);
edges.emplace_back(1, 2, 3);
edges.emplace_back(1, 3, 2);
edges.emplace_back(1, 4, 2);
edges.emplace_back(3, 2, 5);
edges.emplace_back(3, 1, 1);
edges.emplace_back(4, 3, -3);
vector<int> dist = bellmanFord(0, V, edges);
if (!dist.empty()) {
cout << "从顶点0到各顶点的最短距离:" << endl;
for (int i = 0; i < V; i++) {
if (dist[i] == INT_MAX) {
cout << "顶点" << i << ": 不可达" << endl;
} else {
cout << "顶点" << i << ": " << dist[i] << endl;
}
}
}
return 0;
}SPFA(Shortest Path Faster Algorithm)算法是Bellman-Ford算法的一种优化版本,它利用队列来减少不必要的松弛操作,提高了算法的效率。SPFA算法特别适合处理含有负权边但没有负权回路的图。
SPFA算法的基本步骤:
SPFA算法的C++实现:
#include <iostream>
#include <vector>
#include <queue>
#include <climits>
using namespace std;
struct Edge {
int to;
int weight;
Edge(int t, int w) : to(t), weight(w) {}
};
vector<int> spfa(int start, const vector<vector<Edge>>& graph) {
int n = graph.size();
vector<int> dist(n, INT_MAX);
vector<bool> inQueue(n, false);
vector<int> inCount(n, 0); // 记录每个顶点的入队次数
queue<int> q;
dist[start] = 0;
q.push(start);
inQueue[start] = true;
inCount[start]++;
while (!q.empty()) {
int u = q.front();
q.pop();
inQueue[u] = false;
for (const Edge& edge : graph[u]) {
int v = edge.to;
int weight = edge.weight;
if (dist[u] != INT_MAX && dist[u] + weight < dist[v]) {
dist[v] = dist[u] + weight;
if (!inQueue[v]) {
q.push(v);
inQueue[v] = true;
inCount[v]++;
// 如果入队次数超过n,说明存在负权回路
if (inCount[v] > n) {
cout << "图中存在负权回路" << endl;
return {};
}
}
}
}
}
return dist;
}
int main() {
int V = 5;
vector<vector<Edge>> graph(V);
// 构建图
graph[0].emplace_back(1, -1);
graph[0].emplace_back(2, 4);
graph[1].emplace_back(2, 3);
graph[1].emplace_back(3, 2);
graph[1].emplace_back(4, 2);
graph[3].emplace_back(2, 5);
graph[3].emplace_back(1, 1);
graph[4].emplace_back(3, -3);
vector<int> dist = spfa(0, graph);
if (!dist.empty()) {
cout << "从顶点0到各顶点的最短距离:" << endl;
for (int i = 0; i < V; i++) {
if (dist[i] == INT_MAX) {
cout << "顶点" << i << ": 不可达" << endl;
} else {
cout << "顶点" << i << ": " << dist[i] << endl;
}
}
}
return 0;
}Floyd-Warshall算法是一种用于解决多源最短路径问题的动态规划算法,它可以处理含有负权边的图,但不能处理含有负权回路的图。Floyd-Warshall算法的基本思想是通过三重循环,逐步更新任意两个顶点之间的最短路径。
Floyd-Warshall算法的基本步骤:
Floyd-Warshall算法的C++实现:
#include <iostream>
#include <vector>
#include <climits>
using namespace std;
const int INF = INT_MAX;
vector<vector<int>> floydWarshall(vector<vector<int>>& graph) {
int n = graph.size();
vector<vector<int>> dist = graph;
// 对每个顶点k作为中间顶点
for (int k = 0; k < n; k++) {
// 更新任意两个顶点i和j之间的最短路径
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (dist[i][k] != INF && dist[k][j] != INF && dist[i][k] + dist[k][j] < dist[i][j]) {
dist[i][j] = dist[i][k] + dist[k][j];
}
}
}
}
// 检查是否存在负权回路
for (int i = 0; i < n; i++) {
if (dist[i][i] < 0) {
cout << "图中存在负权回路" << endl;
return {};
}
}
return dist;
}
int main() {
int V = 4;
vector<vector<int>> graph = {
{0, 3, INF, 7},
{8, 0, 2, INF},
{5, INF, 0, 1},
{2, INF, INF, 0}
};
vector<vector<int>> dist = floydWarshall(graph);
if (!dist.empty()) {
cout << "任意两个顶点之间的最短距离:" << endl;
for (int i = 0; i < V; i++) {
for (int j = 0; j < V; j++) {
if (dist[i][j] == INF) {
cout << "INF ";
} else {
cout << dist[i][j] << " ";
}
}
cout << endl;
}
}
return 0;
}算法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
Dijkstra | 非负权边的图,求单源最短路径 | 时间复杂度低,效率高 | 不能处理负权边 |
Bellman-Ford | 含有负权边的图,求单源最短路径 | 可以处理负权边,可以检测负权回路 | 时间复杂度高 |
SPFA | 含有负权边的图,求单源最短路径 | 效率高于Bellman-Ford,实用性强 | 在最坏情况下时间复杂度仍较高 |
Floyd-Warshall | 任意权边的图(不含负权回路),求多源最短路径 | 实现简单,代码简洁 | 时间复杂度高,仅适用于小规模图 |
最小生成树(Minimum Spanning Tree,MST)是指在一个加权的无向图中,找到一棵生成树,使得生成树中所有边的权重之和最小。最小生成树问题在网络设计、电路设计、交通规划等领域有着广泛的应用。
生成树的定义:一个连通图的生成树是一个包含图中所有顶点的树,即生成树是图的一个子图,它是连通的,并且没有回路,同时包含图中所有的顶点。
最小生成树的性质:
Kruskal算法是一种基于贪心策略的最小生成树算法,它的基本思想是按照边的权重从小到大的顺序选择边,每次选择一条不构成回路的边,直到选择了n-1条边为止。
Kruskal算法的基本步骤:
Kruskal算法的C++实现:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
struct Edge {
int from;
int to;
int weight;
Edge(int f, int t, int w) : from(f), to(t), weight(w) {}
bool operator<(const Edge& other) const {
return weight < other.weight;
}
};
struct UnionFind {
vector<int> parent;
vector<int> rank;
UnionFind(int n) {
parent.resize(n);
rank.resize(n, 0);
for (int i = 0; i < n; i++) {
parent[i] = i;
}
}
int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]); // 路径压缩
}
return parent[x];
}
void unite(int x, int y) {
x = find(x);
y = find(y);
if (x == y) return;
if (rank[x] < rank[y]) {
parent[x] = y;
} else {
parent[y] = x;
if (rank[x] == rank[y]) {
rank[x]++;
}
}
}
};
vector<Edge> kruskal(int V, vector<Edge>& edges) {
sort(edges.begin(), edges.end());
UnionFind uf(V);
vector<Edge> mst;
for (const Edge& edge : edges) {
if (uf.find(edge.from) != uf.find(edge.to)) {
uf.unite(edge.from, edge.to);
mst.push_back(edge);
if (mst.size() == V - 1) {
break;
}
}
}
return mst;
}
int main() {
int V = 4;
vector<Edge> edges;
// 构建图
edges.emplace_back(0, 1, 10);
edges.emplace_back(0, 2, 6);
edges.emplace_back(0, 3, 5);
edges.emplace_back(1, 3, 15);
edges.emplace_back(2, 3, 4);
vector<Edge> mst = kruskal(V, edges);
cout << "最小生成树的边:" << endl;
int totalWeight = 0;
for (const Edge& edge : mst) {
cout << edge.from << " -- " << edge.to << " (" << edge.weight << ")" << endl;
totalWeight += edge.weight;
}
cout << "最小生成树的总权重:" << totalWeight << endl;
return 0;
}Prim算法也是一种基于贪心策略的最小生成树算法,它的基本思想是从一个顶点开始,每次选择一条连接生成树和非生成树顶点的最小权边,直到生成树包含所有顶点。
Prim算法的基本步骤:
Prim算法的C++实现(使用优先队列优化):
#include <iostream>
#include <vector>
#include <queue>
#include <climits>
using namespace std;
struct Edge {
int to;
int weight;
Edge(int t, int w) : to(t), weight(w) {}
};
vector<pair<int, int>> prim(int V, const vector<vector<Edge>>& graph) {
vector<int> minDist(V, INT_MAX); // 记录每个顶点到生成树的最小距离
vector<int> parent(V, -1); // 记录生成树中每个顶点的父节点
vector<bool> inMST(V, false); // 记录顶点是否在生成树中
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
int start = 0;
minDist[start] = 0;
pq.push({0, start});
while (!pq.empty()) {
int u = pq.top().second;
pq.pop();
if (inMST[u]) continue;
inMST[u] = true;
for (const Edge& edge : graph[u]) {
int v = edge.to;
int weight = edge.weight;
if (!inMST[v] && weight < minDist[v]) {
minDist[v] = weight;
parent[v] = u;
pq.push({minDist[v], v});
}
}
}
// 构建最小生成树的边
vector<pair<int, int>> mstEdges;
for (int i = 1; i < V; i++) {
if (parent[i] != -1) {
mstEdges.push_back({parent[i], i});
}
}
return mstEdges;
}
int main() {
int V = 4;
vector<vector<Edge>> graph(V);
// 构建图
graph[0].emplace_back(1, 10);
graph[0].emplace_back(2, 6);
graph[0].emplace_back(3, 5);
graph[1].emplace_back(0, 10);
graph[1].emplace_back(3, 15);
graph[2].emplace_back(0, 6);
graph[2].emplace_back(3, 4);
graph[3].emplace_back(0, 5);
graph[3].emplace_back(1, 15);
graph[3].emplace_back(2, 4);
vector<pair<int, int>> mstEdges = prim(V, graph);
cout << "最小生成树的边:" << endl;
for (const auto& edge : mstEdges) {
cout << edge.first << " -- " << edge.second << endl;
}
return 0;
}算法 | 时间复杂度 | 空间复杂度 | 适用场景 | 优势 |
|---|---|---|---|---|
Kruskal | O(E log E) | O(E + V) | 稀疏图 | 实现简单,适合边数少的图 |
Prim | O(E log V) | O(E + V) | 稠密图 | 对于顶点数少的图效率更高 |
连通性是图论中的一个基本概念,它描述了图中顶点之间的可达性。在无向图中,如果两个顶点之间存在路径,则称这两个顶点是连通的;在有向图中,如果两个顶点之间存在双向的路径,则称这两个顶点是强连通的。
连通性问题主要包括以下几种类型:
无向图的连通分量是指最大的顶点子集,其中任意两个顶点之间都存在路径。计算无向图的连通分量可以使用DFS或BFS算法。
使用DFS计算无向图的连通分量的C++实现:
#include <iostream>
#include <vector>
using namespace std;
void dfs(int u, const vector<vector<int>>& adj, vector<bool>& visited, vector<int>& component, int label) {
visited[u] = true;
component[u] = label;
for (int v : adj[u]) {
if (!visited[v]) {
dfs(v, adj, visited, component, label);
}
}
}
vector<int> findComponents(int V, const vector<vector<int>>& adj) {
vector<bool> visited(V, false);
vector<int> component(V, -1);
int label = 0;
for (int i = 0; i < V; i++) {
if (!visited[i]) {
dfs(i, adj, visited, component, label);
label++;
}
}
return component;
}
int main() {
int V = 8;
vector<vector<int>> adj(V);
// 构建图
adj[0].push_back(1);
adj[1].push_back(0);
adj[1].push_back(2);
adj[2].push_back(1);
adj[3].push_back(4);
adj[4].push_back(3);
adj[5].push_back(6);
adj[5].push_back(7);
adj[6].push_back(5);
adj[7].push_back(5);
vector<int> component = findComponents(V, adj);
cout << "每个顶点所属的连通分量:" << endl;
for (int i = 0; i < V; i++) {
cout << "顶点" << i << ":连通分量" << component[i] << endl;
}
return 0;
}有向图的强连通分量是指最大的顶点子集,其中任意两个顶点之间都存在双向的路径。计算有向图的强连通分量的常用算法有Kosaraju算法、Tarjan算法和Gabow算法。
Kosaraju算法是一种基于两次DFS的强连通分量算法,它的基本思想是:
Kosaraju算法的C++实现:
#include <iostream>
#include <vector>
#include <algorithm>
#include <stack>
using namespace std;
void dfs1(int u, const vector<vector<int>>& adj, vector<bool>& visited, stack<int>& finishOrder) {
visited[u] = true;
for (int v : adj[u]) {
if (!visited[v]) {
dfs1(v, adj, visited, finishOrder);
}
}
finishOrder.push(u);
}
void dfs2(int u, const vector<vector<int>>& transposedAdj, vector<bool>& visited, vector<int>& component, int label) {
visited[u] = true;
component[u] = label;
for (int v : transposedAdj[u]) {
if (!visited[v]) {
dfs2(v, transposedAdj, visited, component, label);
}
}
}
vector<int> kosaraju(int V, const vector<vector<int>>& adj) {
vector<bool> visited(V, false);
stack<int> finishOrder;
// 第一次DFS,记录完成顺序
for (int i = 0; i < V; i++) {
if (!visited[i]) {
dfs1(i, adj, visited, finishOrder);
}
}
// 构建转置图
vector<vector<int>> transposedAdj(V);
for (int u = 0; u < V; u++) {
for (int v : adj[u]) {
transposedAdj[v].push_back(u);
}
}
// 第二次DFS,按照完成顺序的逆序访问转置图
fill(visited.begin(), visited.end(), false);
vector<int> component(V, -1);
int label = 0;
while (!finishOrder.empty()) {
int u = finishOrder.top();
finishOrder.pop();
if (!visited[u]) {
dfs2(u, transposedAdj, visited, component, label);
label++;
}
}
return component;
}
int main() {
int V = 5;
vector<vector<int>> adj(V);
// 构建图
adj[0].push_back(1);
adj[1].push_back(2);
adj[2].push_back(0);
adj[2].push_back(3);
adj[3].push_back(4);
adj[4].push_back(3);
vector<int> component = kosaraju(V, adj);
cout << "每个顶点所属的强连通分量:" << endl;
for (int i = 0; i < V; i++) {
cout << "顶点" << i << ":强连通分量" << component[i] << endl;
}
return 0;
}Tarjan算法是一种基于一次DFS的强连通分量算法,它的基本思想是使用两个数组dfn和low来记录顶点的发现时间和能够回溯到的最早的顶点的发现时间。如果一个顶点的dfn值等于其low值,则该顶点是一个强连通分量的根。
Tarjan算法的C++实现:
#include <iostream>
#include <vector>
#include <stack>
#include <algorithm>
using namespace std;
void tarjan(int u, const vector<vector<int>>& adj, vector<int>& dfn, vector<int>& low, vector<bool>& inStack, stack<int>& s, vector<vector<int>>& components, int& time) {
dfn[u] = low[u] = ++time;
s.push(u);
inStack[u] = true;
for (int v : adj[u]) {
if (!dfn[v]) { // 未访问过
tarjan(v, adj, dfn, low, inStack, s, components, time);
low[u] = min(low[u], low[v]);
} else if (inStack[v]) { // 在栈中,说明是回边
low[u] = min(low[u], dfn[v]);
}
}
if (dfn[u] == low[u]) { // 找到一个强连通分量的根
vector<int> component;
while (true) {
int v = s.top();
s.pop();
inStack[v] = false;
component.push_back(v);
if (v == u) break;
}
components.push_back(component);
}
}
vector<vector<int>> findSCC(int V, const vector<vector<int>>& adj) {
vector<int> dfn(V, 0);
vector<int> low(V, 0);
vector<bool> inStack(V, false);
stack<int> s;
vector<vector<int>> components;
int time = 0;
for (int i = 0; i < V; i++) {
if (!dfn[i]) {
tarjan(i, adj, dfn, low, inStack, s, components, time);
}
}
return components;
}
int main() {
int V = 5;
vector<vector<int>> adj(V);
// 构建图
adj[0].push_back(1);
adj[1].push_back(2);
adj[2].push_back(0);
adj[2].push_back(3);
adj[3].push_back(4);
adj[4].push_back(3);
vector<vector<int>> components = findSCC(V, adj);
cout << "强连通分量:" << endl;
for (int i = 0; i < components.size(); i++) {
cout << "强连通分量" << i << ": ";
for (int v : components[i]) {
cout << v << " ";
}
cout << endl;
}
return 0;
}割点(Articulation Point)是指无向图中,删除该顶点及其相关的边后,图的连通分量数量增加的顶点。桥(Bridge)是指无向图中,删除该边后,图的连通分量数量增加的边。
寻找割点的常用算法是Tarjan算法的一个变种,它的基本思想是使用dfn和low两个数组来记录顶点的发现时间和能够回溯到的最早的顶点的发现时间。对于根顶点,如果有两个或更多的子树,则根顶点是割点;对于非根顶点,如果存在一个子节点,其low值大于等于当前顶点的dfn值,则当前顶点是割点。
寻找割点的C++实现:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
void findArticulationPoints(int u, int parent, const vector<vector<int>>& adj, vector<int>& dfn, vector<int>& low, vector<bool>& isArticulation, int& time) {
dfn[u] = low[u] = ++time;
int children = 0;
for (int v : adj[u]) {
if (v == parent) continue;
if (!dfn[v]) { // 未访问过
children++;
findArticulationPoints(v, u, adj, dfn, low, isArticulation, time);
low[u] = min(low[u], low[v]);
// 根顶点的情况
if (parent == -1 && children > 1) {
isArticulation[u] = true;
}
// 非根顶点的情况
if (parent != -1 && low[v] >= dfn[u]) {
isArticulation[u] = true;
}
} else { // 已访问过,更新low值
low[u] = min(low[u], dfn[v]);
}
}
}
vector<int> getArticulationPoints(int V, const vector<vector<int>>& adj) {
vector<int> dfn(V, 0);
vector<int> low(V, 0);
vector<bool> isArticulation(V, false);
int time = 0;
for (int i = 0; i < V; i++) {
if (!dfn[i]) {
findArticulationPoints(i, -1, adj, dfn, low, isArticulation, time);
}
}
vector<int> articulationPoints;
for (int i = 0; i < V; i++) {
if (isArticulation[i]) {
articulationPoints.push_back(i);
}
}
return articulationPoints;
}
int main() {
int V = 5;
vector<vector<int>> adj(V);
// 构建图
adj[0].push_back(1);
adj[1].push_back(0);
adj[1].push_back(2);
adj[2].push_back(1);
adj[2].push_back(3);
adj[3].push_back(2);
adj[3].push_back(4);
adj[4].push_back(3);
vector<int> articulationPoints = getArticulationPoints(V, adj);
cout << "割点:";
for (int u : articulationPoints) {
cout << u << " ";
}
cout << endl;
return 0;
}寻找桥的算法也是Tarjan算法的一个变种,它的基本思想是对于一条边(u, v),如果low[v] > dfn[u],则这条边是桥。
寻找桥的C++实现:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
void findBridges(int u, int parent, const vector<vector<int>>& adj, vector<int>& dfn, vector<int>& low, vector<pair<int, int>>& bridges, int& time) {
dfn[u] = low[u] = ++time;
for (int v : adj[u]) {
if (v == parent) continue;
if (!dfn[v]) { // 未访问过
findBridges(v, u, adj, dfn, low, bridges, time);
low[u] = min(low[u], low[v]);
// 检查是否是桥
if (low[v] > dfn[u]) {
bridges.push_back({u, v});
}
} else { // 已访问过,更新low值
low[u] = min(low[u], dfn[v]);
}
}
}
vector<pair<int, int>> getBridges(int V, const vector<vector<int>>& adj) {
vector<int> dfn(V, 0);
vector<int> low(V, 0);
vector<pair<int, int>> bridges;
int time = 0;
for (int i = 0; i < V; i++) {
if (!dfn[i]) {
findBridges(i, -1, adj, dfn, low, bridges, time);
}
}
return bridges;
}
int main() {
int V = 5;
vector<vector<int>> adj(V);
// 构建图
adj[0].push_back(1);
adj[1].push_back(0);
adj[1].push_back(2);
adj[2].push_back(1);
adj[2].push_back(3);
adj[3].push_back(2);
adj[3].push_back(4);
adj[4].push_back(3);
vector<pair<int, int>> bridges = getBridges(V, adj);
cout << "桥:" << endl;
for (const auto& bridge : bridges) {
cout << bridge.first << " -- " << bridge.second << endl;
}
return 0;
}有向无环图(Directed Acyclic Graph,DAG)是指没有回路的有向图。DAG在计算机科学中有着广泛的应用,如任务调度、依赖关系分析、编译优化等。
DAG的性质:
拓扑排序(Topological Sorting)是将有向无环图中的所有顶点排成一个线性序列,使得对于图中的每条有向边(u, v),顶点u在序列中都出现在顶点v之前。
拓扑排序的常用算法:
Kahn算法是一种基于BFS的拓扑排序算法,它的基本思想是:
Kahn算法的C++实现:
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
vector<int> topologicalSortKahn(int V, const vector<vector<int>>& adj) {
vector<int> inDegree(V, 0);
vector<int> result;
queue<int> q;
// 计算每个顶点的入度
for (int u = 0; u < V; u++) {
for (int v : adj[u]) {
inDegree[v]++;
}
}
// 将所有入度为0的顶点加入队列
for (int u = 0; u < V; u++) {
if (inDegree[u] == 0) {
q.push(u);
}
}
// 进行BFS
while (!q.empty()) {
int u = q.front();
q.pop();
result.push_back(u);
for (int v : adj[u]) {
inDegree[v]--;
if (inDegree[v] == 0) {
q.push(v);
}
}
}
// 检查是否存在回路
if (result.size() != V) {
cout << "图中存在回路,无法进行拓扑排序" << endl;
return {};
}
return result;
}
int main() {
int V = 6;
vector<vector<int>> adj(V);
// 构建图
adj[5].push_back(2);
adj[5].push_back(0);
adj[4].push_back(0);
adj[4].push_back(1);
adj[2].push_back(3);
adj[3].push_back(1);
vector<int> topoOrder = topologicalSortKahn(V, adj);
if (!topoOrder.empty()) {
cout << "拓扑排序结果:";
for (int u : topoOrder) {
cout << u << " ";
}
cout << endl;
}
return 0;
}基于DFS的拓扑排序算法的基本思想是:
DFS-based拓扑排序的C++实现:
#include <iostream>
#include <vector>
#include <stack>
#include <algorithm>
using namespace std;
void dfs(int u, const vector<vector<int>>& adj, vector<bool>& visited, stack<int>& result) {
visited[u] = true;
for (int v : adj[u]) {
if (!visited[v]) {
dfs(v, adj, visited, result);
}
}
// 将顶点加入栈中,后序遍历的逆序就是拓扑排序的顺序
result.push(u);
}
vector<int> topologicalSortDFS(int V, const vector<vector<int>>& adj) {
vector<bool> visited(V, false);
stack<int> result;
// 对每个未访问的顶点进行DFS
for (int u = 0; u < V; u++) {
if (!visited[u]) {
dfs(u, adj, visited, result);
}
}
// 检查是否存在回路(这里简化处理,实际应用中需要额外检测)
// 将栈中的顶点依次弹出,得到拓扑排序的结果
vector<int> topoOrder;
while (!result.empty()) {
topoOrder.push_back(result.top());
result.pop();
}
return topoOrder;
}
int main() {
int V = 6;
vector<vector<int>> adj(V);
// 构建图
adj[5].push_back(2);
adj[5].push_back(0);
adj[4].push_back(0);
adj[4].push_back(1);
adj[2].push_back(3);
adj[3].push_back(1);
vector<int> topoOrder = topologicalSortDFS(V, adj);
cout << "拓扑排序结果:";
for (int u : topoOrder) {
cout << u << " ";
}
cout << endl;
return 0;
}拓扑排序在计算机科学中有广泛的应用,主要包括:
二分图(Bipartite Graph)是指可以将顶点集分成两个不相交的子集U和V,使得图中的每条边都连接U中的一个顶点和V中的一个顶点的图。二分图在匹配问题、资源分配等领域有着广泛的应用。
二分图的性质:
二分图的判定:可以使用染色法或BFS/DFS来判断一个图是否是二分图。
二分图判定的C++实现(使用BFS):
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
bool isBipartite(int V, const vector<vector<int>>& adj) {
vector<int> color(V, -1); // -1表示未染色,0和1表示两种不同的颜色
for (int start = 0; start < V; start++) {
if (color[start] == -1) { // 如果未染色
queue<int> q;
q.push(start);
color[start] = 0;
while (!q.empty()) {
int u = q.front();
q.pop();
for (int v : adj[u]) {
if (color[v] == -1) { // 未染色,染成相反的颜色
color[v] = color[u] ^ 1;
q.push(v);
} else if (color[v] == color[u]) { // 已经染色,且颜色相同,不是二分图
return false;
}
}
}
}
}
return true; // 是二分图
}
int main() {
int V = 4;
vector<vector<int>> adj(V);
// 构建图
adj[0].push_back(1);
adj[0].push_back(3);
adj[1].push_back(0);
adj[1].push_back(2);
adj[2].push_back(1);
adj[2].push_back(3);
adj[3].push_back(0);
adj[3].push_back(2);
if (isBipartite(V, adj)) {
cout << "这是一个二分图" << endl;
} else {
cout << "这不是一个二分图" << endl;
}
return 0;
}二分图匹配是指在二分图中寻找一个边集,使得这个边集中的任意两条边都没有公共顶点。二分图匹配在资源分配、任务调度等领域有着广泛的应用。
最大匹配问题是指在二分图中寻找一个边数最大的匹配。求解最大匹配问题的常用算法有匈牙利算法和Hopcroft-Karp算法。
匈牙利算法是一种基于DFS的最大匹配算法,它的基本思想是:对于左部的每个顶点,尝试寻找一条增广路径(一条从该顶点出发,最终到达右部未匹配顶点的路径),如果能够找到,则将这条路径上的匹配边和非匹配边互换,从而增加匹配的边数。
匈牙利算法的C++实现:
#include <iostream>
#include <vector>
#include <cstring>
using namespace std;
bool dfs(int u, const vector<vector<int>>& adj, vector<bool>& visited, vector<int>& match) {
for (int v : adj[u]) {
if (!visited[v]) {
visited[v] = true;
if (match[v] == -1 || dfs(match[v], adj, visited, match)) {
match[v] = u;
return true;
}
}
}
return false;
}
int hungarian(int V, const vector<vector<int>>& adj) {
vector<int> match(V, -1); // 记录右部顶点匹配的左部顶点
int result = 0;
for (int u = 0; u < V; u++) {
vector<bool> visited(V, false);
if (dfs(u, adj, visited, match)) {
result++;
}
}
return result;
}
int main() {
int V = 4; // 左部和右部顶点数均为4
vector<vector<int>> adj(V); // adj[u]表示左部顶点u可以匹配的右部顶点
// 构建二分图
adj[0].push_back(1);
adj[0].push_back(2);
adj[1].push_back(0);
adj[1].push_back(2);
adj[2].push_back(3);
adj[3].push_back(3);
int maxMatch = hungarian(V, adj);
cout << "最大匹配边数:" << maxMatch << endl;
return 0;
}Hopcroft-Karp算法是一种基于BFS和DFS的最大匹配算法,它的时间复杂度比匈牙利算法更低,适用于大规模的二分图匹配问题。Hopcroft-Karp算法的基本思想是:每次通过BFS分层,然后通过DFS寻找多条不相交的增广路径,从而一次性增加多个匹配边。
Hopcroft-Karp算法的C++实现:
#include <iostream>
#include <vector>
#include <queue>
#include <cstring>
using namespace std;
const int INF = 0x3f3f3f3f;
bool bfs(int V, const vector<vector<int>>& adj, vector<int>& pairU, vector<int>& pairV, vector<int>& dist) {
queue<int> q;
// 初始化距离数组,将所有未匹配的左部顶点加入队列
for (int u = 0; u < V; u++) {
if (pairU[u] == -1) {
dist[u] = 0;
q.push(u);
} else {
dist[u] = INF;
}
}
// 初始化右部顶点的距离为INF
int distV = INF;
while (!q.empty()) {
int u = q.front();
q.pop();
if (dist[u] < distV) {
for (int v : adj[u]) {
if (pairV[v] == -1) {
distV = dist[u] + 1;
} else if (dist[pairV[v]] == INF) {
dist[pairV[v]] = dist[u] + 1;
q.push(pairV[v]);
}
}
}
}
return distV != INF;
}
bool dfs(int u, const vector<vector<int>>& adj, vector<int>& pairU, vector<int>& pairV, vector<int>& dist) {
for (int v : adj[u]) {
if (pairV[v] == -1 || (dist[pairV[v]] == dist[u] + 1 && dfs(pairV[v], adj, pairU, pairV, dist))) {
pairU[u] = v;
pairV[v] = u;
return true;
}
}
dist[u] = INF;
return false;
}
int hopcroftKarp(int V, const vector<vector<int>>& adj) {
vector<int> pairU(V, -1); // pairU[u]表示左部顶点u匹配的右部顶点
vector<int> pairV(V, -1); // pairV[v]表示右部顶点v匹配的左部顶点
vector<int> dist(V); // 记录左部顶点的距离
int result = 0;
while (bfs(V, adj, pairU, pairV, dist)) {
for (int u = 0; u < V; u++) {
if (pairU[u] == -1) {
if (dfs(u, adj, pairU, pairV, dist)) {
result++;
}
}
}
}
return result;
}
int main() {
int V = 4; // 左部和右部顶点数均为4
vector<vector<int>> adj(V); // adj[u]表示左部顶点u可以匹配的右部顶点
// 构建二分图
adj[0].push_back(1);
adj[0].push_back(2);
adj[1].push_back(0);
adj[1].push_back(2);
adj[2].push_back(3);
adj[3].push_back(3);
int maxMatch = hopcroftKarp(V, adj);
cout << "最大匹配边数:" << maxMatch << endl;
return 0;
}二分图匹配在计算机科学和实际生活中有广泛的应用,主要包括:
网络流是指在一个有向图中,从源点到汇点的一种流量分配。网络流问题在通信网络、交通网络、资源分配等领域有着广泛的应用。
网络流的基本概念:
求解最大流问题的常用算法有Ford-Fulkerson算法、Edmonds-Karp算法、Dinic算法等。
Ford-Fulkerson算法是一种迭代算法,它的基本思想是:每次在剩余网络中寻找一条从源点到汇点的增广路径,然后沿这条路径增加流量,直到无法找到增广路径为止。
Ford-Fulkerson算法的C++实现(使用DFS寻找增广路径):
#include <iostream>
#include <vector>
#include <cstring>
#include <climits>
using namespace std;
const int INF = INT_MAX;
bool dfs(int u, int t, int minFlow, const vector<vector<int>>& capacity, vector<vector<int>>& flow, vector<bool>& visited, vector<vector<int>>& adj) {
if (u == t) return true;
visited[u] = true;
for (int v : adj[u]) {
if (!visited[v] && capacity[u][v] > flow[u][v]) {
int currentMinFlow = min(minFlow, capacity[u][v] - flow[u][v]);
if (dfs(v, t, currentMinFlow, capacity, flow, visited, adj)) {
flow[u][v] += currentMinFlow;
flow[v][u] -= currentMinFlow; // 反向边
return true;
}
}
}
return false;
}
int fordFulkerson(int V, int s, int t, const vector<vector<int>>& capacity, vector<vector<int>>& adj) {
vector<vector<int>> flow(V, vector<int>(V, 0));
int maxFlow = 0;
while (true) {
vector<bool> visited(V, false);
if (!dfs(s, t, INF, capacity, flow, visited, adj)) {
break;
}
maxFlow++;
}
return maxFlow;
}
int main() {
int V = 6;
int s = 0; // 源点
int t = 5; // 汇点
// 构建容量矩阵
vector<vector<int>> capacity(V, vector<int>(V, 0));
capacity[0][1] = 16;
capacity[0][2] = 13;
capacity[1][2] = 10;
capacity[1][3] = 12;
capacity[2][1] = 4;
capacity[2][4] = 14;
capacity[3][2] = 9;
capacity[3][5] = 20;
capacity[4][3] = 7;
capacity[4][5] = 4;
// 构建邻接表
vector<vector<int>> adj(V);
for (int i = 0; i < V; i++) {
for (int j = 0; j < V; j++) {
if (capacity[i][j] > 0) {
adj[i].push_back(j);
adj[j].push_back(i); // 反向边
}
}
}
int maxFlow = fordFulkerson(V, s, t, capacity, adj);
cout << "最大流:" << maxFlow << endl;
return 0;
}Edmonds-Karp算法是Ford-Fulkerson算法的一个特例,它使用BFS来寻找增广路径,从而保证了算法的时间复杂度为O(VE²)。
Edmonds-Karp算法的C++实现:
#include <iostream>
#include <vector>
#include <queue>
#include <cstring>
#include <climits>
using namespace std;
const int INF = INT_MAX;
int edmondsKarp(int V, int s, int t, vector<vector<int>>& capacity, vector<vector<int>>& adj) {
vector<vector<int>> flow(V, vector<int>(V, 0));
int maxFlow = 0;
while (true) {
vector<int> parent(V, -1);
vector<bool> visited(V, false);
queue<int> q;
q.push(s);
visited[s] = true;
// BFS寻找增广路径
while (!q.empty()) {
int u = q.front();
q.pop();
for (int v : adj[u]) {
if (!visited[v] && capacity[u][v] > flow[u][v]) {
parent[v] = u;
visited[v] = true;
q.push(v);
if (v == t) break;
}
}
}
// 如果无法到达汇点,结束算法
if (parent[t] == -1) break;
// 计算增广路径上的最小剩余容量
int minFlow = INF;
for (int v = t; v != s; v = parent[v]) {
int u = parent[v];
minFlow = min(minFlow, capacity[u][v] - flow[u][v]);
}
// 更新流量
for (int v = t; v != s; v = parent[v]) {
int u = parent[v];
flow[u][v] += minFlow;
flow[v][u] -= minFlow; // 反向边
}
maxFlow += minFlow;
}
return maxFlow;
}
int main() {
int V = 6;
int s = 0; // 源点
int t = 5; // 汇点
// 构建容量矩阵
vector<vector<int>> capacity(V, vector<int>(V, 0));
capacity[0][1] = 16;
capacity[0][2] = 13;
capacity[1][2] = 10;
capacity[1][3] = 12;
capacity[2][1] = 4;
capacity[2][4] = 14;
capacity[3][2] = 9;
capacity[3][5] = 20;
capacity[4][3] = 7;
capacity[4][5] = 4;
// 构建邻接表
vector<vector<int>> adj(V);
for (int i = 0; i < V; i++) {
for (int j = 0; j < V; j++) {
if (capacity[i][j] > 0) {
adj[i].push_back(j);
adj[j].push_back(i); // 反向边
}
}
}
int maxFlow = edmondsKarp(V, s, t, capacity, adj);
cout << "最大流:" << maxFlow << endl;
return 0;
}Dinic算法是一种高效的最大流算法,它的基本思想是:首先通过BFS对剩余网络进行分层,然后通过DFS在分层网络中寻找阻塞流(无法再增广的流),重复这两个步骤直到无法找到增广路径为止。Dinic算法的时间复杂度为O(V²E),在实际应用中比Edmonds-Karp算法效率更高。
Dinic算法的C++实现:
#include <iostream>
#include <vector>
#include <queue>
#include <cstring>
#include <climits>
using namespace std;
const int INF = INT_MAX;
bool bfs(int V, int s, int t, const vector<vector<int>>& capacity, const vector<vector<int>>& adj, vector<int>& level) {
fill(level.begin(), level.end(), -1);
queue<int> q;
level[s] = 0;
q.push(s);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int v : adj[u]) {
if (level[v] == -1 && capacity[u][v] > 0) {
level[v] = level[u] + 1;
q.push(v);
if (v == t) return true;
}
}
}
return false;
}
int dfs(int u, int t, int minFlow, vector<vector<int>>& capacity, const vector<vector<int>>& adj, vector<int>& level, vector<int>& iter) {
if (u == t) return minFlow;
for (int& i = iter[u]; i < adj[u].size(); i++) {
int v = adj[u][i];
if (level[v] > level[u] && capacity[u][v] > 0) {
int flow = dfs(v, t, min(minFlow, capacity[u][v]), capacity, adj, level, iter);
if (flow > 0) {
capacity[u][v] -= flow;
capacity[v][u] += flow; // 反向边
return flow;
}
}
}
return 0;
}
int dinic(int V, int s, int t, vector<vector<int>>& capacity, const vector<vector<int>>& adj) {
int maxFlow = 0;
vector<int> level(V);
while (bfs(V, s, t, capacity, adj, level)) {
vector<int> iter(V, 0);
int flow;
while ((flow = dfs(s, t, INF, capacity, adj, level, iter)) > 0) {
maxFlow += flow;
}
}
return maxFlow;
}
int main() {
int V = 6;
int s = 0; // 源点
int t = 5; // 汇点
// 构建容量矩阵
vector<vector<int>> capacity(V, vector<int>(V, 0));
capacity[0][1] = 16;
capacity[0][2] = 13;
capacity[1][2] = 10;
capacity[1][3] = 12;
capacity[2][1] = 4;
capacity[2][4] = 14;
capacity[3][2] = 9;
capacity[3][5] = 20;
capacity[4][3] = 7;
capacity[4][5] = 4;
// 构建邻接表
vector<vector<int>> adj(V);
for (int i = 0; i < V; i++) {
for (int j = 0; j < V; j++) {
if (capacity[i][j] > 0) {
adj[i].push_back(j);
adj[j].push_back(i); // 反向边
}
}
}
int maxFlow = dinic(V, s, t, capacity, adj);
cout << "最大流:" << maxFlow << endl;
return 0;
}根据最大流最小割定理,在一个流网络中,最大流的值等于最小割的容量。因此,可以通过求解最大流来得到最小割。
寻找最小割的C++实现(基于Dinic算法):
#include <iostream>
#include <vector>
#include <queue>
#include <cstring>
#include <climits>
#include <algorithm>
using namespace std;
const int INF = INT_MAX;
bool bfs(int V, int s, int t, const vector<vector<int>>& capacity, const vector<vector<int>>& adj, vector<int>& level) {
fill(level.begin(), level.end(), -1);
queue<int> q;
level[s] = 0;
q.push(s);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int v : adj[u]) {
if (level[v] == -1 && capacity[u][v] > 0) {
level[v] = level[u] + 1;
q.push(v);
if (v == t) return true;
}
}
}
return false;
}
int dfs(int u, int t, int minFlow, vector<vector<int>>& capacity, const vector<vector<int>>& adj, vector<int>& level, vector<int>& iter) {
if (u == t) return minFlow;
for (int& i = iter[u]; i < adj[u].size(); i++) {
int v = adj[u][i];
if (level[v] > level[u] && capacity[u][v] > 0) {
int flow = dfs(v, t, min(minFlow, capacity[u][v]), capacity, adj, level, iter);
if (flow > 0) {
capacity[u][v] -= flow;
capacity[v][u] += flow; // 反向边
return flow;
}
}
}
return 0;
}
int dinic(int V, int s, int t, vector<vector<int>>& capacity, const vector<vector<int>>& adj) {
int maxFlow = 0;
vector<int> level(V);
while (bfs(V, s, t, capacity, adj, level)) {
vector<int> iter(V, 0);
int flow;
while ((flow = dfs(s, t, INF, capacity, adj, level, iter)) > 0) {
maxFlow += flow;
}
}
return maxFlow;
}
void findMinCut(int V, int s, const vector<vector<int>>& capacity, const vector<vector<int>>& adj, vector<bool>& visited) {
queue<int> q;
q.push(s);
visited[s] = true;
while (!q.empty()) {
int u = q.front();
q.pop();
for (int v : adj[u]) {
if (!visited[v] && capacity[u][v] > 0) {
visited[v] = true;
q.push(v);
}
}
}
}
int main() {
int V = 6;
int s = 0; // 源点
int t = 5; // 汇点
// 构建容量矩阵
vector<vector<int>> capacity(V, vector<int>(V, 0));
capacity[0][1] = 16;
capacity[0][2] = 13;
capacity[1][2] = 10;
capacity[1][3] = 12;
capacity[2][1] = 4;
capacity[2][4] = 14;
capacity[3][2] = 9;
capacity[3][5] = 20;
capacity[4][3] = 7;
capacity[4][5] = 4;
// 构建邻接表
vector<vector<int>> adj(V);
for (int i = 0; i < V; i++) {
for (int j = 0; j < V; j++) {
if (capacity[i][j] > 0) {
adj[i].push_back(j);
adj[j].push_back(i); // 反向边
}
}
}
// 深拷贝容量矩阵,因为dinic算法会修改容量矩阵
vector<vector<int>> originalCapacity = capacity;
int maxFlow = dinic(V, s, t, capacity, adj);
cout << "最大流:" << maxFlow << endl;
// 寻找最小割
vector<bool> visited(V, false);
findMinCut(V, s, capacity, adj, visited);
cout << "最小割的边:" << endl;
int minCutCapacity = 0;
for (int u = 0; u < V; u++) {
if (visited[u]) {
for (int v : adj[u]) {
if (!visited[v] && originalCapacity[u][v] > 0) {
cout << u << " -> " << v << " (容量:" << originalCapacity[u][v] << ")" << endl;
minCutCapacity += originalCapacity[u][v];
}
}
}
}
cout << "最小割的容量:" << minCutCapacity << endl;
return 0;
}最小费用流问题是指在流网络中,找到一个最大流,使得流的总费用最小。最小费用流问题的常用算法有SPFA寻找最短增广路径算法、Dijkstra寻找最短增广路径算法等。
使用SPFA寻找最短增广路径的最小费用流算法的C++实现:
#include <iostream>
#include <vector>
#include <queue>
#include <cstring>
#include <climits>
using namespace std;
const int INF = INT_MAX;
struct Edge {
int to;
int capacity;
int cost;
int rev;
Edge(int t, int c, int co, int r) : to(t), capacity(c), cost(co), rev(r) {}
};
void addEdge(vector<vector<Edge>>& graph, int from, int to, int capacity, int cost) {
graph[from].emplace_back(to, capacity, cost, graph[to].size());
graph[to].emplace_back(from, 0, -cost, graph[from].size() - 1);
}
pair<int, int> minCostFlow(vector<vector<Edge>>& graph, int V, int s, int t, int maxFlow) {
vector<int> h(V, 0); // 势函数,用于Johnson算法
vector<int> prevv(V);
vector<int> preve(V);
int flow = 0;
int cost = 0;
while (flow < maxFlow) {
vector<int> dist(V, INF);
vector<bool> inQueue(V, false);
queue<int> q;
dist[s] = 0;
q.push(s);
inQueue[s] = true;
// SPFA寻找最短增广路径
while (!q.empty()) {
int u = q.front();
q.pop();
inQueue[u] = false;
for (int i = 0; i < graph[u].size(); i++) {
Edge& edge = graph[u][i];
if (edge.capacity > 0 && dist[edge.to] > dist[u] + edge.cost + h[u] - h[edge.to]) {
dist[edge.to] = dist[u] + edge.cost + h[u] - h[edge.to];
prevv[edge.to] = u;
preve[edge.to] = i;
if (!inQueue[edge.to]) {
q.push(edge.to);
inQueue[edge.to] = true;
}
}
}
}
if (dist[t] == INF) break; // 无法再增广
// 更新势函数
for (int v = 0; v < V; v++) {
h[v] += dist[v];
}
// 计算增广路径上的最小剩余容量
int d = maxFlow - flow;
for (int v = t; v != s; v = prevv[v]) {
d = min(d, graph[prevv[v]][preve[v]].capacity);
}
// 更新流量和费用
flow += d;
cost += d * h[t];
// 更新剩余容量
for (int v = t; v != s; v = prevv[v]) {
Edge& edge = graph[prevv[v]][preve[v]];
edge.capacity -= d;
graph[v][edge.rev].capacity += d;
}
}
return {flow, cost};
}
int main() {
int V = 4;
int s = 0; // 源点
int t = 3; // 汇点
int maxFlow = 5; // 最大流
vector<vector<Edge>> graph(V);
// 添加边
addEdge(graph, 0, 1, 10, 2);
addEdge(graph, 0, 2, 2, 4);
addEdge(graph, 1, 2, 6, 1);
addEdge(graph, 1, 3, 6, 3);
addEdge(graph, 2, 3, 10, 2);
pair<int, int> result = minCostFlow(graph, V, s, t, maxFlow);
cout << "最小费用最大流的流量:" << result.first << endl;
cout << "最小费用最大流的费用:" << result.second << endl;
return 0;
}树链剖分是一种将树分割成若干条链,以便在树上进行高效查询和更新操作的算法。树链剖分的主要思想是将树分割成若干条重链,使得树中的任意路径都可以被分解为不超过O(log n)条重链的拼接。
树链剖分的基本步骤:
树链剖分的C++实现:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const int MAXN = 10005;
vector<int> adj[MAXN]; // 树的邻接表
int fa[MAXN]; // 父节点
int dep[MAXN]; // 深度
int siz[MAXN]; // 子树大小
int son[MAXN]; // 重儿子
int top[MAXN]; // 链顶
int dfn[MAXN]; // DFS序
int rnk[MAXN]; // 逆DFS序
int cnt = 0; // DFS序计数器
// 第一次DFS,计算父节点、深度、子树大小和重儿子
void dfs1(int u, int f) {
fa[u] = f;
dep[u] = dep[f] + 1;
siz[u] = 1;
son[u] = 0;
for (int v : adj[u]) {
if (v != f) {
dfs1(v, u);
siz[u] += siz[v];
if (siz[v] > siz[son[u]]) {
son[u] = v;
}
}
}
}
// 第二次DFS,分配链顶和DFS序
void dfs2(int u, int topf) {
top[u] = topf;
dfn[u] = ++cnt;
rnk[cnt] = u;
if (son[u]) { // 先处理重儿子
dfs2(son[u], topf);
for (int v : adj[u]) {
if (v != fa[u] && v != son[u]) {
dfs2(v, v); // 轻儿子作为新链的链顶
}
}
}
}
// 线段树的实现(此处省略)
// 查询树中两个节点之间的路径上的最大值
int queryPath(int u, int v) {
int res = 0;
while (top[u] != top[v]) {
if (dep[top[u]] < dep[top[v]]) swap(u, v);
// 在线段树中查询区间[dfn[top[u]], dfn[u]]的最大值
// res = max(res, segQuery(1, 1, cnt, dfn[top[u]], dfn[u]));
u = fa[top[u]];
}
if (dep[u] > dep[v]) swap(u, v);
// 在线段树中查询区间[dfn[u], dfn[v]]的最大值
// res = max(res, segQuery(1, 1, cnt, dfn[u], dfn[v]));
return res;
}
int main() {
int n; // 节点数
cin >> n;
// 构建树
for (int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u);
}
// 进行树链剖分
dfs1(1, 0);
dfs2(1, 1);
// 后续操作(如查询路径上的最大值等)
return 0;
}动态树是一种数据结构,用于处理动态树连接问题,支持动态地连接两个树、断开树中的一条边、查询树中的路径信息等操作。Link-Cut Tree是一种常见的动态树实现方式。
Link-Cut Tree的基本操作:
Link-Cut Tree的C++实现:
#include <iostream>
#include <algorithm>
using namespace std;
const int MAXN = 10005;
struct Node {
Node *ch[2], *fa;
bool rev;
int val, sum; // 节点的值和子树的和
Node() : fa(nullptr), rev(false), val(0), sum(0) {
ch[0] = ch[1] = nullptr;
}
bool isRoot() {
return fa == nullptr || (fa->ch[0] != this && fa->ch[1] != this);
}
void pushUp() {
sum = val;
if (ch[0]) sum += ch[0]->sum;
if (ch[1]) sum += ch[1]->sum;
}
void pushDown() {
if (rev) {
swap(ch[0], ch[1]);
if (ch[0]) ch[0]->rev ^= 1;
if (ch[1]) ch[1]->rev ^= 1;
rev = false;
}
}
} T[MAXN];
// 旋转操作
void rotate(Node *x) {
Node *y = x->fa, *z = y->fa;
int k = (y->ch[1] == x);
if (!y->isRoot()) z->ch[z->ch[1] == y] = x;
x->fa = z;
y->ch[k] = x->ch[k^1];
if (x->ch[k^1]) x->ch[k^1]->fa = y;
x->ch[k^1] = y;
y->fa = x;
y->pushUp();
x->pushUp();
}
// 维护路径上的标记
void splay(Node *x) {
// 记录路径上的节点,以便pushDown
static Node *stk[MAXN];
int top = 0;
stk[top++] = x;
for (Node *u = x; !u->isRoot(); u = u->fa) {
stk[top++] = u->fa;
}
while (top--) stk[top]->pushDown();
while (!x->isRoot()) {
Node *y = x->fa, *z = y->fa;
if (!y->isRoot()) {
rotate((y->ch[0] == x) ^ (z->ch[0] == y) ? x : y);
}
rotate(x);
}
}
// 建立从根到x的偏爱路径
void access(Node *x) {
for (Node *pre = nullptr; x; pre = x, x = x->fa) {
splay(x);
x->ch[1] = pre;
x->pushUp();
}
}
// 将x设置为树的根
void makeroot(Node *x) {
access(x);
splay(x);
x->rev ^= 1;
}
// 找到x所在树的根
Node* findroot(Node *x) {
access(x);
splay(x);
while (x->ch[0]) {
x->pushDown();
x = x->ch[0];
}
splay(x); // 确保下次操作的效率
return x;
}
// 提取x到y的路径
void split(Node *x, Node *y) {
makeroot(x);
access(y);
splay(y);
}
// 连接x和y所在的树
void link(Node *x, Node *y) {
makeroot(x);
if (findroot(y) != x) {
x->fa = y;
}
}
// 断开x和y之间的边
void cut(Node *x, Node *y) {
makeroot(x);
if (findroot(y) == x && y->fa == x && !y->ch[0]) {
y->fa = nullptr;
x->ch[1] = nullptr;
x->pushUp();
}
}
// 查询x到y路径上的值的和
int querySum(Node *x, Node *y) {
split(x, y);
return y->sum;
}
int main() {
int n; // 节点数
cin >> n;
// 初始化节点的值
for (int i = 1; i <= n; i++) {
cin >> T[i].val;
T[i].sum = T[i].val;
}
// 后续操作(如link、cut、查询等)
return 0;
}平面图是指可以嵌入平面上,且没有边相交的图。平面图在地图绘制、集成电路设计等领域有着广泛的应用。
平面图的判定可以使用Euler公式或Kuratowski定理。Euler公式指出,对于一个连通的平面图,有V - E + F = 2,其中V是顶点数,E是边数,F是面数。Kuratowski定理指出,一个图是平面图当且仅当它不包含K₅(5个顶点的完全图)或K₃,₃(二分图,左右部各有3个顶点,且所有顶点之间都有边相连)作为子图。
平面图的对偶图是指将平面图中的每个面映射为一个顶点,将平面图中的每条边映射为一条边的图。对偶图在平面图的染色问题中有着重要的应用。
图的染色问题是指用最少的颜色对图中的顶点或边进行着色,使得相邻的顶点或边颜色不同。图的染色问题在调度问题、频率分配、模式识别等领域有着广泛的应用。
顶点染色是指用最少的颜色对图中的顶点进行着色,使得相邻的顶点颜色不同。顶点染色的最小颜色数称为图的色数。对于一般图来说,顶点染色问题是NP难的,但对于一些特殊图(如二分图、平面图等),存在多项式时间算法。
边染色是指用最少的颜色对图中的边进行着色,使得相邻的边颜色不同。边染色的最小颜色数称为图的边色数。根据Vizing定理,对于任何简单图,其边色数等于其最大度数或最大度数加1。
路径问题是IO竞赛中最常见的图论问题之一,主要包括:
连通性问题也是IO竞赛中的常见图论问题,主要包括:
匹配问题在IO竞赛中也经常出现,主要包括:
网络流问题在IO竞赛中是较为高级的图论问题,主要包括:
在IO竞赛中,选择合适的图的表示方法对于算法的效率至关重要。一般来说:
在IO竞赛中,为了提高算法的效率,常常需要对算法进行优化,主要包括:
在IO竞赛中,图论算法常常容易出现错误,主要包括:
调试图论算法的常用技巧包括:
图论是IO竞赛中的重要内容,掌握图论算法对于在IO竞赛中取得好成绩至关重要。本文从图的基本概念出发,深入解析了各类图论算法的原理和应用,包括最短路径算法、最小生成树算法、图的连通性算法、拓扑排序与有向无环图、二分图与匹配算法、网络流算法以及图论高级算法与技巧等。通过学习和实践这些算法,相信读者一定能够在IO竞赛中取得优异的成绩。
基础概念 → 图的表示 → 遍历算法 → 最短路径算法 → 最小生成树算法 → 连通性算法 → 拓扑排序 → 二分图匹配 → 网络流 → 高级算法