欢迎光临
我们一直在努力

CSP-J 历年复赛 T2 及解析(2019~2025)

摘要:本文系统整理了 2019-2025 年 CSP-J 复赛 T2 的历年真题,涵盖公交换乘、直播获奖、插入排序、解密、公路、地图探险、座位等 7 道题目。每道题均提供清晰的思路要点、解题步骤、易错点分析和完整参考代码,帮助选手掌握 T2常考的模拟、贪心、二分、数学等核心算法,提升解题能力。

说明 & 备考建议

题目都可以在洛谷上搜名称就会出来,题目名称也都加了链接点击就能跳转到做题页面。

T2 一般考察基础算法,难度在【普及- ~ 普及/提高- 】之间。 常考算法知识点有:二分、排序、贪心、大模拟,其中简化题意又包含了数学思维推导的考察。 最近两年都是矩阵模拟操作类题目,其实思维难度有降低,但非常考验基本功和细致度。

一些需要注意的点:

  • T2 的数据量一般不会很小,还可能涉及多组查询,超 10^9 开 long long,优化查询上前缀和,大区间搜索想到二分。
  • 复杂流程类题目先理清楚各种可能情况和处理方案再转换成代码实现。
  • 看似越复杂抽象的题目往往有隐含规律,最终能推导出很简洁优雅的数学模型降维求解。
  • 就算想不出正解写暴力也能骗到数据量小的测试点或者某些特殊 case 的部分分,尽量不要弃题。
  • Day Day Up!祝大家都能在比赛取得好成绩!✿✿ヽ(°▽°)ノ✿

    历年 CSP-J 复赛 T2(2019~2025)

    题目列表

    年份题目知识点难度
    2019 公交换乘 模拟、队列 普及-
    2020 直播获奖 模拟、排序 普及-
    2021 插入排序 枚举、排序 普及/提高−
    2022 解密 数学、二分 普及-
    2023 公路 模拟、贪心、前缀和 普及-
    2024 地图探险 模拟 普及-
    2025 座位 模拟、数学、排序 普及-

    题目详解

    公交换乘

    思路要点

    简单来说,小轩要坐

    n

    n

    n 次车,要么是地铁(0),要么是公交(1)。

    • 坐地铁:必须花钱,但会赠送一张“优惠券”。这张券有两个限制:① 限时:45 分钟内有效;② 限价:只能用于票价

      \\le

      这次地铁票价的公交车。

    • 坐公交:能用券就优先用券(免单),有多张能用的券时,先用最早获得的。没券可用就只能老老实实花钱。

    • 目标:计算坐完

      n

      n

      n 次车后的最低总花费。

    这道题的本质是:线性结构(动态数组/队列)的模拟,并利用双指针(滑动窗口)进行时间复杂度的优化。

    关键思路

    暴力思路的局限性

    最直观的想法是:每次遇到公交车,我们就从头到尾扫描一遍之前所有获得过的优惠券,看看哪张没用过、没过期、且价格够。但是,看一下数据范围:

    n

    10

    5

    n \\le 10^5

    n105。如果每次坐公交都全量扫描前面的优惠券,最坏情况下的时间复杂度是

    O

    (

    n

    2

    )

    O(n^2)

    O(n2),总运算量会达到

    10

    10

    10^{10}

    1010级别,在竞赛中绝对会 TLE(超时)。

    核心优化:双指针/队列思想

    如何优化?题目中藏着两个非常关键的“隐藏线索”:

    • 时间递增:输入数据是按乘车时间单调递增给出的。

    • 45分钟时效:优惠券有效期只有 45 分钟,且同一分钟内不会有两次记录。这意味着在任何一个时间点,还没过期的优惠券最多只有 45 张。

    既然过期的券再也不可能被使用了,我们就可以像队列一样,维护一个“有效券池”。每次遇到新记录:

  • 如果是地铁,直接把新券加到池子的尾部(tail++)。

  • 如果是公交,我们先从池子的头部开始,把已经过期的券彻底剔除(head++)。因为时间是往前走的,现在过期的券,以后更不可能有用。

  • 剔除过期券后,池子里剩下的券最多只有 45 张!此时我们再从前往后(满足“最早优先”)遍历这几十张券,找到第一张价格满足要求的,标记为已使用即可。这样,每次处理公交车的时间变成了

    O

    (

    1

    )

    O(1)

    O(1)常数级,总时间复杂度成功优化到了

    O

    (

    n

    )

    O(n)

    O(n)

  • 解题步骤

    我们以以下样例输入为例,模拟计算机的执行过程:

    6
    0 10 3
    1 5 46
    0 12 50
    1 3 96
    0 5 110
    1 6 135

  • 变量定义与初始化:

    • s:记录总花费。

    • head, tail:手写队列的头尾指针,初始化为 0。

    • tk[maxn]:结构体数组,存储优惠券的票价 p、时间 t 和使用状态 u。

  • 模拟运行:

    • 第 1 条记录:0 10 3(地铁,票价 10,时间 3)

      • x = 0,进入地铁分支。s += p

        \\rightarrow

        s = 10。

      • 放入券池:tk[0] = {10, 3, false},tail 变为 1。

    • 第 2 条记录:1 5 46(公交,票价 5,时间 46)

      • x = 1,进入公交分支。

      • 清理过期券:当前 t = 46,检查队头 tk[0].t = 3。

        46

        3

        =

        43

        45

        46 – 3 = 43 \\le 45

        463=4345,未过期。while 循环不执行,head 保持 0。

      • 查找可用券:从 head(0) 遍历到 tail(1)。检查 tk[0]:票价

        10

        5

        10 \\ge 5

        105 且未用过。找到了!

      • 标记使用:tk[0].u = 1,f = 1(代表找到),结束查找。s 保持 10。

    • 第 3 条记录:0 12 50(地铁,票价 12,时间 50)

      • x = 0,进入地铁分支。s += p

        \\rightarrow

        s = 10 + 12 = 22。

      • 放入券池:tk[1] = {12, 50, false},tail 变为 2。

    • 第 4 条记录:1 3 96(公交,票价 3,时间 96)

      • x = 1,进入公交分支。

      • 清理过期券:当前 t = 96。

        • 检查 tk[0] (

          t

          =

          3

          t=3

          t=3):

          96

          3

          =

          93

          >

          45

          96 – 3 = 93 > 45

          963=93>45,已过期!head++ 变为 1。

        • 检查 tk[1] (

          t

          =

          50

          t=50

          t=50):

          96

          50

          =

          46

          >

          45

          96 – 50 = 46 > 45

          9650=46>45,已过期!head++ 变为 2。

        • 此时 head == tail,清空完毕。

      • 查找可用券:head 到 tail 之间没有元素,循环不执行,f = 0。

      • 买票自费:!f 成立,s += p

        \\rightarrow

        s = 22 + 3 = 25。

    • 第 5 条记录:0 5 110(地铁,票价 5,时间 110)

      • x = 0,进入地铁分支。s += p

        \\rightarrow

        s = 25 + 5 = 30。

      • 放入券池:tk[2] = {5, 110, false},tail 变为 3。

    • 第 6 条记录:1 6 135(公交,票价 6,时间 135)

      • x = 1,进入公交分支。

      • 清理过期券:当前 t = 135,检查队头 tk[2].t = 110。

        135

        110

        =

        25

        45

        135 – 110 = 25 \\le 45

        135110=2545,未过期。head 保持 2。

      • 查找可用券:遍历 tk[2]。票价

        5

        <

        6

        5 < 6

        5<6,价格不足!查找结束,f = 0。

      • 买票自费:s += p

        \\rightarrow

        s = 30 + 6 = 36。

  • 输出结果: 最终 s 的值为 36,与样例完全一致。

  • 本题易错点
    • 坑一:忽略换乘价格条件

      要点提醒:免费换乘的条件有两个:一个是时间要不超过 45 分钟,另一个是“公交票价

      \\le

      地铁票价”,两个都必须满足才能换乘。代码中 tk[j].p 是地铁票,p 是公交票,因此是 tk[j].p >= p。

    • 坑二:数据范围与类型

      要点提醒: 总花费 s 最大可能为

      10

      5

      ×

      1000

      =

      10

      8

      10^5 \\times 1000 = 10^8

      105×1000=108,在 int 的表示范围内(约

      2

      ×

      10

      9

      2 \\times 10^9

      2×109)。虽然此题不需要开 long long,但养成每道题评估数据总和、防止溢出的习惯是非常重要的。

    • 坑三:不作任何剔除,每次都从 0 遍历到 i

      要点提醒: 这就是前面提到的

      O

      (

      n

      2

      )

      O(n^2)

      O(n2)纯暴力,在

      n

      =

      10

      5

      n = 10^5

      n=105的数据面前会超时。必须学会利用“时间单调递增”这个性质做单向的 head++ 优化。

    参考代码-手写队列

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

    int n, x, p, t, s; // n:记录数, x:类型, p:票价, t:时间, s:总花费
    int head, tail; // 手写队列的头指针和尾指针

    struct T{
    int p, t; // p:地铁票价, t:获得时间
    bool u; // u:是否被用过(默认初始化为 false/0)
    }tk[maxn]; // 优惠券队列池

    int main(){
    scanf("%d", &n);
    for(int i = 1; i <= n; i++){
    scanf("%d %d %d", &x, &p, &t);
    if(!x){ // 如果是地铁(x == 0)
    s += p; // 地铁必须雷打不动地花钱
    tk[tail++] = {p, t}; // 获得一张优惠券,入队,尾指针后移
    }
    else{ // 如果是公交车(x == 1)
    bool f = 0; // 标记变量,记录当前公交是否成功使用了优惠券
    // 核心优化:利用时间单调性,将绝对过期的券从队头永久剔除
    while(head < tail && t tk[head].t > 45){
    head++; // 队头后移,相当于出队
    }
    // 在未过期的有效券池中,从前往后寻找第一张能用的券
    for(int j = head; j < tail; j++){
    // 找第一张价格 ok 且没用过的换乘票
    if(tk[j].p >= p && !tk[j].u){
    tk[j].u = 1; // 标记用过
    f = 1; // 标记找到
    break; // 结束查找
    }
    }
    if(!f){ // 如果遍历完所有有效的券都无法免单
    s += p; // 只能自费购买公交票
    }
    }
    }
    printf("%d", s); // 输出最终的总花费
    return 0;
    }

    补充解法:使用 STL 容器 – deque

    这种解法非常优雅,充分利用了 C++ 标准库的强大功能。

    在上一个手写数组的版本中,我们是通过 打标记(tk[j].u = 1) 来表示优惠券被用掉了。而在这个 STL 版本中,思路变得更加直观和暴力:用掉了,就直接把它从队列里“撕掉”(删除)!

    可以通过下面这个简单的映射,来看看 STL 是如何简化代码逻辑的:

  • 入队操作:

    • 手写版:tk[tail++] = {p, t};

    • STL版:tk.push_back({p, t}); (直接在队尾追加,省去了管理 tail 指针的麻烦)

  • 剔除过期券:

    • 手写版:while(head < tail && t – tk[head].t > 45) { head++; }

    • STL版:while(!tk.empty() && t – tk.front().t > 45) { tk.pop_front(); } (直接从队头弹出 pop_front(),真正释放了空间)

  • 寻找并使用券:

    • 手写版:遍历数组,找到后把 u 标记为 1。

    • STL版:利用迭代器(Iterator)从头到尾扫描 deque,一旦发现满足 it->p >= p 的券,直接执行 tk.erase(it);。也就是说,能用的券直接从队列里彻底抹去。这里的 erase 操作实际上是

      O

      (

      1

      )

      O(1)

      O(1)级别的。整个算法的总时间复杂度依然是极其优秀的

      O

      (

      n

      )

      O(n)

      O(n)

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

    // n: 乘车记录数, x: 交通工具类型(0地铁, 1公交), p: 当次票价, t: 乘车时间, s: 总花费
    int n, x, p, t, s;

    struct T{ // 定义优惠票的结构体
    int p, t; // p: 产生该票时的地铁票价, t: 获得该票的时间
    };

    int main(){
    // 读入乘车记录的总数
    scanf("%d", &n);
    // 使用双端队列存储手中现有的优惠票
    deque<T> tk; // 因为时间是递增的,先获得的票一定在队列头部,方便后续过期清理

    for(int i = 1; i <= n; i++){
    // 读入当前记录:交通工具类型、票价、时间
    scanf("%d %d %d", &x, &p, &t);

    if(!x){ // 【情况 0】:乘坐的是地铁
    s += p; // 地铁必须自费,累加到总花费中
    tk.push_back({p, t}); // 获得一张新的优惠票,放入队列尾部
    }
    else{ // 【情况 1】:乘坐的是公交车
    bool f = 0; // 记录当前公交车是否使用了优惠票(0-没用,1-用了)
    // 核心步骤 1:清理过期票
    // 因为乘车时间单调递增,已经过期的票直接从队头永久弹出
    while(!tk.empty() && t tk.front().t > 45){
    tk.pop_front();
    }
    // 核心步骤 2:寻找最早可用的优惠票
    // 从队头(时间最早)开始向后遍历当前所有未过期的优惠票
    for(auto it = tk.begin(); it != tk.end(); ++it){
    // 优惠票面额(地铁票价) >= 当前公交车票价,说明满足免费条件
    if(it->p >= p){
    tk.erase(it); // 消耗掉这张优惠票,从队列中抹去
    f = 1; // 标记成功使用优惠票
    break; // 找到第一张可用的就立即结束查找
    }
    }

    // 核心步骤 3:未享受优惠的处理(f = 0)
    // 如果遍历了所有有效票都没有找到合适的,公交车只能自费
    if(!f){
    s += p; // 累加公交车票价到总花费
    }
    }
    }

    // 输出最终计算出的总花费
    printf("%d", s);
    return 0;
    }


    直播获奖

    思路要点

    这道题目的核心场景是:一场比赛不断有选手的成绩被公布,我们需要在每一次公布新成绩时,实时计算并播报当前的“获奖分数线”。

    • 当前参赛总人数:随着成绩公布依次递增(从 1 到

      n

      n

      n)。

    • 当前计划获奖人数:公式为

      max

      (

      1

      ,

      当前人数

      ×

      获奖百分比

      )

      \\max(1, \\lfloor \\text{当前人数} \\times \\text{获奖百分比} \\rfloor)

      max(1,当前人数×获奖百分比⌋)

    • 分数线定义:将所有已出成绩从高到低排序,排在“计划获奖人数”名次上的那个分数,就是当前的分数线。

    剥去“直播”、“获奖率”的外壳,它本质在考察数据的动态插入与第

    K

    K

    K 大值的快速查询。

    关键思路

    如果在每次读入新成绩后都用 std::sort 对已有成绩进行排序,时间复杂度将达到

    O

    (

    n

    2

    log

    n

    )

    O(n^2 \\log n)

    O(n2logn)。结合题目给出的最大数据量

    n

    =

    10

    5

    n = 10^5

    n=105,必定会导致超时(TLE)。

    突破口在哪里?

    观察数据范围:虽然选手最多有

    10

    5

    10^5

    105 人,但每个选手的成绩最高只有 600 分!

    当数据的值域(0~600)远远小于数据的个数(

    10

    5

    10^5

    105)时,我们应该立刻条件反射般地想到一种以空间换时间的利器:桶排序(计数排序)。

    我们可以建立一个大小为 605 的数组 c 作为“桶”,下标表示分数,存储的值表示“获得该分数的总人数”。每次新来一个成绩,只需要用

    O

    (

    1

    )

    O(1)

    O(1) 的时间把它丢进对应的桶里。查询时,从 600 分向 0 分倒序遍历,累加每个桶里的人数,一旦累加人数达到或超过当前的“计划获奖人数”,当前的桶代表的分数就是我们要找的分数线。

    解题步骤

    我们以局部数据为例,假设

    w

    =

    30

    w = 30

    w=30,依次读入前

    3

    3

    3 个成绩:100 100 600,模拟代码的执行过程:

  • 变量定义与初始化:

    • 定义整型变量

      n

      ,

      w

      ,

      x

      n, w, x

      n,w,x

    • 定义整型数组

      c

      [

      605

      ]

      c[605]

      c[605],作为“分数桶”,全局数组默认初始化均为

      0

      0

      0

  • 读入第 1 个数据: 输入成绩

    100

    100

    100,此时变量

    x

    =

    100

    x = 100

    x=100

    • 记录成绩: 执行

      c

      [

      100

      ]

      c[100]

      c[100] 加一,此时

      c

      [

      100

      ]

      =

      1

      c[100] = 1

      c[100]=1

    • 计算计划获奖人数

      t

      t

      t: 计算

      1

      ×

      30

      /

      100

      1 \\times 30 / 100

      1×30/100,结果为

      0

      0

      0

    • 确保最少

      1

      1

      1 人获奖: 执行

      max

      (

      1

      ,

      0

      )

      \\max(1, 0)

      max(1,0),最终获奖人数

      t

      =

      1

      t = 1

      t=1

    • 倒推分数线: 循环变量

      j

      j

      j 从满分

      600

      600

      600 开始倒序遍历到

      0

      0

      0。当遍历到

      j

      =

      100

      j = 100

      j=100 时,发现对应的桶

      c

      [

      100

      ]

      =

      1

      c[100] = 1

      c[100]=1(有

      1

      1

      1 个人)。

    • 分配名额: 剩余名额

      t

      =

      1

      1

      =

      0

      t = 1 – 1 = 0

      t=11=0。由于

      t

      0

      t \\le 0

      t0,名额分配完毕,当前的分数线就是

      100

      100

      100。输出

      100

      100

      100 并结束这轮寻找。

  • 读入第 2 个数据: 输入成绩

    100

    100

    100,此时变量

    x

    =

    100

    x = 100

    x=100

    • 记录成绩: 执行

      c

      [

      100

      ]

      c[100]

      c[100] 再加一,此时

      c

      [

      100

      ]

      =

      2

      c[100] = 2

      c[100]=2

    • 计算计划获奖人数

      t

      t

      t: 前

      2

      2

      2 个人,计算

      2

      ×

      30

      /

      100

      2 \\times 30 / 100

      2×30/100,结果为

      0

      0

      0

    • 确保最少

      1

      1

      1 人获奖: 执行

      max

      (

      1

      ,

      0

      )

      \\max(1, 0)

      max(1,0),最终获奖人数

      t

      =

      1

      t = 1

      t=1

    • 倒推分数线: 依然从

      600

      600

      600 往下查,到

      j

      =

      100

      j = 100

      j=100 时,桶内人数

      c

      [

      100

      ]

      =

      2

      c[100] = 2

      c[100]=2

    • 分配名额: 剩余名额

      t

      =

      1

      2

      =

      1

      t = 1 – 2 = -1

      t=12=1。由于

      t

      0

      t \\le 0

      t0(说明同分的人把名额占满甚至超额了,符合题意中的并列情况),当前的分数线就是

      100

      100

      100。输出

      100

      100

      100

  • 读入第 3 个数据: 输入成绩

    600

    600

    600,此时变量

    x

    =

    600

    x = 600

    x=600

    • 记录成绩: 执行

      c

      [

      600

      ]

      c[600]

      c[600] 加一,此时

      c

      [

      600

      ]

      =

      1

      c[600] = 1

      c[600]=1

    • 计算计划获奖人数

      t

      t

      t: 前

      3

      3

      3 个人,计算

      3

      ×

      30

      /

      100

      3 \\times 30 / 100

      3×30/100,结果为

      0

      0

      0

    • 确保最少

      1

      1

      1 人获奖: 执行

      max

      (

      1

      ,

      0

      )

      \\max(1, 0)

      max(1,0),最终获奖人数

      t

      =

      1

      t = 1

      t=1

    • 倒推分数线: 从

      600

      600

      600 往下查,刚查到

      j

      =

      600

      j = 600

      j=600 时,桶内人数

      c

      [

      600

      ]

      =

      1

      c[600] = 1

      c[600]=1

    • 分配名额: 剩余名额

      t

      =

      1

      1

      =

      0

      t = 1 – 1 = 0

      t=11=0。由于

      t

      0

      t \\le 0

      t0,当前的分数线直接定格在

      600

      600

      600。输出

      600

      600

      600

  • 本题易错点
    • 坑一:暴力排序法

      要点提醒:用一个数组存入前

      i

      i

      i 个数,每读入一个数就调用一遍 sort。时间复杂度高达

      O

      (

      N

      2

      log

      N

      )

      O(N^2 \\log N)

      O(N2logN),只能拿到前几个

      N

      =

      10

      N=10

      N=10

      N

      =

      500

      N=500

      N=500 的部分分测试点。

    • 坑二:忽略了同分并列机制

      要点提醒: 题目提到“实际获奖人数可能比计划中多”。桶排序天然解决了这个问题:只要减去当前分数的人数后 t <= 0,就意味着这个分数段的人都能获奖,且刚好包含或超额满足了剩余名额。

    • 坑三:桶数组开得太小

      要点提醒: 成绩范围是

      0

      score

      600

      0 \\le \\text{score} \\le 600

      0score600,有些同学习惯性写 c[600],导致存储满分 600 分时数组越界。正确的开法是至少开到 601(代码中开 605 是非常好的竞赛习惯,留有余地)。

    参考代码

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

    int n, w, x, c[605]; // n为总人数, w为获奖率, x为当前成绩, c为桶数组(成绩最大600)

    int main(){
    scanf("%d %d", &n, &w);
    for(int i = 1; i <= n; i++){
    scanf("%d", &x);
    c[x]++; // 将新读入的成绩放入对应的桶中,该分数人数+1

    // 计算当前计划获奖人数
    int t = i * w / 100.0;
    t = max(1, t); // 保证至少有1人获奖

    // 分配获奖名额:从满分600分开始向低分遍历
    for(int j = 600; j >= 0; j){
    if(c[j]){ // 如果有选手考了 j 分
    t -= c[j]; // 消耗对应的获奖名额
    if(t <= 0){ // 名额分配完毕(或由于同分并列导致超额分配)
    printf("%d ", j); // 此时的 j 即为分数线
    break; // 找到分数线后立刻跳出内层循环
    }
    }
    }
    }
    return 0;
    }


    插入排序

    思路要点

    这道题目的外壳包装了“插入排序算法”的伪代码,并给出了两种操作。如果直接照着题目去每次做插入排序,就掉进了出题人的陷阱。题目本质其实就是在维护一个数组。

    • 操作 1:单点修改,把数组中某一个元素的值改掉。

    • 操作 2:单点查询,问如果我们此刻对数组进行一次稳定的升序排序(插入排序的特性),原来在第

      x

      x

      x 个位置的元素,现在排在第几号位置?(注意:查询不会真的改变原数组的顺序)。

    考察核心知识点:稳定排序(Stable Sort)的相对位置关系、离线与在线查询、时间复杂度分析。

    关键思路

    如何快速知道原来在第

    x

    x

    x 个位置的元素,排序后排第几?容易想到每次遇到操作 2,就把数组复制一份,然后用 std::sort 排个序找位置。这种做法每次查询需要

    O

    (

    n

    log

    n

    )

    \\mathcal{O}(n \\log n)

    O(nlogn) 的时间,总时间复杂度会直接爆炸,导致超时(TLE)。

    破局点在于:计算“排名”(Rank)

    要确定一个元素排序后的位置,我们根本不需要把整个数组排好序,**只需要数一数“有多少个元素应该排在它前面”即可。**根据稳定排序的规则:

    • 原本在

      x

      x

      x 前面的元素(

      i

      <

      x

      i < x

      i<x):只有当它们的值严格大于

      a

      x

      a_x

      ax 时,才会被挤到

      x

      x

      x 后面去。否则它们依然在

      x

      x

      x 前面。

    • 原本在

      x

      x

      x 后面的元素(

      i

      >

      x

      i > x

      i>x):只有当它们的值严格小于

      a

      x

      a_x

      ax 时,才会跑到

      x

      x

      x 前面去。如果它们等于

      a

      x

      a_x

      ax,因为是“稳定排序”,它们依然要乖乖待在

      x

      x

      x 后面。

    因此,我们假设

    a

    x

    a_x

    ax 的初始排名就是

    x

    x

    x。遍历整个数组:

    • 遇到

      i

      <

      x

      i < x

      i<x

      a

      i

      >

      a

      x

      a_i > a_x

      ai>ax,说明这个元素跑到后面去了,

      x

      x

      x 的排名往前升一位(排名数字减 1)。

    • 遇到

      i

      >

      x

      i > x

      i>x

      a

      i

      <

      a

      x

      a_i < a_x

      ai<ax,说明这个元素跑到前面来了,

      x

      x

      x 的排名往后降一位(排名数字加 1)。

    每次查询只需遍历一遍数组,时间复杂度

    O

    (

    n

    )

    \\mathcal{O}(n)

    O(n)

    解题步骤

    我们以以下样例输入为例,模拟计算机的执行过程:

    3 4
    3 2 1
    2 3
    1 3 2
    2 2
    2 3

    初始数组:n = 3,数组 a = [3, 2, 1]

  • 第一条指令:2 3 (查询原第 3 个元素

    a

    3

    a_3

    a3 的排名)

    • 此时

      x

      =

      3

      x = 3

      x=3

      a

      3

      =

      1

      a_3 = 1

      a3=1。假设它的排名 p = x,即 p = 3。

    • 检查它前面的元素(

      i

      <

      3

      i < 3

      i<3):

      • i

        =

        1

        i = 1

        i=1

        a

        1

        =

        3

        a_1 = 3

        a1=3,因为

        3

        >

        a

        3

        3 > a_3

        3>a3,原来在前面的大元素排到了后面,p–,此时 p = 2。

      • i

        =

        2

        i = 2

        i=2

        a

        2

        =

        2

        a_2 = 2

        a2=2,因为

        2

        >

        a

        3

        2 > a_3

        2>a3,原来在前面的大元素排到了后面,p–,此时 p = 1。

    • 检查它后面的元素(

      i

      >

      3

      i > 3

      i>3):没有元素。

    • 输出结果:1。(数组排序为 1,2,3,原第 3 个元素

      1

      1

      1 排在第 1 位)。

  • 第二条指令:1 3 2 (将第 3 个元素修改为 2)

    • 执行 a[3] = 2。此时数组变为 a = [3, 2, 2]。
  • 第三条指令:2 2 (查询原第 2 个元素

    a

    2

    a_2

    a2 的排名)

    • 此时

      x

      =

      2

      x = 2

      x=2

      a

      2

      =

      2

      a_2 = 2

      a2=2。假设它的排名 p = x,即 p = 2。

    • 检查它前面的元素(

      i

      <

      2

      i < 2

      i<2):

      • i

        =

        1

        i = 1

        i=1

        a

        1

        =

        3

        a_1 = 3

        a1=3,因为

        3

        >

        a

        2

        3 > a_2

        3>a2,p–,此时 p = 1。

    • 检查它后面的元素(

      i

      >

      2

      i > 2

      i>2):

      • i

        =

        3

        i = 3

        i=3

        a

        3

        =

        2

        a_3 = 2

        a3=2。判断

        a

        3

        <

        a

        2

        a_3 < a_2

        a3<a2 吗?不小于(等于)。因为是稳定排序,相等元素不改变相对前后位置,所以条件不成立,p 不变。

    • 输出结果:1。(排序后为 2,2,3,原第 2 个元素优先排在第 1 位)。

  • 本题易错点
    • 坑一:遇到相同大小的元素(

      a

      i

      =

      a

      x

      a_i = a_x

      ai=ax)时的处理规则

      要点提醒:插入排序是稳定排序。所谓稳定,就是指如果两个元素大小一样,原本在前面的,排序后依然在前面。

      • 对于

        i

        <

        x

        i < x

        i<x(原本在前面):只有

        a

        i

        >

        a

        x

        a_i > a_x

        ai>ax 才会改变相对位置。如果

        a

        i

        =

        a

        x

        a_i = a_x

        ai=ax,它依然在

        x

        x

        x 前面,不影响

        x

        x

        x 的排名。所以在第一个循环中,判断条件必须是严格大于 (>),绝不能写成 >=。

      • 对于

        i

        >

        x

        i > x

        i>x(原本在后面):只有

        a

        i

        <

        a

        x

        a_i < a_x

        ai<ax 才会越过

        x

        x

        x 跑到前面。如果

        a

        i

        =

        a

        x

        a_i = a_x

        ai=ax,它依然要在

        x

        x

        x 后面,不影响

        x

        x

        x 的排名。所以在第二个循环中,判断条件必须是严格小于 (<),绝不能写成 <=。

    • 坑二:使用 sort() 排序

      要点提醒: 如果每次查询都排序,单次查询复杂度为

      O

      (

      n

      log

      n

      )

      \\mathcal{O}(n \\log n)

      O(nlogn)。 最坏情况下,有 200,000 次查询,总运算量大约为

      200000

      ×

      8000

      ×

      log

      2

      (

      8000

      )

      2

      ×

      10

      9

      200000 \\times 8000 \\times \\log_2(8000) \\approx 2 \\times 10^9

      200000×8000×log2(8000)2×109 次运算,这在正规比赛 1 秒的时间限制内绝对会超时(TLE)。

    参考代码

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

    int n, q, op, x, v, a[maxn];

    int main(){
    scanf("%d %d", &n, &q); // 读入元素个数 n 和操作次数 q
    for(int i = 1; i <= n; i++){
    scanf("%d", &a[i]); // 循环读入初始数组
    }
    while(q){
    scanf("%d %d", &op, &x); // 循环处理 $Q$ 次操作,读入操作类型和目标下标
    if(op == 1){
    scanf("%d", &v); // 若为操作 1,读入修改后的新值 $v$
    a[x] = v; // $O(1)$ 完成单点修改
    }
    else{
    int p = x; // 假设排位就是 x
    for(int i = 1; i < x; i++){
    if(a[i] > a[x]) p; // 稳定排序核心:原先在前面且严格大于 a[x] 的元素会排到它后面,故 a[x] 排名升 1(数字减 1)
    }
    for(int i = x + 1; i <= n; i++){
    if(a[i] < a[x]) p++; // 稳定排序核心:原先在后面且严格小于 a[x] 的元素会排到它前面,故 a[x] 排名降 1(数字加 1)
    }
    printf("%d\\n", p); // 打印计算出来的真实排位
    }
    }
    return 0;
    }


    解密

    思路要点

    题目一共给出了

    k

    k

    k组询问。每组询问包含三个正整数

    n

    ,

    d

    ,

    e

    n, d, e

    n,d,e,要求我们找出两个正整数

    p

    p

    p

    q

    q

    q(满足

    p

    q

    p \\le q

    pq),使得它们同时满足以下两个方程:

    • n

      =

      p

      ×

      q

      n = p \\times q

      n=p×q

    • e

      ×

      d

      =

      (

      p

      1

      )

      (

      q

      1

      )

      +

      1

      e \\times d = (p – 1)(q – 1) + 1

      e×d=(p1)(q1)+1

    如果能找到这样的

    p

    p

    p

    q

    q

    q,就输出它们;如果找不到满足条件的正整数,则输出 NO。

    本题表面上是一个复杂的“加密/解密”故事,剥去这层外壳后,它的算法本质是:求解二元方程组 / 一元二次方程的整数解。这是一个典型的数学推导题,考察的是同学们将代数式变形并转化为编程逻辑的能力。

    关键思路

    如果我们尝试去暴力枚举

    p

    p

    p(从

    1

    1

    1

    n

    \\sqrt{n}

    n

    ),由于

    n

    10

    18

    n \\le 10^{18}

    n1018

    n

    \\sqrt{n}

    n

    最大可达

    10

    9

    10^9

    109,再加上

    k

    =

    10

    5

    k = 10^5

    k=105 次询问,总运算量会达到惊人的

    10

    14

    10^{14}

    1014 级别,绝对会超时(TLE)。

    因此,我们必须对已知的方程进行数学化简:

  • **展开方程 **:

    • e

      ×

      d

      =

      p

      ×

      q

      p

      q

      +

      1

      +

      1

      e \\times d = p \\times q – p – q + 1 + 1

      e×d=p×qpq+1+1

    • e

      ×

      d

      =

      p

      ×

      q

      (

      p

      +

      q

      )

      +

      2

      e \\times d = p \\times q – (p + q) + 2

      e×d=p×q(p+q)+2

  • 将题目中已知的

    n

    =

    p

    ×

    q

    n = p \\times q

    n=p×q 代入上式:

    • e

      ×

      d

      =

      n

      (

      p

      +

      q

      )

      +

      2

      e \\times d = n – (p + q) + 2

      e×d=n(p+q)+2

  • 移项,把未知数

    p

    +

    q

    p + q

    p+q 放到左边:

    • p

      +

      q

      =

      n

      e

      ×

      d

      +

      2

      p + q = n – e \\times d + 2

      p+q=ne×d+2

  • 这时候你会惊奇地发现,题目提示中的

    m

    =

    n

    e

    ×

    d

    +

    2

    m = n – e \\times d + 2

    m=ne×d+2 其实就是

    p

    +

    q

    p + q

    p+q 的值!

    现在,问题转化为了一个经典的数学模型——已知两数之积与两数之和,求这两个数:

    • p

      ×

      q

      =

      n

      p \\times q = n

      p×q=n

    • p

      +

      q

      =

      m

      p + q = m

      p+q=m

    根据韦达定理,

    p

    p

    p

    q

    q

    q 恰好是一元二次方程

    x

    2

    m

    x

    +

    n

    =

    0

    x^2 – mx + n = 0

    x2mx+n=0 的两个根!根据求根公式,解得:

    x

    =

    m

    ±

    m

    2

    4

    n

    2

    x = \\frac{m \\pm \\sqrt{m^2 – 4n}}{2}

    x=2m±m24n

    因为题目要求

    p

    q

    p \\le q

    pq,所以我们让

    p

    p

    p 取减号,

    q

    q

    q 取加号:

    • p

      =

      m

      m

      2

      4

      n

      2

      p = \\frac{m – \\sqrt{m^2 – 4n}}{2}

      p=2mm24n

    • q

      =

      m

      +

      m

      2

      4

      n

      2

      q = \\frac{m + \\sqrt{m^2 – 4n}}{2}

      q=2m+m24n

    我们只需要在程序中判断判别式

    Δ

    =

    m

    2

    4

    n

    \\Delta = m^2 – 4n

    Δ=m24n 是否大于 0 并且为完全平方数,以及分子是否能被 2 整除即可。

    解题步骤

    我们以样例输入中的第一组数据 770 77 5 为例,模拟代码的执行过程:

  • 变量定义与初始化:

    • 定义 int k; 记录询问次数。

    • 定义 ll n, e, d, p, q; 存储输入的各项参数。全线使用 long long 防止计算过程中乘法溢出。

  • 读入数据:

    • 首先读入 k = 10。进入 while(k–) 循环,读入第一组数据:n = 770, d = 77, e = 5。
  • 公式计算:

    • 计算 ll m = n – e * d + 2;:

      m

      =

      770

      5

      ×

      77

      +

      2

      =

      770

      385

      +

      2

      =

      387

      m = 770 – 5 \\times 77 + 2 = 770 – 385 + 2 = 387

      m=7705×77+2=770385+2=387

    • 计算判别式 ll dt = m * m – 4 * n;:

      d

      t

      =

      387

      2

      4

      ×

      770

      =

      149769

      3080

      =

      146689

      dt = 387^2 – 4 \\times 770 = 149769 – 3080 = 146689

      dt=38724×770=1497693080=146689

    • 由于 dt >= 0 成立,执行 r = sqrt(dt);:

      146689

      =

      383

      \\sqrt{146689} = 383

      146689

      =383,此时变量 r = 383。

  • 合法性检查:

    • 代码中通过 if(dt < 0 || r * r != dt || (m – r) % 2) 进行三重拦截判断:

      • dt < 0:判别式小于 0,无实数根(本例为 146689,不满足)。

      • r * r != dt:检查 dt 是否为完全平方数(本例

        383

        t

        i

        m

        e

        s

        383

        =

        146689

        =

        =

        d

        t

        383 \\\\times 383 = 146689 == dt

        383times383=146689==dt,通过检查)。

      • (m – r) % 2:检查分子是否为偶数,即能否整除 2(本例

        (

        387

        383

        )

        %

        2

        =

        4

        %

        2

        =

        0

        (387 – 383) \\% 2 = 4 \\% 2 = 0

        (387383)%2=4%2=0,通过检查)。

      • 由于三项拦截均未触发,说明存在正整数解。

  • 求解与格式化输出:

    • 计算 p = (m – r) / 2;:

      p

      =

      387

      383

      2

      =

      2

      p = \\frac{387 – 383}{2} = 2

      p=2387383=2

    • 计算 q = m – p;:

      q

      =

      387

      2

      =

      385

      q = 387 – 2 = 385

      q=3872=385

    • 执行 printf("%lld %lld\\n", p, q);。输出结果:2 385。

  • 本题易错点
    • 坑一:数据范围溢出

      要点提醒:看清数据范围:

      n

      10

      18

      n \\le 10^{18}

      n1018

      e

      ×

      d

      10

      18

      e \\times d \\le 10^{18}

      e×d1018。这意味如果用普通的 int 都会直接爆掉。代码全线要使用 ll(long long),并且中间计算 m * m 和 4 * n 时也用 ll 承接,才能完美避开这个大坑。

    • 坑二:漏掉整除判断

      要点提醒: 求根公式中有一个“除以 2”的操作。如果

      m

      r

      m – r

      mr是个奇数,除以 2 就会产生小数(在计算机整数除法中会向下取整导致答案错误),这就不能满足题目要求的“正整数”了。代码里的 (m – r) % 2 成功拦截了这种情况。

    • 坑三:暴力循环枚举

      要点提醒: 妄图通过 for(ll p=1; p*p<=n; p++) 来找答案。测试点 7~10 的

      n

      n

      n都在

      10

      18

      10^{18}

      1018级别,直接卡死卡到 0 分。

    参考代码 – 二次方程构造法

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

    int k;
    ll n, e, d, p, q;
    int main(){
    scanf("%d", &k); // 读入总询问次数
    while(k){
    scanf("%lld %lld %lld", &n, &d, &e); // 严格按照题目顺序读入 n, d, e
    ll m = n e * d + 2; // 数学推导变形,m 核心代表 p + q 的值
    // (x – p) * (x – q) = x^2 – (p + q) * x + p * q
    ll dt = m * m 4 * n, r = 0; // dt 为一元二次方程判别式 b^2 – 4ac
    if(dt >= 0) r = sqrt(dt); // 当判别式大于等于0时,计算其平方根
    // 核心拦截:无实根、非完全平方数、分子不能整除2
    if(dt < 0 || r * r != dt || (m r) % 2){
    printf("NO\\n"); // 只要有一项不满足,即为无整数解
    continue; // 跳过本次循环,继续下一次询问
    }
    p = (m r) / 2; // 依据公式计算出较小的正整数根 p
    q = m p; // 依据和的定义计算出较大的正整数根 q
    printf("%lld %lld\\n", p, q); // 格式化输出 p 和 q,保证了 p <= q
    }
    return 0;
    }


    参考代码 – 二分法

    经过前面的数学推导,我们已经成功把原方程组化简为了经典的“知和知积”模型:

    p

    +

    q

    =

    m

    p + q = m

    p+q=m

    p

    ×

    q

    =

    n

    p \\times q = n

    p×q=n

    此时,我们可以把

    q

    q

    q

    m

    p

    m – p

    mp 代替,消去变量

    q

    q

    q,得到关于

    p

    p

    p 的一元方程:

    p

    ×

    (

    m

    p

    )

    =

    n

    p \\times (m – p) = n

    p×(mp)=n

    由于题目设定

    p

    q

    p \\le q

    pq,当

    p

    p

    p

    q

    q

    q 无限接近时,

    p

    p

    p 最大只能取到

    m

    2

    \\frac{m}{2}

    2m。因此,

    p

    p

    p 的合法取值范围被严格锁定在整数区间

    [

    1

    ,

    m

    2

    ]

    [1, \\frac{m}{2}]

    [1,2m]

    我们设一个关于

    p

    p

    p 的函数:

    f

    (

    p

    )

    =

    p

    ×

    (

    m

    p

    )

    f(p) = p \\times (m – p)

    f(p)=p×(mp)

    在线性代数或二次函数图像中,当

    p

    [

    1

    ,

    m

    2

    ]

    p \\in [1, \\frac{m}{2}]

    p[1,2m] 时,随着

    p

    p

    p 的增大,

    f

    (

    p

    )

    f(p)

    f(p) 的值是严格单调递增的。

    • 如果我们猜的

      p

      p

      p 偏小,计算出来的乘积

      p

      ×

      q

      p \\times q

      p×q 就会小于

      n

      n

      n

    • 如果我们猜的

      p

      p

      p 偏大,计算出来的乘积

      p

      ×

      q

      p \\times q

      p×q 就会大于

      n

      n

      n

    既然区间固定,且函数值具备严格的单调性,这便是天造地设的“二分答案”场景! 我们可以直接在

    [

    1

    ,

    m

    2

    ]

    [1, \\frac{m}{2}]

    [1,2m] 区间内二分枚举

    p

    p

    p 的值。

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

    int k;
    ll n, e, d, p, q;
    int main(){
    scanf("%d", &k); // 读入总询问次数
    while(k){
    bool f = 0; // 核心标记:初始化为 0 表示当前这组询问尚未找到解
    scanf("%lld %lld %lld", &n, &d, &e); // 按照题目顺序读入 n, d, e
    ll m = n e * d + 2; // 数学推导变形,m 代表 p + q 的值
    ll l = 1, r = m / 2; // 根据 p <= q 锁定 p 的上下界,建立二分闭区间 [l, r]
    while(l <= r){
    ll mid = (l + r) >> 1; // mid 二分枚举 p 的可能取值
    ll q = m mid; // 联动计算出对应的 q 值
    if(mid * q < n){
    l = mid + 1; // 实际乘积偏小,说明 mid 猜小了,左边界右移
    }
    else if(mid * q > n){
    r = mid 1; // 实际乘积偏大,说明 mid 猜大了,右边界左移
    }
    else{
    printf("%lld %lld\\n", mid, q); // 乘积完美相等,找到正整数解并打印
    f = 1; // 将标记置为 1,代表有解
    break; // 提前结束二分查找,节省效率
    }
    }
    if(!f){
    printf("NO\\n"); // 如果二分区间找满仍未触发 else,说明无整数解,输出 NO
    }
    }

    return 0;
    }

    公路

    思路要点

    想象一下你正在进行一场自驾游,从

    1

    1

    1 号点开到

    n

    n

    n 号点。每个站点之间的距离不同,而且每个站点的油价也高低不一。 你的油箱是无限大的,但你每到一个新站点,都只能买整数升的油。车子每升油能跑

    d

    d

    d 公里。

    我们的目标是:妥善安排在哪些站点加多少油,使得安全到达终点

    n

    n

    n 时的总花费最少。本题表面上是一个复杂的自驾游模拟,剥去它的“外壳”,其本质是一个贪心算法(Greedy),配合前缀最小值的维护。

    关键思路
    • 买油的“反悔”思维:既然油箱无限大,我们在前进的过程中,如果发现当前的站点的油价比之前经过的所有站点都贵,那我们其实应该在“之前最便宜的那个站”就把油加够。

    • 前缀最小值维护:我们只需要在从左到右遍历站点时,顺便记录一下“到目前为止,我见过的最便宜的油价是多少”。每当需要为下一段路程买油时,都按这个历史最便宜的价格来算账。

    • 余量管理:因为只能买整数升的油,所以每次加完油往往会“跑超”一点点。这个超出的里程(里程余量)可以留给下一段路使用,必须精准扣除。

    凡是题目中出现“资源可以无限囤积,沿途代价不断变化,要求单向移动的总代价最小”的路线规划题,基本都可以锁定为“历史最低价加油”的贪心模型。

    解题步骤

    我们以样例输入

    n

    =

    5

    ,

    d

    =

    4

    n = 5, d = 4

    n=5,d=4,路程

    v

    =

    [

    10

    ,

    10

    ,

    10

    ,

    10

    ]

    v = [10, 10, 10, 10]

    v=[10,10,10,10],油价

    a

    =

    [

    9

    ,

    8

    ,

    9

    ,

    6

    ,

    5

    ]

    a = [9, 8, 9, 6, 5]

    a=[9,8,9,6,5] 为例,手演一遍程序的执行过程。

    在这个过程中,我们初始化总花费 s = 0,历史最低油价 mina = INT_MAX,历史最低油价站点 p = 1,以及之前的里程余量 r = 0。

    • 第 1 站 (

      i

      =

      1

      i = 1

      i=1):读入当前站油价 a[1] = 9。

      • 更新历史最低价:由于

        9

        <

        INT_MAX

        9 < \\text{INT\\_MAX}

        9<INT_MAX,所以 mina 变成

        9

        9

        9,最便宜站点指针 p = 1。

      • 计算当前段所需里程:下一段路长 v[1] = 10,减去之前的余量 r = 0,实际还需要跑

        10

        10

        10 公里。

      • 计算加油量:用 ceil(1.0 * 10 / 4) 向上取整得到 t = 3 升。

      • 算账:在第 p 站(即第 1 站)买

        3

        3

        3 升油,花费 s += 3 * 9,此时总花费 s = 27。

      • 更新余量:

        3

        3

        3 升油能跑

        12

        12

        12 公里,实际只跑了

        10

        10

        10 公里,剩下 r = 2 公里留给下一站。

    • 第 2 站 (

      i

      =

      2

      i = 2

      i=2):读入当前站油价 a[2] = 8。

      • 更新历史最低价:由于

        8

        <

        9

        8 < 9

        8<9,发现更便宜的站了!更新 mina = 8,p = 2。

      • 计算当前段所需里程:下一段路长 v[2] = 10,减去上一站留下的余量 r = 2,实际还需要跑

        8

        8

        8 公里。

      • 计算加油量:用 ceil(1.0 * 8 / 4) 刚好整除,得到 t = 2 升。

      • 算账:在当前最便宜的第 p 站(即第 2 站)买

        2

        2

        2 升油,花费 s += 2 * 8,此时总花费 s = 27 + 16 = 43。

      • 更新余量:

        2

        2

        2 升油跑

        8

        8

        8 公里,刚好把路跑完,余量 r = 0。

    • 第 3 站 (

      i

      =

      3

      i = 3

      i=3):读入当前站油价 a[3] = 9。

      • 更新历史最低价:由于

        9

        >

        8

        9 > 8

        9>8,不更新。当前最便宜的油价依然是 mina = 8,对应的站点依然是 p = 2。

      • 计算当前段所需里程:下一段路长 v[3] = 10,减去余量 r = 0,实际还需要跑

        10

        10

        10 公里。

      • 计算加油量:用 ceil(1.0 * 10 / 4) 向上取整得到 t = 3 升。

      • 算账:虽然现在在第 3 站,但我们可以假装在第 p 站(第 2 站)多买了

        3

        3

        3 升油!花费 s += 3 * 8,此时总花费 s = 43 + 24 = 67。

      • 更新余量:

        3

        3

        3 升油跑

        12

        12

        12 公里,减去路程

        10

        10

        10,留下余量 r = 2 公里。

    • 第 4 站 (

      i

      =

      4

      i = 4

      i=4):读入当前站油价 a[4] = 6。

      • 更新历史最低价:由于

        6

        <

        8

        6 < 8

        6<8,又找到了更便宜的站!更新 mina = 6,p = 4。

      • 计算当前段所需里程:下一段路长 v[4] = 10,减去余量 r = 2,实际还需要跑

        8

        8

        8 公里。

      • 计算加油量:用 ceil(1.0 * 8 / 4) 得到 t = 2 升。

      • 算账:在最便宜的第 p 站(第 4 站)买

        2

        2

        2 升油,花费 s += 2 * 6,此时总花费 s = 67 + 12 = 79。

      • 更新余量:余量 r = 0。

    • 第 5 站 (

      i

      =

      5

      i = 5

      i=5):

      • 读入当前站油价 a[5] = 5。

      • 因为已经到达终点(i == n),触发 continue,结束循环。

      • 最终输出结果:s = 79。

    本题易错点
    • 坑一:数据范围爆 int

      要点提醒:是本题最大的陷阱。看数据范围,总路程可以达到

      10

      5

      ×

      10

      5

      =

      10

      10

      10^5 \\times 10^5 = 10^{10}

      105×105=1010 公里。如果油价也是

      10

      5

      10^5

      105,总花费最大可能达到

      10

      15

      10^{15}

      1015 左右,远远超出了 int 的最大值(约

      2

      ×

      10

      9

      2 \\times 10^9

      2×109)。因此,总花费 s 必须开 long long,且在计算 t * a[p] 时,必须引入 1LL 强制进行类型提升。

    • 坑二:局部贪心(走一步看一步)

      要点提醒: 有同学误以为到了

      i

      i

      i 站,就只买刚刚好走到

      i

      +

      1

      i+1

      i+1 站的油。这会导致如果

      i

      i

      i 站很便宜,而

      i

      +

      1

      i+1

      i+1 站很贵,车主却无法在

      i

      i

      i 站多买点油囤着,从而浪费了省钱的机会。

    参考代码

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

    int n, d, v[maxn], a[maxn];
    long long s; // 终点总花费,用 long long 防止爆 int

    int main(){
    // 读入站点数和每升油可前进距离
    scanf("%d %d", &n, &d);
    for(int i = 1; i < n; i++){
    scanf("%d", &v[i]);
    }

    int mina = INT_MAX, p = 1, r = 0; // mina:历史最低油价, p:最低油价站点, r:之前的里程余量

    for(int i = 1; i <= n; i++){
    scanf("%d", &a[i]);
    if(i == n) continue; // 到达终点,不需要在终点加油

    // 贪心策略:维护到达当前位置为止的最低油价和站点
    if(a[i] < mina){
    mina = a[i];
    p = i;
    }

    // 计算在当前历史最便宜站点 p 应该购买的油量 t
    int t = ceil(1.0 * (v[i] r) / d);
    s += 1LL * t * a[p]; // 1LL 防止 t * a[p] 计算时爆 int
    r += (t * d v[i]); // 更新走完这一段后,多出来的里程余量
    }

    printf("%lld", s);

    return 0;
    }


    地图探险

    思路要点

    题目给我们一个

    n

    ×

    m

    n \\times m

    n×m的格子地图,里面有两种格子:. 代表可以通过的空地,x 代表无法通过的障碍物。

    机器人有一个初始位置

    (

    x

    0

    ,

    y

    0

    )

    (x_0, y_0)

    (x0,y0)和一个初始朝向

    d

    0

    d_0

    d0(0东、1南、2西、3北)。它一共要连续做

    k

    k

    k步操作。每一步的行动规则很简单:

    • 试探着往当前方向向前走一步。

    • 如果前方在地图内且是空地,它就开心地走过去,方向不变。

    • 如果前方超出了地图边界或者踩到了障碍物,它就不动,原地向右转 90 度(顺时针转弯)。

    我们要做的,就是帮它数一数:在走完

    k

    k

    k步之后,地图上有多少个不同的格子被它踩过(注意:起点也算踩过,重复踩到的格子只能算一次)。

    这是一道非常典型的二维网格图状态模拟题。它本质上考查的是 “数组边界控制” 与 “方向数组(常数偏移量)” 的联合运用。题目没有任何复杂的算法(如不需要深搜 DFS 或广搜 BFS),只要完全按照题目给定的规则,用代码一步步模拟机器人的移动即可。

    关键思路

    由于机器人的下一步行动是由当前状态唯一确定的,它没有“分支选择”,因此直接进行顺序模拟是最高效、最稳妥的方法。我们如何将复杂的方向转化和格子移动优雅地写成代码呢?

    • 方向数组的妙用:

    处理二维网格移动时,千万不要写一堆 if (d == 0) y++; else if (d == 1) x++; 这样的冗长代码。我们可以定义一个二维数组来表示方向的变化量(偏移量):int d[4][2] = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};这样,当方向为 p 时,下一步的坐标就是 x + d[p][0] 和 y + d[p][1]。

    • 顺时针旋转的数学表达:

    题目中说向右转,对应的数字变化是:0

    \\rightarrow

    1

    \\rightarrow

    2

    \\rightarrow

    3

    \\rightarrow

    0。

    这在数学上可以用取模运算(%)完美解决:

    p

    =

    (

    p

    +

    1

    )

    m

    o

    d

    4

    p = (p + 1) \\bmod 4

    p=(p+1)mod4

    • **数据范围与时空复杂度:**对于所有数据,

      T

      5

      T \\le 5

      T5

      n

      ,

      m

      10

      3

      n, m \\le 10^3

      n,m103

      k

      10

      6

      k \\le 10^6

      k106

      • 时间复杂度:每一组数据我们只需要循环模拟

        k

        k

        k次,每次移动的操作都是

        O

        (

        1

        )

        O(1)

        O(1)的。因此总时间复杂度为

        O

        (

        T

        ×

        k

        )

        O(T \\times k)

        O(T×k),最大计算量在

        5

        ×

        10

        6

        5 \\times 10^6

        5×106左右。在计算机 1 秒可以运行约

        10

        8

        10^8

        108次运算的背景下,这个纯模拟解法可以轻松 AC。

      • 空间复杂度:由于地图最大为

        1000

        ×

        1000

        1000 \\times 1000

        1000×1000,开辟两个 1005 × 1005 的 bool 数组只需要约 2MB 内存,远低于比赛限制。

    解题步骤

    我们以样例 1 的第一组数据为例,模拟代码的执行过程,输入数据为:

    1 5 4
    1 1 2
    ….x

    n

    =

    1

    ,

    m

    =

    5

    ,

    k

    =

    4

    n = 1, m = 5, k = 4

    n=1,m=5,k=4,初始位置

    (

    x

    ,

    y

    )

    =

    (

    1

    ,

    1

    )

    (x, y) = (1, 1)

    (x,y)=(1,1),初始方向

    p

    =

    2

    p = 2

    p=2(向西)。地图为 ….x。

  • 变量定义与初始化:
    • 定义全局地图数组 a[1005][1005] 存储障碍物,b[1005][1005] 记录每个位置是否被访问过。

    • 定义常数方向数组 d[4][2] = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}}。

  • 读入数据与地图重置:
    • 读入当前组的参数。通过双重循环读入字符,如果是 'x' 则令 a[i][j] = 1,同时将 b[i][j] 全部重置为 0(清空上一组数据的影响)。
  • 起点标记:
    • 此时机器人在

      (

      1

      ,

      1

      )

      (1, 1)

      (1,1),我们令计数器 s = 1,并标记起点 b[1][1] = 1。

  • 循环模拟

    k

    k

    k 次操作(进入 while(k–) 循环):

    • 第 1 步 (

      k

      =

      4

      k=4

      k=4):

      • 计算下一步位置:

        x

        1

        =

        x

        +

        d

        [

        2

        ]

        [

        0

        ]

        =

        1

        +

        0

        =

        1

        x_1 = x + d[2][0] = 1 + 0 = 1

        x1=x+d[2][0]=1+0=1

        y

        1

        =

        y

        +

        d

        [

        2

        ]

        [

        1

        ]

        =

        1

        +

        (

        1

        )

        =

        0

        y_1 = y + d[2][1] = 1 + (-1) = 0

        y1=y+d[2][1]=1+(1)=0

      • 检查条件:由于

        y

        1

        =

        0

        y_1 = 0

        y1=0,不满足

        1

        y

        1

        m

        1 \\le y_1 \\le m

        1y1m,说明越出了地图左边界。

      • 执行 else 分支:改变方向,

        p

        =

        (

        2

        +

        1

        )

        m

        o

        d

        4

        =

        3

        p = (2 + 1) \\bmod 4 = 3

        p=(2+1)mod4=3(朝北)。位置保持

        (

        1

        ,

        1

        )

        (1, 1)

        (1,1)不变。

    • 第 2 步 (

      k

      =

      3

      k=3

      k=3):

      • 计算下一步位置:

        x

        1

        =

        x

        +

        d

        [

        3

        ]

        [

        0

        ]

        =

        1

        +

        (

        1

        )

        =

        0

        x_1 = x + d[3][0] = 1 + (-1) = 0

        x1=x+d[3][0]=1+(1)=0

        y

        1

        =

        y

        +

        d

        [

        3

        ]

        [

        1

        ]

        =

        1

        +

        0

        =

        1

        y_1 = y + d[3][1] = 1 + 0 = 1

        y1=y+d[3][1]=1+0=1

      • 检查条件:由于

        x

        1

        =

        0

        x_1 = 0

        x1=0,不满足

        1

        x

        1

        n

        1 \\le x_1 \\le n

        1x1n,越出了地图上边界。

      • 执行 else 分支:改变方向,

        p

        =

        (

        3

        +

        1

        )

        m

        o

        d

        4

        =

        0

        p = (3 + 1) \\bmod 4 = 0

        p=(3+1)mod4=0(朝东)。位置保持

        (

        1

        ,

        1

        )

        (1, 1)

        (1,1)不变。

    • 第 3 步 (

      k

      =

      2

      k=2

      k=2):

      • 计算下一步位置:

        x

        1

        =

        1

        +

        0

        =

        1

        x_1 = 1 + 0 = 1

        x1=1+0=1

        y

        1

        =

        1

        +

        1

        =

        2

        y_1 = 1 + 1 = 2

        y1=1+1=2

      • 检查条件:

        (

        1

        ,

        2

        )

        (1, 2)

        (1,2)在地图范围内,且 a[1][2] == 0(是空地)。条件全部通过!

      • 更新状态:机器人成功走到新位置,

        x

        =

        1

        ,

        y

        =

        2

        x = 1, y = 2

        x=1,y=2

      • 统计数量:发现 b[1][2] 为 0,说明这个新格子之前没来过。令 b[1][2] = 1,并且 ++s,此时 s = 2。

    • 第 4 步 (

      k

      =

      1

      k=1

      k=1):

      • 计算下一步位置:

        x

        1

        =

        1

        +

        0

        =

        1

        x_1 = 1 + 0 = 1

        x1=1+0=1

        y

        1

        =

        2

        +

        1

        =

        3

        y_1 = 2 + 1 = 3

        y1=2+1=3

      • 检查条件:

        (

        1

        ,

        3

        )

        (1, 3)

        (1,3)在地图范围内且为空地。通过!

      • 更新状态:机器人移动到

        x

        =

        1

        ,

        y

        =

        3

        x = 1, y = 3

        x=1,y=3

      • 统计数量:b[1][3] 之前为 0。令 b[1][3] = 1,并且 ++s,此时 s = 3。

  • 格式化输出:
    • 循环结束,执行 printf("%d\\n", s);,屏幕输出结果:3。与样例输出完全一致。
    本题易错点
    • 坑一:多组数据未清空(重置)

      要点提醒:本题有多组测试数据(

      T

      5

      T \\le 5

      T5)。如果忘记清空 b 数组,上一组数据的足迹会直接污染下一组数据,导致后面输出偏大。

    • 坑二:起点漏算

      要点提醒: 机器人只要站在起点,起点就已经算是“被经过的位置”了。所以初始化必须是 s = 1; b[x][y] = 1;。

    • 坑三:坐标系与边界问题

      要点提醒: 计算机的字符串通常是从 0 开始索引的,而题目明确指出地图坐标是从

      (

      1

      ,

      1

      )

      (1, 1)

      (1,1)

      (

      n

      ,

      m

      )

      (n, m)

      (n,m)。代码中采用了 scanf("%s", c + 1); 将字符串读入转为 1 基准,配合 x1 >= 1 && x1 <= n 的判断,成功避免了边界越界造成的段错误(RE)。

    参考代码

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

    int T, n, m, k, x, y, p, s;
    bool a[1005][1005]; // 存储地图障碍,1表示障碍'x',0表示空地'.'
    bool b[1005][1005]; // 访问标记数组,1表示该位置已被机器人经过
    // 方向偏移量数组:d[0]向东, d[1]向南, d[2]向西, d[3]向北
    int d[4][2] = {{0, 1}, {1, 0}, {0, 1}, {1, 0}};

    int main(){
    // 读入测试数据组数
    scanf("%d", &T);
    while(T){
    scanf("%d %d %d", &n, &m, &k);
    scanf("%d %d %d", &x, &y, &p);

    char c[1005];
    for(int i = 1; i <= n; i++){
    scanf("%s", c + 1); // 从 c[1] 开始读入一行的整串字符串
    for(int j = 1; j <= m; j++){
    a[i][j] = ((c[j] == 'x') ? 1 : 0); // 标记障碍物
    b[i][j] = 0; // 关键:多组数据必须在每次开始前手动清空标记
    }
    }

    s = 1; // 初始位置算作经过的第 1 个位置
    b[x][y] = 1; // 标记起点为已访问

    while(k){
    // 预测下一步的坐标
    int x1 = x + d[p][0], y1 = y + d[p][1];
    // 边界检查与障碍物检查
    if(x1 >= 1 && x1 <= n && y1 >= 1 && y1 <= m && !a[x1][y1]){
    x = x1, y = y1; // 检查通过,更新当前坐标
    if(!b[x][y]){ // 如果这个位置之前从未走过
    b[x][y] = 1; // 标记为走过
    ++s; // 答案总数加 1
    }
    }
    else{
    p = (p + 1) % 4; // 碰壁或遇障,向右转 90 度
    }
    }

    printf("%d\\n", s); // 输出当前测试组的最终答案
    }

    return 0;
    }


    座位

    思路要点

    拿到题目,不要被“考场”、“成绩”、“蛇形排座”这些长篇大论吓到。我们把题目的“外壳”剥去,它本质上在考察两个核心操作:

    • 求排名(统计):在一个数列中,找出一个特定数字(小 R 的成绩)从大到小排在第几名。

    • 一维坐标转二维坐标(数学映射):已知排名,按照“蛇形填列”的规则,推算出该排名对应的二维坐标 (列 c, 行 r)。

    关键思路

    这道题最容易想到的“暴力解法”是:用一个数组存下所有成绩,从大到小排序,找到小 R 的名次;然后开一个二维数组,用两层循环把名次按照蛇形填进去,最后找出小 R 的坐标。 但这不够简洁优雅!

    • 降维打击(不用排序):题目保证所有人成绩不同。要求小 R 的排名,只需要看有几个人成绩比他高。如果有

      k

      k

      k 个人成绩比他高,那他就是第

      k

      +

      1

      k + 1

      k+1 名。这样我们连数组都不用开,一边读入数据一边统计即可,空间复杂度直接降为

      O

      (

      1

      )

      O(1)

      O(1)

    • 数学推导(不用模拟填表):知道了名次

      p

      p

      p 之后,没必要真的去画个矩阵。假设每列有

      n

      n

      n 个座位,我们可以直接用除法和取模运算精准定位:

      • 列号计算:看前面排满了几个整列。

      • 行号计算:看在这列里是第几个。由于是蛇形,奇数列从上往下,偶数列从下往上,分情况讨论一下就行。

    解题步骤

    我们以以下样例输入为例,模拟计算机的执行过程:

    2 2
    99 100 97 98

  • 变量定义与初始化: 定义 n, m, x, t,以及排名 p = 1。

  • 读入数据与统计排名:

    • 读入 n = 2, m = 2。读入小 R 的成绩:此时 x = 99。

    • 进入循环,读取剩下的

      2

      ×

      2

      1

      =

      3

      2 \\times 2 – 1 = 3

      2×21=3 个人的成绩:

      • 读入 t = 100:因为

        100

        >

        99

        100 > 99

        100>99,比小 R 高,排名后移,p 变成 2。

      • 读入 t = 97:因为

        97

        <

        99

        97 < 99

        97<99,不影响小 R 排名,p 保持 2。

      • 读入 t = 98:因为

        98

        <

        99

        98 < 99

        98<99,不影响小 R 排名,p 保持 2。

    • 循环结束,小 R 的最终排名

      p

      =

      2

      p = 2

      p=2

  • 公式计算(核心推导):

    • 计算列号 c:

      c

      =

      p

      1

      n

      +

      1

      c = \\lfloor \\frac{p – 1}{n} \\rfloor + 1

      c=np1+1。代入

      p

      =

      2

      ,

      n

      =

      2

      p = 2, n = 2

      p=2,n=2:计算

      (

      2

      1

      )

      /

      2

      +

      1

      (2 – 1) / 2 + 1

      (21)/2+1,结果为

      c

      =

      1

      c = 1

      c=1。说明在第 1 列。

    • 计算所在列的顺序位 r(注意这里的 r 暂时代表从该列起点数的第几个):

      r

      =

      (

      p

      1

      )

      m

      o

      d

      n

      +

      1

      r = (p – 1) \\bmod n + 1

      r=(p1)modn+1。代入

      p

      =

      2

      ,

      n

      =

      2

      p = 2, n = 2

      p=2,n=2:计算

      (

      2

      1

      )

      m

      o

      d

      2

      +

      1

      (2 – 1) \\bmod 2 + 1

      (21)mod2+1,结果为

      r

      =

      2

      r = 2

      r=2。说明是该列的第 2 个位置。

  • 蛇形方向判断与输出:

    • 此时 c = 1,列数输出 1。

    • 判断 c % 2:1 % 2 结果为真(奇数列)。奇数列是从上往下的正常顺序,所以真正的行号就是刚才算出的

      r

      r

      r

    • 输出结果:1 2。

  • 本题易错点
    • 坑一:先输出列,再输出行

      要点提醒:平时我们做二维数组题,习惯了 (行, 列) 的思维,但本题输出格式明确要求先 c 后 r。没仔细看题很容易在此吃亏。

    • 坑二:1-based 索引的整除与取模

      要点提醒: 排名是从 1 开始的。直接用

      p

      /

      n

      p / n

      p/n 算列号是错的。比如

      n

      =

      2

      n=2

      n=2, 第 2 名显然在第 1 列,但

      2

      /

      2

      =

      1

      2/2 = 1

      2/2=1,如果加 1 就变成第 2 列了。必须先减 1 转成 0-based,运算完后再加 1:(p – 1) / n + 1,这是极其关键的数学技巧。

    • 坑三:倒序求行号公式

      要点提醒: 从下往上数时,原本的第

      r

      r

      r 个位置应该变成第几个?公式是:总数 + 1 – 顺数排位。即 n + 1 – r,千万不要写成 n – r(因为这样算出来可能会出现 0 行)。

    参考代码:解法一 – 数学推导法(高效,推荐)

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

    int main(){
    int n, m, x, t, p = 1; // n,m为行列, x为小R成绩, t为当前读入成绩, p为小R排位
    cin >> n >> m >> x;

    for(int i = 2; i <= n * m; i++){
    cin >> t;
    if(t > x) p++; // p : r 的排位(遇到比小R分数高的,排位就往后移)
    }

    // 数学推导 – 模拟排位
    int r, c;
    c = (p 1) / n + 1; // 利用 0-based 思想计算列号 c
    cout << c << " ";

    r = (p 1) % n + 1; // 计算在该列中从上往下数是第几个
    if(c % 2){ // 奇数列:从上往下数第 r 行
    cout << r;
    }
    else{ // 偶数列:从下往上数第 r 行
    cout << n + 1 r; // 倒数行号的转化公式:总长度 + 1 – 顺数位置
    }

    return 0;
    }


    参考代码:解法二 – 模拟法 (暴力,也能过)

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

    int main() {
    int n, m;
    cin >> n >> m;

    int a[110] = {}; // 题目 n,m <= 10,总人数最多 100,开 110 足够,防止数组越界
    for(int i = 1; i <= n * m; i++) {
    cin >> a[i];
    }

    int r = a[1]; // 记录小 R 的成绩,作为后续模拟寻找的 Target (目标)

    // 从大到小排序,greater<int>() 是关键。a[t] 就是第 t 名的成绩。
    sort(a + 1, a + n * m + 1, greater<int>());

    // x 为行,y 为列,t 为当前正在分配座位的人的排名索引 (从第 1 名开始处理)
    // p 为方向控制开关 (非常经典的状态变量):1 表示向下(行号递增),0 表示向上(行号递减)
    int x = 1, y = 1, t = 1, p = 1;

    while(t <= n * m) {
    // 1. 检查当前座位是不是小 R 的
    if(a[t] == r) { // 如果当前发座位的人的分数就是小 R 的分数,直接输出坐标并结束
    cout << y << " " << x; // ⚠️易错点:注意题目要求先输出列(y),再输出行(x)
    return 0;
    }

    // 2. 模拟移动坐标的过程(为排名第 t+1 的下一个人找好座位)
    if(p) { // 当前 p=1,代表在奇数列,向下走
    x++; // 行号 +1,往下移一格
    if(x == n + 1) { // 越界检查:如果走到了第 n+1 行,说明这一列到底了
    p ^= 1; // 核心技巧:按位异或运算 (1^1=0, 0^1=1),巧妙实现方向状态的 0/1 反转
    x; // 撞墙退回一步,回到合法的第 n 行
    y++; // 换到下一列,准备从下往上排
    }
    } else { // 当前 p=0,代表在偶数列,向上走
    x; // 行号 -1,往上移一格
    if(x == 0) { // 越界检查:如果走到了第 0 行,说明这一列到顶了
    p ^= 1; // 再次 0/1 反转,改变方向,准备变成向下走
    x++; // 撞墙退回一步,回到合法的第 1 行
    y++; // 换到下一列,准备从上往下排
    }
    }
    t++; // 当前这个人的座位已经走过了,处理下一个人
    }

    return 0;
    }


    赞(0)
    未经允许不得转载:171主机测评 » CSP-J 历年复赛 T2 及解析(2019~2025)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址