CSP 2023 提高级第一轮 CSP-S 2023初试题 完善程序第一题解析

三、完善程序(单选题,每小题 3 分,共计 30 分)

一、题目阅读

(第 k 小路径)给定一张 n 个点 m 条边的有向无环图,定点编号从 0 到 n−1,对于一条路径,我们定义“路径序列”为该路径从起点出发依次经过的顶点编号构成的序列。求所有至少包含一个点的简单路径中,“路径序列”字典序第 k 小的路径。保证存在至少 k 条路径。上述参数满足 1≤n,m≤105,1≤k≤1018。

在程序中,我们求出从每个点出发的路径数量。超过 1018 的数都用 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,2有边相连,那么1算一条路径,1 -> 2算一条路径,2也算一条。

按字典序排好后就是:1,1 -> 2,2。

代码解析:

首先处理各个顶点之间的关系,并拓扑排序,然后倒过来(这是动态规划的常态,从最后开始。变色部分会解释)

接下来时整个推算过程。

f[i]表示以 i 为起点的路径总数,那么我们可以推算出这样的一个公式。

f[u] = f[E[u][0]]+f[E[u][1]]+...+f[E[u][n]]   +  1

E[u][1, 2, ... , n]代表u的邻接节点。

为什么呢?假设u只有一个邻接节点v,u为起点的路径总数个数是1(u->v) + f[v](u->v->...,其中v->...部分就是f[v])。

看到第41行,我们知道 f 数组是用来存放以某个节点为起点的路径数量,那么我们应该去遍历它的每个邻接节点,这就是为什么要反转结果了,反转之后,与其他关联最多的总是已经处理(请注意,拓扑排序不是那度数排序!别踩坑)。

然后我们计算出以它为起点出发的路径数量。

next函数的作用如下:

        接着我们第一次调用next函数。

        cand就是Q。

        将它排好序之后就是第一个顶点的选择,那么按字典序来肯定选最小的啊,所以我们取了第一个元素。

        接着按照题意,我们应该看看,我以u做起点的路径个数够不够k,如果够了,那么就不要往下了,反正肯定就是以u开始了。如果不够,那还要取下一个节点做开始,以此类推......

        当够了的时候,我们要把k减小,减小前面的总个数。(这好比把单位一点一点缩小)

        接着每次将对应起点的临界矩阵送给next函数同样道理地处理。

        这里说明一下⑤处是填【k--】的,因为next函数把k减去的部分是经过u的邻接节点构成的所有路径,没有包括u单独组成的一条路径。【那为什么不直接减去f[u]呢?因为k减去的往往只是f[u]里边的一小部分。】

好了如果听了上面还没懂,那就往下看看,我们假定一组输入数据去试试。

k=3

        

这张图拓扑排序之后的结果是:4,1,2,3(结果不唯一,但是按题目做法是这样)

翻转过来之后是:3,2,1,4

那对应的 f数组就是 {

3:1(本身)

2:1(本身)

1:3(本身+f[3]+f[2])

4:4(本身+f[1])

}

按顺序排下来:3,1,1,4。

接下来调用next函数找出第k小路径的第一个节点。

第一次,找到1,因为最小路径的开始一定是最小的。

*①处填【k <= f[u]】

f[1]=3小于4,所以k减去3 = 1

f[2]=1,1<=1,所以返回2。

输出2。

*第④⑤处填【k > 1,不填 > 0是因为next函数里没有减去u节点本身构成的路径】

                    【--k,把u本身构成的路径减去】

然而,k==1,无法进入循环,所以2就是答案。

三、题目分析

1. ①处应填(B)

A. k >= f[u]

B. k <= f[u]
C. k > f[u]
D. k < f[u]

【当剩下的第k项包括在f[u]内时,说明可以确定u了】

2. ②处应填()

A. deg[v] == 1

B.  deg[v] == 0
C.  deg[v] > 1
D.  deg[v] > 0

【因为他--deg[v]放在后面,所以为1的马上就为0了,这里一定不要掉入陷阱~】

3. ③处应填(A)

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)

【这题千万不要想复杂,他只是限制最大值而已,题目规定超过LIM的用LIM表示,而剩下的我们只需要加上与自己相关的边的答案~】

4. ④处应填(D)

A. u != -1

B. !E[u].empty()

C. k > 0
D.  k > 1

不填 > 0是因为next函数里没有减去u节点本身构成的路径】

5. ⑤处应填(C)

A. K+=f[u]

B.  k-=f[u]
C. --k
D. ++k

把u本身构成的路径减去】

看不懂评论区见,大家一起解答,有问题请反应~

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值