常见的算法技巧——图算法
简单介绍
图算法是解决图论问题的一类算法,主要应用于图的遍历、路径搜索、最小生成树等问题。图是由节点(顶点)和连接节点的边组成的数据结构。
算法分类
下面我将介绍几种常见的图算法,并提供使用 C++ 实现的示例程序:
深度优先搜索(DFS)
深度优先搜索是一种用于图遍历的算法,它通过递归或栈的方式对图进行深度优先遍历。深度优先搜索从起始节点开始,沿着一条路径尽可能深地访问图的节点,直到无法继续下去,然后回溯到上一个节点,继续探索其他路径。下面是一个使用深度优先搜索查找图中是否存在路径的示例:
#include <iostream>
#include <vector>
bool dfs(std::vector<std::vector<int>>& graph, int start, int target, std::vector<bool>& visited) {
if (start == target) {
return true; // 找到目标节点,存在一条路径
}
visited[start] = true; // 标记当前节点为已访问
for (int neighbor : graph[start]) {
if (!visited[neighbor]) {
if (dfs(graph, neighbor, target, visited)) {
return true; // 从相邻节点出发能够找到目标节点
}
}
}
return false; // 从当前节点无法找到目标节点
}
int main() {
std::vector<std::vector<int>> graph = {
{
1, 2}, {
2, 3}, {
3}, {
}};
int start = 0;
int target = 3;
int n = graph.size();
std::vector<bool> visited(n, false); // 记录节点的访问状态
bool result = dfs(graph, start, target, visited);
std::cout << std::boolalpha << result << std::endl; // 输出:true
return 0;
}
广度优先搜索(BFS)
广度优先搜索是一种用于图遍历的算法,它从起始节点开始,逐层地遍历图的节点,先访问距离起始节点最近的节点,再逐渐访问距离增加的节点。广度优先搜索使用队列来保存待访问的节点,保证按照层次顺序进行遍历。下面是一个使用广度优先搜索查找图中是否存在路径的示例:
#include <iostream>
#include <vector>
#include <queue>
bool bfs(std::vector<std::vector<int>>& graph, int start, int target) {
int n = graph.size();
std::vector<bool> visited(n, false); // 记录节点的访问状态
std::queue<int> q; // 创建队列用于广度优先遍历
q.push(start); // 将起始节点放入队列
visited[start] = true;
while (!q.empty()) {
int curr = q.front();
q.pop();
if (curr == target) {
return true; // 找到目标节点,存在一条路径
}
for (int neighbor : graph[curr]) {
if (!visited[neighbor]) {
q.push(neighbor);
visited[neighbor] = true;
}
}
}
return false; // 无法找到目标节点
}
int main() {
std::vector<std::vector<int>> graph = {
{
1, 2}, {
2, 3}, {
3}, {
}};
int start = 0;
int target = 3;
bool result = bfs(graph, start, target);
std::cout << std::boolalpha << result << std::endl; // 输出:true
return 0;
}
最短路径算法
Dijkstra算法和Bellman-Ford算法
Dijkstra算法
Dijkstra算法用于解决单源最短路径问题,即从一个固定的起始节点到其他所有节点的最短路径。它使用贪心策略,逐步更新起始节点到各个节点的最短路径长度。
#include <iostream>
#include <vector>
#include <queue>
#include <limits>
#define INF std::numeric_limits<int>::max()
void dijkstra(std::vector<std::vector<std::pair<int, int>>>& graph, int start, std::vector<int>& dist) {
int n = graph.size();
std::vector<bool> visited(n, false);
std::priority_queue<std::pair<int, int>, std::vector<std::pair<int, int>>, std::greater<std::pair<int, int>>> pq;
dist[start] = 0;
pq.push(std::make_pair(0, start));
while (!pq.empty()) {
int u = pq.top().second;
pq.pop();
if (visited[u]) {
continue;
}
visited[u] = true;
for (const auto& neighbor : graph[u]) {
int v = neighbor.first;
int weight = neighbor.second;
if (dist[u] != INF && dist[u] + weight < dist[v]) {
dist[v] = dist[u] + weight;
pq.push(std::make_pair(dist[v], v));
}
}
}
}
int main() {
int n = 6; // 节点数
std::vector<std::vector<std::pair<int, int>>> graph(n); // 邻接表表示的图
// 添加边
graph[0].push_back(std::make_pair(1, 2));
graph[0].push_back(std::make_pair(2, 4));
graph[1].push_back(std::make_pair(2, 1));
graph[1].push_back(std::make_pair(3, 7));
graph[2].push_back(std::make_pair(4, 3));
graph[3].push_back(std::make_pair(4, 2));
graph[3].push_back(std::make_pair(5, 1));
graph[4].push_back(std::make_pair(5, 5));
int start = 0; // 起始节点
std::vector<int> dist(n, INF); // 保存起始节点到各个节点的最短路径长度
dijkstra(graph, start, dist);
std::cout << "Shortest distances from node " << start << ":\n";
for (int i = 0; i < n; i++) {
std::cout << "Node " << i << "'s shortest distance: " << dist[i] << std::endl;
}
return 0;
}
Bellman-Ford算法
Bellman-Ford算法用于解决含有负权边的图的单源最短路径问题。它通过对所有边进行松弛操作,逐步更新起始节点到各个节点的最短路径长度。
#include <iostream>
#include <vector>
#include <limits>
#define INF std::numeric_limits<int>::max()
struct Edge {
int src, dest, weight;
};
void bellmanFord(std::vector<Edge>& edges, int n, int start, std::vector<int>& dist) {
dist[start] = 0;
for (int i = 1; i <= n - 1; i++) {
for (const auto& edge : edges) {
int u = edge.src;
int v = edge.dest;
int weight = edge.weight;
if (dist[u] != INF && dist[u] + weight < dist[v]) {
dist[v] = dist[u] + weight;
}
}
}
// 检查是否存在负权环
for (const auto& edge : edges) {
int u = edge.src;
int v = edge.dest;
int weight = edge.weight;
if (dist[u] != INF && dist[u] + weight < dist[v]) {
std::cout << "Graph contains a negative-weight cycle" << std::endl;
return;
}
}
}
int main() {
int n = 5; // 节点数
std::vector<Edge> edges = {
{
0, 1, -1},
{
0, 2, 4},
{
1, 2, 3},
{
1, 3, 2},
{
1, 4, 2},
{
3, 2, 5},
{
3, 1, 1},
{
4, 3, -3},
};
int start = 0; // 起始节点
std::vector<int> dist(n, INF); // 保存起始节点到各个节点的最短路径长度
bellmanFord(edges, n, start, dist);
std::cout << "Shortest distances from node " << start << ":\n";
for (int i = 0; i < n; i++) {
std::cout << "Node " << i << "'s shortest distance: " << dist[i] << std::endl;
}
return 0;
}
最小生成树算法
Prim算法和Kruskal算法
Prim算法
Prim算法是一种用于求解加权无向图的最小生成树的算法。它从一个起始节点开始,逐步扩展最小生成树的边,直到包含所有节点为止。算法的核心思想是贪心策略,每次从当前的最小生成树中选择一条连接已经选择的节点和未选择的节点的最小权重边。
#include <iostream>
#include <vector>
#include <queue>
#define INF std::numeric_limits<int>::max()
struct Edge {
int src, dest, weight;
};
struct Compare {
bool operator()(const Edge& e1, const Edge& e2) {
return e1.weight > e2.weight;
}
};
std::vector<Edge> primMST(std::vector<std::vector<std::pair<int, int>>>& graph) {
int n = graph.size();
std::vector<Edge> mst; // 保存最小生成树的边
std::vector<bool> visited(n, false);
std::priority_queue<Edge, std::vector<Edge>, Compare> pq;
// 从节点0开始
visited[0] = true;
for (const auto& neighbor : graph[0]) {
int v = neighbor.first;
int weight = neighbor.second;
pq.push({
0, v, weight});
}
while (!pq.empty()) {
Edge e = pq.top();
pq.pop();
int src = e.src;
int dest = e.dest;
int weight = e.weight;
if (visited[dest]) {
continue;
}
visited[dest] = true;
mst.push_back({
src, dest, weight});
for (const auto& neighbor : graph[dest]) {
int v = neighbor.first;
int w = neighbor.second;
if (!visited[v]) {
pq.push({
dest, v, w});
}
}
}
return mst;
}
int main() {
int n = 5; // 节点数
std::vector<std::vector<std::pair<int, int>>> graph(n); // 邻接表表示的图
// 添加边
graph[0].push_back(std::make_pair(1, 2));
graph[1].push_back(std::make_pair(0, 2));
graph[0].push_back(std::make_pair(2, 3));
graph[2].push_back(std::make_pair(0, 3));
graph[1].push_back(std::make_pair(2, 4));
graph[2].push_back(std::make_pair(1, 4));
graph[1].push_back(std::make_pair(3, 3));
graph[3].push_back(std::make_pair(1, 3));
graph[1].push_back(std::make_pair(4, 1));
graph[4].push_back(std::make_pair(1, 1));
graph[2].push_back(std::make_pair(3, 2));
graph[3].push_back(std::make_pair(2, 2));
graph[3].push_back(std::make_pair(4, 5));
graph[4].push_back(std::make_pair(3, 5));
std::vector<Edge> mst = primMST(graph);
std::cout << "Minimum Spanning Tree:\n";
for (const auto& edge : mst) {
std::cout << edge.src << " - " << edge.dest << " : " << edge.weight << std::endl;
}
return 0;
}
Kruskal算法
Kruskal算法是一种用于求解加权无向图的最小生成树的算法。它通过按照边的权重从小到大的顺序选择边,并判断是否会形成环路,若不会形成环路,则将该边加入最小生成树中。算法的基本思想是贪心策略,每次选择权重最小的边,直到生成树中包含了所有的节点。
#include <iostream>
#include <vector>
#include <algorithm>
struct Edge {
int src, dest, weight;
};
struct UnionFind {
std::vector<int> parent;
std::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) {
int rootX = find(x);
int rootY = find(y);
if (rootX != rootY) {
if (rank[rootX] < rank[rootY]) {
parent[rootX] = rootY;
} else if (rank[rootX] > rank[rootY]) {
parent[rootY] = rootX;
} else {
parent[rootY] = rootX;
rank[rootX]++;
}
}
}
};
bool compareEdges(const Edge& e1, const Edge& e2) {
return e1.weight < e2.weight;
}
std::vector<Edge> kruskalMST(std::vector<Edge>& edges, int n) {
std::vector<Edge> mst; // 保存最小生成树的边
UnionFind uf(n);
std::sort(edges.begin(), edges.end(), compareEdges);
for (const auto& edge : edges) {
int src = edge.src;
int dest = edge.dest;
if (uf.find(src) != uf.find(dest)) {
mst.push_back(edge);
uf.unite(src, dest);
}
}
return mst;
}
int main() {
int n = 5; // 节点数
std::vector<Edge> edges = {
{
0, 1, 2},
{
0, 2, 3},
{
1, 2, 4},
{
1, 3, 3},
{
1, 4, 1},
{
2, 3, 2},
{
3, 4, 5},
};
std::vector<Edge> mst = kruskalMST(edges, n);
std::cout << "Minimum Spanning Tree:\n";
for (const auto& edge : mst) {
std::cout << edge.src << " - " << edge.dest << " : " << edge.weight << std::endl;
}
return 0;
}
文章小结
这些算法在解决图论问题时非常有用,可以根据问题的需求和图的特性选择合适的算法。需要注意的是,在实际应用中,可能需要根据具体问题对算法进行适当的调整和优化,以满足问题的需求。