讲义
课程
作业
真题
竞赛
课时
模板
  • 2023天津CSP-J组 第一轮 成绩
  • 2023天津CSP-S组 第一轮 成绩
  • 2023天津CSP第二轮晋级名单(J&S)
  • 2023竞赛大纲
  • 2023机试环境(禁止使用手机下载,镜像文件过大)
课程
作业
真题
竞赛
课时
模板
  • 2023天津CSP-J组 第一轮 成绩
  • 2023天津CSP-S组 第一轮 成绩
  • 2023天津CSP第二轮晋级名单(J&S)
  • 2023竞赛大纲
  • 2023机试环境(禁止使用手机下载,镜像文件过大)

2023 csp-j 复赛真题

2024/10/13

[CSP-J 2023] 小苹果

题目描述

小 Y 的桌子上放着 nnn 个苹果从左到右排成一列,编号为从 111 到 nnn。

小苞是小 Y 的好朋友,每天她都会从中拿走一些苹果。

每天在拿的时候,小苞都是从左侧第 111 个苹果开始、每隔 222 个苹果拿走 111 个苹果。随后小苞会将剩下的苹果按原先的顺序重新排成一列。

小苞想知道,多少天能拿完所有的苹果,而编号为 nnn 的苹果是在第几天被拿走的?

输入格式

输入的第一行包含一个正整数 nnn,表示苹果的总数。

输出格式

输出一行包含两个正整数,两个整数之间由一个空格隔开,分别表示小苞拿走所有苹果所需的天数以及拿走编号为 nnn 的苹果是在第几天。

样例 #1

样例输入 #1

8

样例输出 #1

5 5

提示

【样例 111 解释】

小苞的桌上一共放了 888 个苹果。
小苞第一天拿走了编号为 111、444、777 的苹果。
小苞第二天拿走了编号为 222、666 的苹果。
小苞第三天拿走了编号为 333 的苹果。
小苞第四天拿走了编号为 555 的苹果。
小苞第五天拿走了编号为 888 的苹果。

【样例 222】

见选手目录下的 apple/apple2.in 与 apple/apple2.ans。

【数据范围】

对于所有测试数据有:1≤n≤1091\leq n\leq 10^91≤n≤109。

测试点n≤n\leqn≤特殊性质
1∼21\sim 21∼2101010无
3∼53\sim 53∼510310^3103无
6∼76\sim 76∼710610^6106有
8∼98\sim 98∼910610^6106无
10101010910^9109无

特殊性质:小苞第一天就取走编号为 nnn 的苹果。

解析

题目描述

有 nnn 个苹果手机排成一排,编号为 1 到 n。每天从最左边的第 1 个苹果手机开始,每隔 2 个拿一个苹果手机,拿完后将剩下的苹果手机重新排成一排。我们需要求解以下问题:

  1. 多少天能拿完所有苹果手机?
  2. 第 nnn 个苹果手机是在第几天被拿走的?

解决方案

我们可以将“每隔 2 个拿一个苹果手机”理解为:将苹果手机每 3 个分为一组,每次取该组的第一个苹果手机。例如,对于有 11 个苹果手机的情况:

苹果手机编号1234567891011
取走情况取取取取取

标红的表示在这一轮被拿走的苹果手机。若剩余的苹果手机不足以构成完整的 3 个一组,则按不完整的一组来处理。

思路 1:求天数

每一轮取走的苹果手机数量等于分成的组数,即 ⌈n3⌉\lceil \frac{n}{3} \rceil⌈3n​⌉。我们通过暴力模拟,逐轮计算,将 nnn 减去 ⌈n3⌉\lceil \frac{n}{3} \rceil⌈3n​⌉,直到 n=0n = 0n=0,操作的次数即为天数。

思路 2:第 n 个苹果手机的取走时间

最后一个苹果手机必然是在最后一组中。若要取走它,相当于该苹果手机处于最后一组的第一个。例如,对于 11 和 10 个苹果手机的情况:

  • n=11n = 11n=11 时,最后一个苹果手机编号为 11,且它位于最后一组的第一个;
  • n=10n = 10n=10 时,最后一个苹果手机编号为 10,但它不是这一组的第一个,故不在该轮被取走。

可以发现,当 nmod  3=1n \mod 3 = 1nmod3=1 时,最后一个苹果手机是在该轮被取走的。因此,我们在暴力模拟中判断当前的 nnn 是否满足 nmod  3=1n \mod 3 = 1nmod3=1,若满足,则记录该轮数作为答案。

