CSP-S 2023 提高级 第一轮(初赛) 完善程序(1)

【题目】

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} 1n,m105,1k1018
在程序中,我们求出从每个点出发的路径数量。超过 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 pcand.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] 1f[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]+1f[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]+1f[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]+1kf[u0]+f[u1]范围内,也就是 k − f [ u 0 ] k-f[u_0] kf[u0]是否满足 1 ≤ k − f [ u 0 ] ≤ f [ u 1 ] 1\le k-f[u_0]\le f[u_1] 1kf[u0]f[u1]
因此先让k减去 f [ u 0 ] f[u_0] f[u0],再判断k是否满足 k ≤ f [ u 1 ] k\le f[u_1] kf[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] kf[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。

【答案】

  1. B
  2. A
  3. A
  4. D
  5. C
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值