【题目】
CSP-S 2023 提高级 第一轮(初赛) 完善程序(1)
(第k小路径)给定一张n个点m条边的有向无环图,顶点编号从0到n−1,对于一条路径,我们定义“路径序列”为该路径从起点出发依次经过的顶点编号构成的序列。求所有至少包含一个点的简单路径中,“路径序列”字典序第k小的路径。保证存在至少k条路径。上述参数满足
1
≤
n
,
m
≤
1
0
5
,
1
≤
k
≤
1
0
18
1 \le n,m \le 10^5, 1 \le k \le 10^{18}
1≤n,m≤105,1≤k≤1018
在程序中,我们求出从每个点出发的路径数量。超过
1
0
18
10^{18}
1018的数都用
1
0
18
10^{18}
1018表示。然后我们根据k的值和每个顶点的路径数量,确定路径的起点,然后可以类似地依次求出路径中的每个点。
试补全程序。
#include <iostream>
#include <algorithm>
#include <vector>
const int MAXN = 100000;
const long long LIM = 1000000000000000000ll;
int n, m, deg[MAXN];
std::vector<int> E[MAXN];
long long k, f[MAXN];
int next(std::vector<int> cand, long long &k) {
std::sort(cand.begin(), cand.end());
for (int u : cand) {
if (①) return u;
k -= f[u];
}
return -1;
}
int main() {
std::cin >> n >> m >> k;
for (int i = 0; i < m; ++i) {
int u, v;
std::cin >> u >> v; // 一条从u到v的边
E[u].push_back(v);
++deg[v];
}
std::vector<int> Q;
for (int i = 0; i < n; ++i)
if (!deg[i]) Q.push_back(i);
for (int i = 0; i < n; ++i) {
int u = Q[i];
for (int v : E[u]) {
if (②)
Q.push_back(v);
--deg[v];
}
}
std::reverse(Q.begin(), Q.end());
for (int u : Q) {
f[u] = 1;
for (int v : E[u])
f[u] = ③;
}
int u = next(Q, k);
std::cout << u << std::endl;
while (④) {
⑤;
u = next(E[u], k);
std::cout << u << std::endl;
}
return 0;
}
1.①处应填()
A. k >= f[u]
B. k <= f[u]
C. k > f[u]
D. k < f[u]
2.②处应填()
A. deg[v] == 1
B. deg[v] == 0
C. deg[v] > 1
D. deg[v] > 0
3.③处应填()
A. std::min(f[u] + f[v], LIM)
B. std::min(f[u] + f[v] + 1, LIM)
C. std::min(f[u] * f[v], LIM)
D. std::min(f[u] * (f[v] + 1), LIM)
4.④处应填()
A. u != -1
B. !E[u].empty()
C. k > 0
D. k > 1
5.⑤处应填()
A. K+=f[u]
B. k-=f[u]
C. --k
D. ++k
【题目考点】
1. 图论:拓扑排序
2. 有向无环图动规
【解题思路】
std::cin >> n >> m >> k;
for (int i = 0; i < m; ++i) {
int u, v;
std::cin >> u >> v; // 一条从u到v的边
E[u].push_back(v);
++deg[v];
}
std::vector<int> Q;
for (int i = 0; i < n; ++i)
if (!deg[i]) Q.push_back(i);
for (int i = 0; i < n; ++i) {
int u = Q[i];
for (int v : E[u]) {
if (②)
Q.push_back(v);
--deg[v];
}
}
输入顶点数n,边数m,和k(要找第k条路径)。
输入m条边,使用vector表示的邻接表来存图。
E[u].push_back(v)
表示u到v有一条边,deg[v]
表示v的入度,++deg[v]
表示v的入度增加1。
接下来声明顺序表Q,遍历所有顶点,只要顶点i的入度为0,则把顶点加入Q。
该过程就是拓扑排序的过程,只不过该代码使用的是vector而不是队列queue。其原理是相同的。
而后遍历Q,只要Q中存在顶点u,就把u的所有邻接点v的入度减少1。根据拓扑排序的算法,如果v的入度减少1后入队,而现在是在v的入度减少前判断,如果满足该条件就将v添加进Q,那么此时应该填的条件为v的入度为1,则把Q加入顺序表Q。第(2)空填deg[v] == 1
,选A。
该算法遍历顺序表Q的过程,和原拓扑排序算法不断取队头的过程是一样的,后面添加进Q的顶点,随着i的增大还是会遍历到的。拓扑排序也是对整个图遍历的过程,每个顶点会访问一次,题目给定该图是有向无环图,一定可以完成拓扑排序,所以添加进Q的顶点数最终会有n个。当该循环结束时,Q中保存的就是该图中所有顶点的一个拓扑排序序列。
std::reverse(Q.begin(), Q.end());
for (int u : Q) {
f[u] = 1;
for (int v : E[u])
f[u] = ③;
}
接下来把顺序表Q中的元素前后顺序颠倒,而后遍历Q。取出顶点u。题目中提示了要“求出从每个点出发的路径数量”,现在f数组保存的就是从顶点u出发的路径数量。
这里我用(a, b, …, n)表示一条从a到b一直到n的路径。
那么顶点u出发的路径的一定包含(u),就是从u出发到u的路径,有1条,所以f[u]初值为1。
而后遍历u的邻接点v,对于每个顶点v,都有f[v]个从v出发的路径,根据拓扑排序的顺序,此时f[v]的值已经确定了。那么从u出发的路径,就要增加从u到v,接下来从v出发的所有路径,增加的路径数为f[v]。所以应该让f[u] = f[u]+f[v]
。
而统计路径数最大只统计到
1
0
18
10^{18}
1018,也就是代码中的LIM,因此f[u]的最大值如果超过LIM,就是设为LIM。因此该处填f[u] = min(f[u]+f[v], LIM)
,第(3)空选A。
接下来看一下next函数
int next(std::vector<int> cand, long long &k) {
std::sort(cand.begin(), cand.end());
for (int u : cand) {
if (①) return u;
k -= f[u];
}
return -1;
}
给定一个vector,名字是cand,传入k的引用。传引用可以使得在函数内部可以改变函数外部传入的变量的值。
先对cand进行从小到大的排序,根据主函数中的代码,cand要么是Q,要么是E[u],vector中保存的都是顶点编号,因此此处是根据顶点编号从小到大排序。
遍历顺序表cand,如果满足某一条件,就返回u。每次循环k减少f[u]。如果在循环中不产生返回值,最后返回-1。
比如现在是调用int u = next(Q, k);
,而进入了next函数。
已知k是当前要找的路径在所有路径中字典序的编号。
cand排序后其中的元素为cand[0], cand[1], ..., cand[cand.size()-1]
,记
p
p
p为cand.size()-1
,此处将这些顶点们记为
u
0
,
.
.
.
,
u
p
u_0,..., u_p
u0,...,up
从每个顶点出发的路径数分别为
f
[
u
0
]
,
f
[
u
1
]
,
.
.
.
,
f
[
u
p
]
f[u_0], f[u_1], ..., f[u_p]
f[u0],f[u1],...,f[up]
现在要找第k条路径
路径起点 | 路径字典序编号范围 |
---|---|
u 0 u_0 u0 | 1 ∼ f [ u 0 ] 1\sim f[u_0] 1∼f[u0] |
u 1 u_1 u1 | f [ u 0 ] + 1 ∼ f [ u 0 ] + f [ u 1 ] f[u_0]+1\sim f[u_0]+f[u_1] f[u0]+1∼f[u0]+f[u1] |
u 2 u_2 u2 | f [ u 0 ] + f [ u 1 ] + 1 ∼ f [ u 0 ] + f [ u 1 ] + f [ u 2 ] f[u_0]+f[u_1]+1\sim f[u_0]+f[u_1]+f[u_2] f[u0]+f[u1]+1∼f[u0]+f[u1]+f[u2] |
… | … |
如果
k
<
=
f
[
u
0
]
k<=f[u_0]
k<=f[u0],那么第k小路径的第一个顶点就是
u
0
u_0
u0,
如果
k
>
f
[
u
0
]
k> f[u_0]
k>f[u0],那么就看
k
k
k是否满足
f
[
u
0
]
+
1
≤
k
≤
f
[
u
0
]
+
f
[
u
1
]
f[u_0]+1\le k \le f[u_0]+f[u_1]
f[u0]+1≤k≤f[u0]+f[u1]范围内,也就是
k
−
f
[
u
0
]
k-f[u_0]
k−f[u0]是否满足
1
≤
k
−
f
[
u
0
]
≤
f
[
u
1
]
1\le k-f[u_0]\le f[u_1]
1≤k−f[u0]≤f[u1]。
因此先让k减去
f
[
u
0
]
f[u_0]
f[u0],再判断k是否满足
k
≤
f
[
u
1
]
k\le f[u_1]
k≤f[u1],如果满足,那么第k小路径的第一个顶点就是
u
1
u_1
u1。
再让k减去
f
[
u
1
]
f[u_1]
f[u1],判断k是否满足
k
≤
f
[
u
2
]
k\le f[u_2]
k≤f[u2],如果满足,那么第k小路径的第一个顶点就是
u
2
u_2
u2,依此类推。
因此第(1)空应该填k <= f[u]
,选B。
int u = next(Q, k);
std::cout << u << std::endl;
while (④) {
⑤;
u = next(E[u], k);
std::cout << u << std::endl;
}
从Q中选出了第k小路径的起始顶点u,并输出。
接下来是一个循环,每次从u的邻接点E[u]中选出第k小路径的下一个顶点u,并输出。在这一过程中,k是不断减小的。
在选择了顶点u之后,要找的是从顶点u出发的第k小的路径。假设u的邻接点(即E[u]中的顶点)从小到大排序后为
v
0
,
v
1
,
v
2
,
.
.
.
v_0, v_1, v_2,...
v0,v1,v2,...
从u出发的路径有:
只有一个u:
(
u
)
(u)
(u)
u下一个顶点是
v
0
v_0
v0的路径:
(
u
,
v
0
,
.
.
.
)
(u, v_0, ...)
(u,v0,...)
u下一个顶点是
v
1
v_1
v1的路径:
(
u
,
v
1
,
.
.
.
)
(u, v_1, ...)
(u,v1,...)
…
如果在输出u后k为1,那么说明要选的路径是从u出发的第一条路径,这条路路径上只有一个u。而这个u已经输出了,因此任务已经完成了,应该跳出循环,所以循环进行的条件是
k
>
1
k>1
k>1,因此第(4)空填k > 1
,选D。
再次进入while循环,说明满足
k
>
1
k>1
k>1
next函数求是u的下一个顶点在顶点序列
v
0
,
v
1
,
v
2
,
.
.
.
v_0, v_1, v_2,...
v0,v1,v2,...中时,第k条路径的下一个顶点是哪一个。此时第1条路径u的下一个顶点为
v
0
v_0
v0,而不应该是只有一个u的路径
(
u
)
(u)
(u)。因此应该先让k减少1,表示不再考路径
(
u
)
(u)
(u)。因此第(5)空应该填--k
,选C。
【答案】
- B
- A
- A
- D
- C