注意:我们只记录第一次出现 nmod  3=1n \mod 3 = 1nmod3=1 的轮数,后续若有再出现则不再记录。

时间复杂度分析

每次操作将 nnn 减去 ⌈n3⌉\lceil \frac{n}{3} \rceil⌈3n​⌉,大约相当于 n←23nn \leftarrow \frac{2}{3}nn←32​n,可以近似认为每次将 nnn 缩小一半。因此,时间复杂度为 Θ(log⁡2n)\Theta(\log_2 n)Θ(log2​n),实际运行时可能会稍高一些。

代码实现

#include <iostream>
#include <cmath> 

using namespace std;

int n, res1, res2;

int main() {
    freopen("apple.in", "r", stdin);  // 重定向输入文件
    freopen("apple.out", "w", stdout);  // 重定向输出文件
    
    cin >> n;  // 读取苹果手机的数量
    
    for (int i = 1;; ++i) {
        if (n == 0) break;  // 当苹果手机数量为 0 时,停止
        if (!res2 && n % 3 == 1) res2 = i;  // 记录第一次 n % 3 == 1 的轮数
        n -= ceil(n / 3.0);  // 每轮取走的苹果数量
        ++res1;  // 记录天数
    }
    
    cout << res1 << ' ' << res2 << '\n';  // 输出答案
    
    return 0;  
}

代码解析

  1. 输入输出重定向:通过 freopen 函数实现从文件中读取输入并将输出写入文件。
  2. 输入 n:表示苹果手机的数量。
  3. 主循环:
    • 每一轮循环中,将苹果手机数量按规则减少。
    • 在第一次满足 nmod  3=1n \mod 3 = 1nmod3=1 时,记录当前的轮数。
  4. 结果输出:输出总共的天数 res1 以及第 nnn 个苹果手机被取走的轮数 res2。

[CSP-J 2023] 公路

题目描述

小苞准备开着车沿着公路自驾。

公路上一共有 nnn 个站点,编号为从 111 到 nnn。其中站点 iii 与站点 i+1i + 1i+1 的距离为 viv_ivi​ 公里。

公路上每个站点都可以加油,编号为 iii 的站点一升油的价格为 aia_iai​ 元,且每个站点只出售整数升的油。

小苞想从站点 111 开车到站点 nnn,一开始小苞在站点 111 且车的油箱是空的。已知车的油箱足够大,可以装下任意多的油,且每升油可以让车前进 ddd 公里。问小苞从站点 111 开到站点 nnn,至少要花多少钱加油?

输入格式

输入的第一行包含两个正整数 nnn 和 ddd,分别表示公路上站点的数量和车每升油可以前进的距离。

输入的第二行包含 n−1n - 1n−1 个正整数 v1,v2…vn−1v_1, v_2\dots v_{n-1}v1​,v2​…vn−1​,分别表示站点间的距离。

输入的第三行包含 nnn 个正整数 a1,a2…ana_1, a_2 \dots a_na1​,a2​…an​,分别表示在不同站点加油的价格。

输出格式

输出一行,仅包含一个正整数,表示从站点 111 开到站点 nnn,小苞至少要花多少钱加油。

样例 #1

样例输入 #1

5 4
10 10 10 10
9 8 9 6 5

样例输出 #1

79

提示

【样例 1 解释】

最优方案下:小苞在站点 111 买了 333 升油,在站点 222 购买了 555 升油,在站点 444 购买了 222 升油。

【样例 2】

见选手目录下的 road/road2.in 与 road/road2.ans。

【数据范围】

对于所有测试数据保证:1≤n≤1051 \leq n \leq 10^51≤n≤105,1≤d≤1051 \leq d \leq 10^51≤d≤105,1≤vi≤1051 \leq v_i \leq 10^51≤vi​≤105,1≤ai≤1051 \leq a_i \leq 10^51≤ai​≤105。

测试点n≤n \leqn≤特殊性质
1∼51\sim 51∼5888无
6∼106\sim 106∼1010310^3103无
11∼1311\sim 1311∼1310510^5105A
14∼1614\sim 1614∼1610510^5105B
17∼2017\sim 2017∼2010510^5105无
  • 特殊性质 A:站点 111 的油价最低。
  • 特殊性质 B:对于所有 1≤i<n1 \leq i < n1≤i<n,viv_ivi​ 为 ddd 的倍数。

解析

反悔贪心思想

