1.

cpp
#include<iostream>
#include<vector>
#include<queue>
#include <climits>
using namespace std;
const long long INF = (1LL << 31) - 1;
int main()
{
int n, m, s; cin >> n >> m >> s;
vector<vector<pair<int, int>>>graph(n + 1);
while (m--)
{
int u, v, w;
cin >> u >> v >> w;
graph[u].push_back({ v,w });
}
priority_queue<pair<long long, int>, vector<pair<long long, int>>, greater<pair<long long, int>>>pq;
vector<long long>dist(n + 1, LLONG_MAX);
vector<bool>visited(n + 1, false);
dist[s] = 0;
pq.push({ dist[s], s });
while (!pq.empty())
{
long long d = pq.top().first;
int u = pq.top().second;
pq.pop();
if (visited[u]) continue;
visited[u] = true;
for (int i = 0; i < graph[u].size(); i++)
{
int v = graph[u][i].first;
int w = graph[u][i].second;
if (dist[v] > dist[u] + w)
{
dist[v] = dist[u] + w;
pq.push({ dist[v],v });
}
}
}
for (int i = 1; i <= n; i++)
{
if (dist[i] == LLONG_MAX)
{
cout << INF << " ";
}
else cout << dist[i] << " ";
}
return 0;
}
单源最短路径(Dijkstra算法)从零理解到代码实现
这道题一看就是经典的最短路问题:给你一个有向图,从起点 s 出发,求到所有点的最短距离;如果某个点走不到,就输出一个固定的大数;这种"单源最短路径"问题,在边权非负的情况下,最稳的解法就是 Dijkstra 算法。
先把问题想简单一点,本质就是:从起点开始,一步一步往外扩展,每次都优先走"当前距离最短"的那个点;因为一旦某个点被确定为当前最短,那么之后不可能再有更短的路径到它,这一点是整个算法成立的核心。
你的代码其实已经是标准写法了;先用邻接表存图 graph[u] 里面放的是所有从 u 出发的边 (v, w);然后用一个 dist 数组记录从 s 到每个点的最短距离,初始全部设为无穷大,只有 dist[s] = 0;再用一个优先队列(小根堆)来维护"当前还没确定最短路径的点中,距离最小的那个"。
算法过程可以这样理解:一开始把 (0, s) 放进堆里;每次从堆里取出一个点 u(它当前的距离是最小的),如果这个点已经被访问过,就直接跳过;否则就"确认"这个点的最短距离,然后用它去更新所有相邻的点 v,如果发现 dist[v] > dist[u] + w,就说明找到了更短的路径,于是更新 dist[v],并把新的 (dist[v], v) 再丢进堆里;这个过程一直重复,直到堆为空。
这里有几个关键细节;visited 数组是为了避免一个点被重复处理(因为同一个点可能被多次加入堆);优先队列里存的是 pair<距离, 点编号>,并且用 greater 来实现小根堆;dist 用 long long 是为了防止路径和溢出;最后输出的时候,如果还是 LLONG_MAX,说明这个点不可达,就输出题目要求的 (2^31 - 1)。
再说一下复杂度,这种写法是典型的 O((n + m) log n),对于图论题来说已经是非常高效的了;只要边权是非负的,就可以放心用这个算法,如果有负边,就要换成 Bellman-Ford 或 SPFA(不过一般会卡)。
总结一下,这道题本质就是模板题,但关键不在于背模板,而是理解这句话:每次从"当前最短的点"出发去更新别人,一旦这个点被取出来,它的最短路径就已经确定;只要你把这件事想明白,Dijkstra 就不会写错。
2.

cpp
#include<bits/stdc++.h>
using namespace std;
int n,m,s;
vector<vector<pair<int,int>>>graph;
vector<bool>visited;
vector<long long>dist;
int main()
{
cin>>n>>m>>s;
graph.resize(n+1);
visited.resize(n+1,false);
dist.resize(n+1,1e9+5);
while(m--)
{
int u,v,w;
cin>>u>>v>>w;
graph[u].push_back({v,w});
}
priority_queue<pair<long long,int>,vector<pair<long long,int>>,greater<pair<long long,int>>>pq;
dist[s]=0;
pq.push({0,s});
while(!pq.empty())
{
long long d=pq.top().first;
int u=pq.top().second;
pq.pop();
if(visited[u]) continue;
visited[u]=true;
for(int i=0;i<graph[u].size();i++)
{
int v=graph[u][i].first;
int w=graph[u][i].second;
if(dist[v]>dist[u]+w)
{
dist[v]=dist[u]+w;
pq.push({dist[v],v});
}
}
}
for(int i=1;i<=n;i++)
{
cout<<dist[i]<<" ";
}
return 0;
}
单源最短路径模板(P4779):Dijkstra一遍过,别再写崩了
这题就是最标准的单源最短路径模板题,而且数据范围已经明确告诉你必须用堆优化的 Dijkstra,否则直接超时;说白了,这题不是考你会不会,而是考你"写得稳不稳"。
先说核心思路,其实就一句话:从起点 s 出发,每次选择当前距离最小的点去更新其他点,并且这个点一旦被取出来,它的最短路就已经确定了;这个"每次选最小"的过程,就是用优先队列来实现的。
你的代码整体是完全正确的,已经是标准写法了;邻接表存图,priority_queue 做小根堆,dist 数组存最短距离,visited 防止重复处理节点,这一套就是竞赛模板;流程也很清晰:初始化 dist[s]=0,把 (0,s) 丢进堆;每次取出堆顶,如果这个点已经访问过就跳过,否则标记访问,然后用它去松弛所有邻边;如果发现更短路径就更新并重新入堆。
不过这题有几个细节是必须注意的,不然很容易挂:
第一个是 dist 的初始化,你现在用的是 1e9+5,这在很多题里是够用的,但这题边权最大是 1e9,而且路径可能叠加很多次,所以更稳的写法是用 long long 并初始化成一个很大的数,比如 1e18,否则极端情况下是可能被卡的。
第二个是优先队列里其实可能会存在"过期状态",也就是说同一个点可能被多次加入堆,但只有第一次被取出的时候才是最短的,所以你用 visited 来剪枝是完全正确的,这也是最常见的写法。
第三个是这题数据保证"所有点都能到达",所以其实不用处理不可达情况,这也是你代码最后可以直接输出 dist[i] 的原因;如果是别的题,就要像之前那题一样判断是不是无穷大。
这题本质就是模板,但你要理解的是它为什么对:因为在非负权图中,一旦一个点被当前最小距离取出,就不可能再被更短路径更新,这一点保证了算法的正确性;如果边权有负数,这个性质就会被破坏,Dijkstra 就不能用了。
最后总结一句:图论里最常用的三板斧之一就是 Dijkstra,这种"邻接表 + 小根堆 + visited"的写法一定要练到条件反射,比赛里能一遍写对才算真的掌握了。
3.

