CSP-S 2025 入门级 第一轮(初赛) 完善程序(1)
【题目】
(特殊最短路)给定一个含 NNN 个点、MMM 条边的带权无向图,边权非负。起点为 SSS,终点为 TTT;对于一条从 SSS 到 TTT 的路径,可以在整条路径中至多选择一条边作为"免费边"。当第一次经过这条被选中的边时,费用视为 000;如果之后再次经过该边,则仍按其原有权值计算。点和边均允许重复经过。求从 SSS 到 TTT 的最小总费用。
以下代码求解了上述问题,试补全程序。
cpp
01 #include <algorithm>
02 #include <iostream>
03 #include <queue>
04 #include <vector>
05 using namespace std;
06 const long long INF = 1e18;
07
08 struct Edge {
09 int to;
10 int weight;
11 };
12
13 struct State {
14 long long dist;
15 int u;
16 int used_freebie; // 0 for not used, 1 for used
17 bool operator>(const State &other) const {
18 return dist > other.dist;
19 }
20 };
21
22 int main() {
23 int n, m, s, t;
24 cin >> n >> m >> s >> t;
25
26 vector<vector<Edge>> adj(n + 1);
27 for (int i = 0; i < m; ++i) {
28 int u, v, w;
29 cin >> u >> v >> w;
30 adj[u].push_back({v, w});
31 adj[v].push_back({u, w});
32 }
33
34 vector<vector<long long>> d(n + 1, vector<long long>(2, INF));
35 priority_queue<State, vector<State>, greater<State>> pq;
36
37 d[s][0] = 0;
38 pq.push({0, s, ___①___});
39
40 while (!pq.empty()) {
41 State current = pq.top();
42 pq.pop();
43
44 long long dist = current.dist;
45 int u = current.u;
46 int used = current.used_freebie;
47
48 if (dist > ___②___) {
49 continue;
50 }
51
52 for (const auto &edge : adj[u]) {
53 int v = edge.to;
54 int w = edge.weight;
55
56 if (d[u][used] + w < ___③___) {
57 ___③___ = d[u][used] + w;
58 pq.push({___③___, v, used});
59 }
60
61 if (used == 0) {
62 if (___④___ < d[v][1]) {
63 d[v][1] = d[u][used];
64 pq.push({d[v][1], v, 1});
65 }
66 }
67 }
68 }
69
70 cout << __⑤__ << endl;
71 return 0;
72 }
-
① 处应填( )。
A.
0B.
1C.
-1D.
false -
② 处应填( )。
A.
d[u][!used]B.
d[u][used]C.
d[t][used]D.
INF -
③ 处应填( )。
A.
d[v][1]B.
d[v][used]C.
d[u][used]D.
d[v][0] -
④ 处应填( )。
A.
d[v][0]B.
d[v][1]C.
d[u][0]D.
d[u][1] -
⑤ 处应填( )。
A.
d[t][1]B.
d[t][0]C.
min(d[t][0], d[t][1])D.
d[t][0]+d[t][1]
【题目难度】:D
【题目考点】
1. 图论:Dijkstra堆优化算法
2. 图论:分层图
3. STL vector 保存二维矩阵
说明使用vector声明二维矩阵的写法。
如果vector中每个元素为vector,那么就是保存了很多个一维的序列,多个一维序列构成了一个二维矩阵。
vector<T> v(n, val);或vector<T>(n, val);,
使用该构造函数可以使得声明得到的vector包含nnn个值为valvalval的元素。如果不传valvalval,各元素初值为T类型的默认值(如果T为基础类型如int, double,则默认值为0)。下标范围为[0,n−1][0,n-1][0,n−1]
如果要声明一个nnn行mmm列的保存TTT类型元素的二维矩阵,则每一行是一个长为mmm的序列,声明方法为vector<T>(m),二维矩阵可以看作每一行为一个元素,每个元素的类型为vector<T>,初值为vector<T>(m),该矩阵初始有nnn行,
因此声明方法为vector<vector<T>> mat(n, vector<T>(m)),得到的matmatmat对象可以当做一个nnn行mmm列二维数组使用,行列下标从000开始。该方法相比于直接声明二维数组,可以先输入行数列数,而后再根据行列数声明出只占用n∗mn*mn∗m量级的空间大小的对象,比声明二维数组节省空间。
【题目解析】
浏览代码整体结构,可知该题使用了Dijkstra堆优化算法。
cpp
22 int main() {
23 int n, m, s, t;
24 cin >> n >> m >> s >> t;
25
26 vector<vector<Edge>> adj(n + 1);
27 for (int i = 0; i < m; ++i) {
28 int u, v, w;
29 cin >> u >> v >> w;
30 adj[u].push_back({v, w});
31 adj[v].push_back({u, w});
32 }
输入顶点数量nnn,边数mmm,起点sss,终点ttt。adjadjadj是邻接表,adf[i]adf[i]adf[i]保存顶点iii的邻接点。循环mmm次,每次输入一条无向边(u,v)(u,v)(u,v),权值www,将该边加入邻接表。该图是无向图。
cpp
08 struct Edge {
09 int to;
10 int weight;
11 };
12
13 struct State {
14 long long dist;
15 int u;
16 int used_freebie; // 0 for not used, 1 for used
17 bool operator>(const State &other) const {
18 return dist > other.dist;
19 }
20 };
EdgeEdgeEdge结构体保存一条边,tototo为到达的顶点,weightweightweight为边权,adjadjadj中保存的是EdgeEdgeEdge类型对象。
StateStateState类表示DijkstraDijkstraDijkstra堆优化算法进行过程中保存在优先队列中的状态,该状态表示一条从sss到uuu的路径。uuu是路径终点,distdistdist表示该路径的长度,used_freebieused\_freebieused_freebie表示是否使用过免费边。
cpp
34 vector<vector<long long>> d(n + 1, vector<long long>(2, INF));
35 priority_queue<State, vector<State>, greater<State>> pq;
36
37 d[s][0] = 0;
38 pq.push({0, s, ___①___});
声明ddd为n+1n+1n+1行222列的矩阵,结合下面在ddd的第二维使用了usedusedused变量,因此第二维的值表示是否用过免费边。d[i][j]d[i][j]d[i][j]表示从sss出发到顶点iii,使用免费边的情况为jjj(jjj为0表示未使用过免费边,jjj为1表示使用过免费边),的所有路径中,长度最短的路径的长度。到达一个顶点是否使用免费边,可以看作两种情况,如果将这两种情况看作两个顶点,那么该图就是一个分层图。
声明优先队列pqpqpq,使用的比较规则为greater<State>,STL中greater的实现大概为:
cpp
template<class T>
struct Greater
{
bool operator () (const &T a, const &T b)
{
return a > b;
}
}
本题中TTT类型为StateStateState,因此StateStateState类必须重载>运算符,看17~19行,实现优先队列,如果自己与传入的参数bbb比较,返回值为真时bbb更优先,即需要返回bbb优先的条件。本题代码中bbb为otherotherother,返回dist > other.dist,表明优先队列各StateStateState类型元素中,distdistdist属性越小,优先级越高。因此每次取优先队列的队头(堆顶),取到的是优先队列中distdistdist属性最小的StateStateState对象。
首先到达起点sss的不使用免费边的路径长度为000,设d[s][0]=0d[s][0]=0d[s][0]=0,而后将该路径状态{dist=0,u=s,used_freebie=0}\{dist=0,u=s,used\_freebie = 0\}{dist=0,u=s,used_freebie=0}加入优先队列,因此第(1)空填0,选A。
cpp
40 while (!pq.empty()) {
41 State current = pq.top();
42 pq.pop();
43
44 long long dist = current.dist;
45 int u = current.u;
46 int used = current.used_freebie;
47
48 if (dist > ___②___) {
49 continue;
50 }
只要优先队列不空,每次循环出队一个路径状态。取到出队的状态的属性:到达顶点uuu,路径长度distdistdist,是否使用过免费边usedusedused。
如果先前已经存在过到达顶点uuu,使用免费边情况为usedusedused的路径,长度为d[u][used]d[u][used]d[u][used],称该状态为状态1。当前出队的状态称为状态2。如果状态2表示的路径的长度distdistdist比状态1的路径长度d[u][used]d[u][used]d[u][used]更大,那么通过状态1扩展得到的路径的长度一定小于通过状态2扩展得到的路径的长度,本题求最短路径长度,因此状态2就没有必要继续扩展了,直接进行下一次循环。所以第(2)空填d[u][used]d[u][used]d[u][used],选B
cpp
52 for (const auto &edge : adj[u]) {
53 int v = edge.to;
54 int w = edge.weight;
55
56 if (d[u][used] + w < ___③___) {
57 ___③___ = d[u][used] + w;
58 pq.push({___③___, v, used});
59 }
60
61 if (used == 0) {
62 if (___④___ < d[v][1]) {
63 d[v][1] = d[u][used];
64 pq.push({d[v][1], v, 1});
65 }
66 }
遍历uuu的邻接点,其邻接点为vvv,边(u,v)(u,v)(u,v)的权值为www
首先考虑不将(u,v)(u,v)(u,v)边当做免费边的情况,那么到达顶点uuu和vvv使用免费边的情况usedusedused相同。那么下面一段可以暂时不考虑免费边使用情况usedusedused
已知到达顶点uuu的路径长度为d[u][used]d[u][used]d[u][used],先通过该路径到达uuu,而后再通过(u,v)(u,v)(u,v)边到达vvv,这样一条到达顶点vvv的路径的长度为d[u][used]+wd[u][used]+wd[u][used]+w。如果这条新路径的长度d[u][used]+wd[u][used]+wd[u][used]+w比已有的到达顶点vvv的最短路径长度d[v][used]d[v][used]d[v][used]更小,那么到达顶点vvv,使用免费边情况为usedusedused的最短路径的长度d[v][used]d[v][used]d[v][used]就应该设为d[u][used]+wd[u][used]+wd[u][used]+w,将新的路径状态{dist=d[v][used],u=v,used_freebie=used}\{dist=d[v][used],u=v,used\_freebie = used\}{dist=d[v][used],u=v,used_freebie=used}加入优先队列。因此第(3)空填d[v][used]d[v][used]d[v][used],选B 。
接下来考虑(u,v)(u,v)(u,v)当做免费边的情况:如果当前路径usedusedused为0,说明该路径上还没有用过免费边。当前到达顶点uuu的路径的长度为distdistdist,也就是d[u][0]d[u][0]d[u][0]。如果从顶点uuu再通过一条免费边到达顶点vvv,该到达顶点vvv的路径的长度为d[u][0]d[u][0]d[u][0],如果该长度比已经记录的到达顶点vvv已用过免费边的最短路径长度d[v][1]d[v][1]d[v][1]更短,那么到达顶点vvv的已用过免费边的最短路径长度要设为d[u][0]d[u][0]d[u][0],存在一条到达顶点vvv,路径长度为d[v][1]d[v][1]d[v][1],已使用过免费边的路径,将状态{dist=d[v][1],u=v,used_freebie=1}\{dist=d[v][1],u=v,used\_freebie = 1\}{dist=d[v][1],u=v,used_freebie=1}加入优先队列。
因此第(4)空填distdistdist或d[u][0]d[u][0]d[u][0],选C:d[u][0]d[u][0]d[u][0]
cpp
70 cout << __⑤__ << endl;
71 return 0;
72 }
求sss到ttt的最短路径的长度
- 如果使用了免费边,sss到ttt的最短路径的长度为d[t][1]d[t][1]d[t][1]
- 如果没有使用免费边,sss到ttt的最短路径的长度为d[t][0]d[t][0]d[t][0]
最终结果应该为二者的最小值。第(5)空填min(d[t][1],d[t][0]min(d[t][1], d[t][0]min(d[t][1],d[t][0]),选C。
当起点sss和终点ttt相同时,使用免费边反而可能使得路径长度增加。
如s=1,t=2s=1, t=2s=1,t=2,图中只存在一条权值为111的无向边(1,2)(1,2)(1,2),根据该算法,如果不使用免费边,求出的d[t][0]=0d[t][0]=0d[t][0]=0。如果从111到222使用免费边,从222到111就不能再使用免费边,222到111的边权为1,因此求出的d[t][1]=1d[t][1]=1d[t][1]=1,实际上最短路径长度应该为min(d[t][0],d[t][1])=0min(d[t][0],d[t][1])=0min(d[t][0],d[t][1])=0
【题目答案】
- A
- B
- B
- C
- C