从左到右遍历每个加油站,当需要加油时,从之前经过的最便宜的加油站加油。这种策略确保每次加油的成本最小。

关键思路:

  1. 维护变量 fff:表示当前状态。

    • f<0f < 0f<0 表示当前还剩下的油能行驶的公里数。
    • f≥0f \geq 0f≥0 表示需要加油,加油量为 ⌈sd⌉\lceil \frac{s}{d} \rceil⌈ds​⌉,即需要加油来继续行驶。
  2. 优化加油成本:当我们走到一个加油站时,回顾之前经过的加油站,选择加油费用最低的站点进行加油,以达到节省总费用的目的。

代码中的实现细节:

  • 需要注意数据范围较大,因此所有涉及油量和费用的变量都需要使用 long long 类型。
  • 时间复杂度为 O(n)O(n)O(n),因为只需要遍历每个加油站一次。

代码实现

#include <bits/stdc++.h>  // 标准库的集合

using namespace std;  // 使用标准命名空间

using LL = long long;  // 定义 long long 类型的别名

const int N = 1e5 + 10;  // 定义最大数组长度

int v[N], a[N];  // v 用来存储距离,a 用来存储加油站的价格
int n, d;  // n 表示加油站数量,d 表示每次加油能走的距离

int main() {
    scanf("%d%d", &n, &d);  // 输入加油站数量和每次加油的最大行驶距离
    for (int i = 1; i < n; i++) scanf("%d", &v[i]);  // 输入各加油站间的距离
    int mi = INT_MAX;  // 初始化最小加油价格为最大整型值
    LL ans = 0, s = 0;  // ans 表示总费用,s 表示当前还需行驶的距离
    for (int i = 1; i < n; i++) {
        scanf("%d", &a[i]);  // 输入每个加油站的价格
        s += v[i];  // 累加当前需要行驶的距离
        mi = min(mi, a[i]);  // 更新到目前为止的最低加油价格
        if (s > 0) {  // 如果需要加油
            ans += (s + d - 1) / d * mi;  // 计算加油费用
            s -= (s + d - 1) / d * d;  // 减去已加油能够行驶的距离
        }
    }
    printf("%lld\n", ans);  // 输出总费用
    return 0;  // 结束程序
}

代码解析

  1. 输入与初始化:首先读取加油站的数量 nnn 和每次加油能够行驶的距离 ddd,然后初始化最小价格变量 mimimi 和总费用 ansansans。

  2. 距离与价格的计算:遍历每个加油站,计算累计需要行驶的距离 sss 并不断更新最小加油价格 mimimi。当需要加油时,根据当前行驶的距离和最小价格计算费用,并更新剩余的行驶距离。

  3. 高效加油策略:通过这种贪心策略,每次都选择最便宜的加油站加油,确保总体费用最低。

总结

该方案利用贪心思想解决了最小化加油成本的问题。其核心在于通过回顾性选择,确保每次加油的费用尽可能低。

[CSP-J 2023] 一元二次方程

题目背景

众所周知,对一元二次方程 ax2+bx+c=0,(a≠0)ax ^ 2 + bx + c = 0, (a \neq 0)ax2+bx+c=0,(a=0),可以用以下方式求实数解:

  • 计算 Δ=b2−4ac\Delta = b ^ 2 - 4acΔ=b2−4ac,则:
    1. 若 Δ<0\Delta < 0Δ<0,则该一元二次方程无实数解。
    2. 否则 Δ≥0\Delta \geq 0Δ≥0,此时该一元二次方程有两个实数解 x1,2=−b±Δ2ax _ {1, 2} = \frac{-b \pm \sqrt \Delta}{2a}x1,2​=2a−b±Δ​​。

例如:

  • x2+x+1=0x ^ 2 + x + 1 = 0x2+x+1=0 无实数解,因为 Δ=12−4×1×1=−3<0\Delta = 1 ^ 2 - 4 \times 1 \times 1 = -3 < 0Δ=12−4×1×1=−3<0。
  • x2−2x+1=0x ^ 2 - 2x + 1 = 0x2−2x+1=0 有两相等实数解 x1,2=1x _ {1, 2} = 1x1,2​=1。
  • x2−3x+2=0x ^ 2 - 3x + 2 = 0x2−3x+2=0 有两互异实数解 x1=1,x2=2x _ 1 = 1, x _ 2 = 2x1​=1,x2​=2。

