图论最短路径算法:高级应用与扩展
最短路径算法进阶应用
本文旨在探讨最短路径算法(特别是Dijkstra算法)的几种进阶应用与扩展技巧,包括最短路径计数、奇偶性最短路径问题以及点集中任意两点间的最短路径求解。
1. 最短路径计数
在标准Dijkstra算法的基础上,我们可以扩展其功能以统计从起点到图中每个顶点的最短路径总数。这在某些场景下,仅仅知道最短距离是不够的,还需要知道达到该距离有多少种不同的方式。
核心思想: 在Dijkstra的松弛操作中,除了更新距离数组外,还需要维护一个用于记录路径数量的数组,例如path_counts。
转移逻辑:
- 当发现一条严格更短的路径:如果从当前节点
u经过边(u, v)到达节点v的距离dist[u] + weight小于当前dist[v],则说明找到了新的最短路径。此时,不仅要更新dist[v],还要将path_counts[v]的值直接设置为path_counts[u],因为所有通过旧路径到达v的方式都被新路径取代了。 - 当发现一条等长的最短路径:如果
dist[u] + weight等于dist[v],这意味着发现了一条与已知最短路径等长的新路径。此时,应将path_counts[u]累加到path_counts[v]上,因为两种路径都提供了最短的到达方式。结果通常需要对一个大质数取模。
#include <iostream>
#include <vector>
#include <queue>
#include <limits> // For numeric_limits
const long long INF = std::numeric_limits<long long>::max();
const int MOD = 100003; // 模数
const int MAX_NODES_COUNT = 100005; // 最大节点数
struct Edge {
int to_node;
int weight; // 边权重
};
// 优先队列中的状态:节点ID和当前累积距离
struct DijkstraState {
int node_id;
long long current_dist;
// 比较器,用于构建最小堆
bool operator<(const DijkstraState& other) const {
return current_dist > other.current_dist;
}
};
std::vector<Edge> adjacency_list[MAX_NODES_COUNT];
long long min_distances[MAX_NODES_COUNT];
int path_counts[MAX_NODES_COUNT];
int num_graph_nodes, num_graph_edges;
void dijkstra_shortest_path_count(int start_node) {
for (int i = 1; i <= num_graph_nodes; ++i) {
min_distances[i] = INF;
path_counts[i] = 0;
}
std::priority_queue<DijkstraState> pq;
min_distances[start_node] = 0;
path_counts[start_node] = 1; // 起点到自身的最短路径为1条(距离0)
pq.push({start_node, 0});
while (!pq.empty()) {
DijkstraState current_state = pq.top();
pq.pop();
int u = current_state.node_id;
long long d_u = current_state.current_dist;
// 如果已找到更短路径,则忽略当前状态
if (d_u > min_distances[u]) {
continue;
}
for (const auto& edge : adjacency_list[u]) {
int v = edge.to_node;
int weight = edge.weight;
if (min_distances[v] > d_u + weight) {
min_distances[v] = d_u + weight;
path_counts[v] = path_counts[u]; // 继承前驱节点的路径数
pq.push({v, min_distances[v]});
} else if (min_distances[v] == d_u + weight) {
// 发现等长最短路径,累加路径数
path_counts[v] = (path_counts[v] + path_counts[u]) % MOD;
}
}
}
}
/*
// 示例用法
int main() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
std::cin >> num_graph_nodes >> num_graph_edges;
for (int i = 0; i < num_graph_edges; ++i) {
int u, v;
std::cin >> u >> v;
// 假设无向图且边权为1
adjacency_list[u].push_back({v, 1});
adjacency_list[v].push_back({u, 1});
}
dijkstra_shortest_path_count(1); // 从节点1开始
for (int i = 1; i <= num_graph_nodes; ++i) {
std::cout << path_counts[i] << std::endl;
}
return 0;
}
*/
2. 状态拆分:奇偶最短路径
在某些图论问题中,路径的长度不仅仅关注其数值大小,还可能涉及到路径包含的边数(步数)的奇偶性。例如,如果一个进程需要在L阶段完成,并且其相邻节点需要在L-1阶段完成,这会引入一个关于路径长度奇偶性的依赖关系。起点(阶段0)作为原材料提供者,需要判断某个节点在L阶段生产零件时,是否能在奇偶性匹配的步数内从起点接收到原材料。
核心思路: 这种问题可以通过Dijkstra算法的状态拆分来解决。我们将每个节点的状态拆分为两个:到达该节点的"最短偶数步路径"和"最短奇数步路径"。
算法实现:
- 定义一个二维距离数组,例如
min_steps[node_id][parity],其中parity为0表示偶数步路径,1表示奇数步路径。初始化为无穷大。 - 在Dijkstra算法的优先队列中,存储的状态也需要包含当前路径的奇偶性,或者可以通过累计步数来推导。当从节点
u通过一条边到达节点v时,如果当前路径到u的步数是P_u,边的权重(步数)是1,那么到达v的步数就是P_u + 1,其奇偶性会反转。 - 松弛操作时,根据新路径的奇偶性更新对应的
min_steps[v][new_parity]。
#include <iostream>
#include <vector>
#include <queue>
#include <cstring> // For memset
const int INF_STEPS = 0x3f3f3f3f; // 表示无穷大步数
const int MAX_NODES_PARITY = 100005;
struct GraphLink {
int target_node;
int step_cost; // 边的步数成本,通常为1
};
// 优先队列中的状态:节点ID和当前累积步数
struct ParityPathState {
int node_id;
int total_accumulated_steps;
bool operator<(const ParityPathState& other) const {
return total_accumulated_steps > other.total_accumulated_steps;
}
};
std::vector<GraphLink> graph_adj[MAX_NODES_PARITY];
// min_steps[node][0] 存储到达节点node的最短偶数步路径长度
// min_steps[node][1] 存储到达节点node的最短奇数步路径长度
int min_steps[MAX_NODES_PARITY][2];
int N_parity_nodes, M_parity_edges, Q_parity_queries;
void dijkstra_parity_shortest_path(int start_node) {
// 初始化所有节点的奇偶步数距离为无穷大
for (int i = 1; i <= N_parity_nodes; ++i) {
min_steps[i][0] = INF_STEPS;
min_steps[i][1] = INF_STEPS;
}
std::priority_queue<ParityPathState> pq;
min_steps[start_node][0] = 0; // 起点到自身0步,为偶数
pq.push({start_node, 0});
while (!pq.empty()) {
ParityPathState current = pq.top();
pq.pop();
int u = current.node_id;
int current_total_steps = current.total_accumulated_steps;
int current_parity = current_total_steps % 2;
// 如果当前取出的路径已劣于已知最短路径,则跳过
if (current_total_steps > min_steps[u][current_parity]) {
continue;
}
for (const auto& link : graph_adj[u]) {
int v = link.target_node;
int step_cost = link.step_cost;
int next_total_steps = current_total_steps + step_cost;
int next_parity = next_total_steps % 2; // 步数奇偶性反转
if (min_steps[v][next_parity] > next_total_steps) {
min_steps[v][next_parity] = next_total_steps;
pq.push({v, next_total_steps});
}
}
}
}
/*
// 示例用法
int main() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
std::cin >> N_parity_nodes >> M_parity_edges >> Q_parity_queries;
for (int i = 0; i < M_parity_edges; ++i) {
int u, v;
std::cin >> u >> v;
// 假设边权为1,表示1步
graph_adj[u].push_back({v, 1});
graph_adj[v].push_back({u, 1});
}
dijkstra_parity_shortest_path(1); // 从节点1开始
for (int i = 0; i < Q_parity_queries; ++i) {
int target_node, required_L_stage;
std::cin >> target_node >> required_L_stage;
// 检查是否存在到 target_node 的最短路径,其步数与required_L_stage奇偶性相同
// 且步数小于等于 required_L_stage
if (min_steps[target_node][required_L_stage % 2] <= required_L_stage) {
std::cout << "Yes\n";
} else {
std::cout << "No\n";
}
}
return 0;
}
*/
3. 点集中任意两点的最短路径
给定一个图(可能是有向图)和一个包含 k 个特殊节点的集合,目标是找出这个集合中任意两个不同节点之间的最短路径的最小值。由于直接对每对特殊节点运行最短路径算法(如k次Dijkstra)可能效率低下,特别是当k很大时,我们需要更优化的方法。
方法一:二进制分组结合超级源汇点
思路: 这种方法通过巧妙地将特殊节点分组,并引入"超级源点"和"超级汇点"来加速计算。其核心思想是,任何一对特殊节点(x, y),其ID的二进制表示至少有一位不同。我们可以利用这一点来设计分组策略。
实现步骤:
- 迭代所有可能的二进制位(例如,对于18位整数,从0到17)。
- 在每次迭代中,根据特殊节点ID的当前二进制位是0还是1,将其分入两个集合
A和B。 - 构建临时图:
- 创建一个"超级源点"
S(例如节点ID 0),并从S向集合A中的所有特殊节点连接权重为0的边。 - 创建一个"超级汇点"
T(例如节点IDN+1),并从集合B中的所有特殊节点向T连接权重为0的边。
- 创建一个"超级源点"
- 在构建好的图上运行一次Dijkstra算法,计算从超级源点
S到超级汇点T的最短路径。这个距离即为从集合A到集合B的最短路径。 - 对于有向图,由于路径方向性,还需要反转分组角色(即源点连
B,A连汇点),再运行一次Dijkstra,以覆盖所有可能的B到A路径。 - 每次迭代结束后,清除超级源点和超级汇点的临时边,为下一次迭代做准备。
- 通过遍历所有二进制位,可以确保任意一对不同的特殊节点,至少有一次会被分到不同的集合中,从而其最短路径能够被计算到。全局答案取所有Dijkstra结果的最小值。
#include <iostream>
#include <vector>
#include <queue>
#include <limits>
#include <algorithm> // For std::min
const long long INF_PATH = std::numeric_limits<long long>::max();
const int MAX_NODES_GROUP = 100005; // 最大节点数,额外预留给超级源汇点
struct AdjEntry {
int target;
int weight;
};
// 优先队列中的状态:节点ID和当前累积距离
struct PriorityQueueState {
int node_idx;
long long distance_val;
bool operator<(const PriorityQueueState& other) const {
return distance_val > other.distance_val;
}
};
std::vector<AdjEntry> graph_adj_list[MAX_NODES_GROUP + 2]; // +2 for super source/sink
int designated_special_nodes[MAX_NODES_GROUP];
long long overall_min_path_len = INF_PATH;
int N_group_nodes, M_group_edges, K_group_special_nodes;
long long run_dijkstra_with_virtual_nodes(int super_source, int super_sink) {
std::vector<long long> current_distances(N_group_nodes + 2, INF_PATH);
std::priority_queue<PriorityQueueState> pq;
current_distances[super_source] = 0;
pq.push({super_source, 0});
while (!pq.empty()) {
PriorityQueueState current = pq.top();
pq.pop();
int u = current.node_idx;
long long d_u = current.distance_val;
if (d_u > current_distances[u]) { // 已找到更短路径
continue;
}
for (const auto& entry : graph_adj_list[u]) {
int v = entry.target;
int weight = entry.weight;
if (current_distances[v] > d_u + weight) {
current_distances[v] = d_u + weight;
pq.push({v, current_distances[v]});
}
}
}
return current_distances[super_sink];
}
void solve_binary_grouping() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
std::cin >> N_group_nodes >> M_group_edges >> K_group_special_nodes;
for (int i = 0; i < M_group_edges; ++i) {
int u, v, w;
std::cin >> u >> v >> w;
graph_adj_list[u].push_back({v, w});
}
for (int i = 1; i <= K_group_special_nodes; ++i) {
std::cin >> designated_special_nodes[i];
}
int virtual_super_source = 0; // 使用0作为超级源点
int virtual_super_sink = N_group_nodes + 1; // 使用 N+1 作为超级汇点
// 遍历二进制位的每一位 (通常到17或18位,取决于节点ID的最大值)
for (int bit_pos = 0; bit_pos < 18; ++bit_pos) {
// 清除上一轮添加的虚拟边
graph_adj_list[virtual_super_source].clear();
// 需要清除从特殊节点到超级汇点的虚拟边。
// 原代码通过pop_back实现,这里也遵循此逻辑,但需注意其潜在风险。
// 更健壮的做法是使用单独的容器存储虚拟边,并在Dijkstra中特殊处理。
// ====== 第一方向:从二进制位为0的特殊节点到二进制位为1的特殊节点 ======
// 构建虚拟边
for (int i = 1; i <= K_group_special_nodes; ++i) {
int current_node_id = designated_special_nodes[i];
if ((current_node_id >> bit_pos) & 1) { // 如果当前位是1,连接到超级汇点
graph_adj_list[current_node_id].push_back({virtual_super_sink, 0});
} else { // 如果当前位是0,从超级源点连接
graph_adj_list[virtual_super_source].push_back({current_node_id, 0});
}
}
overall_min_path_len = std::min(overall_min_path_len,
run_dijkstra_with_virtual_nodes(virtual_super_source, virtual_super_sink));
// 清除虚拟边(第一方向)
graph_adj_list[virtual_super_source].clear();
for (int i = 1; i <= K_group_special_nodes; ++i) {
int current_node_id = designated_special_nodes[i];
if ((current_node_id >> bit_pos) & 1) {
// 移除从 current_node_id 到 virtual_super_sink 的虚拟边
graph_adj_list[current_node_id].pop_back();
}
}
// ====== 第二方向:从二进制位为1的特殊节点到二进制位为0的特殊节点 (针对有向图) ======
// 重新构建虚拟边(角色反转)
for (int i = 1; i <= K_group_special_nodes; ++i) {
int current_node_id = designated_special_nodes[i];
if ((current_node_id >> bit_pos) & 1) { // 如果当前位是1,现在从超级源点连接
graph_adj_list[virtual_super_source].push_back({current_node_id, 0});
} else { // 如果当前位是0,现在连接到超级汇点
graph_adj_list[current_node_id].push_back({virtual_super_sink, 0});
}
}
overall_min_path_len = std::min(overall_min_path_len,
run_dijkstra_with_virtual_nodes(virtual_super_source, virtual_super_sink));
// 清除虚拟边(第二方向)
graph_adj_list[virtual_super_source].clear();
for (int i = 1; i <= K_group_special_nodes; ++i) {
int current_node_id = designated_special_nodes[i];
if (!((current_node_id >> bit_pos) & 1)) { // 如果当前位是0,移除连接到超级汇点的虚拟边
graph_adj_list[current_node_id].pop_back();
}
}
}
std::cout << overall_min_path_len << std::endl;
// 清理所有图数据,以防多组测试用例
for (int i = 0; i <= N_group_nodes + 1; ++i) {
graph_adj_list[i].clear();
}
}
/*
int main() {
// 调用 solve_binary_grouping()
solve_binary_grouping();
return 0;
}
*/
方法二:正反图结合多源Dijkstra与染色
思路: 这种方法的核心是利用两次多源Dijkstra算法,分别在原图和反向图上运行,并记录每个节点的最短路径来源(或去向)的特殊节点,然后通过遍历所有边来合并结果。
实现步骤:
- 正向Dijkstra(从特殊点出发):
- 初始化一个距离数组
dist_from_special[node_id]和一个来源标记数组source_node_origin[node_id]。 - 将所有特殊节点以距离0压入优先队列。
- 运行Dijkstra算法。当松弛操作更新
dist_from_special[v]时,同时将source_node_origin[v]设置为当前路径的来源特殊节点source_node_origin[u]。
- 初始化一个距离数组
- 反向Dijkstra(到特殊点结束):
- 构建原图的反向图(即所有边方向反转)。
- 初始化一个距离数组
dist_to_special[node_id]和一个目标标记数组target_node_origin[node_id]。 - 将所有特殊节点以距离0压入优先队列(在反向图上相当于从它们出发)。
- 运行Dijkstra算法。当松弛操作更新
dist_to_special[v]时,同时将target_node_origin[v]设置为当前路径的终点特殊节点target_node_origin[u]。
- 合并答案:
- 遍历原图中的所有边
(u, v),权重为w。 - 对于每一条边,检查条件
source_node_origin[u] != target_node_origin[v]。这个条件确保了从特殊节点source_node_origin[u]到u,再通过边(u, v),最后从v到达特殊节点target_node_origin[v],路径的起点和终点是两个不同的特殊节点。 - 如果条件满足,则这条路径的总长度是
dist_from_special[u] + w + dist_to_special[v]。用这个值更新全局最小答案。
- 遍历原图中的所有边
#include <iostream>
#include <vector>
#include <queue>
#include <limits>
#include <algorithm> // For std::min
const long long INF_DIST_COL = std::numeric_limits<long long>::max();
const int MAX_NODES_COLORED = 100005;
struct StoredEdge {
int from;
int to;
int weight;
};
struct GraphNeighbor {
int neighbor_node;
int edge_weight;
};
// 优先队列状态:节点ID和当前累积距离
struct PriorityState {
int node_id;
long long accumulated_distance;
bool operator<(const PriorityState& other) const {
return accumulated_distance > other.accumulated_distance;
}
};
std::vector<GraphNeighbor> adj_forward_graph[MAX_NODES_COLORED];
std::vector<GraphNeighbor> adj_backward_graph[MAX_NODES_COLORED]; // 反向图
StoredEdge all_original_edges[MAX_NODES_COLORED]; // 存储所有原始边,用于最后遍历
long long dist_from_special[MAX_NODES_COLORED]; // 从特殊节点出发的最短距离
long long dist_to_special[MAX_NODES_COLORED]; // 到特殊节点的最短距离 (在反向图上计算)
int source_node_origin[MAX_NODES_COLORED]; // 记录路径的原始特殊起点
int target_node_origin[MAX_NODES_COLORED]; // 记录路径的最终特殊终点
int list_of_key_special_nodes[MAX_NODES_COLORED];
int N_colored_nodes, M_colored_edges, K_colored_special_nodes;
void multi_source_dijkstra_forward() {
std::priority_queue<PriorityState> pq;
for (int i = 1; i <= N_colored_nodes; ++i) {
dist_from_special[i] = INF_DIST_COL;
source_node_origin[i] = 0; // 0 表示尚未被任何特殊节点触达
}
for (int i = 1; i <= K_colored_special_nodes; ++i) {
int s_node = list_of_key_special_nodes[i];
pq.push({s_node, 0});
dist_from_special[s_node] = 0;
source_node_origin[s_node] = s_node; // 自身是特殊起点
}
while (!pq.empty()) {
PriorityState current = pq.top();
pq.pop();
int u = current.node_id;
long long d_u = current.accumulated_distance;
if (d_u > dist_from_special[u]) {
continue;
}
for (const auto& neighbor : adj_forward_graph[u]) {
int v = neighbor.neighbor_node;
int weight = neighbor.edge_weight;
if (dist_from_special[v] > d_u + weight) {
dist_from_special[v] = d_u + weight;
source_node_origin[v] = source_node_origin[u]; // 传播原始特殊起点
pq.push({v, dist_from_special[v]});
}
}
}
}
void multi_source_dijkstra_backward() {
std::priority_queue<PriorityState> pq;
for (int i = 1; i <= N_colored_nodes; ++i) {
dist_to_special[i] = INF_DIST_COL;
target_node_origin[i] = 0; // 0 表示尚未被任何特殊节点触达
}
for (int i = 1; i <= K_colored_special_nodes; ++i) {
int s_node = list_of_key_special_nodes[i];
pq.push({s_node, 0});
dist_to_special[s_node] = 0;
target_node_origin[s_node] = s_node; // 自身是特殊终点
}
while (!pq.empty()) {
PriorityState current = pq.top();
pq.pop();
int u = current.node_id;
long long d_u = current.accumulated_distance;
if (d_u > dist_to_special[u]) {
continue;
}
for (const auto& neighbor : adj_backward_graph[u]) { // 使用反向图
int v = neighbor.neighbor_node;
int weight = neighbor.edge_weight;
if (dist_to_special[v] > d_u + weight) {
dist_to_special[v] = d_u + weight;
target_node_origin[v] = target_node_origin[u]; // 传播原始特殊终点
pq.push({v, dist_to_special[v]});
}
}
}
}
void solve_multi_source_coloring() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
std::cin >> N_colored_nodes >> M_colored_edges >> K_colored_special_nodes;
for (int i = 1; i <= M_colored_edges; ++i) {
int u, v, w;
std::cin >> u >> v >> w;
all_original_edges[i] = {u, v, w}; // 存储原始边
adj_forward_graph[u].push_back({v, w});
adj_backward_graph[v].push_back({u, w}); // 构建反向图
}
for (int i = 1; i <= K_colored_special_nodes; ++i) {
std::cin >> list_of_key_special_nodes[i];
}
multi_source_dijkstra_forward();
multi_source_dijkstra_backward();
long long overall_min_path_result = INF_DIST_COL;
for (int i = 1; i <= M_colored_edges; ++i) {
const auto& edge = all_original_edges[i];
int u = edge.from;
int v = edge.to;
int w = edge.weight;
// 检查路径是否合法:
// 1. u和v都必须可达自/达至特殊节点 (即 source_node_origin[u] 和 target_node_origin[v] 不为0)
// 2. 路径的起始特殊节点和终止特殊节点必须不同
// 3. 避免无穷大距离相加导致溢出
if (source_node_origin[u] != 0 && target_node_origin[v] != 0 &&
source_node_origin[u] != target_node_origin[v]) {
if (dist_from_special[u] != INF_DIST_COL && dist_to_special[v] != INF_DIST_COL) {
overall_min_path_result = std::min(overall_min_path_result, dist_from_special[u] + w + dist_to_special[v]);
}
}
}
std::cout << overall_min_path_result << std::endl;
// 清理所有图数据,以防多组测试用例
for (int i = 0; i <= N_colored_nodes; ++i) {
adj_forward_graph[i].clear();
adj_backward_graph[i].clear();
}
}
/*
int main() {
// 调用 solve_multi_source_coloring()
solve_multi_source_coloring();
return 0;
}
*/