Dijkstra 算法
- 算法前提:在没有负边的情况下使用。
- 算法思路:将结点分成已确定最短路长度的点集 S 和未确定最短路长度的点集 T,每次从 T 集合中选取最短路长度最小的结点移到 S 集合中,并对其出边执行更新操作
- 从T集合中,选取一个最短路长度最小的结点,移到S集合中。
- 对那些刚刚被加入S集合的结点的所有出边执行松弛操作。
cpp
struct edge {
int v, w; // 边的终点和权值
};
struct node {
int dis, u; // 距离和顶点
bool operator > (const node &a ) const {
return dis>a.dis; // 优先队列比较函数,小根堆
}
};
vector<edge> e[MAXN]; // 邻接表存储边
int dis[MAXN], vis[MAXN]; // dis存储最短距离,vis标记是否已确定最短距离
priority_queue<node, vector<node>, greater<node>> q; // 小根堆优先队列
void dijkstra(int n, int s) {
memset(dis, 0x3f, (n + 1) * sizeof(int)); // 初始化最短距离为无穷大
memset(vis, 0, (n + 1) * sizeof(int)); // 初始化标记数组为0
dis[s] = 0; // 起点到自身的距离为0
q.push({0, s}); // 起点入队
while (!q.empty()) {
int u = q.top().u; // 取出距离最小的顶点
q.pop();
if (vis[u]) continue; // 如果已确定最短距离,跳过
vis[u] = 1; // 标记为已确定最短距离
for (auto ed : e[u]) { // 遍历u的所有出边
int v = ed.v, w = ed.w; // 边的终点和权值
// 如果可以通过u得到更短的距离到v
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w; // 更新最短距离
q.push({dis[v], v}); // 将v入队
}
}
}
}
例题
cpp
#i#include <bits/stdc++.h>
using namespace std;
const int MAXN = 2010; // 最大节点数(人数),题目中 n≤2000,这里多开一点防止越界
double dis[MAXN]; // 存储从起点 A 到各节点能保留的金额比例,初始化为极小值
bool vis[MAXN]; // 标记节点是否已确定最长路径,避免重复处理
int n, m, A, B; // n 是总人数,m 是转账对数,A 是转账起点,B 是转账终点
// 边的结构体,用于构建图的邻接表,to 表示转账目标节点,rate 表示转账后能保留的比例(1 - 手续费比例)
struct Edge {
int to;
double rate;
};
vector<Edge> graph[MAXN]; // 邻接表存储图结构,graph[u] 存所有从 u 出发能转账到的节点及对应保留比例
// 优先队列中的节点结构体,用于 Dijkstra 算法,node 表示节点编号,val 表示到达该节点时能保留的金额比例
struct Node {
int node;
double val;
// 重载小于运算符,让优先队列按照 val 从大到小排序(大顶堆),这样每次取出当前能保留比例最大的路径
bool operator<(const Node& other) const {
return val < other.val;
}
};
// Dijkstra 算法实现,求解从 A 出发到各节点能保留的最大金额比例
void dijkstra() {
// 初始化 dis 数组,将起点 A 的保留比例设为 1(还没转账,金额完整保留),其他设为极小值
fill(dis, dis + MAXN, 0);
dis[A] = 1;
// 优先队列,大顶堆,用于每次选当前能保留比例最大的路径拓展
priority_queue<Node> pq;
pq.push({A, 1});
while (!pq.empty()) {
Node cur = pq.top();
pq.pop();
int u = cur.node;
double currentRate = cur.val;
// 如果当前节点已经处理过(已确定最长路径),跳过
if (vis[u]) continue;
vis[u] = true; // 标记为已处理
// 遍历当前节点 u 的所有邻接边,尝试更新邻接节点的最大保留比例
for (const Edge& e : graph[u]) {
int v = e.to;
double newRate = currentRate * e.rate;
// 如果经过 u 转账到 v 能获得更大的保留比例,就更新,并将 v 加入队列等待处理
if (newRate > dis[v]) {
dis[v] = newRate;
pq.push({v, newRate});
}
}
}
}
int main() {
// 输入总人数 n 和转账对数 m
scanf("%d%d", &n, &m);
for (int i = 0; i < m; ++i) {
int x, y, z;
// 输入转账的两人 x、y 以及手续费比例 z
scanf("%d%d%d", &x, &y, &z);
double rate = 1 - z / 100.0; // 计算转账后能保留的比例(1 - 手续费比例)
// 因为是互相转账,所以添加两条有向边(x 到 y 和 y 到 x)
graph[x].push_back({y, rate});
graph[y].push_back({x, rate});
}
// 输入转账起点 A 和终点 B
scanf("%d%d", &A, &B);
dijkstra(); // 执行 Dijkstra 算法,计算从 A 到各节点的最大保留比例
// B 要收到 100 元,那么 A 需要的初始金额 = 100 / (A 到 B 能保留的比例)
double result = 100 / dis[B];
// 输出结果,精确到小数点后 8 位
printf("%.8lf\n", result);
return 0;
}