在题面描述中 aaa 和 bbb 的最大公因数使用 gcd⁡(a,b)\gcd(a, b)gcd(a,b) 表示。例如 121212 和 181818 的最大公因数是 666,即 gcd⁡(12,18)=6\gcd(12, 18) = 6gcd(12,18)=6。

题目描述

现在给定一个一元二次方程的系数 a,b,ca, b, ca,b,c,其中 a,b,ca, b, ca,b,c 均为整数且 a≠0a \neq 0a=0。你需要判断一元二次方程 ax2+bx+c=0a x ^ 2 + bx + c = 0ax2+bx+c=0 是否有实数解,并按要求的格式输出。

在本题中输出有理数 vvv 时须遵循以下规则:

  • 由有理数的定义,存在唯一的两个整数 ppp 和 qqq,满足 q>0q > 0q>0,gcd⁡(p,q)=1\gcd(p, q) = 1gcd(p,q)=1 且 v=pqv = \frac pqv=qp​。

  • 若 q=1q = 1q=1,则输出 {p},否则输出 {p}/{q},其中 {n} 代表整数 nnn 的值;

  • 例如:

    • 当 v=−0.5v = -0.5v=−0.5 时,ppp 和 qqq 的值分别为 −1-1−1 和 222,则应输出 -1/2;
    • 当 v=0v = 0v=0 时,ppp 和 qqq 的值分别为 000 和 111,则应输出 0。

对于方程的求解,分两种情况讨论:

  1. 若 Δ=b2−4ac<0\Delta = b ^ 2 - 4ac < 0Δ=b2−4ac<0,则表明方程无实数解,此时你应当输出 NO;

  2. 否则 Δ≥0\Delta \geq 0Δ≥0,此时方程有两解(可能相等),记其中较大者为 xxx,则:

    1. 若 xxx 为有理数,则按有理数的格式输出 xxx。

    2. 否则根据上文公式,xxx 可以被唯一表示为 x=q1+q2rx = q _ 1 + q _ 2 \sqrt rx=q1​+q2​r​ 的形式,其中:

      • q1,q2q _ 1, q _ 2q1​,q2​ 为有理数,且 q2>0q _ 2 > 0q2​>0;
      • rrr 为正整数且 r>1r > 1r>1,且不存在正整数 d>1d > 1d>1 使 d2∣rd ^ 2 \mid rd2∣r(即 rrr 不应是 d2d ^ 2d2 的倍数);

    此时:

    1. 若 q1≠0q _ 1 \neq 0q1​=0,则按有理数的格式输出 q1q _ 1q1​,并再输出一个加号 +;
    2. 否则跳过这一步输出;

    随后:

    1. 若 q2=1q _ 2 = 1q2​=1,则输出 sqrt({r});
    2. 否则若 q2q _ 2q2​ 为整数,则输出 {q2}*sqrt({r});
    3. 否则若 q3=1q2q _ 3 = \frac 1{q _ 2}q3​=q2​1​ 为整数,则输出 sqrt({r})/{q3};
    4. 否则可以证明存在唯一整数 c,dc, dc,d 满足 c,d>1,gcd⁡(c,d)=1c, d > 1, \gcd(c, d) = 1c,d>1,gcd(c,d)=1 且 q2=cdq _ 2 = \frac cdq2​=dc​,此时输出 {c}*sqrt({r})/{d};

    上述表示中 {n} 代表整数 {n} 的值,详见样例。

    如果方程有实数解,则按要求的格式输出两个实数解中的较大者。否则若方程没有实数解,则输出 NO。

输入格式

输入的第一行包含两个正整数 T,MT, MT,M,分别表示方程数和系数的绝对值上限。

接下来 TTT 行,每行包含三个整数 a,b,ca, b, ca,b,c。

输出格式

输出 TTT 行,每行包含一个字符串,表示对应询问的答案,格式如题面所述。

每行输出的字符串中间不应包含任何空格。

样例 #1

样例输入 #1

9 1000
1 -1 0
-1 -1 -1
1 -2 1
1 5 4
4 4 1
1 0 -432
1 -3 1
2 -4 1
1 7 1

样例输出 #1

1
NO
1
-1
-1/2
12*sqrt(3)
3/2+sqrt(5)/2
1+sqrt(2)/2
-7/2+3*sqrt(5)/2

提示

【样例 #2】

见附件中的 uqe/uqe2.in 与 uqe/uqe2.ans。

【数据范围】