cpp
#include<bits/stdc++.h>
using namespace std;
vector<vector<pair<int,int>>>graph;
vector<long long >dist;
vector<bool>visited;
vector<vector<pair<int,int>>>graph2;
vector<long long >dist2;
vector<bool>visited2;
int n,m;
int main()
{
cin>>n>>m;
graph.resize(n+1);
dist.resize(n+1,1e9+5);
visited.resize(n+1,false);
graph2.resize(n+1);
dist2.resize(n+1,1e9+5);
visited2.resize(n+1,false);
while(m--)
{
int u,v,w;
cin>>u>>v>>w;
graph[u].push_back({v,w});
graph2[v].push_back({u,w});
}
priority_queue<pair<long long,int>,vector<pair<long long,int>>,greater<pair<long long,int>>>pq;
priority_queue<pair<long long,int>,vector<pair<long long,int>>,greater<pair<long long,int>>>pq2;
dist[1]=0;
pq.push({0,1});
dist2[1]=0;
pq2.push({0,1});
while(!pq.empty())
{
long long d=pq.top().first;
int u=pq.top().second;
pq.pop();
if(visited[u]) continue;
visited[u]=true;
for(int i=0;i<graph[u].size();i++)
{
int v=graph[u][i].first;
int w=graph[u][i].second;
if(dist[v]>dist[u]+w)
{
dist[v]=dist[u]+w;
pq.push({dist[v],v});
}
}
}
while(!pq2.empty())
{
long long d=pq2.top().first;
int u=pq2.top().second;
pq2.pop();
if(visited2[u]) continue;
visited2[u]=true;
for(int i=0;i<graph2[u].size();i++)
{
int v=graph2[u][i].first;
int w=graph2[u][i].second;
if(dist2[v]>dist2[u]+w)
{
dist2[v]=dist2[u]+w;
pq2.push({dist2[v],v});
}
}
}
long long sum=0;
for(int i=2;i<=n;i++)
{
sum+=dist[i];
sum+=dist2[i];
}
cout<<sum;
return 0;
}
邮递员送信(P1629):两次最短路,其实就是"去 + 回"的总和
这道题如果一上来就想模拟"送一趟再回来再送一趟",基本就会把自己绕晕;其实它的本质非常简单,可以用一句话概括:每个点都要从 1 出发走一遍,再从这个点回到 1,再把所有这些最短路径加起来。
题目说邮递员从 1 出发,要把东西送到 2 ~ n,并且每次送完都要回到 1,再送下一个;关键点在于"每次只能带一件东西",这就意味着每个点 i 都要单独走一趟:1 → i → 1;那总时间就是所有点的"去的最短路 + 回的最短路"的总和。
于是问题就被拆成了两部分;第一部分是从 1 出发,到所有点的最短路 dist[i];第二部分是从所有点回到 1 的最短路,这个如果直接算会很麻烦,但有个经典技巧:把图的所有边反向,然后从 1 再跑一遍最短路,得到的 dist2[i],其实就是"原图中 i → 1 的最短距离"。
你的代码正是这么做的;graph 存原图,graph2 存反图;先在原图上跑一遍 Dijkstra,得到 dist;再在反图上跑一遍,得到 dist2;最后把所有 i(2 到 n)的 dist[i] + dist2[i] 加起来,就是答案。
这里有几个细节值得注意;首先,这题边是单向的,所以"去"和"回"不一定走同一条路,这也是为什么必须建反图;其次,dist 和 dist2 都要用 long long,并且初始化最好用 1e18 级别的无穷大(你这里用 1e9+5 在这题也能过,但严格来说不够安全);再就是 visited 数组的作用,避免重复处理节点,这是堆优化 Dijkstra 的标准写法。
换个角度理解这题:你其实不是在模拟一个人跑来跑去,而是在算 n-1 条最短路径的总和;每条路径都是独立的,所以可以拆开来算;而"反图再跑一遍"这个技巧,在很多"需要反向最短路"的题里都会用到,是一个非常值得记住的套路。
最后总结一下:这题的核心不是 Dijkstra,而是建模;一旦你意识到这是"所有点的往返最短路之和",并且想到"反图可以求回程",整题就变成两次模板题了。