【题目】
CSP-S 2024 提高级 第一轮(初赛) 完善程序(2)
(2)(次短路)已知一个n个点m条边的有向图G,并且给定图中的两个点s和t,求次短路(长度严格大于最短路的最短路径)。如果不存在,输出一行“-1”。如果存在,输出两行,第一行表示次短路的长度,第二行表示次短路的一个方案。
#include <cstdio>
#include <queue>
#include <utility>
#include <cstring>
using namespace std;
const int maxn = 2e5 + 10, maxm = 1e6 + 10, inf = 522133279;
int n, m, s, t;
int head[maxn], nxt[maxm], to[maxm], w[maxm], tot = 1;
int dis[maxn << 1], *dis2;
int pre[maxn << 1], *pre2;
bool vis[maxn << 1];
void add(int a, int b, int c) {++tot;nxt[tot] = head[a];to[tot] = b;w[tot] = c;head[a] = tot;
}
bool upd(int a, int b, int d, priority_queue<pair<int, int> > &q) {if (d >= dis[b])return false;if (b < n) ___(1)___;q.push(___(2)___);dis[b] = d;pre[b] = a;return true;
}
void solve() {priority_queue<pair<int, int> >q;q.push(make_pair(0, s));memset(dis, ___(3)___, sizeof(dis));memset(pre, -1, sizeof(pre));dis2 = dis + n;pre2 = pre + n;dis[s] = 0;while (!q.empty()) {int aa = q.top().second; q.pop();if (vis[aa])continue;vis[aa] = true;int a = aa % n;for (int e = head[a]; e ; e = nxt[e]) {int b = to[e], c = w[e];if (aa < n) {if (!upd(a, b, dis[a] + c, q))___(4)___ } else {upd(n + a, n + b, dis2[a] + c, q);}}}
}
void out(int a) {if (a != s) {if (a < n) out(pre[a]);else out(___(5)___);}printf("%d%c", a % n + 1, " \n"[a == n + t]);
}
int main() {scanf("%d%d%d%d", &n, &m,&s,&t);s--, t--;for (int i = 0; i < m; ++i) {int a, b, c;scanf("%d%d%d", &a, &b, &c);add(a - 1, b - 1, c);}solve();if (dis2[t] == inf) puts("-1");else {printf("%d\n", dis2[t]);out(n + t);}return 0;
}
- (1)处应填( )
A.udp(pre[b],n+b,dis[b],q)
B.upd(a,n+b,d,q)
C.upd(pre[b],b,dis[b],q)
D.upd(a,b,d,q) - (2)处应填( )
A.make_pair(-d,b)
B.make_pair(d,b)
C.make_pair(b,d)
D.make_pair(-b,d) - (3)处应填( )
A.0xff
B.0x1f
C.0x3f
D.0x7f - (4)处应填( )
A.upd(a,n+b,dis[a]+c,q)
B.upd(n+a,n+b,dis2[a]+c.q)
C.upd(n+a,b,dis2[a]+c,q)
D.upd(a,b,dis[a]+c,q) - (5)处应填( )
A.pre2[a%n]
B.pre[a%n]
C.pre2[a]
D.pre[a%n]+1
【题目考点】
1. 图论:次短路径
2. 图论:最短路 Dijkstra堆优化
【解题思路】
int main() {scanf("%d%d%d%d", &n, &m,&s,&t);s--, t--;for (int i = 0; i < m; ++i) {int a, b, c;scanf("%d%d%d", &a, &b, &c);add(a - 1, b - 1, c);}solve();if (dis2[t] == inf) puts("-1");else {printf("%d\n", dis2[t]);out(n + t);}return 0;
}
输入顶点数n,边数m,起点s,终点t
s和t都减1,以及下面添加边时传入a-1, b-1,表示该题输入时顶点编号为1~n,而代码内顶点编号从0~n-1。
循环m次,每次循环输入一条边,表示存在有向边<a, b>,权值为c。
void add(int a, int b, int c) {++tot;nxt[tot] = head[a];to[tot] = b;w[tot] = c;head[a] = tot;
}
add函数的链式前向星实现邻接表的常规操作,tot为链表中用到的最大结点地址,nxt[i]
表示地址为i的结点的下一个结点的地址,to[i]
表示地址为i的结点上的顶点编号,w[i]
表示地址为i的结点上的边的权值。head[a]
表示顶点a后面的单链表中第一个结点的地址。
主函数中线调用solve函数,而后如果dist2[t]为inf,也就是没有找到从起点s到终点t的严格次短路,则输出-1。否则输出起点s到终点t的严格次短路的长度dist2[t],再通过out函数输出次短路的路径。
void solve() {priority_queue<pair<int, int> >q;q.push(make_pair(0, s));memset(dis, ___(3)___, sizeof(dis));memset(pre, -1, sizeof(pre));dis2 = dis + n;pre2 = pre + n;dis[s] = 0;
}
solve函数的Dijkstra堆优化的模板。
优先队列q中保存数对(first, second)的意义是:存在到顶点second的长为first的路径。
首先存在到顶点s的长为0的路径,因此把数对(s, 0)加入优先队列。
优先队列的默认比较规则为less仿函数,该函数传入a、b两个优先队列中的元素,返回a<b,其意义为当满足b>a时,b更优先,优先级最高的元素在优先队列的堆顶。
pair类型已重载<运算符,比较方式是先比较第一个属性first,再比较第二个属性seond。先比较的first属性表示路径长度,first的值越大越优先。而作为求最短路的算法,应该是路径长度越小越优先。为了解决这一点,可以取路径长度的相反数作为pair对象的first属性,first作为负值越大,表明first的相反数越小。这样虽然优先队列是大顶堆,但维护的实际是已知的路径长度最小的路径。
int dis[maxn << 1], *dis2;
int pre[maxn << 1], *pre2;
...
dis2 = dis + n;
pre2 = pre + n;
根据dis数组长度为2*maxn,以及dis2实际指向dis[n],可知dis[0]~dis[n-1]
中dis[i]保存的是从s出发到i的最短路径长度。dis[n]~dis[2*n-1]
也就是dis2[0]~dis2[n]
中dis2[i]表示的是从s出发到i的严格次短路径。
同理,pre[n]~pre[2*n-1]
也就是pre2[0]~pre2[n]
pre[i]是顶点从s到i的最短路径上i的前一个顶点。
pre2[i]是顶点从s到i的严格次路径上i的前一个顶点。
主函数中,dis2[t]和inf比较,可知dis2数组,也就是dis数组的初值为inf。
inf的值为522133279,第(3)空每字节的值根据选项可能为0xff,0x1f, 0x3f, 0x7f
经过memset后,int类型dis数组中每个元素的值为4字节,
如每字节的值为0xff
,元素的初值为0xffffffff
如每字节的值为0x1f
,元素的初值为0x1f1f1f1f
如每字节的值为0x3f
,元素的初值为0x3f3f3f3f
如每字节的值为0x7f
,元素的初值为0x7f7f7f7f
元素初值的区别在十六进制下的右边数第2位,要想得到这一位的数字需要将元素值先整除16再模16。
⌊ 522133279 16 ⌋ m o d 16 = 1 \lfloor \frac{522133279}{16}\rfloor \mod16=1 ⌊16522133279⌋mod16=1,所以每字节的值为0x1f
,第(3)空选B。
pre初值都设为-1。
while (!q.empty()) {int aa = q.top().second; q.pop();if (vis[aa])continue;vis[aa] = true;int a = aa % n;for (int e = head[a]; e ; e = nxt[e]) {int b = to[e], c = w[e];if (aa < n) {if (!upd(a, b, dis[a] + c, q))___(4)___ } else {upd(n + a, n + b, dis2[a] + c, q);}}}
vis[i]的意义是顶点i是否已出队。对于Dijkstra堆优化算法,从起点到已经出队的顶点的最短路径长度不会再更新,因为后入队的路径的长度一定大于到该顶点的路径长度。因此如果堆顶元素中second是已经出队的顶点,就不再更新到其邻接点的最短路径。这就是为什么有if(vis[aa]) continue;
因为aa可能是大于等于n,用来表示次短路的顶点编号,所以先进行a = aa%n
把aa处理成其对应的真正顶点编号a。
遍历a的邻接点,b是a的一个邻接点,c是便<a, b>的权值。
如果aa<n,说明从优先队列出队的是一条从起点s到a的最短路径,这样的路径可以更新最短路以及次短路。
否则如果aa>=n,那么从优先队列出队的是一条从起点s到a的次短路径,这样的路径只可能更新次短路径。
更新次短路的核心思路为:
如果新路径比最短路径短更短,那么原最短路变为次短路,新路径作为最短路。
否则,如果新路径比严格次短路短,但是比最短路长,那么新路径作为严格次短路。
bool upd(int a, int b, int d, priority_queue<pair<int, int> > &q) {if (d >= dis[b])return false;if (b < n) ___(1)___;q.push(___(2)___);dis[b] = d;pre[b] = a;return true;
}
upd的意思是update,更新。
结合调用语句upd(a, b, dis[a] + c, q)
可知
参数a、b、d的意义是从存在一条从起点s到b的长为d的路径,最后一步是从a走到b的。
参数q是优先队列的引用,一直传q即可。
如果新路径长度d比已有的路径长度dis[b](可能是最短或次短路)更长,则不发生更新,返回false。
如果终点b<n,则当前到b新路径比到b的最短路长度更小,需要使用当前的最短路长度dis[b]更新到b的次短路长度dis2[b],实现方法为再次调用upd函数,参数a填路径上b的前一个顶点pre[b],参数b填b,参数d为dis[b],所以第(1)空填udp(pre[b],n+b,dis[b],q)
,选A。
无论b<n时dis[b]表示最短路,还是b>=n时dis[b]表示次短路,都需要使用新的到顶点b的长为d的路径来更新自己。
前文提过,在优先队列中保存的pair的first属性应该是路径长度的相反数,second属性为到达顶点。因此第(2)空填make_pair(-d, b)
,选A。
接下来将到顶点b的距离更新为d,路径上b的前一个顶点设为a。
只要成功更新路径,就返回true。
if (aa < n) {if (!upd(a, b, dis[a] + c, q))___(4)___ } else {upd(n + a, n + b, dis2[a] + c, q);}
如果upd(a, b, dis[a] + c, q)
返回假,也就是到顶点b的路径长度dis[a]+c大于等于最短路dis[b],不能更新最短路,那么尝试更新次短路。
调用upd函数更新到顶点b的次短路,前一个顶点是a,第一个参数传a。到b的次短路长度保存在dis[b+n],因此第二个参数传b+n,路径长度为dis[a]+c。因此第(4)空填upd(a, b+n, dis[a]+c, q)
,选A。
如果aa>=n,则出队的路径是个次短路径,只能更新次短路径,此时到达的顶点为n+b,其前一个顶点为aa,也就是a+n。新的路径长度为dis[a+n]+c,也就是dis2[a]+c。
调用solve函数,完成Dijkstra堆优化算法,求出了dis数组,其中包含到每个顶点的最短路和次短路的信息,pre数组保存了到每个顶点的最短路和次短路上该顶点的前一个顶点。
void out(int a) {if (a != s) {if (a < n) out(pre[a]);else out(___(5)___);}printf("%d%c", a % n + 1, " \n"[a == n + t]);
}
如果存在从s到t的次短路,最后输出次短路长度和次短路径。
先看输出语句,a % n + 1
是无论a<n还是a>=n,a%n
都将a处理成其表示的顶点编号,再加1,是因为原题目中顶点编号从1~n,而代码中顶点编号从0~n-1。
" \n"[a == n + t]
是一种很诡异的写法," \n"
相当于char s[2] = {' ', '\n'}
中的s。
当a != n+t
时," \n"[0]
,就是s[0]
,是' '
。
当a == n+t
时,也就是传入的a是次短路的终点t时," \n"[1]
,就是s[1]
,是'\n'
。
如果a==s
,则是递归出口,只做输出。
如果a!=s
,输出从起点s到a的次短路径,就是先输出从起点s到a的前一个顶点pre[a]的次短路径,再输出顶点a。
调用out(pre[a])
就可以输出从起点s到a的前一个顶点pre[a]的次短路径,无论a是否小于n,pre[a]都保存了次短路径上a的前一个顶点。
当a<n
时,调用了out(pre[a])
当a>=n
时,也应该调用out(pre[a])
,但是没有这个选项,看一看有没有等价的选项。
pre[a] = pre[a-n+n] = pre2[a-n] = pre2[a%n]
因此第(5)空填pre2[a%n]
,选A。