对于所有数据有:1≤T≤50001 \leq T \leq 50001≤T≤5000,1≤M≤1031 \leq M \leq 10 ^ 31≤M≤103,∣a∣,∣b∣,∣c∣≤M|a|,|b|,|c| \leq M∣a∣,∣b∣,∣c∣≤M,a≠0a \neq 0a=0。

测试点编号M≤M \leqM≤特殊性质 A特殊性质 B特殊性质 C
111111是是是
222202020否否否
33310310 ^ 3103是否是
44410310 ^ 3103是否否
55510310 ^ 3103否是是
66610310 ^ 3103否是否
7,87, 87,810310 ^ 3103否否是
9,109, 109,1010310 ^ 3103否否否

其中:

  • 特殊性质 A:保证 b=0b = 0b=0;
  • 特殊性质 B:保证 c=0c = 0c=0;
  • 特殊性质 C:如果方程有解,那么方程的两个解都是整数。

解析

前置知识

我们需要了解解一元二次方程的基础知识。对于形如 ax2+bx+c=0ax^2 + bx + c = 0ax2+bx+c=0 的一元二次方程(其中 a≠0a \neq 0a=0),其根的求解公式为:

x1,2=−b±b2−4ac2ax_{1, 2} = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a} x1,2​=2a−b±b2−4ac​​

根据判别式 Δ=b2−4ac\Delta = b^2 - 4acΔ=b2−4ac,可以讨论方程的实数解:

  1. 当 Δ<0\Delta < 0Δ<0 时,方程无实数解。
  2. 当 Δ≥0\Delta \geq 0Δ≥0 时,方程有两个实数解:

    x1,2=−b±Δ2ax_{1, 2} = \frac{-b \pm \sqrt{\Delta}}{2a} x1,2​=2a−b±Δ​​

题目要求给出多个一元二次方程的较大实数解。

题目简述

给定 TTT 组数据,每组数据包括 aaa、bbb、ccc 三个系数,求解形如 ax2+bx+c=0ax^2 + bx + c = 0ax2+bx+c=0 的一元二次方程的较大实数解。若方程无实数解,则输出 "NO"。

思路分析

这道题主要考查模拟计算以及如何处理输出中的数据化简、约分问题。

情况 1:当 Δ<0\Delta < 0Δ<0

当判别式 Δ=b2−4ac\Delta = b^2 - 4acΔ=b2−4ac 小于 0 时,方程无实数解,直接输出 "NO"。

情况 2:当 Δ≥0\Delta \geq 0Δ≥0

当 Δ≥0\Delta \geq 0Δ≥0 时,方程有两个实数解。为了简化计算并避免正负号的麻烦,可以统一处理为 aaa 取正,即:

if(a < 0) {
    a = -a;
    b = -b;
    c = -c;
}

方程的解为:

x=−b+Δ2ax = \frac{-b + \sqrt{\Delta}}{2a} x=2a−b+Δ​​

需要根据 Δ\DeltaΔ 的值进一步处理。

  1. 当 Δ\DeltaΔ 是完全平方数:此时 Δ\sqrt{\Delta}Δ​ 是有理数,较大实数解为:

    x=−b+Δ2ax = \frac{-b + \sqrt{\Delta}}{2a} x=2a−b+Δ​​

    此时只需将分子 −b+Δ-b + \sqrt{\Delta}−b+Δ​ 和分母 2a2a2a 进行约分并输出。

  2. 当 Δ\DeltaΔ 不是完全平方数:此时 Δ\sqrt{\Delta}Δ​ 为无理数。可将其表示为 ktk\sqrt{t}kt​ 的形式,其中 kkk 是最大的整数,使得 Δ=k2×t\Delta = k^2 \times tΔ=k2×t。于是较大实数解为:

    x=−b2a+kt2ax = \frac{-b}{2a} + \frac{k\sqrt{t}}{2a} x=2a−b​+2akt​​

    这部分需要将两个分数分别约分后再输出。

代码实现

#include <iostream>
#include <cstdio>
#include <cmath>

using namespace std;

// 读取输入
inline int read() {
    int x = 0, y = 1;
    char ch = getchar();
    while (ch < '0' || ch > '9') {
        if (ch == '-') y = -1;
        ch = getchar();
    }
    while (ch >= '0' && ch <= '9') x = (x << 3) + (x << 1) + (ch & 15), ch = getchar();
    return x * y;
}

// 求最大公因数
int gcd(int n, int m) {
    if (n % m == 0) return m;
    else return gcd(m, n % m);
}

// 约分
void YueFen(int &fenzi, int &fenmu) {
    bool f = (fenzi < 0 ? 1 : 0); // 负数处理
    if (f) fenzi *= -1;
    int k = gcd(fenzi, fenmu); // 约分
    fenzi /= k; 
    fenmu /= k;
    if (f) fenzi *= -1;
}

// 将 n 化为 k^2*t 的形式
int k, t;
void solve(int n) {
    for (int i = sqrt(n); i >= 1; i--) {
        int w = n / (i * i);
        if (w * i * i == n) {
            k = i;
            t = w;  // n = k^2 * t
            return;
        }
    }
}

int main() {
    //freopen("uqe.in", "r", stdin);
    //freopen("uqe.out", "w", stdout);
    int T = read(); // 读取测试数据组数
    for (int i = 1; i <= T; i++) {
        int a = read(), b = read(), c = read();
        int delta = b * b - 4 * a * c;
        
        if (delta < 0) {
            printf("NO\n");
            continue;
        }

        if (a < 0) a = -a, b = -b, c = -c;
        int sq = sqrt(delta);
        
        if (sq * sq == delta) { // 有理数情况
            int fenzi = -b + sq;
            int fenmu = 2 * a;
            YueFen(fenzi, fenmu);
            if (fenmu == 1) {
                printf("%d\n", fenzi);
            } else {
                printf("%d/%d\n", fenzi, fenmu);
            }
        } else { // 无理数情况
            solve(delta); // sqrt(delta) = k * sqrt(t)
            int fenzi = -b;
            int fenmu = 2 * a;
            YueFen(fenzi, fenmu);
            
            if (b != 0) {
                if (fenmu == 1) {
                    printf("%d", fenzi);
                } else {
                    printf("%d/%d", fenzi, fenmu);
                }
                printf("+");
            }

            fenzi = k;
            fenmu = 2 * a;
            YueFen(fenzi, fenmu);
            
            if (fenzi == 1 && fenmu == 1) {
                printf("sqrt(%d)", t);
            } else if (fenzi == 1) {
                printf("sqrt(%d)/%d", t, fenmu);
            } else if (fenmu == 1) {
                printf("%d*sqrt(%d)", fenzi, t);
            } else {
                printf("%d*sqrt(%d)/%d", fenzi, t, fenmu);
            }
            
            printf("\n");
        }
    }
    
    return 0;
}

代码详解

  1. 输入函数 read:快速读取输入,适合大规模数据。
  2. 最大公因数 gcd 函数:用于约分分子和分母。
  3. YueFen 函数:负责对分数进行约分,处理负数情况。
  4. solve 函数:将非完全平方数的 Δ\DeltaΔ 表示为 k2×tk^2 \times tk2×t 的形式,方便后续处理。
  5. 主函数 main:处理 TTT 组测试数据,按不同情况输出较大解或 "NO"。

输出解释

  • 当 Δ<0\Delta < 0Δ<0 时,输出 "NO"。
  • 当 Δ\DeltaΔ 是完全平方数时,输出约分后的较大解。
  • 当 Δ\DeltaΔ 不是完全平方数时,输出无理数形式的较大解。

[CSP-J 2023] 旅游巴士

题目描述

小 Z 打算在国庆假期期间搭乘旅游巴士去一处他向往已久的景点旅游。

旅游景点的地图共有 nnn 处地点,在这些地点之间连有 mmm 条道路。其中 111 号地点为景区入口,nnn 号地点为景区出口。我们把一天当中景区开门营业的时间记为 000 时刻,则从 000 时刻起,每间隔 kkk 单位时间便有一辆旅游巴士到达景区入口,同时有一辆旅游巴士从景区出口驶离景区。

所有道路均只能单向通行。对于每条道路,游客步行通过的用时均为恰好 111 单位时间。

小 Z 希望乘坐旅游巴士到达景区入口,并沿着自己选择的任意路径走到景区出口,再乘坐旅游巴士离开,这意味着他到达和离开景区的时间都必须是 kkk 的非负整数倍。由于节假日客流众多,小 Z 在旅游巴士离开景区前只想一直沿着景区道路移动,而不想在任何地点(包括景区入口和出口)或者道路上停留。

出发前,小 Z 忽然得知:景区采取了限制客流的方法,对于每条道路均设置了一个 “开放时间”aia _ iai​,游客只有不早于 aia _ iai​ 时刻才能通过这条道路。

请帮助小 Z 设计一个旅游方案,使得他乘坐旅游巴士离开景区的时间尽量地早。

输入格式

输入的第一行包含 3 个正整数 n,m,kn, m, kn,m,k,表示旅游景点的地点数、道路数,以及旅游巴士的发车间隔。

输入的接下来 mmm 行,每行包含 3 个非负整数 ui,vi,aiu _ i, v _ i, a_ iui​,vi​,ai​,表示第 iii 条道路从地点 uiu _ iui​ 出发,到达地点 viv _ ivi​,道路的“开放时间”为 aia _ iai​。

输出格式

输出一行,仅包含一个整数,表示小 Z 最早乘坐旅游巴士离开景区的时刻。如果不存在符合要求的旅游方案,输出 -1。

样例 #1

样例输入 #1

5 5 3
1 2 0
2 5 1
1 3 0
3 4 3
4 5 1

样例输出 #1

6

提示

【样例 #1 解释】

小 Z 可以在 333 时刻到达景区入口,沿 1→3→4→51 \to 3 \to 4 \to 51→3→4→5 的顺序走到景区出口,并在 666 时刻离开。

【样例 #2】

见附件中的 bus/bus2.in 与 bus/bus2.ans。

【数据范围】

对于所有测试数据有:2≤n≤1042 \leq n \leq 10 ^ 42≤n≤104,1≤m≤2×1041 \leq m \leq 2 \times 10 ^ 41≤m≤2×104,1≤k≤1001 \leq k \leq 1001≤k≤100,1≤ui,vi≤n1 \leq u _ i, v _ i \leq n1≤ui​,vi​≤n,0≤ai≤1060 \leq a _ i \leq 10 ^ 60≤ai​≤106。

测试点编号n≤n \leqn≤m≤m \leqm≤k≤k \leqk≤特殊性质
1∼21 \sim 21∼2101010151515100100100ai=0a _ i = 0ai​=0
3∼53 \sim 53∼5101010151515100100100无
6∼76 \sim 76∼710410 ^ 41042×1042 \times 10 ^ 42×104111ai=0a _ i = 0ai​=0
8∼108 \sim 108∼1010410 ^ 41042×1042 \times 10 ^ 42×104111无
11∼1311 \sim 1311∼1310410 ^ 41042×1042 \times 10 ^ 42×104100100100ai=0a _ i = 0ai​=0
14∼1514 \sim 1514∼1510410 ^ 41042×1042 \times 10 ^ 42×104100100100ui≤viu _ i \leq v _ iui​≤vi​
16∼2016 \sim 2016∼2010410 ^ 41042×1042 \times 10 ^ 42×104100100100无

解析

题目分析

给定一个带有边权的图,要求从起点到终点的最短路径,特别之处在于:

  • 出发时刻和结束时刻都是 kkk 的非负整数倍;
  • 小 Z 不愿意在任何地点(包括景区入口、出口)或道路上停留,意味着当当前时间不满足要求时,必须将出发时间推迟至 kkk 的整数倍。

本题看似不直接是经典的最短路问题,但可以通过边权为 1 的 BFS(广度优先搜索)进行求解,时间复杂度为 O(n)O(n)O(n)。然而,由于时间和路径数必须满足 kkk 的倍数限制,路径计算过程稍显复杂。

核心思想

  1. 路径数与时间的关系:出发时刻和结束时刻都是 kkk 的非负整数倍,这意味着走过的路径数也应为 kkk 的倍数。因此,当路径数和时间不满足要求时,必须调整出发时间至 kkk 的整数倍。
  2. BFS 实现:由于这是一个边权为 1 的图,最短路径可以通过广度优先搜索来解决,同时结合优先队列求最优解。
  3. 避免死循环:运行过程中,可能会进入死循环。为了避免这种情况,可以通过记忆化来记录当前状态是否已经被处理过。

BFS 算法实现

void bfs(int st){
	priority_queue<node> que;  // 使用优先队列进行搜索
	memset(vis, 0x3f, sizeof vis);  // 初始化访问数组为极大值,表示未访问过
	que.push((node){0, 0, st});  // 初始状态,时间为 0,路径数为 0,从起点 st 开始
	vis[1][0] = 0;  // 起点状态的访问记录
	
	while (!que.empty()) {  // 当队列不为空时
		node h = que.top();
		que.pop();  // 弹出队列的顶部元素,当前节点 h
		int u = h.id, tim = h.t, dis = h.d;  // 当前节点 u 的编号、时间 tim、路径数 dis
		
		if (u == n && dis % k == 0) {  // 若到达终点并且路径数为 k 的倍数
			ans = tim + dis;  // 计算答案
			break;  // 退出循环
		}
		
		// 遍历 u 的所有邻接边
		for (int i = head[u]; i; i = e[i].to) {
			int v = e[i].v, w = e[i].w;  // 邻接点 v,边权 w
			int mod = (dis + 1) % k;  // 下一次路径数取模 k 的值
			
			// 如果当前时间可以直接通过该边
			if (tim + dis >= w) {
				if (vis[v][mod] <= tim) continue;  // 若该状态已经被访问过,跳过
				vis[v][mod] = tim;  // 更新访问时间
				que.push((node){tim, dis + 1, v});  // 将新的状态加入队列
			} else {  // 若当前时间无法通过该边
				int time = tim + ((w - tim - dis - 1) / k + 1) * k;  // 调整时间
				if (vis[v][mod] <= time) continue;  // 若该状态已经被访问过,跳过
				vis[v][mod] = time;  // 更新访问时间
				que.push((node){time, dis + 1, v});  // 将新的状态加入队列
			}
		}
	}
}

记忆化的必要性

考虑到可能存在多条路径到达同一个节点,为了避免多次处理相同的状态,采用记忆化来保存到达该节点时的最优状态。

具体情况如下:

  1. 如果有两条不同路径到达同一个节点:
    • 假设第一条路径的出发时间为 t1t_1t1​,路径数为 dis1dis_1dis1​;
    • 第二条路径的出发时间为 t2t_2t2​,路径数为 dis2dis_2dis2​;
    • 如果 t1≤t2t_1 \leq t_2t1​≤t2​ 且 dis1≡dis2(mod  k)dis_1 \equiv dis_2 (\mod k)dis1​≡dis2​(modk),且第一条路径在第二条路径之前被处理,则第一条路径一定是更优的选择。

通过这种方式,优先队列能够确保更早到达且路径数满足条件的路径优先被处理,从而避免死循环并确保找到最优解。

完整代码

#include <bits/stdc++.h>
using namespace std;

const int maxn = 1e4 + 5;
int n, m, k, cnt = 1, ans;
int head[maxn], vis[maxn][105];

struct edge {
    int v, to, w;
} e[maxn << 1];

struct node {
    int t, d, id;
    bool operator <(const node &x) const {
        return t + d > x.t + x.d;  // 优先队列按时间和路径数的和进行排序
    }
};

void add(int u, int v, int w) {
    e[cnt].v = v, e[cnt].w = w, e[cnt].to = head[u];
    head[u] = cnt++;
}

void bfs(int st) {
    priority_queue<node> que;
    memset(vis, 0x3f, sizeof vis);  // 初始化访问数组
    que.push((node){0, 0, st});  // 将起点状态加入队列
    vis[1][0] = 0;  // 起点状态的访问记录
    
    while (!que.empty()) {
        node h = que.top();
        que.pop();
        int u = h.id, tim = h.t, dis = h.d;
        
        if (u == n && dis % k == 0) {  // 若到达终点且路径数为 k 的倍数
            ans = tim + dis;
            break;
        }
        
        for (int i = head[u]; i; i = e[i].to) {  // 遍历 u 的所有邻接点
            int v = e[i].v, w = e[i].w;
            int mod = (dis + 1) % k;
            
            if (tim + dis >= w) {  // 当前时间可以通过
                if (vis[v][mod] <= tim) continue;
                vis[v][mod] = tim;
                que.push((node){tim, dis + 1, v});
            } else {  // 当前时间无法通过,调整时间
                int time = tim + ((w - tim - dis - 1) / k + 1) * k;
                if (vis[v][mod] <= time) continue;
                vis[v][mod] = time;
                que.push((node){time, dis + 1, v});
            }
        }
    }
}

int main() {
    scanf("%d%d%d", &n, &m, &k);
    for (int i = 1; i <= m; i++) {
        int u, v, w;
        scanf("%d%d%d", &u, &v, &w);
        add(u, v, w);  // 添加边
    }
    ans = -1;
    bfs(1);  // 从起点 1 开始搜索
    printf("%d", ans);  // 输出结果
    return 0;
}

总结

  • 通过优先队列的 BFS 结合记忆化避免死循环,有效解决了路径和时间问题。
  • 算法在时间复杂度上为 O(nlog⁡n)O(n \log n)O(nlogn),优先队列的使用使得每次处理节点时都能优先找到最优解。
津ICP备2020007982号-2